/** * Playlists are ordered collections of dumps their owners curate. This module * types them, with the bodies that create, edit and reorder them. The client * shows them on `/@owner/playlists/:slug`, on profiles and in search results. * * @module */ import type { Dump } from "./dump.ts"; /** * The orders a list of playlists comes in, the first being the default one: * `updated`, by {@link playlistActivity}, the latest first; `new`, by * creation, the newest first; `dumps`, the fullest first. `GET /api/playlists` * and `GET /api/users/:key/playlists` take one as `?sort=`, the `/playlists` * page and a profile's playlists tab show them as tabs and keep the one * picked in their URL's `?sort=`. */ export const PLAYLIST_SORTS = ["updated", "new", "dumps"] as const; /** One of the {@link PLAYLIST_SORTS}. */ export type PlaylistSort = (typeof PLAYLIST_SORTS)[number]; /** * Returns the order a `?sort=` names, the default one when it is missing or * names none: like a page number, an order never fails a list. */ export function parsePlaylistSort(value: string | null): PlaylistSort { return PLAYLIST_SORTS.find((sort) => sort === value) ?? PLAYLIST_SORTS[0]; } /** * Returned by the playlist routes, and by the search, users and dumps routes * that list playlists. The `playlist_created` and `playlist_updated` WebSocket * messages carry it too. */ export interface Playlist { id: string; userId: string; title: string; /** * Names the playlist in its page's path, `playlistPath` * (`shared/paths.ts`) building it with {@link ownerUsername}. Made from the * title on creation, it changes only when edited, and is unique among its * owner's playlists. */ slug: string; /** It is written in Markdown. */ description?: string; /** * Only its owner sees a private playlist. Moderators don't, though they may * still edit or delete it. */ isPublic: boolean; createdAt: Date; /** * `playlistImageUrl` puts it on the cover's URL, so a new cover isn't hidden * by the cached old one. */ updatedAt?: Date; /** It is undefined when the playlist has no cover. */ imageMime?: string; /** * It counts the dumps the viewer may see in it, as its page lists them. It * is undefined when the query didn't count them. */ dumpCount?: number; /** * When the latest of the dumps the viewer may see was added to it, which * {@link playlistActivity} reads. It is undefined when it holds none, or * when the query didn't count them. */ lastDumpAt?: Date; /** The owner's current name, which the playlist's path starts with. */ ownerUsername: string; /** * The ids of the playlist categories it is filed under. The services fill * them in on every playlist they return. */ categoryIds?: string[]; } /** * Returns when `playlist` last took a dump, or was created if it holds none: * what the `updated` order sorts by and the cards show. Its title, cover and * description may change after without bringing it back up a list. */ export function playlistActivity( playlist: Pick, ): Date { return playlist.lastDumpAt ?? playlist.createdAt; } /** * Returns the comparison that puts playlists in the order `sort` names, as * the API pages them, the newer first on a tie. `usePlaylistListSync` finds * with it where a playlist the socket announces goes in a list. */ export function comparePlaylists( sort: PlaylistSort, ): (a: Playlist, b: Playlist) => number { const byCreation = (a: Playlist, b: Playlist) => b.createdAt.getTime() - a.createdAt.getTime(); switch (sort) { case "updated": return (a, b) => playlistActivity(b).getTime() - playlistActivity(a).getTime() || byCreation(a, b); case "dumps": return (a, b) => (b.dumpCount ?? 0) - (a.dumpCount ?? 0) || byCreation(a, b); case "new": return byCreation; } } /** * Returned by `GET /api/users/:key/playlists/:slug` to `PlaylistDetail`, and * by `GET /api/playlists/:playlistId`, with the dumps the caller may see, in * the playlist's order. */ export interface PlaylistWithDumps extends Playlist { dumps: Dump[]; } /** * Returned by `GET /api/me/playlists?contains=:dumpId`, one per playlist of * the caller's. `PlaylistMembershipPanel` ticks the playlists * with `hasDump`, in the add-to-playlist modal and the dump form. */ export interface PlaylistMembership { playlist: Playlist; hasDump: boolean; } /** Passed to `POST /api/playlists` by `PlaylistCreateForm`. */ export interface CreatePlaylistRequest { title: string; description?: string; isPublic: boolean; /** The playlist categories to file it under, none when left out. */ categoryIds?: string[]; } /** * Passed to `PUT /api/playlists/:playlistId` by `PlaylistDetail`. A field left * out keeps its value. */ export interface UpdatePlaylistRequest { title?: string; /** * A new slug, which the edit form's slug field sends. A title change never * changes the slug by itself. */ slug?: string; /** `null` removes the description. */ description?: string | null; isPublic?: boolean; /** Replaces its playlist categories; `[]` takes it out of every one. */ categoryIds?: string[]; } /** * Passed to `PUT /api/playlists/:playlistId/order` by `PlaylistDetail`, after * a drag or to put back a dump whose removal was undone. */ export interface ReorderPlaylistRequest { /** * Every dump of the playlist the caller can see, in the new order. The dumps * hidden from them keep their slots. */ dumpIds: string[]; }