feat(uploads): match release groups with MusicBrainz and AcoustID
ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
This commit is contained in:
1 parent
1e3a149aef
commit
39bf7d4b70
45 files changed
+4792
-417
No files matched your search
@@ -0,0 +1,153 @@
|
||||
//go:build integration
|
||||
|
||||
package uploads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"pipu/api/internal/auth"
|
||||
"pipu/api/internal/library"
|
||||
"pipu/api/internal/musicbrainz"
|
||||
"pipu/api/internal/platform/db"
|
||||
)
|
||||
|
||||
type groupedCatalogue map[string]musicbrainz.Release
|
||||
|
||||
func (c groupedCatalogue) Search(_ context.Context, q musicbrainz.Query) ([]musicbrainz.Release, error) {
|
||||
return []musicbrainz.Release{c[q.Album]}, nil
|
||||
}
|
||||
func (c groupedCatalogue) Cover(context.Context, musicbrainz.Release) (musicbrainz.Cover, error) {
|
||||
return musicbrainz.Cover{Data: []byte{0xff, 0xd8, 0xff, 1}, MIME: "image/jpeg", Exact: true}, nil
|
||||
}
|
||||
|
||||
func TestReleaseGroupsIsolateJobsMetadataAndArtwork(t *testing.T) {
|
||||
address := os.Getenv("TEST_DATABASE_URL")
|
||||
if address == "" {
|
||||
t.Skip("TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := db.Connect(ctx, address)
|
||||
require.NoError(t, err)
|
||||
defer pool.Close()
|
||||
require.NoError(t, db.Migrate(ctx, pool))
|
||||
author := uuid.New()
|
||||
_, err = pool.Exec(ctx, `INSERT INTO users(id,username,password_hash,role) VALUES($1,$2,'x','admin')`, author, "groups-"+author.String())
|
||||
require.NoError(t, err)
|
||||
source, err := library.NewStore(pool).SaveSource(ctx, library.Source{ID: uuid.New(), Name: "groups-test", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, AcceptsUploads: true, WriteTemplate: nullable("{albumartist}/{album}/{track:02} - {title}.{ext}")})
|
||||
require.NoError(t, err)
|
||||
store := NewStore(pool)
|
||||
sumA, sumB := sha256.Sum256([]byte(uuid.NewString())), sha256.Sum256([]byte(uuid.NewString()))
|
||||
cover := "covers/original.jpg"
|
||||
batch, reserved, err := store.CreateBatch(ctx, author, nil, []NewFile{{ClientID: "a", Name: "a.flac", Size: 10, SHA256: sumA[:]}, {ClientID: "b", Name: "b.flac", Size: 10, SHA256: sumB[:]}}, DefaultSettings, &cover)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM upload_batches WHERE id=$1`, batch)
|
||||
_ = store.ReconcileQuota(ctx)
|
||||
}()
|
||||
_, err = pool.Exec(ctx, `UPDATE upload_release_jobs SET state='not_found' WHERE batch_id<>$1`, batch)
|
||||
require.NoError(t, err)
|
||||
for i, f := range reserved {
|
||||
m := Meta{Artist: "Artist", AlbumArtist: "Artist", Album: []string{"Album A", "Album B"}[i], Title: "Song"}
|
||||
raw, _ := json.Marshal(m)
|
||||
_, err = pool.Exec(ctx, `UPDATE upload_files SET status='pending_review',metadata=$2,duration_ms=180000 WHERE id=$1`, f.FileID, raw)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
catalogue := groupedCatalogue{}
|
||||
for _, album := range []string{"Album A", "Album B"} {
|
||||
catalogue[album] = musicbrainz.Release{ID: uuid.NewString(), Title: album, Artist: "Artist", Type: "Album", Tracks: []musicbrainz.Track{{Title: "Song", Artist: "Artist", Disc: 1, Position: 1, DurationMs: 180000}}}
|
||||
}
|
||||
svc := NewService(store, &previewCatalog{source: source}, previewRoot(t.TempDir()), nil, nil, nil, library.FPCalc{}, Config{StagingDir: t.TempDir()}, slog.New(slog.DiscardHandler))
|
||||
svc.SetReleaseCatalogue(catalogue)
|
||||
review := auth.WithPrincipal(ctx, auth.Principal{UserID: author, Role: auth.RoleAdmin, ReviewsUploads: true})
|
||||
root, err := svc.GetReleaseReport(review, batch)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, root.Groups, 2)
|
||||
require.Equal(t, "needs_input", root.State)
|
||||
group := map[string]string{}
|
||||
for _, g := range root.Groups {
|
||||
group[g.Album] = g.ID
|
||||
require.False(t, g.HasCover, "ambiguous original batch artwork must not be copied to every album")
|
||||
}
|
||||
a, b := group["Album A"], group["Album B"]
|
||||
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{})
|
||||
require.ErrorContains(t, err, "release_group_required")
|
||||
_, err = svc.GetReleaseReport(review, batch, uuid.NewString())
|
||||
require.ErrorContains(t, err, "release_group_not_found")
|
||||
require.True(t, svc.lookupNextRelease(ctx))
|
||||
require.True(t, svc.lookupNextRelease(ctx))
|
||||
reportA, err := svc.GetReleaseReport(review, batch, a)
|
||||
require.NoError(t, err)
|
||||
require.True(t, reportA.Candidates[0].Complete)
|
||||
reportB, err := svc.GetReleaseReport(review, batch, b)
|
||||
require.NoError(t, err)
|
||||
require.True(t, reportB.Candidates[0].Complete)
|
||||
_, err = svc.ApplyRelease(review, batch, catalogue["Album B"].ID, true, false, a)
|
||||
require.ErrorContains(t, err, "release_not_found")
|
||||
used, _, err := store.Usage(ctx, author)
|
||||
require.NoError(t, err)
|
||||
_, err = svc.ApplyRelease(review, batch, catalogue["Album B"].ID, true, true, b)
|
||||
require.NoError(t, err)
|
||||
aFiles, err := store.AllInReleaseGroup(ctx, batch, a)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aFiles[0].CoverPath)
|
||||
require.Equal(t, "Album A", aFiles[0].Meta.Album)
|
||||
require.Empty(t, aFiles[0].Tags["musicbrainz_albumid"])
|
||||
bFiles, err := store.AllInReleaseGroup(ctx, batch, b)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, bFiles[0].CoverPath)
|
||||
require.Equal(t, catalogue["Album B"].ID, bFiles[0].Tags["musicbrainz_albumid"])
|
||||
kept, err := store.StagingInUse(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, kept[*bFiles[0].CoverPath])
|
||||
afterA, err := svc.GetReleaseReport(review, batch, a)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, reportA.InputHash, afterA.InputHash)
|
||||
require.Nil(t, afterA.SelectedRelease)
|
||||
newUsed, _, err := store.Usage(ctx, author)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, used, newUsed)
|
||||
// A job lease for A survives a metadata edit in B.
|
||||
_, err = svc.SearchReleases(review, batch, musicbrainz.Query{GroupID: a})
|
||||
require.NoError(t, err)
|
||||
job, err := store.ClaimRelease(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a, job.GroupKey)
|
||||
edited := *bFiles[0].Meta
|
||||
edited.Title = "Edited B"
|
||||
require.NoError(t, store.UpdateMetadata(ctx, bFiles[0].ID, edited, true, placement{}))
|
||||
require.NoError(t, store.SaveRelease(ctx, job, reportA))
|
||||
afterA, err = store.ReleaseReport(ctx, batch, a)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ready", afterA.State)
|
||||
afterB, err := store.ReleaseReport(ctx, batch, b)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "queued", afterB.State)
|
||||
require.NoError(t, store.EnsureReleaseGroups(ctx, batch))
|
||||
groups, err := store.ReleaseGroups(ctx, batch)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, groups, 2)
|
||||
// Explicit whole-batch cover editing still affects every subgroup.
|
||||
shared := "covers/shared.jpg"
|
||||
require.NoError(t, store.SetCover(ctx, batch, &shared))
|
||||
aFiles, err = store.AllInReleaseGroup(ctx, batch, a)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, shared, *aFiles[0].CoverPath)
|
||||
bFiles, err = store.AllInReleaseGroup(ctx, batch, b)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, shared, *bFiles[0].CoverPath)
|
||||
require.NoError(t, store.ChangeStatus(ctx, bFiles[0].ID, []Status{StatusPendingReview}, StatusWithdrawn))
|
||||
unused, err := store.UnusedReleaseCovers(ctx, batch)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, unused, "shared artwork is still needed by group A")
|
||||
require.NoError(t, store.ChangeStatus(ctx, aFiles[0].ID, []Status{StatusPendingReview}, StatusWithdrawn))
|
||||
unused, err = store.UnusedReleaseCovers(ctx, batch)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, unused, shared)
|
||||
}
|
||||
Reference in new issue
Block a user