# Server API & Web UI
`server/api.mjs` (HTTP + cluster + cache) and `server/ui.html` (the web UI). Deployable as **one Node
process + one `corpus.db` file** — no external search engine.
## Endpoints
| Method/Path | Returns |
|-------------|---------|
| `GET /` | The web UI (`ui.html`). |
| `GET /search?q=…&allowFemale=0&kidZone=1&blockVideos=1&k=8` | Ranked, **category-grouped**, content-filtered results: `{q, count, categories:{artists, songs, albums, singles, videos, playlists}}`. The `playlists` category includes both artist-owned **and** community-discovered playlists (each row carries `source: "artist"\|"community"`). Song/video rows also carry `durationSec` + `playCount` + `releaseDate` (nullable). **Detail metadata:** `/artist` `songs`/`videos` carry `durationSec`+`playCount`+`releaseDate`+`thumbnail` (square album art from the track's album, `null` for standalone singles/videos)+`album` (`{id,name}` or `null`) and `songs` are sorted by `playCount` desc (real "Top songs"); `/album` `tracks` carry `durationSec`+`trackNumber`+`releaseDate`; `/playlist` `tracks` carry `durationSec`. **Album objects** (album/single rows in `/search` + `/artist`, and the `/album` header) carry `type` (album/single/ep), `trackCount`, `totalDurationSec`, `releaseDate` — read-time aggregates over `album_track`∪`track` (no stored column). **`releaseDate`** (ISO-8601) is the item's real release date: for a song, its OWN `/player` upload date (falling back to its album's); for an album, its lead-track date. Durations/counts are extracted from cache; dates are maintained off-datacenter. Nullable where unknown. |
| `GET /artist?id=UC…&allowFemale=0&kidZone=1&blockVideos=1` | Artist catalog `{artist, songs, videos, albums, singles, playlists}` (from the DB). Honors the content-filter flags: a female artist (`allowFemale=0`) or a non-KidZone artist (`kidZone=1`) returns **404** (treated as not-found); `blockVideos` empties the `videos` category. Songs/videos carry `durationSec`+`playCount`+`releaseDate` and **`songs` are sorted by `playCount` desc** (real "Top songs"); album/single rows carry `type`+`trackCount`+`totalDurationSec`+`releaseDate`. |
| `GET /album?id=MPRE…&allowFemale=0&kidZone=1&blockVideos=1` | `{album, tracks}` (from the DB, ordered). Honors the flags: a female/non-KidZone artist's album → **404**; `blockVideos` drops video tracks; filtered **per track** so a mixed compilation keeps only allowed tracks. Tracks carry `durationSec`+`trackNumber`+`releaseDate`; the `album` header carries `playlistId` (the audio-playlist id for playing/queuing the album) + `type`+`trackCount`+`totalDurationSec`+`releaseDate` (full-album aggregates). |
| `GET /playlist?id=…&allowFemale=0&kidZone=1&blockVideos=1` | `{playlist, tracks, total, whitelisted}` — fetched **on demand** (cached) from YouTube and filtered to whitelisted-corpus / whitelisted-channel tracks. Works for **any** playlist id: artist-owned, community-discovered, or even an unindexed id (the filter is purely id-based, so it can never serve a non-whitelisted track). The header meta falls back to `community_playlist` when the id isn't an artist playlist. The content-filter flags are applied **per song** — a mixed (e.g. male+female) playlist keeps the allowed songs and drops only the filtered ones; it is **never** blocked wholesale (an all-female list just opens empty for a blocked-female user). Tracks carry `durationSec`. |
| `POST /user-playlist` and `GET /user_playlist/<id>` (or `?id=`) and `GET /.well-known/assetlinks.json` | **User-SHARED playlists** (zemer-app#176) — person-to-person link sharing, NOT a public index (reachable only by knowing the unguessable link → no browse/search surface, no moderation). `POST` body `{title, videoIds[1..500], device?}` → members validated against the corpus (whitelist-pure by construction; non-corpus ids dropped, count reported) → `{id, url, kept, dropped}`; title ≤120 chars; 50 shares/device/day. `GET` returns JSON for the app (`{playlist:{id,title,createdAt,trackCount}, tracks:[{videoId,title,artist,artistId,thumbnail,durationSec,explicit,isVideo}], source:"zemer-user"}`) with the RECEIVER's content flags + blocked-ids applied per-request and since-de-whitelisted members silently dropped — or, for a browser `Accept: text/html` (`?format=json` overrides), a branded landing page (tracklist + "Open in the Zemer app"). Snapshots are immutable — re-sharing after an edit mints a new id. `assetlinks.json` (served from `data/assetlinks.json` when the app team provides package+cert; 404 until then) enables Android App Links so the URL deep-links straight into the app. |
| `GET /stations` and `GET /station?id=chasidish\|dj\|israeli&next=1..10` and `GET /stations/cover?id=…` | **Zemer Stations** — SYNCHRONIZED broadcast radio (one shared wall-clock program per station; every listener hears the same track at the same moment — see [stations.md](stations.md)). `/stations` → `{count, stations:[{id, title, thumbnail, live, nowPlaying}], serverT
This file has been truncated. show original