Files
MusicServer/internal/backend/covers_integration_test.go
SansanandClaude Opus 5.5 bb8de5548d
CI / docker (push) Failing after 22s
CI / integration (push) Successful in 26s
CI / test (push) Successful in 26s
CI / publish (push) Skipped
Add alternative names and cover art for soundtracks
Alternative names (#32):
- PUT /api/v1/soundtracks/{uuid}/alternative-names replaces a soundtrack's
  alternative names (trimmed, deduplicated, without the soundtrack's own name).
- alternative_names is in the soundtrack and statistics responses.
- /find matches them too and shows which name matched.

Covers (#12), stored in COVERS_PATH and off when it is not set:
- GET, PUT (upload) and DELETE /api/v1/soundtracks/{uuid}/cover, and
  PUT .../cover/source to pick a SteamGridDB game or AniList anime by ID.
  Uploaded and picked covers are never replaced automatically.
- With STEAMGRIDDB_API_KEY, a background job searches SteamGridDB, then
  AniList, by name, alternative names and the name without bracketed parts,
  at startup, after a sync and after names change. Search results are ranked
  by matching words, so "Civilization IV" gets "Sid Meier's Civilization IV".
- cover_modified (Unix seconds) is in the soundtrack and statistics responses,
  so clients can cache covers.

The new columns are added to migration 000006, which is not released yet.

Closes #12, closes #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:57:18 +02:00

99 lines
3.4 KiB
Go

//go:build integration
package backend
import (
"context"
"path/filepath"
"testing"
"time"
"music-server/internal/db/repository"
"music-server/internal/testutil/pgtest"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAlternativeNamesAndCoversWithPostgres checks the alternative name and cover queries
// against a real database.
func TestAlternativeNamesAndCoversWithPostgres(t *testing.T) {
ctx := context.Background()
q := repository.New(pgtest.NewMigratedDatabase(t).Pool)
s := NewSyncService(q, copyTestMusic(t), nil)
require.True(t, s.Start(false))
s.Wait()
library := NewLibraryService(q)
soundtracks, err := library.Soundtracks(ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, len(soundtracks), 2)
first, second := soundtracks[0], soundtracks[1]
assert.Equal(t, []string{}, first.AlternativeNames)
assert.Nil(t, first.CoverModified)
// Alternative names, in every soundtrack response.
updated, err := library.SetAlternativeNames(ctx, first.Uuid, []string{" Alt ", "alt", first.Name, "Другое"})
require.NoError(t, err)
assert.Equal(t, []string{"Alt", "Другое"}, updated.AlternativeNames)
stats, err := NewStatisticsService(q).AllSoundtracks(ctx)
require.NoError(t, err)
assert.Equal(t, []string{"Alt", "Другое"}, stats[0].AlternativeNames)
never, err := NewStatisticsService(q).NeverPlayedSoundtracks(ctx)
require.NoError(t, err)
assert.Equal(t, []string{"Alt", "Другое"}, never[0].AlternativeNames)
search, err := NewMusicService(q, t.TempDir()).SearchSoundtracks(ctx)
require.NoError(t, err)
assert.Equal(t, []string{"Alt", "Другое"}, search[0].AlternativeNames)
// Covers.
c, err := NewCoverService(q, filepath.Join(t.TempDir(), "covers"), CoverOptions{})
require.NoError(t, err)
require.NoError(t, c.Upload(ctx, first.Uuid, pngBytes(t)))
withCover, err := library.Soundtrack(ctx, first.Uuid)
require.NoError(t, err)
require.NotNil(t, withCover.CoverModified)
stats, err = NewStatisticsService(q).AllSoundtracks(ctx)
require.NoError(t, err)
assert.Equal(t, withCover.CoverModified, stats[0].CoverModified)
needing, err := q.ListSoundtracksNeedingCover(ctx, nowPlus(0))
require.NoError(t, err)
for _, row := range needing {
assert.NotEqual(t, first.Uuid, row.Uuid, "a soundtrack with a cover is not searched")
}
require.NoError(t, q.MarkCoverChecked(ctx, second.Uuid))
needing, err = q.ListSoundtracksNeedingCover(ctx, nowPlus(-60))
require.NoError(t, err)
for _, row := range needing {
assert.NotEqual(t, second.Uuid, row.Uuid, "searched recently")
}
// New names make a soundtrack without a cover searched again.
_, err = library.SetAlternativeNames(ctx, second.Uuid, []string{"Other"})
require.NoError(t, err)
needing, err = q.ListSoundtracksNeedingCover(ctx, nowPlus(-60))
require.NoError(t, err)
var found bool
for _, row := range needing {
if row.Uuid == second.Uuid {
found = true
assert.Equal(t, []string{"Other"}, row.AlternativeNames)
}
}
assert.True(t, found)
files, err := q.ListCoverFiles(ctx)
require.NoError(t, err)
assert.Equal(t, []string{first.Uuid.String() + ".png"}, files)
require.NoError(t, c.Remove(ctx, first.Uuid))
files, err = q.ListCoverFiles(ctx)
require.NoError(t, err)
assert.Empty(t, files)
}
func nowPlus(seconds int) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: time.Now().Add(time.Duration(seconds) * time.Second), Valid: true}
}