/playlists is the new page of every public playlist, newest first (GET /api/playlists), and /playlists/<slug> a playlist category's, with a menu of the categories, linked from the user menu as "All playlists"; `playlists` is reserved as a dump category's slug, and one that had it is renamed. A profile's Playlists tab narrows to a playlist category with the same menu as its Dumps tab (?category=), and so do the Dumps and Playlists tabs of /search, each with its own kind (category and playlistCategory for GET /api/search), another tab dropping it. The category service takes the kind throughout, so a dump can't be filed under a playlist category nor the other way round, and categoryJoin is the one filter join the dump, follow and playlist services share. useCategories(kind) gives one kind's set from the one fetch, CategoryChips replaces the chips the dump card and page each drew, useCategoryFilter keeps a list to its category as the socket brings more, and useCategorySelection holds a form's picked categories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
/**
|
|
* Checks which slugs a dump category may take. A dump category's feed lives at
|
|
* the URL root (`/music`), so its slug must not shadow another top-level page.
|
|
* The category service runs this check when a dump category is created or
|
|
* renamed, and the migrations that reserve more words rename the categories
|
|
* that had one with {@link renameReservedCategories}. A playlist category's
|
|
* page is `/playlists/<slug>`, under a reserved word, so it reserves nothing.
|
|
*
|
|
* Dump and playlist slugs reserve nothing: they live under their owner's
|
|
* `dumps/` and `playlists/`, where nothing else is named by a user. A user
|
|
* can't clash with a category either, since `@` is never in a slug.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { DatabaseSync } from "node:sqlite";
|
|
import { FEED_TABS } from "../../shared/paths.ts";
|
|
|
|
/**
|
|
* Lists the top-level path segments a category slug may not take: the feed
|
|
* tabs (`/hot`), the SPA's own pages (`src/App.tsx`), and `api`, `ws`,
|
|
* `assets` and `emojibase`, which the server answers before the SPA. Keep it
|
|
* in step with the route table and the build's top-level folders.
|
|
*/
|
|
export const RESERVED_SLUGS: readonly string[] = [
|
|
...FEED_TABS,
|
|
"search",
|
|
"login",
|
|
"register",
|
|
"notifications",
|
|
"reset-password",
|
|
"unsubscribe",
|
|
"settings",
|
|
"manage",
|
|
"community",
|
|
"playlists",
|
|
"authorize",
|
|
"federation",
|
|
"api",
|
|
"ws",
|
|
"assets",
|
|
"emojibase",
|
|
];
|
|
|
|
/** Checks whether `slug` is one of the {@link RESERVED_SLUGS}. */
|
|
export function isReservedSlug(slug: string): boolean {
|
|
return RESERVED_SLUGS.includes(slug);
|
|
}
|
|
|
|
/**
|
|
* Renames each category whose slug is now reserved, such as `hot`, to the
|
|
* first of `<slug>-2`, `<slug>-3`… that is free, and logs it. Migration 0021
|
|
* runs it for v4's words, 0022 for `community`, 0024 for `authorize` and
|
|
* `federation`, and 0025 for `playlists`.
|
|
*
|
|
* It reads every category: until 0025 gave categories a `kind`, each was a
|
|
* dump category, and 0025 makes no other. A later migration that reserves
|
|
* more words must leave the playlist categories alone, and limit it to
|
|
* `kind = 'dump'`, which the earlier ones can't, having no such column.
|
|
*/
|
|
export function renameReservedCategories(db: DatabaseSync): void {
|
|
const rows = db.prepare(`SELECT id, slug FROM categories;`).all() as {
|
|
id: string;
|
|
slug: string;
|
|
}[];
|
|
const taken = new Set(rows.map((r) => r.slug));
|
|
for (const { id, slug } of rows) {
|
|
if (!isReservedSlug(slug)) continue;
|
|
let n = 2;
|
|
while (taken.has(`${slug}-${n}`) || isReservedSlug(`${slug}-${n}`)) n++;
|
|
const renamed = `${slug}-${n}`;
|
|
taken.add(renamed);
|
|
db.prepare(`UPDATE categories SET slug = ? WHERE id = ?;`).run(renamed, id);
|
|
console.log(
|
|
`[migrate] category "${slug}" is now "${renamed}": its slug is reserved`,
|
|
);
|
|
}
|
|
}
|