package uploads import ( "context" "github.com/google/uuid" "github.com/stretchr/testify/require" "pipu/api/internal/auth" "pipu/api/internal/library" "pipu/api/internal/musicbrainz" "testing" ) func TestReleaseOperationsRequireReviewPermission(t *testing.T) { s := &Service{} for _, ctx := range []context.Context{context.Background(), auth.WithPrincipal(context.Background(), auth.Principal{UserID: uuid.New(), Role: auth.RoleAdmin})} { _, err := s.GetReleaseReport(ctx, uuid.New()) require.Error(t, err) _, err = s.SearchReleases(ctx, uuid.New(), musicbrainz.Query{}) require.Error(t, err) _, err = s.ReleaseCover(ctx, uuid.New(), uuid.NewString()) require.Error(t, err) _, err = s.ApplyRelease(ctx, uuid.New(), uuid.NewString(), true, true) require.Error(t, err) } } func releaseFile(title string, number, disc, duration int) File { return File{ID: uuid.New(), Status: StatusPendingReview, Meta: &Meta{Artist: "Artist", AlbumArtist: "Artist", Album: "Album", Title: title, Track: &number, Disc: &disc}, DurationMs: &duration} } func TestReleaseCompletenessUsesTracksAndEditions(t *testing.T) { r := musicbrainz.Release{ID: uuid.NewString(), Artist: "Artist", Type: "Single", Tracks: []musicbrainz.Track{ {Title: "One", Artist: "Artist", Disc: 1, Position: 1, DurationMs: 180000}, {Title: "Two", Artist: "Artist", Disc: 2, Position: 1, DurationMs: 200000}, }} files := []File{releaseFile("One", 1, 1, 180100), releaseFile("Two", 1, 2, 199000)} c := matchRelease(files, r) require.True(t, c.Complete) require.Equal(t, 2, c.Matched) require.Equal(t, "high", c.Confidence) // Two encodings of the same song cover one catalogue slot, never two. c = matchRelease([]File{files[0], releaseFile("One", 1, 1, 180000)}, r) require.False(t, c.Complete) require.Equal(t, 1, c.Matched) require.Len(t, c.Missing, 1) // A deluxe edition with another track must remain incomplete. deluxe := r deluxe.Tracks = append(append([]musicbrainz.Track{}, r.Tracks...), musicbrainz.Track{Title: "Bonus", Artist: "Artist", Disc: 2, Position: 2, DurationMs: 220000}) require.False(t, matchRelease(files, deluxe).Complete) // A one-track upload does not turn the two-track single into a full single. require.False(t, matchRelease(files[:1], r).Complete) } func TestReleaseMatchingDoesNotClaimAmbiguousOrWeakMatchesAreComplete(t *testing.T) { r := musicbrainz.Release{Artist: "Artist", Tracks: []musicbrainz.Track{{Title: "Intro", Artist: "Artist", Disc: 1, Position: 1, DurationMs: 12000}, {Title: "Intro", Artist: "Artist", Disc: 2, Position: 1, DurationMs: 12000}}} c := matchRelease([]File{releaseFile("Intro", 1, 1, 12000)}, r) require.False(t, c.Complete) require.Len(t, c.Unmatched, 1) r.Tracks = r.Tracks[:1] f := releaseFile("Untitled", 1, 1, 12000) c = matchRelease([]File{f}, r) require.False(t, c.Complete) require.Empty(t, c.Matches, "number and duration alone do not identify a differently titled recording") f.Meta.Title = "Intro" f.Meta.Artist = "Someone Else" f.Meta.AlbumArtist = "Someone Else" require.False(t, matchRelease([]File{f}, r).Complete) f = releaseFile("Intro", 1, 1, 12000) f.DurationMs = nil require.False(t, matchRelease([]File{f}, r).Complete) } func TestReleaseQueryPrioritizesIDsAndRejectsMixedAlbums(t *testing.T) { a, b := releaseFile("One", 1, 1, 180000), releaseFile("Two", 2, 1, 180000) b.Meta.Album = "Another Album" _, ok := batchReleaseQuery([]File{a, b}) require.False(t, ok) id := uuid.NewString() a.Tags = map[string]string{"musicbrainz_albumid": id} a.Meta.Album = library.UnknownAlbum q, ok := batchReleaseQuery([]File{a, b}) require.True(t, ok) require.Equal(t, id, q.ReleaseID) b.Tags = map[string]string{"musicbrainz_albumid": uuid.NewString()} _, ok = batchReleaseQuery([]File{a, b}) require.False(t, ok) } func TestReleaseFingerprintInvalidatesEditsButNotApproval(t *testing.T) { f := releaseFile("One", 1, 1, 180000) files := []File{f} hash := releaseHash(files) files[0].Status = StatusApproved require.Equal(t, hash, releaseHash(files)) files[0].Meta.Title = "Changed" require.NotEqual(t, hash, releaseHash(files)) files[0].Status = StatusWithdrawn require.NotEqual(t, hash, releaseHash(files)) } func TestReleaseMatchesOldBloodCreditsWithoutClaimingVerifiedCompleteness(t *testing.T) { tracks := []struct { title, uploaded, guest string seconds int }{ {"Введение", "Введение", "", 34}, {"Ни много ни мало", "Ни много ни мало (Prod. Blacksurfer)", "", 143}, {"MOONWORK", "MOONWORK (feat. Basic Boy) (Prod. SP4K)", "Basic Boy", 214}, {"Angry Toy$", "Angry toy$ (Prod. Ray Qwa)", "", 127}, {"Возраст", "Возраст (feat. Artur Kreem) (Prod. Babyboi)", "Artur Kreem", 138}, {"OLD BLOOD", "OLD BLOOD (Prod. Chivaz)", "", 138}, {"Умы", "Умы (feat. DopeVvs) (Prod. MRKBVNGXR)", "DopeVvs", 125}, {"NO HOOK NO HOES", "NO HOOK NO HOES (feat. Yung Hurn) (Prod. Pretty Scream)", "Yung Hurn", 133}, {"ART HOES", "ART HOES (Prod. DavidRema)", "", 183}, {"Friendly Fire", "Friendly Fire (Prod. Avepack)", "", 122}, {"X2", "X2 (Prod. Gredy)", "", 176}, {"4 шага", "4 шага (Prod. DavidRema)", "", 135}, } r := musicbrainz.Release{Artist: "Boulevard Depo", Title: "OLD BLOOD"} files := []File{} for i, track := range tracks { artist, uploadedArtist := "Boulevard Depo", "BOULEVARD DEPO" if track.guest != "" { artist += " feat. " + track.guest uploadedArtist += ", " + track.guest } r.Tracks = append(r.Tracks, musicbrainz.Track{Title: track.title, Artist: artist, Disc: 1, Position: i + 1, DurationMs: track.seconds * 1000}) f := releaseFile(track.uploaded, i+1, 1, track.seconds*1000+500) f.Meta.Artist, f.Meta.AlbumArtist, f.Meta.Album = uploadedArtist, "BOULEVARD DEPO", "OLD BLOOD" files = append([]File{f}, files...) } c := matchRelease(files, r) require.Equal(t, 12, c.Matched) require.Empty(t, c.Missing) require.Empty(t, c.Unmatched) require.False(t, c.Complete, "credit-normalized matches still require review") require.Equal(t, "medium", c.Confidence) for _, match := range c.Matches { require.Equal(t, files[len(files)-1-match.TrackIndex].ID, match.FileID) } } func TestReleaseCreditMatchingRequiresTitleArtistsPositionAndDuration(t *testing.T) { for _, change := range []string{"none", "different guest", "different title", "remix", "live", "instrumental", "disc", "position", "duration", "unknown duration"} { t.Run(change, func(t *testing.T) { f := releaseFile("Song (feat. Guest) (Prod. Producer)", 3, 1, 180500) f.Meta.Artist = "ARTIST; Guest" r := musicbrainz.Release{Artist: "Artist", Tracks: []musicbrainz.Track{{Title: "Song", Artist: "Artist feat. Guest", Disc: 1, Position: 3, DurationMs: 180000}}} switch change { case "different guest": f.Meta.Artist = "Artist, Someone Else" case "different title": f.Meta.Title = "Another song (Prod. Producer)" f.Meta.Artist = r.Tracks[0].Artist case "remix": f.Meta.Title = "Song (Remix) (feat. Guest)" f.Meta.Artist = r.Tracks[0].Artist case "live": f.Meta.Title = "Song [Live] (feat. Guest)" f.Meta.Artist = r.Tracks[0].Artist case "instrumental": f.Meta.Title = "Song (Instrumental) (feat. Guest)" f.Meta.Artist = r.Tracks[0].Artist case "disc": *f.Meta.Disc = 2 case "position": *f.Meta.Track = 4 case "duration": *f.DurationMs = 190000 case "unknown duration": f.DurationMs = nil } c := matchRelease([]File{f}, r) if change == "none" { require.Equal(t, 1, c.Matched) require.Equal(t, "medium", c.Matches[0].Confidence) } else { require.Zero(t, c.Matched) require.Equal(t, []uuid.UUID{f.ID}, c.Unmatched) } require.False(t, c.Complete) }) } }