Skip to content

feat(genres): allow custom track genres#14424

Merged
dylanjeffers merged 2 commits into
mainfrom
genres/custom
May 29, 2026
Merged

feat(genres): allow custom track genres#14424
dylanjeffers merged 2 commits into
mainfrom
genres/custom

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

Fresh branch off main — supersedes #14419.

Summary

  • Discovery indexer no longer rejects tracks whose genre isn't in the hardcoded allowlist; just caps at 100 chars. The allowlist is retained as a canonical reference for read-side trending/metrics scoping.
  • SDK upload schemas for tracks, albums, and playlists changed from z.enum(Genre) to z.string().min(1).max(100). SDK regenerated from updated swagger (generated Genre enum dropped). Manual Genre enum is now the canonical export from @audius/sdk with PascalCase keys, plus a new GenreString = Genre | string helper type.
  • Web SelectGenreField rewritten from <SelectField> to <TextField> + native <datalist> — users can pick a known genre via autocomplete or type a custom one. maxLength=100 gives the built-in harmony character counter.

Pairs with AudiusProject/api genres/custom — the Go API must ship first (or together) so it stops rejecting custom-genre POSTs before this indexer relaxation lands.

What we deliberately did NOT change

  • index_trending.py and get_genre_metrics.py still scope to genre_allowlist. Custom-genre tracks silently won't appear in trending or genre-metrics endpoints — intentional MVP cut.
  • packages/mobile has its own genre picker that wasn't touched — follow-up PR needed.
  • No write-side normalization. "Hip-Hop" / "Hip Hop" / "hip hop" become three distinct genres.

Breaking change (browser dist only)

packages/sdk/src/sdk/types/Genre.ts keys changed from SCREAMING_SNAKE to PascalCase to match the previously-generated enum that downstream consumers all imported. External integrations referencing window.audiusSdk.Genre.HIP_HOP_RAP etc. via the browser dist need to update to Genre.HipHopRap.

Test plan

  • tsc --noEmit in packages/sdk — clean
  • vitest run in packages/sdk — 17 files, 196 passed, 2 skipped
  • SDK regenerated via node ./src/sdk/api/generator/gen.js --spec <local-edited-swagger> against the updated swagger
  • Type a custom genre in the upload form, confirm it submits
  • Existing known-genre flows still work
  • Verify >100-char input is rejected client and server side
  • Confirm no regression in trending/browse-by-genre for canonical genres

🤖 Generated with Claude Code

- discovery indexer: drop genre allowlist enforcement at write time; cap
  genre at 100 chars; allowlist retained as canonical reference for
  read-side trending/metrics aggregations
- sdk: relax track + album + playlist upload schemas to z.string().min(1).max(100);
  regenerate from updated swagger (generated Genre enum dropped); manual
  Genre enum re-exported from index with PascalCase keys to preserve
  @audius/sdk consumer compatibility; new GenreString = Genre | string
  helper type
- web: SelectGenreField rewritten to TextField + native <datalist> so
  users can pick a known genre via autocomplete or type a custom one

Read-side genre filters (trending, get_genre_metrics) still scope to the
canonical set — tracks tagged with custom genres won't appear in those
endpoints. Mobile genre picker is untouched and will be addressed in a
follow-up PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

⚠️ No Changeset found

Latest commit: 17dd888

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🌐 Web preview ready

Preview URL: https://audius-web-preview-pr-14424.audius.workers.dev

Unique preview for this PR (deployed from this branch).
Workflow run

dylanjeffers added a commit to AudiusProject/api that referenced this pull request May 29, 2026
Fresh branch off main — supersedes #874.

## Summary
- `api/v1_track.go`: dropped the `oneof='Electronic' 'Rock' ...`
validators on `CreateTrackRequest.Genre` (required) and
`UpdateTrackRequest.Genre` (omitempty); replaced with `min=1,max=100`
and `max=100`.
- `api/swagger/swagger-v1.yaml`: `genre` schema changed from string-enum
to plain string with `maxLength: 100`. Canonical values moved into the
description so generated docs/SDKs still surface them as autocomplete
suggestions.
- `indexer/constants.go`: added `MaxTrackGenreLength = 100` next to
`Entity_Track`. Forward-looking — when the Go indexer takes over track
entity-event consumption from the Python `discovery-provider`, this is
the rule the new handler should enforce, mirroring discovery-provider's
`CHARACTER_LIMIT_GENRE`.

Pairs with
[AudiusProject/apps#14424](AudiusProject/apps#14424)
— the SDK regen, Python indexer relaxation, and web UI live there.
**Ship this first (or together)** so the Python indexer relaxation isn't
gated on a Go API that still rejects custom genres at the validate-tag
layer.

## Test plan
- [x] Existing test fixtures using `genre: "Electronic"` still pass
(those values are still valid strings).
- [x] `go build ./indexer` clean.
- [ ] POST `/v1/tracks` with `genre: "Phonk"` succeeds.
- [ ] POST with `genre: ""` returns 400 (`min=1`).
- [ ] POST with a 101-char genre returns 400 (`max=100`).
- [ ] Generated swagger docs at `/v1/swagger.yaml` show the new schema
with description containing canonical values.
- [ ] When the Go indexer's track-event consumer is implemented, it
references `indexer.MaxTrackGenreLength` rather than introducing its own
enum check.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves import/order lint errors in AlbumsApi.test.ts and
PlaylistsApi.test.ts that failed SDK CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dylanjeffers dylanjeffers merged commit e822316 into main May 29, 2026
19 checks passed
@dylanjeffers dylanjeffers deleted the genres/custom branch May 29, 2026 22:11
dylanjeffers added a commit that referenced this pull request May 29, 2026
…14427)

Follow-up to #14424. The first genres PR shipped custom-genre support on
the SDK and web edit form, but the audit afterward found three gaps that
block the same UX on mobile and silently re-tighten the rule in common's
form schema.

## Summary

### 1. Mobile track upload/edit picker (Tier 1 #1)

`packages/mobile/src/screens/edit-track-screen/screens/SelectGenreScreen.tsx`
was a strict `ListSelectionScreen` populated from `GENRES` — users on
mobile had no way to enter a custom genre. Rewritten to:
- Disable `ListSelectionScreen`'s internal search and pass a controlled
`TextInput` in the `header` prop instead.
- Compute the suggestion list from the typed input: filter `GENRES` by
substring match, and when the input is non-empty and doesn't match any
known label/value exactly, prepend a synthetic `{ value: <input>, label:
'Use "<input>" as a custom genre' }` item at the top.
- Tap any item — known or custom — commits via the existing
`onChange`/`setValue` flow.
- `maxLength={100}` caps to match the SDK/API limit.

### 2. Form schema + GenreString re-export (Tier 1 #2)
- `packages/common/src/schemas/upload/uploadFormSchema.ts:58` was still
`z.enum(Object.values(Genre))` despite the SDK schema relaxing in
#14424. The web `SelectGenreField` lets users type "Phonk", but this
form-level validator would reject it before the submit handler ever
called the SDK. Changed to `z.string().min(1).max(100)`.
- `packages/common/src/utils/genres.ts` now re-exports `GenreString`
from `@audius/sdk` so downstream consumers can opt into the loose type
for read-side / metadata paths.

I audited all 12 files flagged in the post-merge audit. The state/filter
typings (`Search.ts`, `trending/types.ts`, `trending/actions.ts`,
`lineups/useTrending.ts`, `search/useSearchResults.ts`,
`quickSearch.ts`, `Analytics.ts`) are deliberately left as strict
`Genre` — those represent user selections from the canonical filter UI,
not received track data. Two files (`hooks/useTrackMetadata.ts` and
`store/upload/types.ts`) already handled string values fine.

### 3. Mobile upload example (Tier 1 #3)
`packages/mobile/examples/upload/App.tsx` had its own inline hardcoded
`GENRES` chip list. Dropped the array, replaced the horizontal chip
ScrollView with a freeform `TextInput` (placeholder hints common
genres). Reference example for SDK consumers no longer implies "genres
must be from this list."

## Verification

- [x] `tsc --noEmit` in `packages/common` — clean
- [x] `tsc --noEmit` in `packages/web` — clean
- [x] `tsc --noEmit` in `packages/mobile` — clean
- [ ] Mobile manual test: open the edit-track flow, navigate to Select
Genre, type "Phonk", tap the "Use 'Phonk'…" row, confirm the form
reflects "Phonk" and the upload succeeds end-to-end.
- [ ] Mobile manual test: type a known prefix like "Tech" — confirm
"Techno" and "Tech House" suggestions appear and tap committing them
still works.
- [ ] Web regression: confirm the custom-genre flow shipped in #14424
still works (no functional change there; only the form schema was
loosened, which could only ever accept *more* inputs).

## Still deferred (Tier 2/3 from the audit)

- Search and trending genre filters on web (`SearchFilters.tsx`,
`TrendingPageContent.tsx`, `TrendingGenreSelectionPage.tsx`) and mobile
(`SearchFilters.tsx`, `TrendingFilter*.tsx`) — still scoped to the
canonical list. Even if loosened, the Python read-side aggregations
(`index_trending.py`, `get_genre_metrics.py`) still `WHERE genre IN
genre_allowlist`, so no data would back custom-genre filters until
that's also addressed.
- Write-side normalization ("Hip-Hop" vs "Hip Hop" vs "hip hop" still
become distinct genres).
- Mobile signup-flow `SelectGenresScreen` — preference picker,
intentionally stays canonical.

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant