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>
This commit is contained in:
1 parent
f79f51ea4c
commit
bb8de5548d
39 files changed
+2418
-99
No files matched your search
@@ -193,6 +193,93 @@ func (s *Store) GetSoundtrackByUUID(_ context.Context, id uuid.UUID) (repository
|
||||
return repository.Soundtrack{}, pgx.ErrNoRows
|
||||
}
|
||||
|
||||
func (s *Store) soundtrackByUUID(id uuid.UUID) int {
|
||||
return slices.IndexFunc(s.Soundtracks, func(st repository.Soundtrack) bool { return st.Uuid == id })
|
||||
}
|
||||
|
||||
func (s *Store) updateSoundtrackByUUID(id uuid.UUID, f func(*repository.Soundtrack)) error {
|
||||
defer s.lock()()
|
||||
if s.Err != nil {
|
||||
return s.Err
|
||||
}
|
||||
if i := s.soundtrackByUUID(id); i >= 0 {
|
||||
f(&s.Soundtracks[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) SetAlternativeNames(_ context.Context, arg repository.SetAlternativeNamesParams) (repository.Soundtrack, error) {
|
||||
defer s.lock()()
|
||||
if s.Err != nil {
|
||||
return repository.Soundtrack{}, s.Err
|
||||
}
|
||||
i := s.soundtrackByUUID(arg.Uuid)
|
||||
if i < 0 || s.Soundtracks[i].Deleted != nil {
|
||||
return repository.Soundtrack{}, pgx.ErrNoRows
|
||||
}
|
||||
st := &s.Soundtracks[i]
|
||||
st.AlternativeNames = slices.Clone(arg.Names)
|
||||
if st.CoverFile == nil {
|
||||
st.CoverChecked = pgtype.Timestamptz{}
|
||||
}
|
||||
return *st, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListSoundtracksNeedingCover(
|
||||
_ context.Context, checkedBefore pgtype.Timestamptz,
|
||||
) ([]repository.ListSoundtracksNeedingCoverRow, error) {
|
||||
defer s.lock()()
|
||||
if s.Err != nil {
|
||||
return nil, s.Err
|
||||
}
|
||||
var rows []repository.ListSoundtracksNeedingCoverRow
|
||||
for _, st := range sortedByName(slices.Clone(s.Soundtracks)) {
|
||||
if st.Deleted != nil || st.CoverFile != nil || st.CoverManual ||
|
||||
(st.CoverChecked.Valid && !st.CoverChecked.Time.Before(checkedBefore.Time)) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, repository.ListSoundtracksNeedingCoverRow{
|
||||
Uuid: st.Uuid, SoundtrackName: st.SoundtrackName, AlternativeNames: st.AlternativeNames,
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetCover(_ context.Context, arg repository.SetCoverParams) error {
|
||||
return s.updateSoundtrackByUUID(arg.Uuid, func(st *repository.Soundtrack) {
|
||||
now := pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
||||
st.CoverFile, st.CoverSource, st.CoverExternalID = arg.File, arg.Source, arg.ExternalID
|
||||
st.CoverManual, st.CoverModified, st.CoverChecked = arg.Manual, now, now
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) MarkCoverChecked(_ context.Context, id uuid.UUID) error {
|
||||
return s.updateSoundtrackByUUID(id, func(st *repository.Soundtrack) {
|
||||
st.CoverChecked = pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ClearCover(_ context.Context, id uuid.UUID) error {
|
||||
return s.updateSoundtrackByUUID(id, func(st *repository.Soundtrack) {
|
||||
st.CoverFile, st.CoverSource, st.CoverExternalID, st.CoverManual = nil, nil, nil, false
|
||||
st.CoverModified, st.CoverChecked = pgtype.Timestamptz{}, pgtype.Timestamptz{}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ListCoverFiles(context.Context) ([]string, error) {
|
||||
defer s.lock()()
|
||||
if s.Err != nil {
|
||||
return nil, s.Err
|
||||
}
|
||||
var files []string
|
||||
for _, st := range s.Soundtracks {
|
||||
if st.CoverFile != nil {
|
||||
files = append(files, *st.CoverFile)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *Store) MarkSoundtracksDeletedExcept(_ context.Context, ids []int32) error {
|
||||
defer s.lock()()
|
||||
if s.Err != nil {
|
||||
|
||||
Reference in new issue
Block a user