Skip to content

feat: popular genres endpoint#961

Merged
dylanjeffers merged 1 commit into
mainfrom
codex/popular-genres-api
Jun 17, 2026
Merged

feat: popular genres endpoint#961
dylanjeffers merged 1 commit into
mainfrom
codex/popular-genres-api

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

Adds GET /v1/genres/popular returning genres ranked by track usage. Powers freeform genre autocomplete on upload.

What it does

  • New route g.Get("/genres/popular", app.v1GenresPopular) in api/server.go.
  • Handler api/v1_genres_popular.go wraps the existing GetGenres sqlc query (GROUP BY genre ORDER BY count DESC, filtering is_current, non-empty genre, created_at > start_time, and access_authorities IS NULL).
  • Query params: start_time (unix, default 0), limit (1–100, default 100), offset (≥0, default 0), min_count (1–1e6, default 1).
  • Swagger documented under a new genres tag with popular_genres_response / popular_genre schemas.

Tests

  • api/v1_genres_popular_test.go covers ordering + fixture presence, min_count filtering, and access_authorities exclusion.
  • Not run locally — the local Docker/postgres test environment is broken in the reviewer's machine (containerd/overlay2 I/O errors), so TestGenresPopular could not be executed here. CI will run the full suite on this PR.

Review note (non-blocking)

min_count is filtered in Go after the SQL LIMIT/OFFSET, so a page can be trimmed below the requested limit and low-count genres still consume a page slot. Harmless for the autocomplete use case (default limit=100, min_count=1), but moving it to a HAVING count >= @min_count clause would make pagination exact. Worth a follow-up if min_count + pagination get used together.

🤖 Generated with Claude Code

@dylanjeffers dylanjeffers merged commit 65eb077 into main Jun 17, 2026
5 checks passed
@dylanjeffers dylanjeffers deleted the codex/popular-genres-api branch June 17, 2026 20:54
dylanjeffers added a commit that referenced this pull request Jun 18, 2026
## What

Collapses genre spelling variants — e.g. `Hip Hop` / `hip-hop` /
`hiphop`, `r&b` / `rnb` — to a single canonical form, building on the
`/v1/genres/popular` endpoint added in #961.

## Design decisions

**Normalize on read (display only), not on write, and not on filter
params.** The Go bridge service does **not** write the `tracks.genre`
column — tracks (including genre) are populated upstream by the Python
discovery provider during chain indexing. So there is no write path in
this repo to hook.

**`NormalizeGenre()`** (`api/genre_normalize.go`):
- trims surrounding whitespace, collapses internal whitespace runs
- title-cases by default, preserving internal separators (`hip-hop/rap`
→ `Hip-Hop/Rap`)
- maps known special cases via a collapsed lookup key (lowercase,
alphanumeric-only) so every punctuation/spacing variant routes through
one entry: `R&B`, `EDM`, `DJ`, `Hip Hop`, `Drum & Bass`, `Lo-Fi`,
`K-Pop`, `J-Pop`
- already-canonical values (`Electronic`, `R&B`, `Hip Hop`) pass through
unchanged

**`/v1/genres/popular`** — normalize each name, then **merge and sum
counts** for variants that collapse together, re-sort by total desc, and
apply `min_count` to the merged total. Caveat (in code): the SQL still
`GROUP BY`s and paginates on the raw genre, so merging only catches
variants within the same page; full at-rest aggregation requires
normalizing on write upstream.

**Genre filter params were intentionally NOT normalized.** An earlier
revision normalized the `genre` param on the trending / underground /
latest / users-genre-top endpoints, but that is **unsound**: because the
stored `genre` column is not normalized at rest, canonicalizing the
param stops it matching the raw stored value. CI caught this
(`TestGetLatestWithGenre`: a query for `LatestTestGenreA` was
title-cased to `Latesttestgenrea` and matched zero rows). Reverted in
01ba2ba — filtering is left untouched. Collapsing variants for
*filtering* requires normalizing genre on write upstream.

## Tests

`TestGenreNormalize` covers trim, internal-whitespace collapse, casing,
`hip-hop`/`hiphop` → `Hip Hop`, `r&b`/`rnb` → `R&B`, special cases, and
already-canonical pass-through.

```
go test ./api/ -run TestGenreNormal   # PASS (24 subtests)
```

`go build` and `go vet ./api/` are clean. DB-backed tests run in CI
(Docker is broken in the local dev environment).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant