fix: only move album annotations if moving a track would empty the old album

This commit is contained in:
zerovox 2026-08-16 21:00:18 -06:00
parent cb185e567b
commit 9da12262a6
4 changed files with 42 additions and 16 deletions

View File

@ -93,13 +93,20 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta
return fmt.Errorf("delete discarded track: %w", err)
}
// Reassign album annotations (starred, rating) and preserve album created_at on album change
if oldAlbumID != newAlbumID {
if err := tx.Album(ctx).ReassignAnnotation(oldAlbumID, newAlbumID); err != nil {
log.Warn(ctx, "Could not reassign album annotations", "from", oldAlbumID, "to", newAlbumID, err)
// Reassign album annotations (starred, rating) if the old album is now empty
oldAlbumTracks, err := tx.MediaFile(ctx).CountAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": oldAlbumID}})
if err != nil {
return fmt.Errorf("get old album tracks: %w", err)
}
if err := tx.Album(ctx).CopyAttributes(oldAlbumID, newAlbumID, "created_at"); err != nil && !errors.Is(err, model.ErrNotFound) {
log.Warn(ctx, "Could not copy album created_at", "from", oldAlbumID, "to", newAlbumID, err)
if oldAlbumTracks == 0 {
if err := tx.Album(ctx).ReassignAnnotation(oldAlbumID, newAlbumID); err != nil {
return fmt.Errorf("reassign album annotations: %w", err)
}
// Copy across the create_at timestamp from the old album
if err := tx.Album(ctx).CopyAttributes(oldAlbumID, newAlbumID, "created_at"); err != nil && !errors.Is(err, model.ErrNotFound) {
return fmt.Errorf("copy album attributes: %w", err)
}
}
}
return nil

View File

@ -272,18 +272,32 @@ var _ = Describe("Maintenance", func() {
Expect(ds.GCCalled).To(BeTrue())
})
It("reassigns album annotations when the album changes", func() {
It("reassigns album annotations when the old album is emptied", func() {
albumRepo := ds.MockedAlbum.(*extendedAlbumRepo)
mfRepo.SetData(model.MediaFiles{
{ID: "m1", AlbumID: "album1", Missing: true},
{ID: "t1", AlbumID: "album2", Missing: false},
})
mfRepo.SetCountAll(0)
Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(Succeed())
Expect(albumRepo.ReassignAnnotationCalls).To(HaveKeyWithValue("album1", "album2"))
})
It("does not reassign album annotations when the old album is not emptied", func() {
albumRepo := ds.MockedAlbum.(*extendedAlbumRepo)
mfRepo.SetData(model.MediaFiles{
{ID: "m1", AlbumID: "album1", Missing: true},
{ID: "m2", AlbumID: "album1", Missing: false},
{ID: "t1", AlbumID: "album2", Missing: false},
})
Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(Succeed())
Expect(albumRepo.ReassignAnnotationCalls).To(BeEmpty())
})
It("does not reassign annotations when the album is unchanged", func() {
albumRepo := ds.MockedAlbum.(*extendedAlbumRepo)
mfRepo.SetData(model.MediaFiles{

View File

@ -311,7 +311,7 @@ var _ = Describe("phaseMissingTracks", func() {
When("PurgeMissing is 'always'", func() {
BeforeEach(func() {
conf.Server.Scanner.PurgeMissing = consts.PurgeMissingAlways
mr.CountAllValue = 3
mr.SetCountAll(3)
mr.DeleteAllMissingValue = 3
})
It("should purge missing files", func() {
@ -325,7 +325,7 @@ var _ = Describe("phaseMissingTracks", func() {
When("PurgeMissing is 'full'", func() {
BeforeEach(func() {
conf.Server.Scanner.PurgeMissing = consts.PurgeMissingFull
mr.CountAllValue = 2
mr.SetCountAll(2)
mr.DeleteAllMissingValue = 2
})
It("should not purge missing files if not a full scan", func() {
@ -346,7 +346,7 @@ var _ = Describe("phaseMissingTracks", func() {
When("PurgeMissing is 'never'", func() {
BeforeEach(func() {
conf.Server.Scanner.PurgeMissing = consts.PurgeMissingNever
mr.CountAllValue = 1
mr.SetCountAll(1)
mr.DeleteAllMissingValue = 1
})
It("should not purge missing files", func() {

View File

@ -24,8 +24,9 @@ type MockMediaFileRepo struct {
model.MediaFileRepository
Data map[string]*model.MediaFile
Err bool
// Add fields and methods for controlling CountAll and DeleteAllMissing in tests
CountAllValue int64
// Add fields and methods for controlling CountAll and DeleteAllMissing in tests.
// A nil CountAllValue is unset, and CountAll falls back to counting rows in Data.
CountAllValue *int64
CountAllOptions model.QueryOptions
DeleteAllMissingValue int64
Options model.QueryOptions
@ -40,6 +41,10 @@ func (m *MockMediaFileRepo) SetError(err bool) {
m.Err = err
}
func (m *MockMediaFileRepo) SetCountAll(count int64) {
m.CountAllValue = &count
}
func (m *MockMediaFileRepo) SetData(mfs model.MediaFiles) {
m.Data = make(map[string]*model.MediaFile)
for i, mf := range mfs {
@ -259,11 +264,11 @@ func (m *MockMediaFileRepo) CountAll(opts ...model.QueryOptions) (int64, error)
if m.Err {
return 0, errors.New("error")
}
if m.CountAllValue != 0 {
if len(opts) > 0 {
m.CountAllOptions = opts[0]
}
return m.CountAllValue, nil
if len(opts) > 0 {
m.CountAllOptions = opts[0]
}
if m.CountAllValue != nil {
return *m.CountAllValue, nil
}
return int64(len(m.Data)), nil
}