v3.247, plus the 10-08 approval fix). The source of truth stays the code and docs/music.md §3.2; file references are in grey under each section.A release is the unit that goes live. Its type comes from the track count: single = 1, EP = 2–6, album = 7+; the creator can also tick compilation (it can't be worked out from the count). While a release has never gone out, the type is just calculated; the first time it is published or sent to review the type is written down and fixed (the compilation tick too), and adding tracks later never changes it. Limits after that: a single takes no more tracks, an EP holds at most 6, an album / compilation 50; a draft release takes up to 50. Name 1–60 characters, description up to 200.
One recording can be on many releases (a single first, later an EP or album). It stays one song: one audio (locked once), one play count, one tips pool, one split. Publishing a release never changes a song that is already live. Only the release owner can add tracks, and only their own songs; taken-down songs can't be added.
| When | What |
|---|---|
| 10-08 server, live 16:36 | Admin approval of a release in review now runs the same go-live steps as publishing directly (goLiveRelease): before, approval only flipped the release to published, so its draft tracks stayed draft and listeners saw an incomplete release. |
| v3.247 server + app, 10-09 | The seven release rules (Haikal QC 10-08 / 10-09): own songs only; all tracks' splits frozen on publish, no new collaborator round; live songs never trigger review again; taken-down warnings on publish and on song takedown; picker "Already released" section, "Also add to release", one-step "Move to release"; cover / badge follow the first release. No migration. |
| Where to test | Server rules are live now. The web version hoop-web.pages.dev shows the v3.247 screens once the web deploy has run (it may lag); phones get them with the next TestFlight build. |
backend/internal/domain/music/release.go (releaseKind, kindFor, trackCap, canAddTrack) · release_api.go (createRelease, editRelease: compilation tick only while kind='') · CHANGELOG v3.247 and "管理员放行待审的发行物" (10-08)
There is no "rejected" state: a rejected release goes back to draft. Only a release that was never published can be deleted.
Same four states as on music-takedown. A song's state is its own: being on a release doesn't give it a second state. Per release a track can also be "taken down on this release only" (removed_at); listeners then don't see it there.
| Song is… | Release is published (directly or approved) | Release goes to review | Listeners see the track on a published release? |
|---|---|---|---|
| Draft | Becomes published (audio locked, split frozen) | Stays draft; audio locked, split frozen | Yes, once it is published |
| In review (its own single-song review) | Untouched: stays in review, its own review goes on | Untouched | No, until its own review approves it |
| Published (already live) | Untouched: same audio, same state; split frozen if it wasn't yet | Untouched | Yes |
| Removed (taken down) | Untouched: never brought back by a publish (except the songs taken down with this release, see ⑦) | Untouched | No |
For listeners a release is a door: not published → not found; published but no live track left → not found too (the owner can still open it); otherwise it shows only its live tracks, numbered without gaps.
migration 000216 (releases.status: draft | review | published | removed) · release.go publishTracksSQL (only draft is promoted) · release_api.go hallReleaseDetail, creatorReleases · release_admin.go hallReleases (releaseLiveTracksExpr > 0)
| Rule | How it works |
|---|---|
| Who | Only the release owner, and only their own songs. A song where they are only a collaborator is refused (403) and the picker doesn't list it. |
| Which songs | Draft, In review and Live songs can be added. Taken-down songs can't: 409 song_taken_down "This song has been taken down, so it can't be added to a release — restore it first, then add it". |
| Room | Checked after the song: a published single → 409 cap_reached "A single is closed once it's released — start an EP instead"; a full EP (6) → "An EP holds 6 tracks and this one is full — start an album for more"; otherwise 50. |
| Picker | "Add my songs" lists drafts and songs in review first; live songs go under a small Already released heading, with the chip "Live · also on <first release>" (just "Live" if it isn't on any release yet). Songs already on this release are not listed. |
| Live song: Also add to release | On a live song's page in Studio the release line opens "Also add to release": "It stays on the release it's already on — this only adds it to one more." It never takes the song off its current release. A "+ Add to another release" line is always there under it. |
| Draft song: Move to release | Drafts keep "Move to release". It is now one server step (move_from): add to the new release and take out of the old one in one transaction. If the old one is already published the whole move is refused (409 published) and neither release changes. Before, the app made two calls and a failure could leave the song on both. |
| Taking a track out | Only while the release is not published (draft / review / removed). On a published release: "This release is already out, so tracks can't be pulled from it — take this one song down instead". |
| Publish panel on a song | "Publish" on a song offers As a single (goes live now, on its own), Add to a release (goes live when that release does), or Start a new release (saved as a draft). |
| "As a single" | Creates a single release and publishes it in one transaction (type single, audio locked, split frozen, followers' feed). No cover check (the single uses the song's cover, and new AI songs get one after publishing). If the song is already on any release (even a draft one) it is refused: 409 already_in_release "This song is already on an EP or album — release that instead". An uploaded song doesn't get a release here: it goes to single-song review instead (see ⑤). |
release_api.go addTrack (owner → song owner + state → cap → move), canAddSongToRelease, removeTrack · release_move.go moveTrack · song_releases.go canRemoveTrack · publish_single.go publishAsSingle · app: music_albums_screen.dart (canAddSongToRelease, ReleaseSongPickerList, releasePickerLiveLabel) · music_studio_screen.dart (_alsoAddToRelease, _moveSongToRelease)
cannot_publish "This release has no tracks yet".cannot_publish "A track still has no audio — finish it before you release".cannot_publish "It still needs cover art…".invite_pending), a split is set (409 split_required) and everyone has agreed to it (409 split_unagreed). The refusal names the song (song) and who it waits for (who). Live tracks are not re-asked: their collaborators already agreed to that song's split.Only a draft uploaded track that would go live with this release sends the whole release to review. AI songs, songs already live (even uploaded ones, already reviewed), songs in their own review and taken-down songs don't count. The whole release waits together: never "the others go first". The app then says "It contains uploaded songs, so the whole release waits for review — it goes live automatically once approved".
goLiveRelease does (one transaction)published (or review); the first publish date is kept forever.After it commits: freezes the split of every track still on the release (not only the new drafts; already-frozen ones are unchanged; taken-down tracks are skipped), for both publish and review. Followers are notified only the first time the release really goes live (an EP / album sends a notice; a single only goes to the feed); adding tracks and publishing again, or going to review, notifies nobody.
Publishing is not blocked by a taken-down track (song taken down, or taken down on this release only); it simply won't be on the release. The confirm dialog lists each one: "⚠️ “X” is taken down and won't be on this release", and the count in "the audio of these N songs is locked" only counts tracks that go out (one track: "this song"). The publish answer repeats the list in taken_down.
release_api.go publishDecision, goLiveRelease, publishRelease · release.go releaseTrackStateSQL, releaseSplitGate, releaseFreezeSQL, lockTracksSQL, publishTracksSQL, releaseTakenDownSQL · handler.go publishSplitGate · split.go lockSplitOnPublish · release_notify.go · app: music_albums_screen.dart releasePublishMessage
| Admin action | What happens |
|---|---|
ApprovePOST /v1/music/admin/releases/{id}/approve | Runs the same goLiveRelease as a direct publish (fixed 10-08): all checks again, lock new audio, promote draft tracks (history says approved by the admin), release published, freeze splits, notify followers if it is the first time. If anything changed since it was sent (a track added later has no audio, a collaborator hasn't accepted or agreed) the whole approval is refused with the same codes (400 cannot_publish, 409 invite_pending / split_*) and nothing is written. A track taken down by a reviewer meanwhile stays down; the rest goes live. Not in review any more (already rejected, or approved by someone else first): 404 "No such release is waiting for review". |
RejectPOST …/reject | The release goes back to draft; tracks are untouched (audio stays locked, splits stay frozen). A note is optional (empty body is fine). Not in review: 404. The type written at submit stays written. |
While in review the creator can still add tracks; the approval checks them.
An uploaded song published on its own (old "Publish", or "As a single") does not get a release: the song goes to review (split frozen). Admin approve sets the song to published (no release is created); reject sets it back to draft with a reason (default "Didn't pass review"). Publishing again while in review: 409 already_in_review. This path was not changed in 10-08 / v3.247.
release_admin.go adminApproveRelease, adminRejectRelease · release_api.go goLiveRelease (approving, FOR UPDATE + status check) · moderation.go adminApprove, adminReject · handler.go publish · test release_approve_1008_db_test.go
| Thing | Rule |
|---|---|
| Plays, likes, tips | Per song, combined: a play from the single and a play from the album count on the same song; tips go to the same pool. |
| Split | Per song: the same split on every release, frozen once. Adding it to a new release never asks collaborators again. |
| Audio | One locked version for the song, the same on every release. |
| Creator page: Top songs | The song shows once. |
| Creator page: Discography | Both releases are listed (each with its own live-track count). |
| Song cover, type badge, "from …" release | Follow the release it was first added to (by time added; that can be a draft release). A single later put on an album keeps the single's face and the Single badge. The picker's "Live · also on X" names the same release. |
| Situation | Result |
|---|---|
| Take down the live single while it is also on a draft album | The take-down dialog adds "It's also on your draft release “Y”; it won't appear there either." The song is down. Publishing the album: warning "“X” is taken down and won't be on this release"; the album goes out without it. |
| Song on two published releases; take down one release | The release goes down; the song stays live (it's still on the other live release). Discography lists only the other one. |
| Same, but the other release is already down (or draft) | The song goes down with this release (as the creator's own takedown, no strike). |
| Republish the taken-down release | The songs that went down with it come back; a song taken down separately stays down. |
| Delete a draft release that holds a live single | Only the draft shell goes; the song and its single are unchanged. |
| Delete (purge) a song that was ever on a published release | Blocked (in_release / was_published): take it down instead. |
| Draft song on a draft EP, published "As a single" | Refused 409 already_in_release: publish the EP, or move it out first. |
handler.go coverImgExpr, kindExpr, mine (first_release: release_tracks.added_at ASC LIMIT 1) · creator_songs.go · release_api.go creatorReleases · release_admin.go releaseTakedownSongsSQL · docs/music.md §3.2 rule 7
| Action | Rule |
|---|---|
| Take down a release (owner) | Release → removed, and in the same transaction its tracks go down as the creator's own takedown (no strikes), but only tracks that are: the owner's own (not others' songs on a compilation), live or in review (drafts and already-removed songs untouched; a reviewer's reason is never overwritten), and not on another release that is still published. Each gets a history line "taken down with release <id>". Pressing it again is a no-op. |
| Publish it again | Runs the normal publish (all checks). It brings back only the songs whose latest takedown was "with this release", each to the state it had before. Songs the creator took down on their own stay down. Followers are not notified again. |
| Take down one song | The release stays; listeners see only its live tracks; when none is left the release disappears for listeners. Tracks can't be pulled out of a published release, only taken down. |
| Delete a release | Only if it was never published (first publish date empty and state draft); else 409 published. Deleting removes only the release: its songs stay in Studio as unattached songs. |
| Delete a song | Only Draft and In review songs that were never published, no tips, not removed by a reviewer, no open report. A draft on a draft / in-review release can be deleted (the dialog says it will be removed from that release). Full rules: music-takedown · QC 10-07. |
release_admin.go takedownRelease, releaseTakedownSongsSQL, bringBackReleaseSongs, deleteDraftRelease · restore.go restoreTarget · purge.go purgeBlocker · app: music_studio_screen.dart takedownReleaseNote (ms_td_also_on_draft)
| # | Case | Expected result |
|---|---|---|
| 1 | A live AI single "Sunday Light" is added to a new EP with 2 new AI draft songs; the EP has a cover and is published. | Published directly (no review). EP lists 3 tracks. The 2 drafts go live, audio locked. "Sunday Light" unchanged (same audio, plays, state); its single is still live; its cover / badge stay the single's. Followers notified once. |
| 2 | An EP with 1 AI draft + 1 new uploaded draft is published. | EP → review; both tracks stay draft (audio locked, splits frozen). Admin approves → EP published, both tracks live, history says approved in review, followers notified. |
| 3 | Same as 2, but the admin rejects. | EP back to draft; both tracks untouched (still draft). The creator can fix it and publish again → review again. |
| 4 | An EP with an uploaded song that is already live (reviewed earlier) + 2 AI drafts is published. | Published directly, no review (only new uploaded drafts trigger review). |
| 5 | A draft track has a collaborator who hasn't accepted the invite; publish. | Refused 409 invite_pending naming the song; nothing changes. Same with a split not set (split_required) or not agreed (split_unagreed). |
| 6 | A live song with a collaborator, split never frozen (old song), is added to a new album and the album is published. | No new question to the collaborator; the song's split is frozen at that moment. |
| 7 | A track on a draft EP is taken down before publish. | Confirm dialog: "⚠️ “X” is taken down and won't be on this release". Published without it; listeners see the other tracks. |
| 8 | An EP sent to review; the creator then adds a draft track that has no audio; admin approves. | Approval refused 400 cannot_publish; EP stays in review, nothing written. |
| 9 | An admin presses Approve on an EP that another admin already rejected. | 404 "No such release is waiting for review". |
| 10 | A draft song is moved from draft EP A to draft EP B ("Move to release"). | One step: on B, gone from A. If A were published: refused, still only on A. |
| 11 | A song is on a published single and a published album; the album is taken down. | Album gone; song stays live via the single. Discography shows only the single. |
| 12 | A taken-down album is published again; one of its songs was also taken down separately before. | Album live again with the songs that went down with it; the separately taken-down song stays down. |
| 13 | Add a taken-down song, or add to a published single. | 409 song_taken_down / 409 cap_reached. |
Real-database tests: release_rules_1009_db_test.go, release_approve_1008_db_test.go, release_split_gate_1002_db_test.go, release_takedown_songs_0929_db_test.go, release_add_track_status_0924_db_test.go · app: release_rules_1009_test.dart
On hoop-web.pages.dev (refresh once). The server rules are live now; the v3.247 screens (Already released, Also add to release, warnings) appear on the web once the web deploy has run, and on phones after the next TestFlight build. A = creator, B = second account. Steps marked admin need a reviewer account, 2 accounts need B.
| # | How to test | What you should see |
|---|---|---|
| 1 | A: make an AI song and publish it "As a single". Studio → Releases. | A live Single with that song. Song page: "Release: <single>", "+ Add to another release". |
| 2 | A: new release "Test EP", add a cover. Add my songs. | Drafts / In review on top; the single's song under Already released as "Live · also on <single>". Songs where A is only a collaborator are not listed; taken-down songs are not listed. |
| 3 | A: add the live song + 2 new AI drafts to Test EP; Publish this release. | Published directly (no review). EP shows 3 tracks; the 2 drafts are now Live. The single is unchanged and still live, song cover / Single badge unchanged. |
| 4 | A: open the live song page → tap the release line. | "Also add to release" dialog: "It stays on the release it's already on — this only adds it to one more." Adding it to another draft keeps it on its current releases. |
| 5 | A: a draft song on draft EP 1 → its release line → Move to release → draft EP 2. | It is on EP 2 and no longer on EP 1 (one step). |
| 6 | A: try to move a draft out of a published release. | Greyed / "This release is published, so the song can't be moved out…"; nothing moves. |
| 7 | A: draft EP with 2 tracks; take one song down (it is also on this draft EP) from its song page. | Take-down dialog adds "It's also on your draft release “EP”; it won't appear there either." |
| 8 | A: publish that EP. | Confirm dialog: "⚠️ “X” is taken down and won't be on this release"; one track → "the audio of this song is locked". After publishing, listeners see 1 track. |
| 9 | 2 accounts A invites B on a draft song, B doesn't accept. A adds it to a draft EP and publishes. | Refused: "Someone hasn't accepted your invite yet…" naming the song; the EP stays draft. |
| 10 | 2 accounts B accepts, A sets 70/30, B agrees. Publish again. | Published; the split shows as final (frozen). |
| 11 | 2 accounts Add that live collaborated song to a new EP with one AI draft; publish. | Published directly; B is not asked again; split unchanged. |
| 12 | admin A: draft EP with one uploaded draft song + one AI draft; publish. | "It contains uploaded songs, so the whole release waits for review…"; EP In review, tracks still draft. |
| 13 | admin Admin approves that EP. | EP live with both tracks live (listeners see 2). The songs' history says published with the release (approved in review). |
| 14 | admin Repeat 12, admin rejects. | EP back to draft, tracks still draft and unchanged. |
| 15 | admin A: EP containing an uploaded song that is already live + one AI draft; publish. | Published directly, no review. |
| 16 | 2 accounts Song on a live single and the live EP from 3. A takes the EP down. B opens A's creator page. | The song is still live and plays; Discography lists the single only; Top songs shows the song once. |
| 17 | A publishes the EP again. | EP back; its drafts-turned-live are back; a song A took down on its own stays down. |
| 18 | A: delete a draft release with songs; then try to delete a published one. | Draft: deleted, songs stay in Studio. Published: refused, "…only taken down". |
| When (MYT) | What |
|---|---|
| 10-08 16:12 | Haikal's question: can a song that is already live be put on a new EP / album, and what happens to it? Rules 1–7 proposed in reply (this page, sections ③–⑦). |
| 10-08 16:18 | Haikal approves point 4: admin approval must run the same go-live steps as publishing. Fix live on the server at 16:36 (goLiveRelease shared by publish and approve). |
| 10-09 03:18 | Haikal approves points 1–3 and 5–7 and asks for this doc. |
| v3.247 10-09 | Points 1–3, 5–7 shipped: own songs only; all tracks' splits frozen; review only for new uploaded drafts; taken_down warnings; picker / Also add / atomic move; cover follows first release (written into docs/music.md §3.2). No migration. |
CHANGELOG.md v3.247 · "管理员放行待审的发行物 = 和直接发布同一套上架步骤" · v3.173 (takedown / delete) · commits e76abdee8, 4a754b330, 64986479d