diff --git a/db/migrations/20250701010108_add_multi_library_support.go b/db/migrations/20250701010108_add_multi_library_support.go index 7711335d7..654784d09 100644 --- a/db/migrations/20250701010108_add_multi_library_support.go +++ b/db/migrations/20250701010108_add_multi_library_support.go @@ -79,6 +79,10 @@ func upAddMultiLibrarySupport(ctx context.Context, tx *sql.Tx) error { SELECT t.id, 1, t.album_count, t.media_file_count FROM tag t WHERE EXISTS (SELECT 1 FROM library WHERE id = 1); + + -- Remove global stats from tag table as they are now per-library + ALTER TABLE tag DROP COLUMN album_count; + ALTER TABLE tag DROP COLUMN media_file_count; `) return err diff --git a/model/tag.go b/model/tag.go index a1f4e28da..8f9c60f37 100644 --- a/model/tag.go +++ b/model/tag.go @@ -153,7 +153,7 @@ func (t Tags) Add(name TagName, v string) { } type TagRepository interface { - Add(...Tag) error + Add(libraryID int, tags ...Tag) error UpdateCounts() error } diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go index 5efe26b2a..2e19892a1 100644 --- a/persistence/artist_repository_test.go +++ b/persistence/artist_repository_test.go @@ -730,8 +730,8 @@ var _ = Describe("ArtistRepository", func() { ctx = request.WithUser(ctx, adminUser) adminRepo = NewArtistRepository(ctx, GetDBXBuilder()) - // Create library 2 for testing access restrictions - lib2 = model.Library{ID: 0, Name: "Library 2", Path: "/lib2"} + // Create library for testing access restrictions + lib2 = model.Library{ID: 0, Name: "Artist Test Library", Path: "/artist/test/lib"} lr := NewLibraryRepository(request.WithUser(log.NewContext(context.TODO()), adminUser), GetDBXBuilder()) err := lr.Put(&lib2) Expect(err).ToNot(HaveOccurred()) diff --git a/persistence/genre_repository.go b/persistence/genre_repository.go index c3e4f1049..311eb0a68 100644 --- a/persistence/genre_repository.go +++ b/persistence/genre_repository.go @@ -21,13 +21,7 @@ func NewGenreRepository(ctx context.Context, db dbx.Builder) model.GenreReposito } func (r *genreRepository) selectGenre(opt ...model.QueryOptions) SelectBuilder { - return r.newSelect(opt...). - Columns( - "id", - "tag_value as name", - "album_count", - "media_file_count as song_count", - ) + return r.newSelect(opt...) } func (r *genreRepository) GetAll(opt ...model.QueryOptions) (model.Genres, error) { @@ -40,7 +34,7 @@ func (r *genreRepository) GetAll(opt ...model.QueryOptions) (model.Genres, error // Override ResourceRepository methods to return Genre objects instead of Tag objects func (r *genreRepository) Read(id string) (interface{}, error) { - sel := r.selectGenre().Columns("*").Where(Eq{"id": id}) + sel := r.selectGenre().Where(Eq{"tag.id": id}) var res model.Genre err := r.queryOne(sel, &res) return &res, err diff --git a/persistence/genre_repository_test.go b/persistence/genre_repository_test.go index 7c0bcb130..e7b43689c 100644 --- a/persistence/genre_repository_test.go +++ b/persistence/genre_repository_test.go @@ -24,7 +24,7 @@ var _ = Describe("GenreRepository", func() { BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) - ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe"}) + ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true}) genreRepo := NewGenreRepository(ctx, GetDBXBuilder()) repo = genreRepo restRepo = genreRepo.(model.ResourceRepository) @@ -35,12 +35,18 @@ var _ = Describe("GenreRepository", func() { _, err := db.NewQuery("DELETE FROM tag").Execute() Expect(err).ToNot(HaveOccurred()) + // Ensure library 1 exists and user has access to it + _, err = db.NewQuery("INSERT OR IGNORE INTO library (id, name, path, default_new_users) VALUES (1, 'Test Library', '/test', true)").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 1)").Execute() + Expect(err).ToNot(HaveOccurred()) + // Add comprehensive test data that covers all test scenarios newTag := func(name, value string) model.Tag { return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value} } - err = tagRepo.Add( + err = tagRepo.Add(1, newTag("genre", "rock"), newTag("genre", "pop"), newTag("genre", "jazz"), @@ -167,7 +173,7 @@ var _ = Describe("GenreRepository", func() { TagName: "mood", TagValue: "energetic", } - err := tagRepo.Add(nonGenreTag) + err := tagRepo.Add(1, nonGenreTag) Expect(err).ToNot(HaveOccurred()) count, err := restRepo.Count() diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index 63120d4c7..2ad2428ef 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -59,7 +59,7 @@ func userId(ctx context.Context) string { func loggedUser(ctx context.Context) *model.User { if user, ok := request.UserFrom(ctx); !ok { - return &model.User{} + return &model.User{ID: invalidUserId} } else { return &user } diff --git a/persistence/sql_tags.go b/persistence/sql_tags.go index 3d080b392..b92e18e60 100644 --- a/persistence/sql_tags.go +++ b/persistence/sql_tags.go @@ -59,6 +59,11 @@ func tagIDFilter(name string, idValue any) Sqlizer { ) } +// tagLibraryIdFilter filters tags based on library access through the library_tag table +func tagLibraryIdFilter(_ string, value interface{}) Sqlizer { + return Eq{"library_tag.library_id": value} +} + // baseTagRepository provides common functionality for all tag-based repositories. // It handles CRUD operations with optional filtering by tag name. type baseTagRepository struct { @@ -77,7 +82,8 @@ func newBaseTagRepository(ctx context.Context, db dbx.Builder, tagFilter *model. r.db = db r.tableName = "tag" r.registerModel(&model.Tag{}, map[string]filterFunc{ - "name": containsFilter("tag_value"), + "name": containsFilter("tag_value"), + "library_id": tagLibraryIdFilter, }) r.setSortMappings(map[string]string{ "name": "tag_value", @@ -85,19 +91,50 @@ func newBaseTagRepository(ctx context.Context, db dbx.Builder, tagFilter *model. return r } -// newSelect overrides the base implementation to conditionally apply tag name filtering. +// newSelect overrides the base implementation to apply tag name filtering and library filtering. func (r *baseTagRepository) newSelect(options ...model.QueryOptions) SelectBuilder { + user := loggedUser(r.ctx) + if user.ID == invalidUserId { + // No user context - return empty result set + return SelectBuilder{}.Where(Eq{"1": "0"}) + } sq := r.sqlRepository.newSelect(options...) if r.tagFilter != nil { - sq = sq.Where(Eq{"tag_name": *r.tagFilter}) + sq = sq.Where(Eq{"tag.tag_name": *r.tagFilter}) } + sq = sq.Columns( + "tag.id", + "tag.tag_value as name", + "COALESCE(SUM(library_tag.album_count), 0) as album_count", + "COALESCE(SUM(library_tag.media_file_count), 0) as song_count", + ). + LeftJoin("library_tag on library_tag.tag_id = tag.id"). + // Apply library filtering by joining only with accessible libraries + Join("user_library on user_library.library_id = library_tag.library_id AND user_library.user_id = ?", user.ID). + GroupBy("tag.id", "tag.tag_value") return sq } // ResourceRepository interface implementation func (r *baseTagRepository) Count(options ...rest.QueryOptions) (int64, error) { - return r.count(r.newSelect(), r.parseRestOptions(r.ctx, options...)) + // Create a query that counts distinct tags without GROUP BY + user := loggedUser(r.ctx) + if user.ID == invalidUserId { + return 0, nil + } + + // Build the same base query as newSelect but for counting + sq := Select() + if r.tagFilter != nil { + sq = sq.Where(Eq{"tag.tag_name": *r.tagFilter}) + } + + // Apply the same joins as newSelect + sq = sq.LeftJoin("library_tag on library_tag.tag_id = tag.id"). + Join("user_library on user_library.library_id = library_tag.library_id AND user_library.user_id = ?", user.ID) + + return r.count(sq, r.parseRestOptions(r.ctx, options...)) } func (r *baseTagRepository) Read(id string) (interface{}, error) { diff --git a/persistence/tag_library_filtering_test.go b/persistence/tag_library_filtering_test.go new file mode 100644 index 000000000..8017528fe --- /dev/null +++ b/persistence/tag_library_filtering_test.go @@ -0,0 +1,228 @@ +package persistence + +import ( + "context" + + "github.com/deluan/rest" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/id" + "github.com/navidrome/navidrome/model/request" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pocketbase/dbx" +) + +var _ = Describe("Tag Library Filtering", func() { + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + + // Clean up all relevant tables + db := GetDBXBuilder() + _, err := db.NewQuery("DELETE FROM library_tag").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("DELETE FROM tag").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("DELETE FROM user_library WHERE user_id != 'userid' AND user_id != '2222'").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("DELETE FROM library WHERE id > 1").Execute() + Expect(err).ToNot(HaveOccurred()) + + // Create test libraries + _, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES (2, 'Library 2', '/music/lib2')").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES (3, 'Library 3', '/music/lib3')").Execute() + Expect(err).ToNot(HaveOccurred()) + + // Ensure admin user has access to all libraries (since admin users should have access to all libraries) + _, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 1)").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 2)").Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 3)").Execute() + Expect(err).ToNot(HaveOccurred()) + + // Set up test tags + newTag := func(name, value string) model.Tag { + return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value} + } + + // Create tags in admin context + adminCtx := request.WithUser(log.NewContext(context.TODO()), adminUser) + tagRepo := NewTagRepository(adminCtx, GetDBXBuilder()) + + // Add tags to different libraries + err = tagRepo.Add(1, newTag("genre", "rock")) + Expect(err).ToNot(HaveOccurred()) + err = tagRepo.Add(2, newTag("genre", "pop")) + Expect(err).ToNot(HaveOccurred()) + err = tagRepo.Add(3, newTag("genre", "jazz")) + Expect(err).ToNot(HaveOccurred()) + err = tagRepo.Add(2, newTag("genre", "rock")) + Expect(err).ToNot(HaveOccurred()) + + // Update counts manually for testing + _, err = db.NewQuery("UPDATE library_tag SET album_count = 5, media_file_count = 20 WHERE tag_id = {:tagId} AND library_id = 1").Bind(dbx.Params{"tagId": id.NewTagID("genre", "rock")}).Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("UPDATE library_tag SET album_count = 3, media_file_count = 10 WHERE tag_id = {:tagId} AND library_id = 2").Bind(dbx.Params{"tagId": id.NewTagID("genre", "pop")}).Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("UPDATE library_tag SET album_count = 2, media_file_count = 8 WHERE tag_id = {:tagId} AND library_id = 3").Bind(dbx.Params{"tagId": id.NewTagID("genre", "jazz")}).Execute() + Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("UPDATE library_tag SET album_count = 1, media_file_count = 4 WHERE tag_id = {:tagId} AND library_id = 2").Bind(dbx.Params{"tagId": id.NewTagID("genre", "rock")}).Execute() + Expect(err).ToNot(HaveOccurred()) + + // Set up user library access - Regular user has access to libraries 1 and 2 only + _, err = db.NewQuery("INSERT INTO user_library (user_id, library_id) VALUES ('2222', 2)").Execute() + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("TagRepository Library Filtering", func() { + Context("Admin User", func() { + It("should see all tags regardless of library", func() { + ctx := request.WithUser(log.NewContext(context.TODO()), adminUser) + tagRepo := NewTagRepository(ctx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + tags, err := repo.ReadAll() + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + Expect(tagList).To(HaveLen(3)) + }) + }) + + Context("Regular User with Limited Library Access", func() { + It("should only see tags from accessible libraries", func() { + ctx := request.WithUser(log.NewContext(context.TODO()), regularUser) + tagRepo := NewTagRepository(ctx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + tags, err := repo.ReadAll() + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should see rock (libraries 1,2) and pop (library 2), but not jazz (library 3) + Expect(tagList).To(HaveLen(2)) + }) + + It("should respect explicit library_id filters within accessible libraries", func() { + ctx := request.WithUser(log.NewContext(context.TODO()), regularUser) + tagRepo := NewTagRepository(ctx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + // Filter by library 2 (user has access to libraries 1 and 2) + tags, err := repo.ReadAll(rest.QueryOptions{ + Filters: map[string]interface{}{ + "library_id": 2, + }, + }) + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should see only tags from library 2: pop and rock(lib2) + Expect(tagList).To(HaveLen(2)) + + // Verify the tags are correct + tagValues := make([]string, len(tagList)) + for i, tag := range tagList { + tagValues[i] = tag.TagValue + } + Expect(tagValues).To(ContainElements("pop", "rock")) + }) + + It("should not return tags when filtering by inaccessible library", func() { + ctx := request.WithUser(log.NewContext(context.TODO()), regularUser) + tagRepo := NewTagRepository(ctx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + // Try to filter by library 3 (user doesn't have access) + tags, err := repo.ReadAll(rest.QueryOptions{ + Filters: map[string]interface{}{ + "library_id": 3, + }, + }) + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should return no tags since user can't access library 3 + Expect(tagList).To(HaveLen(0)) + }) + + It("should filter by library 1 correctly", func() { + ctx := request.WithUser(log.NewContext(context.TODO()), regularUser) + tagRepo := NewTagRepository(ctx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + // Filter by library 1 (user has access) + tags, err := repo.ReadAll(rest.QueryOptions{ + Filters: map[string]interface{}{ + "library_id": 1, + }, + }) + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should see only rock from library 1 + Expect(tagList).To(HaveLen(1)) + Expect(tagList[0].TagValue).To(Equal("rock")) + }) + }) + + Context("Admin User with Explicit Library Filtering", func() { + It("should see all tags when no filter is applied", func() { + adminCtx := request.WithUser(log.NewContext(context.TODO()), adminUser) + tagRepo := NewTagRepository(adminCtx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + tags, err := repo.ReadAll() + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + Expect(tagList).To(HaveLen(3)) + }) + + It("should respect explicit library_id filters", func() { + adminCtx := request.WithUser(log.NewContext(context.TODO()), adminUser) + tagRepo := NewTagRepository(adminCtx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + // Filter by library 3 + tags, err := repo.ReadAll(rest.QueryOptions{ + Filters: map[string]interface{}{ + "library_id": 3, + }, + }) + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should see only jazz from library 3 + Expect(tagList).To(HaveLen(1)) + Expect(tagList[0].TagValue).To(Equal("jazz")) + }) + + It("should filter by library 2 correctly", func() { + adminCtx := request.WithUser(log.NewContext(context.TODO()), adminUser) + tagRepo := NewTagRepository(adminCtx, GetDBXBuilder()) + repo := tagRepo.(model.ResourceRepository) + + // Filter by library 2 + tags, err := repo.ReadAll(rest.QueryOptions{ + Filters: map[string]interface{}{ + "library_id": 2, + }, + }) + Expect(err).ToNot(HaveOccurred()) + tagList := tags.(model.TagList) + + // Should see pop and rock from library 2 + Expect(tagList).To(HaveLen(2)) + + tagValues := make([]string, len(tagList)) + for i, tag := range tagList { + tagValues[i] = tag.TagValue + } + Expect(tagValues).To(ContainElements("pop", "rock")) + }) + }) + }) +}) diff --git a/persistence/tag_repository.go b/persistence/tag_repository.go index f56a3a24c..729208999 100644 --- a/persistence/tag_repository.go +++ b/persistence/tag_repository.go @@ -22,7 +22,7 @@ func NewTagRepository(ctx context.Context, db dbx.Builder) model.TagRepository { } } -func (r *tagRepository) Add(tags ...model.Tag) error { +func (r *tagRepository) Add(libraryID int, tags ...model.Tag) error { for chunk := range slices.Chunk(tags, 200) { sq := Insert(r.tableName).Columns("id", "tag_name", "tag_value"). Suffix("on conflict (id) do nothing") @@ -33,34 +33,41 @@ func (r *tagRepository) Add(tags ...model.Tag) error { if err != nil { return err } + + // Create library_tag entries for library filtering + libSq := Insert("library_tag").Columns("tag_id", "library_id", "album_count", "media_file_count"). + Suffix("on conflict (tag_id, library_id) do nothing") + for _, t := range chunk { + libSq = libSq.Values(t.ID, libraryID, 0, 0) + } + _, err = r.executeSQL(libSq) + if err != nil { + return fmt.Errorf("adding library_tag entries: %w", err) + } } return nil } -// UpdateCounts updates the album_count and media_file_count columns in the tag_counts table. +// UpdateCounts updates the library_tag table with per-library statistics. // Only genres are being updated for now. func (r *tagRepository) UpdateCounts() error { template := ` -with updated_values as ( - select jt.value as id, count(distinct %[1]s.id) as %[1]s_count - from %[1]s - join json_tree(tags, '$.genre') as jt - where atom is not null - and key = 'id' - group by jt.value -) -update tag -set %[1]s_count = updated_values.%[1]s_count -from updated_values -where tag.id = updated_values.id; +INSERT INTO library_tag (tag_id, library_id, %[1]s_count) +SELECT jt.value as tag_id, %[1]s.library_id, count(distinct %[1]s.id) as %[1]s_count +FROM %[1]s +JOIN json_tree(%[1]s.tags, '$.genre') as jt ON jt.atom IS NOT NULL AND jt.key = 'id' +GROUP BY jt.value, %[1]s.library_id +ON CONFLICT (tag_id, library_id) +DO UPDATE SET %[1]s_count = excluded.%[1]s_count; ` + for _, table := range []string{"album", "media_file"} { start := time.Now() query := Expr(fmt.Sprintf(template, table)) c, err := r.executeSQL(query) - log.Debug(r.ctx, "Updated tag counts", "table", table, "elapsed", time.Since(start), "updated", c) + log.Debug(r.ctx, "Updated library tag counts", "table", table, "elapsed", time.Since(start), "updated", c) if err != nil { - return fmt.Errorf("updating %s tag counts: %w", table, err) + return fmt.Errorf("updating %s library tag counts: %w", table, err) } } return nil @@ -70,6 +77,11 @@ func (r *tagRepository) purgeUnused() error { del := Delete(r.tableName).Where(` id not in (select jt.value from album left join json_tree(album.tags, '$') as jt + where atom is not null + and key = 'id' + UNION + select jt.value + from media_file left join json_tree(media_file.tags, '$') as jt where atom is not null and key = 'id') `) diff --git a/persistence/tag_repository_test.go b/persistence/tag_repository_test.go index 07b3cb1bc..9b8f93cd9 100644 --- a/persistence/tag_repository_test.go +++ b/persistence/tag_repository_test.go @@ -22,7 +22,7 @@ var _ = Describe("TagRepository", func() { BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) - ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe"}) + ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true}) tagRepo := NewTagRepository(ctx, GetDBXBuilder()) repo = tagRepo restRepo = tagRepo.(model.ResourceRepository) @@ -31,13 +31,23 @@ var _ = Describe("TagRepository", func() { db := GetDBXBuilder() _, err := db.NewQuery("DELETE FROM tag").Execute() Expect(err).ToNot(HaveOccurred()) + _, err = db.NewQuery("DELETE FROM library_tag").Execute() + Expect(err).ToNot(HaveOccurred()) + + // Ensure library 1 exists (if it doesn't already) + _, err = db.NewQuery("INSERT OR IGNORE INTO library (id, name, path, default_new_users) VALUES (1, 'Test Library', '/test', true)").Execute() + Expect(err).ToNot(HaveOccurred()) + + // Ensure the admin user has access to library 1 + _, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 1)").Execute() + Expect(err).ToNot(HaveOccurred()) // Add comprehensive test data that covers all test scenarios newTag := func(name, value string) model.Tag { return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value} } - err = repo.Add( + err = repo.Add(1, // Genre tags newTag("genre", "rock"), newTag("genre", "pop"), @@ -73,7 +83,7 @@ var _ = Describe("TagRepository", func() { TagValue: "experimental", } - err := repo.Add(newTag) + err := repo.Add(1, newTag) Expect(err).ToNot(HaveOccurred()) // Verify tag was added @@ -100,7 +110,7 @@ var _ = Describe("TagRepository", func() { Expect(err).ToNot(HaveOccurred()) Expect(count).To(Equal(int64(20))) // Still 20 tags - err = repo.Add(duplicateTag) + err = repo.Add(1, duplicateTag) Expect(err).ToNot(HaveOccurred()) // Should not error // Count should remain the same diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go index a545c0ad2..24223857f 100644 --- a/persistence/user_repository_test.go +++ b/persistence/user_repository_test.go @@ -336,13 +336,20 @@ var _ = Describe("UserRepository", func() { Describe("Admin User Auto-Assignment", func() { var ( - libRepo model.LibraryRepository - library1 model.Library - library2 model.Library + libRepo model.LibraryRepository + library1 model.Library + library2 model.Library + initialLibCount int ) BeforeEach(func() { libRepo = NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder()) + + // Count initial libraries + existingLibs, err := libRepo.GetAll() + Expect(err).To(BeNil()) + initialLibCount = len(existingLibs) + library1 = model.Library{ID: 0, Name: "Admin Test Library 1", Path: "/admin/test/path1"} library2 = model.Library{ID: 0, Name: "Admin Test Library 2", Path: "/admin/test/path2"} @@ -375,13 +382,13 @@ var _ = Describe("UserRepository", func() { // Admin should automatically have access to all libraries (including existing ones) libraries, err := repo.GetUserLibraries(adminUser.ID) Expect(err).To(BeNil()) - Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1 + Expect(libraries).To(HaveLen(initialLibCount + 2)) // Initial libraries + our 2 test libraries libIDs := make([]int, len(libraries)) for i, lib := range libraries { libIDs[i] = lib.ID } - Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1)) + Expect(libIDs).To(ContainElements(library1.ID, library2.ID)) }) It("automatically assigns all libraries to admin users when updated", func() { @@ -410,14 +417,14 @@ var _ = Describe("UserRepository", func() { // Should now have access to all libraries (including existing ones) libraries, err := repo.GetUserLibraries(regularUser.ID) Expect(err).To(BeNil()) - Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1 + Expect(libraries).To(HaveLen(initialLibCount + 2)) // Initial libraries + our 2 test libraries libIDs := make([]int, len(libraries)) for i, lib := range libraries { libIDs[i] = lib.ID } // Should include our test libraries plus all existing ones - Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1)) + Expect(libIDs).To(ContainElements(library1.ID, library2.ID)) }) It("assigns default libraries to regular users", func() { diff --git a/scanner/phase_1_folders.go b/scanner/phase_1_folders.go index 3b97ed38c..e04f10c70 100644 --- a/scanner/phase_1_folders.go +++ b/scanner/phase_1_folders.go @@ -342,7 +342,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error) } // Save all tags to DB - err = tagRepo.Add(entry.tags...) + err = tagRepo.Add(entry.job.lib.ID, entry.tags...) if err != nil { log.Error(p.ctx, "Scanner: Error persisting tags to DB", "folder", entry.path, err) return err diff --git a/ui/src/dataProvider/wrapperDataProvider.js b/ui/src/dataProvider/wrapperDataProvider.js index 8c03df555..ca46293e1 100644 --- a/ui/src/dataProvider/wrapperDataProvider.js +++ b/ui/src/dataProvider/wrapperDataProvider.js @@ -21,7 +21,7 @@ const getSelectedLibraries = () => { // Function to apply library filtering to appropriate resources const applyLibraryFilter = (resource, params) => { // Content resources that should be filtered by selected libraries - const filteredResources = ['album', 'song', 'artist', 'playlistTrack'] + const filteredResources = ['album', 'song', 'artist', 'playlistTrack', 'tag'] // Get selected libraries from localStorage const selectedLibraries = getSelectedLibraries() @@ -54,7 +54,8 @@ const mapResource = (resource, params) => { } case 'album': case 'song': - case 'artist': { + case 'artist': + case 'tag': { params.filter = params.filter || {} if (!isAdmin()) { params.filter.missing = false