Fix hidden library items not rendering in the browser

Items flagged hidden_from_guests (blacklisted tags) returned 404 for
<img> requests since tags cannot send the auth header. The API now
exposes signed raw_url/thumbnail_url fields (mirroring upload previews
and avatars), and the SPA uses them in the gallery, detail view,
duplicates and delete screens, and upload visual matches.
This commit is contained in:
2026-09-17 13:25:20 -05:00
parent 4df573da43
commit db74f7ab18
11 changed files with 88 additions and 24 deletions
+1 -3
View File
@@ -18,9 +18,7 @@ const ratingLabels: Record<string, string> = {
export function MediaCard({ item }: { item: MediaItem }) {
const preview =
item.kind === "video"
? `/api/files/${item.j_id}/thumbnail/`
: `/api/files/${item.j_id}/raw/`;
item.kind === "video" ? item.thumbnail_url : item.raw_url;
const rating = item.display_rating;
return (
+1 -1
View File
@@ -229,7 +229,7 @@ export default function DeletePage() {
>
<div className="relative aspect-square overflow-hidden bg-ctp-mantle">
<img
src={`/api/files/${item.j_id}/thumbnail/`}
src={item.thumbnail_url}
alt={item.filename}
loading="lazy"
className="h-full w-full object-cover"
@@ -33,6 +33,8 @@ function MemberCard({
selected: boolean;
onToggle: () => void;
}) {
const preview =
entry.thumbnail_url ?? `/api/files/${entry.j_id}/thumbnail/`;
return (
<div
className={cn(
@@ -43,7 +45,7 @@ function MemberCard({
<button type="button" onClick={onToggle} className="block w-full text-left">
<div className="relative aspect-square overflow-hidden bg-ctp-mantle">
<img
src={`/api/files/${entry.j_id}/thumbnail/`}
src={preview}
alt={entry.j_id}
loading="lazy"
className="h-full w-full object-cover"
@@ -219,7 +221,10 @@ export default function DuplicatesPage() {
<div className="w-32 shrink-0">
<div className="aspect-square overflow-hidden rounded-md bg-ctp-mantle">
<img
src={`/api/files/${group.j_id}/thumbnail/`}
src={
group.thumbnail_url ??
`/api/files/${group.j_id}/thumbnail/`
}
alt={group.j_id}
loading="lazy"
className="h-full w-full object-cover"
@@ -300,13 +300,17 @@ export function LibraryDetail({ jId }: { jId: string }) {
const ordering = useUi((state) => state.ordering);
const user = useAuth((state) => state.user);
const [confirmDelete, setConfirmDelete] = useState(false);
const rawUrl = `/api/files/${jId}/raw/`;
const query = useQuery({
queryKey: ["file", jId],
queryFn: () => api<MediaItem>(`/api/files/${jId}/`),
});
const rawUrl = query.data?.raw_url ?? `/api/files/${jId}/raw/`;
const downloadUrl = rawUrl.includes("?")
? `${rawUrl}&download=1`
: `${rawUrl}?download=1`;
const neighborsQuery = useQuery({
queryKey: ["neighbors", jId, ordering],
queryFn: () =>
@@ -335,7 +339,7 @@ export function LibraryDetail({ jId }: { jId: string }) {
if (isTypingTarget(event.target)) return;
if (event.key === "d" || event.key === "D") {
event.preventDefault();
window.location.href = `${rawUrl}?download=1`;
window.location.href = downloadUrl;
return;
}
const neighbors = neighborsQuery.data;
@@ -347,7 +351,7 @@ export function LibraryDetail({ jId }: { jId: string }) {
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [neighborsQuery.data, navigate, rawUrl]);
}, [neighborsQuery.data, navigate, downloadUrl]);
if (query.isPending) {
return (
@@ -400,7 +404,7 @@ export function LibraryDetail({ jId }: { jId: string }) {
}
actions={
<div className="flex flex-wrap items-center gap-2">
<a href={`${rawUrl}?download=1`} className={linkButtonClass}>
<a href={downloadUrl} className={linkButtonClass}>
<Download className="h-4 w-4" /> Download
</a>
{canDelete ? (
+4 -1
View File
@@ -385,7 +385,10 @@ function MetadataModal({
className="flex items-center gap-3 rounded-md border border-ctp-surface0 bg-ctp-surface0/40 p-2"
>
<img
src={`/api/files/${match.j_id}/thumbnail/`}
src={
match.thumbnail_url ??
`/api/files/${match.j_id}/thumbnail/`
}
alt={match.j_id}
loading="lazy"
className="h-12 w-12 shrink-0 rounded object-cover"
+9 -1
View File
@@ -36,6 +36,7 @@ export interface DuplicateEntry {
uploaded_by: string | null;
e621_post_id: number | null;
locations?: { id: number; rel_path: string }[];
thumbnail_url?: string;
similarity?: number;
}
@@ -126,6 +127,8 @@ export interface MediaItem {
e621_post_id: number | null;
e621_data: E621StoredPost | null;
locations: MediaLocation[];
raw_url: string;
thumbnail_url: string;
created_at: string;
updated_at: string;
}
@@ -209,7 +212,12 @@ export interface TempUpload {
custom_notes: string;
iqdb_data: E621IqdbCandidate[] | null;
visual_matches:
| { j_id: string; filename: string; similarity: number }[]
| {
j_id: string;
filename: string;
similarity: number;
thumbnail_url?: string;
}[]
| null;
library_j_id: string | null;
file_url: string | null;