feat(scanner): add library stats to DB (#4229)

* Combine library stats migrations

* test: verify full library stats

* Fix total_songs calculation

* Fix library stats migration

* fix(scanner): log elapsed time and number of libraries updated during scan

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(scanner): refresh library stats conditionally, only if changes were detected

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(scanner): refresh library stats conditionally, only if changes were detected

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(scanner): update queries to exclude missing entries in library stats

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-06-14 15:58:33 -04:00
committed by GitHub
parent 44834204de
commit 5667f6ab75
7 changed files with 180 additions and 15 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/chain"
"github.com/pocketbase/dbx"
)
@@ -146,6 +147,53 @@ func (r *libraryRepository) ScanInProgress() (bool, error) {
return count > 0, err
}
func (r *libraryRepository) RefreshStats(id int) error {
var songsRes, albumsRes, artistsRes, foldersRes, filesRes, missingRes struct{ Count int64 }
var sizeRes struct{ Sum int64 }
err := chain.RunParallel(
func() error {
return r.queryOne(Select("count(*) as count").From("media_file").Where(Eq{"library_id": id, "missing": false}), &songsRes)
},
func() error {
return r.queryOne(Select("count(*) as count").From("album").Where(Eq{"library_id": id, "missing": false}), &albumsRes)
},
func() error {
return r.queryOne(Select("count(*) as count").From("library_artist la").
Join("artist a on la.artist_id = a.id").
Where(Eq{"la.library_id": id, "a.missing": false}), &artistsRes)
},
func() error {
return r.queryOne(Select("count(*) as count").From("folder").Where(Eq{"library_id": id, "missing": false}), &foldersRes)
},
func() error {
return r.queryOne(Select("ifnull(sum(num_audio_files + num_playlists + json_array_length(image_files)),0) as count").From("folder").Where(Eq{"library_id": id, "missing": false}), &filesRes)
},
func() error {
return r.queryOne(Select("count(*) as count").From("media_file").Where(Eq{"library_id": id, "missing": true}), &missingRes)
},
func() error {
return r.queryOne(Select("ifnull(sum(size),0) as sum").From("album").Where(Eq{"library_id": id, "missing": false}), &sizeRes)
},
)()
if err != nil {
return err
}
sq := Update(r.tableName).
Set("total_songs", songsRes.Count).
Set("total_albums", albumsRes.Count).
Set("total_artists", artistsRes.Count).
Set("total_folders", foldersRes.Count).
Set("total_files", filesRes.Count).
Set("total_missing_files", missingRes.Count).
Set("total_size", sizeRes.Sum).
Set("updated_at", time.Now()).
Where(Eq{"id": id})
_, err = r.executeSQL(sq)
return err
}
func (r *libraryRepository) GetAll(ops ...model.QueryOptions) (model.Libraries, error) {
sq := r.newSelect(ops...).Columns("*")
res := model.Libraries{}

View File

@@ -0,0 +1,52 @@
package persistence
import (
"context"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/pocketbase/dbx"
)
var _ = Describe("LibraryRepository", func() {
var repo model.LibraryRepository
var ctx context.Context
var conn *dbx.DB
BeforeEach(func() {
ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid"})
conn = GetDBXBuilder()
repo = NewLibraryRepository(ctx, conn)
})
It("refreshes stats", func() {
libBefore, err := repo.Get(1)
Expect(err).ToNot(HaveOccurred())
Expect(repo.RefreshStats(1)).To(Succeed())
libAfter, err := repo.Get(1)
Expect(err).ToNot(HaveOccurred())
Expect(libAfter.UpdatedAt).To(BeTemporally(">", libBefore.UpdatedAt))
var songsRes, albumsRes, artistsRes, foldersRes, filesRes, missingRes struct{ Count int64 }
var sizeRes struct{ Sum int64 }
Expect(conn.NewQuery("select count(*) as count from media_file where library_id = {:id} and missing = 0").Bind(dbx.Params{"id": 1}).One(&songsRes)).To(Succeed())
Expect(conn.NewQuery("select count(*) as count from album where library_id = {:id} and missing = 0").Bind(dbx.Params{"id": 1}).One(&albumsRes)).To(Succeed())
Expect(conn.NewQuery("select count(*) as count from library_artist la join artist a on la.artist_id = a.id where la.library_id = {:id} and a.missing = 0").Bind(dbx.Params{"id": 1}).One(&artistsRes)).To(Succeed())
Expect(conn.NewQuery("select count(*) as count from folder where library_id = {:id} and missing = 0").Bind(dbx.Params{"id": 1}).One(&foldersRes)).To(Succeed())
Expect(conn.NewQuery("select ifnull(sum(num_audio_files + num_playlists + json_array_length(image_files)),0) as count from folder where library_id = {:id} and missing = 0").Bind(dbx.Params{"id": 1}).One(&filesRes)).To(Succeed())
Expect(conn.NewQuery("select count(*) as count from media_file where library_id = {:id} and missing = 1").Bind(dbx.Params{"id": 1}).One(&missingRes)).To(Succeed())
Expect(conn.NewQuery("select ifnull(sum(size),0) as sum from album where library_id = {:id} and missing = 0").Bind(dbx.Params{"id": 1}).One(&sizeRes)).To(Succeed())
Expect(libAfter.TotalSongs).To(Equal(int(songsRes.Count)))
Expect(libAfter.TotalAlbums).To(Equal(int(albumsRes.Count)))
Expect(libAfter.TotalArtists).To(Equal(int(artistsRes.Count)))
Expect(libAfter.TotalFolders).To(Equal(int(foldersRes.Count)))
Expect(libAfter.TotalFiles).To(Equal(int(filesRes.Count)))
Expect(libAfter.TotalMissingFiles).To(Equal(int(missingRes.Count)))
Expect(libAfter.TotalSize).To(Equal(sizeRes.Sum))
})
})