v3.213). The source of truth stays the code and docs/music.md §3.6 "Genre workflow / rules"; file references are in grey under each section.Genres and moods come only from the Browse catalog (the music_tags table, kind genre / mood). Every song has 1 main genre (required to Compose) + up to 2 more genres + up to 3 moods, 6 tags at most. They are picked in one place, the "Where listeners find this song" card on Create / Edit / Upload, which opens a search sheet with the same coloured tiles as Browse. The picked names are written in front of the style text the AI receives. Magic Pen fills empty groups but never replaces what the creator picked. Admins manage the catalog in Developer Centre → Genres & moods, and listeners only see a genre or mood tile in Browse once it has at least 1 live song (changed from 3 on 10-08 15:30).
| When | What |
|---|---|
| 10-08 10:44 · QC asked | Haikal: redesign the Compose genre module (Spotify-like), align it with Browse, revisit the limits; cover Create, Edit and the draft "+Genre" redirect. Discuss first. |
| v3.202 server + web, 10-08 | Developer Centre "Genres & moods" admin page (add / rename / colour / aliases / reorder / hide, suggestions), 12 new genres + 4 new moods (migration 000535), Browse hides tiles with no live songs. |
| v3.213 server + web, 10-08 | Compose genre / mood redesign (1 + 2 + 3, search sheet, Magic Pen four cases, clear with Undo, tags sent to the AI, genre_required, Sound tab, draft "+Genre", Upload) and the new "Browse all" layout with tile covers. No migration. |
| Phones | iOS / Android get both with the next TestFlight build. Until then, test on hoop-web.pages.dev. Old phone builds keep working (see Rules). |
| Rule | How it works |
|---|---|
| Limits | 1 main genre (required to Compose) + up to 2 more genres + up to 3 moods = at most 6 tags per song. The server enforces the same caps (3 genres, 3 moods, 6 tags); extra tags are silently dropped, in the order sent, so the main genre (sent first) is never the one cut. |
| Where the main genre lives | In songs.genre, the column that already drives the genre pill on song rows. No new column. The app always sends the main genre first; changing only the main genre still saves it, and reopening Edit shows the same main genre. |
| Old songs | Songs that only have tags get the first picked genre (catalog order) as main, once. A free-text genre from before the catalog shows in the main slot without using a place, and gives way as soon as a catalog genre is picked. |
| Required on Compose, not on Save draft | App: with no main genre the Compose button is grey and says "Pick a main genre to compose". Save draft is never blocked. Server: POST /v1/music/generate with a tags key but no recognisable genre → 400 genre_required (checked before polishing and before the quota is charged; the app shows the same line). Old phone builds don't send tags, so they are not checked and work as before. |
| Fixed order | Pills on the card are always in catalog order (the admin's sort), not in tap order. Removing and re-adding a tag puts it back in the same place. |
| Limit message | Tapping one more when a group is full changes nothing and shows "You can pick up to 2 more genres — remove one first" / "You can pick up to 3 moods — remove one first". In the "More genres" sheet the main genre tile is marked "Main" and can't be tapped. |
| The sheet | Search (matches name, Chinese name, slug or alias), Browse colours, a tick on picked tiles, a count next to the title. Main genre is single choice and closes itself; more genres and moods are multi-choice. Tiles that match the style text carry a "Suggested" badge. |
| Sound tab | In "+ Add a style" the old Genre tab is now Sound (same key, so old builds still find it): only instrument / sound words plus the Chinese-style group. Catalog genres are no longer there. Every tab in that panel only adds words to the style text; it never picks categories. Footer: "These only guide the AI…". |
| Upload | Same card, same sheet, same 1 + 2 + 3. Upload is grey until a main genre is picked (when the catalog loaded). |
tags → no genre check and no tag line; the old 3 + 2 caps fit inside the new ones; the old Genre tab just loses the catalog genres. New phone + old server: a 3rd mood is cut to 2, tags is ignored.app/lib/screens/music_category_pick.dart (kMsMaxMoreGenres = 2, kMsMaxPickMoods = 3, MsCatSelection, showMsCategorySheet) · backend/internal/domain/music/browse.go (maxGenresPerSong, maxMoodsPerSong, maxTagsPerSong, setSongTagsSQL) · compose_tags.go · style_catalog.go (styleTabs). Tests: song_tags_caps_1002_db_test.go, compose_tags_1008_db_test.go, TestSoundTabHasNoCatalogGenres, music_browse_tags_0921_test, music_genre_rules_1008_test.
The four cases Haikal approved on 10-08. Which tiles get picked comes from suggestedCategorySlugs: whole-word matches of the style text against each tile's slug, name and aliases (for both genres and moods). The 10-02 behaviour "re-pick every time the pen runs" is gone, because it replaced what the creator had chosen.
| # | Before pressing the pen | What the pen does | Categories afterwards |
|---|---|---|---|
| 1 | Style empty, nothing picked | Writes a style from scratch | Picks main genre + up to 2 more + up to 3 moods to fit it, marked with the pen; the card says "Picked by Magic Pen to fit your style. Remove any you don't want." |
| 2 | Style empty, some picked | Writes a style using the picked English names as the seed ("Rock, Pop") | Keeps every pick; fills only the groups that are still empty |
| 3 | Creator wrote a style | Rewrites (polishes) that text | Picks only if nothing was picked; if anything was picked, touches nothing |
| 4 | Clear button on the style box | Clears the text | Clears all categories too; toast "Style and genres cleared" with Undo, which brings back both the text and the categories |
MsCatSelection.penFill (music_category_pick.dart) · style card trash button (music_studio_screen.dart) · suggestedCategorySlugs (music_ui_api.dart). Tests: music_compose_retry_1001_test ⑥⑥b⑥c⑦⑦b⑦c, ⑪⑪b.
tags (main genre first).music_tags.name_en) and builds one line: Genre: Rock, Pop. Mood: Chill. (no mood part if there are no moods).maxStyle). If it is too long, tags are trimmed first: moods from the last one back, then the extra genres from the last one back. The main genre and the creator's own text are never trimmed.autoTagSong) still reads only the creator's own text, not this line.backend/internal/domain/music/compose_tags.go (composeTagLine, styleWithTags), handler.go generate. Tests: compose_tags_1008_db_test.go ③, TestStyleWithTagsTrimsTagsNotText, music_compose_retry_1001_test ⑦d.
PUT …/tags only accepts the creator), so a chip there would do nothing.openSongEditor(null, 'cats'), _genreChip (music_studio_screen.dart). Tests: music_compose_cats_0923_test ①, music_genre_rules_1008_test ①①b.
24 genres and 12 moods (12 + 8 from migration 000256, 12 + 4 added by 000535 on 10-08), in Browse order. The three special tiles (Music, New Releases, Mood) are not managed here.
| # | Name | 中文 | Since |
|---|---|---|---|
| 1 | Pop | 流行 | 000256 |
| 2 | K-pop | 韩流 | 000256 |
| 3 | Hip-Hop | 嘻哈 | 000256 |
| 4 | Mandopop | 华语流行 | 000256 |
| 5 | Rock | 摇滚 | 000256 |
| 6 | Latin | 拉丁 | 000256 |
| 7 | Dance / Electronic | 电子舞曲 | 000256 |
| 8 | R&B | R&B | 000256 |
| 9 | Jazz | 爵士 | 000256 |
| 10 | Metal | 金属 | 000256 |
| 11 | Classical | 古典 | 000256 |
| 12 | Folk & Acoustic | 民谣 | 000256 |
| 13 | Indie | 独立 | new 10-08 |
| 14 | Chinese Traditional | 国风 | new 10-08 |
| 15 | Malay Pop | 马来流行 | new 10-08 |
| 16 | Soul / Funk | 灵魂 / 放克 | new 10-08 |
| 17 | Country | 乡村 | new 10-08 |
| 18 | Lo-fi | Lo-fi | new 10-08 |
| 19 | Reggae | 雷鬼 | new 10-08 |
| 20 | Blues | 布鲁斯 | new 10-08 |
| 21 | Cantopop | 粤语流行 | new 10-08 |
| 22 | J-pop / Anime | 日系 / 动漫 | new 10-08 |
| 23 | Soundtrack | 原声配乐 | new 10-08 |
| 24 | Worship / Gospel | 敬拜 / 福音 | new 10-08 |
| # | Name | 中文 | Since |
|---|---|---|---|
| 1 | Chill | 放松 | 000256 |
| 2 | Sleep | 助眠 | 000256 |
| 3 | Party | 派对 | 000256 |
| 4 | Focus | 专注 | 000256 |
| 5 | Workout | 运动 | 000256 |
| 6 | Romance | 浪漫 | 000256 |
| 7 | Feel Good | 好心情 | 000256 |
| 8 | Sad | 伤感 | 000256 |
| 9 | Energetic | 活力 | new 10-08 |
| 10 | Nostalgic | 怀旧 | new 10-08 |
| 11 | Dreamy | 梦幻 | new 10-08 |
| 12 | Dark | 暗黑 | new 10-08 |
Each tile can have up to 20 aliases: other words that mean the same thing (guofeng, guzheng, erhu → Chinese Traditional; lofi, chillhop → Lo-fi; ost, film score → Soundtrack). They are used by search in the sheet and by Magic Pen / "Suggested" to match style text. Aliases only have to be unique within the same kind, so the genre Lo-fi and the mood Chill can both know "lofi". 000535 also moved cantopop off Mandopop and soul off R&B onto their new tiles.
admin_audit_log (area music, type music_tag, action create / update / hide / unhide / reorder) in the same transaction. Browse has no cache, so changes show on the next load.backend/migrations/000256_music_tags.up.sql, 000535_music_tags_admin.up.sql · backend/internal/domain/music/tags_admin.go (GET/POST /v1/music/admin/tags, PATCH /v1/music/admin/tags/{slug}, POST /v1/music/admin/tags/reorder) · app/lib/screens/music_tags_admin_screen.dart. Tests: tags_admin_1008_db_test.go, music_tags_admin_1008_test.dart.
| Call | Used by | Gives |
|---|---|---|
GET /v1/music/browse?listener=1 | Browse page, home category pills | No hidden tiles, and no genre / mood with no published song (browseMinSongs = 1; was 3 until 10-08 15:30). Special tiles always. |
GET /v1/music/browse (or ?all=1) | Compose / Edit / Upload, diary music picker, old phone builds | Every tile that isn't hidden, including new ones with 0 songs, plus aliases. |
Why the default is the full catalog: old phone builds call Compose's catalog without any parameter. If the default were narrowed, they couldn't pick new genres and could get stuck unable to compose. And Compose must offer 0-song tiles, otherwise a new genre could never reach its 3rd song.
cover_url (new server field, one query): the most-played published song with a cover in that tile (ties → newest). Music = most played overall; New Releases = newest in 60 days; no cover → a music-note square.backend/internal/domain/music/browse.go (browseMinSongs, browseCoverSQL) · app/lib/screens/music_browse_screen.dart (BrowseTile). Tests: tags_admin_1008_db_test.go, browse_cover_1008_db_test.go, browse_golden_test.dart.
On https://hoop-web.pages.dev (refresh first). Music Studio → New song unless stated.
| # | Do | Expected |
|---|---|---|
| 1 | Create: open New song, write nothing. | Card "Where listeners find this song": Main genre Required, More genres 0/2, Mood 0/3. Compose is grey with "Pick a main genre to compose". |
| 2 | Tap "Save draft" without a genre. | The draft saves; no genre error. |
| 3 | Tap "Choose a main genre", search "guofeng". | Chinese Traditional is found (alias). Tap it: sheet closes, it shows as main, Compose turns active. |
| 4 | More genres → Add: pick 2, then tap a 3rd. | 3rd refused with "You can pick up to 2 more genres — remove one first"; nothing changes. The main genre tile says "Main" and can't be tapped. |
| 5 | Mood → Add: pick 3, then a 4th. | 4th refused with "You can pick up to 3 moods — remove one first". |
| 6 | Remove a middle genre pill, then add it back. | Pills always in catalog order; it returns to the same spot. |
| 7 | Magic Pen case 1: new song, style empty, nothing picked, tap the pen. | Style is written; main genre + up to 2 genres + up to 3 moods picked; "Picked by Magic Pen…" line on the card. |
| 8 | Case 2: new song, pick Rock only, style empty, tap the pen. | Style is about rock; Rock stays main; only empty groups (more genres / moods) are filled. |
| 9 | Case 3: type your own style, pick Pop, tap the pen. | Your text is polished; Pop and nothing else stays picked. Repeat with nothing picked: the pen now picks categories. |
| 10 | Case 4: with style text and picks, tap the style box's clear (trash) button, then Undo. | Text and all categories clear, toast "Style and genres cleared"; Undo restores both. |
| 11 | "+ Add a style" → open each tab. | The old Genre tab is called Sound and has only instrument / sound words; tapping words adds them to the style text but never picks a genre. |
| 12 | Compose with main genre + moods. | The song generates. (The AI receives "Genre: … Mood: …" in front of the style; not visible in the UI.) |
| 13 | Edit an existing song: change only the main genre, save, reopen. | The new main genre is shown; the genre pill in the song list updates. |
| 14 | Open a draft with no genre → tap "+Genre". | Edit opens at the Style card with the Main genre sheet open. With a main genre already: the More genres sheet opens. |
| 15 | Two accounts: B is an accepted collaborator on A's draft; B opens it. | B sees no "+Genre" chip. |
| 16 | Upload a song: try without a main genre, then with. | Upload grey until a main genre is picked; same sheet and limits. |
| 17 | Admin account: Developer Centre → Genres & moods. | Genres / Moods / Suggestions tabs; live song counts, aliases, "Not in Browse yet" on tiles with no live song. Non-admin: page not available. |
| 18 | Admin: add a test genre with an alias; rename it; change colour; move it up; hide it; unhide it. | Each change sticks after reload. While hidden it can't be picked in Compose. There is no Delete. Each action appears in the admin audit log. |
| 19 | Admin: Suggestions → "Add as genre" on one, "Dismiss" on another. | Add opens with the name filled; after saving, the suggestion leaves the list. Dismissed one leaves the list. |
| 20 | Listener: HOOP Music → Browse all. | Music + New Releases wide on top, then Genres, then Moods. No song counts. Covers tilted bottom-right (or a note icon). |
| 21 | 1-song rule: compare the new genre from step 18 (0 songs) in Compose vs Browse. | Pickable in Compose, absent from Browse. As soon as one published song has it, it shows in Browse. |
| 22 | Cover: note the most-played published song in a genre with a cover. | That song's cover is the one on the tile. |
| Time (MYT) | Haikal | Result |
|---|---|---|
| 10:44 | Request: redesign the genre module (Spotify-like), align with Browse, revisit limits; Create, Edit and draft "+Genre". Discuss first. | Audit + proposal sent: main genre + 2 more, up to 3 moods, search sheet with Browse tiles, tags sent to the AI, new genres for Haikal to choose. |
| 12:02 · 12:24 · 12:36 | Answers to the proposal questions: the new genres / moods to add, admin page in Developer Centre, Browse hides tiles under 3 live songs; 12:24 "build now" for the admin page and new tags; Magic Pen still being settled (four cases proposed). | Admin page + 000535 built and shipped in v3.202. |
| 13:38 | Approved the Compose redesign (designs incl. Magic Pen four cases). | Built and shipped in v3.213. |
| 13:41 | Browse redesign in scope for this session too. | New Browse all with cover_url, in v3.213. |
| 14:08 | "Your top genres" later. | Not built; listed under Future. |
| 15:27 · 15:30 | Haikal on web live: only Pop showed, and the old "Mood" tile appeared. Cause: 64 of 68 live songs had no tags. Decision: show a tile from 1 live song (counts are hidden anyway); dry-run auto-tagging of the 64 untagged songs first. | Threshold 1 shipped; backfill waits for the dry-run list OK. |
| 16:01 | Haikal approved the backfill after reading the dry-run list. | One transaction on prod: 226 tags on 59 of the 64 untagged live songs (main genre = the one mentioned first in the style text; songs.genre set where empty on 49). Mandopop aliases + mandarin, chinese song, 中文, 普通话. 5 songs got nothing (uploads / short Chinese text). Undo list: table _bak_20261008_tag_backfill; audit log area music, action backfill. Browse went from 6 tiles to 19 genres + 11 moods. |