diff --git a/core/maintenance.go b/core/maintenance.go index 3b36e9136..b31d6c0c2 100644 --- a/core/maintenance.go +++ b/core/maintenance.go @@ -115,10 +115,22 @@ func (s *maintenanceService) RemapMissingFile(ctx context.Context, missingID, ta return err } - // Refresh statistics in background. album/artist play count aggregates are not recalculated - // here; they are refreshed by the next scan. + // Refresh artist stats + if _, err := s.ds.Artist(ctx).RefreshStats(true); err != nil { + log.Error(ctx, "Error refreshing artist stats after deleting missing files", err) + } else { + log.Debug(ctx, "Successfully refreshed artist stats after deleting missing files") + } + + // Refresh album stats if we have affected albums affectedAlbumIDs := slice.Unique(slice.Filter([]string{oldAlbumID, newAlbumID}, func(id string) bool { return id != "" })) - s.refreshStatsAsync(ctx, affectedAlbumIDs) + if len(affectedAlbumIDs) > 0 { + if err := s.refreshAlbums(ctx, affectedAlbumIDs); err != nil { + log.Error(ctx, "Error refreshing album stats after deleting missing files", err) + } else { + log.Debug(ctx, "Successfully refreshed album stats after deleting missing files", "count", len(affectedAlbumIDs)) + } + } return nil } diff --git a/core/maintenance_test.go b/core/maintenance_test.go index 24d423e4f..dfb8bf8af 100644 --- a/core/maintenance_test.go +++ b/core/maintenance_test.go @@ -326,6 +326,38 @@ var _ = Describe("Maintenance", func() { Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(MatchError(ErrTargetMissing)) }) + It("refreshes artist and album stats right after the remap", func() { + artistRepo := ds.MockedArtist.(*extendedArtistRepo) + albumRepo := ds.MockedAlbum.(*extendedAlbumRepo) + albumRepo.SetData(model.Albums{ + {ID: "album1", Name: "Old Album", SongCount: 2, Size: 1100, Duration: 110}, + {ID: "album2", Name: "New Album", SongCount: 1, Size: 2000, Duration: 200}, + }) + mfRepo.SetData(model.MediaFiles{ + {ID: "m1", Path: "old/1.mp3", Album: "Old Album", AlbumID: "album1", Missing: true, Size: 100, Duration: 10}, + {ID: "k1", Path: "old/2.mp3", Album: "Old Album", AlbumID: "album1", Missing: false, Size: 1000, Duration: 100}, + {ID: "t1", Path: "new/1.mp3", Album: "New Album", AlbumID: "album2", Missing: false, Size: 2000, Duration: 200}, + }) + + Expect(service.RemapMissingFile(ctx, "m1", "t1")).To(Succeed()) + + Expect(artistRepo.IsRefreshStatsCalled()).To(BeTrue(), "Artist stats should be refreshed") + + // The old album lost the remapped track, so its stats are recalculated from the remaining one + oldAlbum, err := albumRepo.Get("album1") + Expect(err).ToNot(HaveOccurred()) + Expect(oldAlbum.SongCount).To(Equal(1)) + Expect(oldAlbum.Size).To(Equal(int64(1000))) + Expect(oldAlbum.Duration).To(BeNumerically("==", 100)) + + // The target album keeps the track, now under the missing file's ID + newAlbum, err := albumRepo.Get("album2") + Expect(err).ToNot(HaveOccurred()) + Expect(newAlbum.SongCount).To(Equal(1)) + Expect(newAlbum.Size).To(Equal(int64(2000))) + Expect(newAlbum.Duration).To(BeNumerically("==", 200)) + }) + It("returns an error if GC fails", func() { mfRepo.SetData(model.MediaFiles{ {ID: "m1", AlbumID: "album1", Missing: true},