Error statuses follow one rule: APIException takes a status and derives the code from it (BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403, NOT_FOUND 404, CONFLICT 409, RATE_LIMITED 429, SERVER_ERROR 500, BAD_GATEWAY 502), so a code can't contradict its status. That fixed real bugs: a wrong current password on change-password answered 401, on which authFetch signs the user out; editing or deleting someone else's comment or chat message answered 401 rather than 403; one invalid dump edit answered 422. A taken username is 409, like a taken email. Along the way: the database is created from schema.sql by connection.ts when the file doesn't exist, so every task works on a fresh checkout (dev and serve never ran db/init.ts, which is gone); the startup purge compared ISO dates with SQLite's datetime() as text and kept anything from the current UTC day, and now compares ISO cutoffs; the seeded admin's hash comes from lib/password.ts (split out of jwt.ts, which needs the JWT secret) rather than a copy of its format; avatar, thumbnail and playlist-cover uploads go through replaceUploadedFile, which writes beside the old file and renames over it before the record announces it (a failed avatar write lost the old avatar; a thumbnail was announced before it existed); upload.ts derives its size message from MAX_IMAGE_SIZE_BYTES and stops re-exporting the upload folders; ALLOWED_IMAGE_MIMES, which duplicated detectImageMime, is gone; the SPA fallback no longer calls next() after answering; a reply must answer a comment on the same dump (a foreign or unknown parent was a 500 or a cross-dump thread). 211 backend and 628 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
/**
|
|
* Previews Bandcamp links from their page's OG tags, for
|
|
* `rich-content-service.ts`.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { RichContent } from "../../../shared/dump.ts";
|
|
import type { RichContentProvider } from "./provider.ts";
|
|
import { extractOgTag } from "../html.ts";
|
|
import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
|
|
import { BANDCAMP_HOST_RE } from "../bandcamp-tralbum.ts";
|
|
|
|
/**
|
|
* Claims bandcamp.com and its subdomains, where artists have their pages. Takes
|
|
* the embed player's URL from the page's `og:video`. The native player fetches the page's streams
|
|
* itself at play time, through `GET /api/bandcamp/tracks`.
|
|
*/
|
|
export const bandcampProvider: RichContentProvider = {
|
|
name: "bandcamp",
|
|
|
|
matches(url: string): boolean {
|
|
try {
|
|
return BANDCAMP_HOST_RE.test(new URL(url).hostname);
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
async fetch(url: string): Promise<RichContent> {
|
|
const res = await fetchWithTimeout(url);
|
|
const contentType = res.headers.get("content-type") ?? "";
|
|
|
|
if (!contentType.startsWith("text/html")) {
|
|
await res.body?.cancel();
|
|
return { type: "bandcamp", siteName: "Bandcamp", url };
|
|
}
|
|
|
|
const html = await readTextCapped(res);
|
|
|
|
return {
|
|
type: "bandcamp",
|
|
siteName: "Bandcamp",
|
|
url,
|
|
title: extractOgTag(html, "title"),
|
|
description: extractOgTag(html, "description"),
|
|
thumbnailUrl: extractOgTag(html, "image"),
|
|
embedUrl: extractOgTag(html, "video") ?? undefined,
|
|
};
|
|
},
|
|
};
|