Files
fofos-admin/web/thumbnail.ts
T
pastilhasandClaude Opus 5 55be0a1860 a video in a carousel gets its poster, not a 404
A video has no width rungs — poster-448.webp and friends are cut from a still —
so asking for 320.webp returns a 404 and the grid drew a broken tile with no clue
why. Every grid in the panel was doing it, and a carousel is explicitly allowed
to hold both kinds.

posterUrl already existed for this and nothing called it. mediaThumbUrl decides
from the extension so a screen showing mixed media does not have to know the
difference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 14:32:52 +01:00

68 lines
3.4 KiB
TypeScript

import { getHeaders } from 'hooks/useClient';
/**
* A thumbnail URL for something under `originals/`, authenticated by `?token=`.
*
* ── Why the token is in the query ──
*
* An `<img>` cannot send an Authorization header. `user-middleware` and the permission gate both accept
* the token in the query string for exactly this, and the platform's own file browser does the same.
* Built without it first, and every picture 401s while the layout looks perfectly correct.
*
* ── Why this is a module ──
*
* It was copied into CarouselsPanel and again into RecordsPanel, and a third copy was about to go into
* MediaField. Three copies of a URL shape is three places to be wrong about the rung when the ladder
* changes — which it did, four times in a day.
*/
export const thumbUrl = (path: string, width: 256 | 320 | 480 | 640 | 960 | 1280 = 320): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/derived/${encoded}/${width}.webp?token=${encodeURIComponent(token)}`;
};
/**
* A video's poster, for the same reason.
*
* A video has no width rungs — `poster-448.webp` and friends are cut from its still — so a slot showing
* one asks for the poster rather than a width that does not exist.
*/
export const posterUrl = (path: string): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/derived/${encoded}/poster-512.webp?token=${encodeURIComponent(token)}`;
};
/**
* An ORIGINAL, rendered at a width, for something drawn at size.
*
* The derived ladder is right for a row thumbnail and wrong for a framing slot: that is most of a panel
* wide and can be zoomed on top of it, so a rung chosen in advance is a rung that is wrong. This asks
* for the width the slot actually draws, from the file that has every pixel.
*
* `width` is a hint the server clamps to 256..4096. Ask for what will be drawn — twice it, on a screen
* that is worth it — rather than a round number.
*/
export const originalUrl = (path: string, width: number): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/original/${encoded}?width=${Math.round(width)}&token=${encodeURIComponent(token)}`;
};
/** Extensions av-club treats as video. The same list the site's own `kindFor` uses. */
const VIDEO = /\.(mp4|mov|webm|m4v|avi|mkv)$/i;
/**
* A thumbnail for anything in the media store, picture or video.
*
* A video has no width rungs — `poster-448.webp` and friends are cut from a still, and asking for
* `320.webp` gets a 404 and a broken image. Every grid in this panel was doing exactly that, so a
* carousel holding a video showed a broken tile with no clue why.
*
* `posterUrl` already existed for this and nothing called it. One helper that decides from the
* extension, so a screen showing mixed media does not have to know the difference — which is the
* whole point, since a carousel is explicitly allowed to hold both.
*/
export const mediaThumbUrl = (path: string, width: 256 | 320 | 480 | 640 | 960 | 1280 = 320): string =>
VIDEO.test(path) ? posterUrl(path) : thumbUrl(path, width);