Add discovery modes, track tags and an artist graph

Tracks get tags from their title, description, artist name and BPM
(styles such as riddim, versions such as VIP/dub/flip, and tempo), stored
in track_tags and shown on cards. Related pages the crawler reads are kept
in track_related, which links artists to each other.

The feed takes ?mode=: riddim (140 BPM, riddim artists and their graph
neighbours first), fresh, deep cuts, and an artist radio that plays an
artist and the artists around them. Opening a radio crawls that artist's
related pages first. The crawler searches riddim-adjacent terms first.

The filters panel picks the feed, each card has a radio button (r), and a
chip in the top bar returns to the normal feed.
This commit is contained in:
nvrl committed 2026-11-01 13:46:52 +01:00
1 parent b4470850bf
commit f6c505907a
14 files changed
+737 -36

No files matched your search

+14 -2
View File
@@ -22,6 +22,8 @@ export interface Track {
upload_id: string | null;
waveform: number[] | null;
followed?: boolean;
/// From titles, descriptions and BPM, see backend/src/tags.rs.
tags?: string[];
}
export interface Stats {
@@ -49,6 +51,12 @@ export interface Me {
export type Filters = Partial<Record<"free" | "bpm_min" | "bpm_max", string>>;
/// Which part of the catalogue the feed draws from. `artist` is an artist radio:
/// that artist and the ones pl8list relates to them.
export type Mode =
| { mode: "classic" | "riddim" | "fresh" | "deep" }
| { mode: "artist"; artist: string; name: string };
export type EventKind = "play" | "like" | "unlike" | "skip" | "complete" | "dislike";
export class HttpError extends Error {
@@ -84,8 +92,12 @@ export function sendEvent(track: Track, kind: EventKind, seconds?: number): void
}
}
export const feed = (filters: Filters) =>
getJSON<{ tracks: Track[]; stats: Stats }>("/local/feed?" + new URLSearchParams({ n: "10", ...filters }));
export function feed(filters: Filters, mode: Mode) {
const p = new URLSearchParams({ n: "10", ...filters });
if (mode.mode !== "classic") p.set("mode", mode.mode);
if (mode.mode === "artist") p.set("artist", mode.artist);
return getJSON<{ tracks: Track[]; stats: Stats }>("/local/feed?" + p);
}
export const likes = () => getJSON<{ tracks: Track[] }>("/local/likes");