diff --git a/core/metrics/insights.go b/core/metrics/insights.go index d48e7d67..df3ca9a4 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -265,6 +265,10 @@ func (c *insightsCollector) collect(ctx context.Context) []byte { if err != nil { log.Trace(ctx, "Error reading active users count", err) } + data.Library.FileSuffixes, err = c.ds.MediaFile(ctx).CountBySuffix() + if err != nil { + log.Trace(ctx, "Error reading file suffixes count", err) + } // Check for smart playlists data.Config.HasSmartPlaylists, err = c.hasSmartPlaylists(ctx) diff --git a/core/metrics/insights/data.go b/core/metrics/insights/data.go index c46eb874..19007fd0 100644 --- a/core/metrics/insights/data.go +++ b/core/metrics/insights/data.go @@ -40,6 +40,7 @@ type Data struct { Libraries int64 `json:"libraries"` ActiveUsers int64 `json:"activeUsers"` ActivePlayers map[string]int64 `json:"activePlayers,omitempty"` + FileSuffixes map[string]int64 `json:"fileSuffixes,omitempty"` } `json:"library"` Config struct { LogLevel string `json:"logLevel,omitempty"` diff --git a/model/mediafile.go b/model/mediafile.go index 0ef26d74..1ae63e75 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -353,6 +353,7 @@ type MediaFileCursor iter.Seq2[MediaFile, error] type MediaFileRepository interface { CountAll(options ...QueryOptions) (int64, error) + CountBySuffix(options ...QueryOptions) (map[string]int64, error) Exists(id string) (bool, error) Put(m *MediaFile) error Get(id string) (*MediaFile, error) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index f18e6d24..7f65540c 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -124,6 +124,25 @@ func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, er return r.count(query, options...) } +func (r *mediaFileRepository) CountBySuffix(options ...model.QueryOptions) (map[string]int64, error) { + sel := r.newSelect(options...). + Columns("lower(suffix) as suffix", "count(*) as count"). + GroupBy("lower(suffix)") + var res []struct { + Suffix string + Count int64 + } + err := r.queryAll(sel, &res) + if err != nil { + return nil, err + } + counts := make(map[string]int64, len(res)) + for _, c := range res { + counts[c.Suffix] = c.Count + } + return counts, nil +} + func (r *mediaFileRepository) Exists(id string) (bool, error) { return r.exists(Eq{"media_file.id": id}) } diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index ab926c00..35fda087 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -41,6 +41,44 @@ var _ = Describe("MediaRepository", func() { Expect(mr.CountAll()).To(Equal(int64(10))) }) + Describe("CountBySuffix", func() { + var mp3File, flacFile1, flacFile2, flacUpperFile model.MediaFile + + BeforeEach(func() { + mp3File = model.MediaFile{ID: "suffix-mp3", LibraryID: 1, Suffix: "mp3", Path: "/test/file.mp3"} + flacFile1 = model.MediaFile{ID: "suffix-flac1", LibraryID: 1, Suffix: "flac", Path: "/test/file1.flac"} + flacFile2 = model.MediaFile{ID: "suffix-flac2", LibraryID: 1, Suffix: "flac", Path: "/test/file2.flac"} + flacUpperFile = model.MediaFile{ID: "suffix-FLAC", LibraryID: 1, Suffix: "FLAC", Path: "/test/file.FLAC"} + + Expect(mr.Put(&mp3File)).To(Succeed()) + Expect(mr.Put(&flacFile1)).To(Succeed()) + Expect(mr.Put(&flacFile2)).To(Succeed()) + Expect(mr.Put(&flacUpperFile)).To(Succeed()) + }) + + AfterEach(func() { + _ = mr.Delete(mp3File.ID) + _ = mr.Delete(flacFile1.ID) + _ = mr.Delete(flacFile2.ID) + _ = mr.Delete(flacUpperFile.ID) + }) + + It("counts media files grouped by suffix with lowercase normalization", func() { + counts, err := mr.CountBySuffix() + Expect(err).ToNot(HaveOccurred()) + + // Should have lowercase keys only + Expect(counts).To(HaveKey("mp3")) + Expect(counts).To(HaveKey("flac")) + Expect(counts).ToNot(HaveKey("FLAC")) + + // mp3: 1 file + Expect(counts["mp3"]).To(Equal(int64(1))) + // flac: 3 files (2 lowercase + 1 uppercase normalized) + Expect(counts["flac"]).To(Equal(int64(3))) + }) + }) + It("returns songs ordered by lyrics with a specific title/artist", func() { // attempt to mimic filters.SongsByArtistTitleWithLyricsFirst, except we want all items results, err := mr.GetAll(model.QueryOptions{