From 1bfbdd28264d4c4db27593a874ae87e78131c715 Mon Sep 17 00:00:00 2001 From: zZebrahz Date: Sat, 30 May 2026 02:19:11 +0000 Subject: [PATCH] fix(people): refresh stale person metadata on view --- internal/api/handlers/people.go | 27 +++++++++++++++++++++++++++ web/src/pages/PersonDetail.tsx | 22 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/api/handlers/people.go b/internal/api/handlers/people.go index 2d71f678..484aa406 100644 --- a/internal/api/handlers/people.go +++ b/internal/api/handlers/people.go @@ -39,6 +39,8 @@ var personRefreshRate = ratelimit.Rate{ Burst: 10, } +const personMetadataStaleAfter = 90 * 24 * time.Hour + // PeopleHandler serves person-related API endpoints. type PeopleHandler struct { personRepo peopleRepository @@ -131,6 +133,8 @@ func (h *PeopleHandler) HandleGetPerson(w http.ResponseWriter, r *http.Request) return } + h.enqueuePersonRefreshIfDue(*person) + writeJSON(w, http.StatusOK, h.toResponse(r.Context(), *person)) } @@ -359,6 +363,29 @@ func (h *PeopleHandler) toResponse(ctx context.Context, p models.Person) personR return resp } +func (h *PeopleHandler) enqueuePersonRefreshIfDue(person models.Person) { + if h.refreshQueue == nil || !personHasRefreshableProviderID(person) { + return + } + + if personMetadataIncomplete(person) { + h.refreshQueue.Enqueue(person.ID) + return + } + + if person.UpdatedAt.Before(time.Now().Add(-personMetadataStaleAfter)) { + h.refreshQueue.Enqueue(person.ID) + } +} + +func personHasRefreshableProviderID(person models.Person) bool { + return person.TmdbID != "" || person.ImdbID != "" || person.TvdbID != "" +} + +func personMetadataIncomplete(person models.Person) bool { + return person.Bio == "" || person.PhotoPath == "" || person.PhotoPath == "-" || person.BirthDate == nil +} + func parseOptionalPersonDate(raw string) (*time.Time, error) { if raw == "" { return nil, nil diff --git a/web/src/pages/PersonDetail.tsx b/web/src/pages/PersonDetail.tsx index b0d583a8..401f1aac 100644 --- a/web/src/pages/PersonDetail.tsx +++ b/web/src/pages/PersonDetail.tsx @@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { Pencil, RefreshCw } from "lucide-react"; import { getPerson } from "@/api/client"; -import { createEmptyQueryDefinition } from "@/api/types"; +import { createEmptyQueryDefinition, type Person } from "@/api/types"; import type { CatalogSearchState } from "@/pages/catalogSearchParams"; import EditPersonDialog from "@/components/EditPersonDialog"; import ItemGrid from "@/components/ItemGrid"; @@ -25,6 +25,7 @@ export default function PersonDetail() { const { id } = useParams<{ id: string }>(); const [typeFilter, setTypeFilter] = useState("all"); const [editOpen, setEditOpen] = useState(false); + const autoRefreshWindowRef = useRef<{ personId: number; until: number } | null>(null); const { user } = useAuth(); const isAdmin = user?.role === "admin"; const refreshMutation = useRefreshPerson(id, isAdmin); @@ -33,6 +34,21 @@ export default function PersonDetail() { queryKey: personKeys.detail(id!), queryFn: () => getPerson(id!), enabled: !!id, + refetchInterval: (query) => { + const data = query.state.data; + if (!data || !isPersonMetadataIncomplete(data)) { + autoRefreshWindowRef.current = null; + return false; + } + + const current = autoRefreshWindowRef.current; + if (!current || current.personId !== data.id) { + autoRefreshWindowRef.current = { personId: data.id, until: Date.now() + 30_000 }; + return 3_000; + } + + return Date.now() < current.until ? 3_000 : false; + }, }); useDocumentTitle(person?.name ?? "Person"); @@ -212,6 +228,10 @@ export default function PersonDetail() { ); } +function isPersonMetadataIncomplete(person: Person) { + return !person.bio || !person.photo_url || !person.birth_date; +} + function PersonDetailSkeleton() { return (