feat: add multi-library support to tag repository system

Implemented comprehensive library filtering for tag repositories to support the multi-library feature. This change ensures that users only see tags from libraries they have access to, while admin users can see all tags.

Key changes:
- Enhanced TagRepository.Add() method to accept libraryID parameter for proper library association
- Updated baseTagRepository to implement library-aware queries with proper joins
- Added library_tag table integration for per-library tag statistics
- Implemented user permission-based filtering through user_library associations
- Added comprehensive test coverage for library filtering scenarios
- Updated UI data provider to include tag filtering by selected libraries
- Modified scanner to pass library ID when adding tags during folder processing

The implementation maintains backward compatibility while providing proper isolation between libraries for tag-based operations like genres and other metadata tags.
This commit is contained in:
Deluan 2025-07-16 16:36:07 -04:00
commit 295900e6f7
13 changed files with 348 additions and 49 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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())

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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) {

View file

@ -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"))
})
})
})
})

View file

@ -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')
`)

View file

@ -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

View file

@ -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() {

View file

@ -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

View file

@ -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