feat: hide missing artists from regular users and Subsonic API (#4092)
* Handle missing artists for non-admin users * feat(artist): enhance ArtistList with missing row styling and class management Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
+3
-1
@@ -32,6 +32,8 @@ type Artist struct {
|
|||||||
SimilarArtists Artists `structs:"similar_artists" json:"-"`
|
SimilarArtists Artists `structs:"similar_artists" json:"-"`
|
||||||
ExternalInfoUpdatedAt *time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt,omitempty"`
|
ExternalInfoUpdatedAt *time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt,omitempty"`
|
||||||
|
|
||||||
|
Missing bool `structs:"missing" json:"missing"`
|
||||||
|
|
||||||
CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
|
CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
|
||||||
UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
|
UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -76,7 +78,7 @@ type ArtistRepository interface {
|
|||||||
UpdateExternalInfo(a *Artist) error
|
UpdateExternalInfo(a *Artist) error
|
||||||
Get(id string) (*Artist, error)
|
Get(id string) (*Artist, error)
|
||||||
GetAll(options ...QueryOptions) (Artists, error)
|
GetAll(options ...QueryOptions) (Artists, error)
|
||||||
GetIndex(roles ...Role) (ArtistIndexes, error)
|
GetIndex(includeMissing bool, roles ...Role) (ArtistIndexes, error)
|
||||||
|
|
||||||
// The following methods are used exclusively by the scanner:
|
// The following methods are used exclusively by the scanner:
|
||||||
RefreshPlayCounts() (int64, error)
|
RefreshPlayCounts() (int64, error)
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ func NewArtistRepository(ctx context.Context, db dbx.Builder) model.ArtistReposi
|
|||||||
"name": fullTextFilter(r.tableName),
|
"name": fullTextFilter(r.tableName),
|
||||||
"starred": booleanFilter,
|
"starred": booleanFilter,
|
||||||
"role": roleFilter,
|
"role": roleFilter,
|
||||||
|
"missing": booleanFilter,
|
||||||
})
|
})
|
||||||
r.setSortMappings(map[string]string{
|
r.setSortMappings(map[string]string{
|
||||||
"name": "order_artist_name",
|
"name": "order_artist_name",
|
||||||
@@ -202,7 +203,7 @@ func (r *artistRepository) getIndexKey(a model.Artist) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO Cache the index (recalculate when there are changes to the DB)
|
// TODO Cache the index (recalculate when there are changes to the DB)
|
||||||
func (r *artistRepository) GetIndex(roles ...model.Role) (model.ArtistIndexes, error) {
|
func (r *artistRepository) GetIndex(includeMissing bool, roles ...model.Role) (model.ArtistIndexes, error) {
|
||||||
options := model.QueryOptions{Sort: "name"}
|
options := model.QueryOptions{Sort: "name"}
|
||||||
if len(roles) > 0 {
|
if len(roles) > 0 {
|
||||||
roleFilters := slice.Map(roles, func(r model.Role) Sqlizer {
|
roleFilters := slice.Map(roles, func(r model.Role) Sqlizer {
|
||||||
@@ -210,6 +211,13 @@ func (r *artistRepository) GetIndex(roles ...model.Role) (model.ArtistIndexes, e
|
|||||||
})
|
})
|
||||||
options.Filters = And(roleFilters)
|
options.Filters = And(roleFilters)
|
||||||
}
|
}
|
||||||
|
if !includeMissing {
|
||||||
|
if options.Filters == nil {
|
||||||
|
options.Filters = Eq{"artist.missing": false}
|
||||||
|
} else {
|
||||||
|
options.Filters = And{options.Filters, Eq{"artist.missing": false}}
|
||||||
|
}
|
||||||
|
}
|
||||||
artists, err := r.GetAll(options)
|
artists, err := r.GetAll(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -236,6 +244,26 @@ func (r *artistRepository) purgeEmpty() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markMissing sets the Missing flag based on album data.
|
||||||
|
func (r *artistRepository) markMissing() (int64, error) {
|
||||||
|
q := Expr(`
|
||||||
|
update artist
|
||||||
|
set missing = not exists (
|
||||||
|
select 1 from album_artists aa
|
||||||
|
join album a on aa.album_id = a.id
|
||||||
|
where aa.artist_id = artist.id and a.missing = false
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
c, err := r.executeSQL(q)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("marking missing artists: %w", err)
|
||||||
|
}
|
||||||
|
if c > 0 {
|
||||||
|
log.Debug(r.ctx, "Marked missing artists", "totalUpdated", c)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshPlayCounts updates the play count and last play date annotations for all artists, based
|
// RefreshPlayCounts updates the play count and last play date annotations for all artists, based
|
||||||
// on the media files associated with them.
|
// on the media files associated with them.
|
||||||
func (r *artistRepository) RefreshPlayCounts() (int64, error) {
|
func (r *artistRepository) RefreshPlayCounts() (int64, error) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/conf/configtest"
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
@@ -94,7 +95,7 @@ var _ = Describe("ArtistRepository", func() {
|
|||||||
er := repo.Put(&artistBeatles)
|
er := repo.Put(&artistBeatles)
|
||||||
Expect(er).To(BeNil())
|
Expect(er).To(BeNil())
|
||||||
|
|
||||||
idx, err := repo.GetIndex()
|
idx, err := repo.GetIndex(false)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(idx).To(HaveLen(2))
|
Expect(idx).To(HaveLen(2))
|
||||||
Expect(idx[0].ID).To(Equal("F"))
|
Expect(idx[0].ID).To(Equal("F"))
|
||||||
@@ -112,7 +113,7 @@ var _ = Describe("ArtistRepository", func() {
|
|||||||
|
|
||||||
// BFR Empty SortArtistName is not saved in the DB anymore
|
// BFR Empty SortArtistName is not saved in the DB anymore
|
||||||
XIt("returns the index when PreferSortTags is true and SortArtistName is empty", func() {
|
XIt("returns the index when PreferSortTags is true and SortArtistName is empty", func() {
|
||||||
idx, err := repo.GetIndex()
|
idx, err := repo.GetIndex(false)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(idx).To(HaveLen(2))
|
Expect(idx).To(HaveLen(2))
|
||||||
Expect(idx[0].ID).To(Equal("B"))
|
Expect(idx[0].ID).To(Equal("B"))
|
||||||
@@ -134,7 +135,7 @@ var _ = Describe("ArtistRepository", func() {
|
|||||||
er := repo.Put(&artistBeatles)
|
er := repo.Put(&artistBeatles)
|
||||||
Expect(er).To(BeNil())
|
Expect(er).To(BeNil())
|
||||||
|
|
||||||
idx, err := repo.GetIndex()
|
idx, err := repo.GetIndex(false)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(idx).To(HaveLen(2))
|
Expect(idx).To(HaveLen(2))
|
||||||
Expect(idx[0].ID).To(Equal("B"))
|
Expect(idx[0].ID).To(Equal("B"))
|
||||||
@@ -151,7 +152,7 @@ var _ = Describe("ArtistRepository", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("returns the index when SortArtistName is empty", func() {
|
It("returns the index when SortArtistName is empty", func() {
|
||||||
idx, err := repo.GetIndex()
|
idx, err := repo.GetIndex(false)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(idx).To(HaveLen(2))
|
Expect(idx).To(HaveLen(2))
|
||||||
Expect(idx[0].ID).To(Equal("B"))
|
Expect(idx[0].ID).To(Equal("B"))
|
||||||
@@ -233,5 +234,91 @@ var _ = Describe("ArtistRepository", func() {
|
|||||||
Expect(m).ToNot(HaveKey("mbz_artist_id"))
|
Expect(m).ToNot(HaveKey("mbz_artist_id"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("Missing artist visibility", func() {
|
||||||
|
var raw *artistRepository
|
||||||
|
var missing model.Artist
|
||||||
|
|
||||||
|
insertMissing := func() {
|
||||||
|
missing = model.Artist{ID: "m1", Name: "Missing", OrderArtistName: "missing"}
|
||||||
|
Expect(repo.Put(&missing)).To(Succeed())
|
||||||
|
raw = repo.(*artistRepository)
|
||||||
|
_, err := raw.executeSQL(squirrel.Update(raw.tableName).Set("missing", true).Where(squirrel.Eq{"id": missing.ID}))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMissing := func() {
|
||||||
|
if raw != nil {
|
||||||
|
_, _ = raw.executeSQL(squirrel.Delete(raw.tableName).Where(squirrel.Eq{"id": missing.ID}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context("regular user", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx := log.NewContext(context.TODO())
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "u1"})
|
||||||
|
repo = NewArtistRepository(ctx, GetDBXBuilder())
|
||||||
|
insertMissing()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() { removeMissing() })
|
||||||
|
|
||||||
|
It("does not return missing artist in GetAll", func() {
|
||||||
|
artists, err := repo.GetAll(model.QueryOptions{Filters: squirrel.Eq{"artist.missing": false}})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(artists).To(HaveLen(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("does not return missing artist in Search", func() {
|
||||||
|
res, err := repo.Search("missing", 0, 10, false)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(res).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("does not return missing artist in GetIndex", func() {
|
||||||
|
idx, err := repo.GetIndex(false)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Only 2 artists should be present
|
||||||
|
total := 0
|
||||||
|
for _, ix := range idx {
|
||||||
|
total += len(ix.Artists)
|
||||||
|
}
|
||||||
|
Expect(total).To(Equal(2))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("admin user", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx := log.NewContext(context.TODO())
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "admin", IsAdmin: true})
|
||||||
|
repo = NewArtistRepository(ctx, GetDBXBuilder())
|
||||||
|
insertMissing()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() { removeMissing() })
|
||||||
|
|
||||||
|
It("returns missing artist in GetAll", func() {
|
||||||
|
artists, err := repo.GetAll()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(artists).To(HaveLen(3))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns missing artist in Search", func() {
|
||||||
|
res, err := repo.Search("missing", 0, 10, true)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(res).To(HaveLen(1))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns missing artist in GetIndex when included", func() {
|
||||||
|
idx, err := repo.GetIndex(true)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
total := 0
|
||||||
|
for _, ix := range idx {
|
||||||
|
total += len(ix.Artists)
|
||||||
|
}
|
||||||
|
Expect(total).To(Equal(3))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ func (s *SQLStore) GC(ctx context.Context) error {
|
|||||||
err := chain.RunSequentially(
|
err := chain.RunSequentially(
|
||||||
trace(ctx, "purge empty albums", func() error { return s.Album(ctx).(*albumRepository).purgeEmpty() }),
|
trace(ctx, "purge empty albums", func() error { return s.Album(ctx).(*albumRepository).purgeEmpty() }),
|
||||||
trace(ctx, "purge empty artists", func() error { return s.Artist(ctx).(*artistRepository).purgeEmpty() }),
|
trace(ctx, "purge empty artists", func() error { return s.Artist(ctx).(*artistRepository).purgeEmpty() }),
|
||||||
|
trace(ctx, "mark missing artists", func() error { _, err := s.Artist(ctx).(*artistRepository).markMissing(); return err }),
|
||||||
trace(ctx, "purge empty folders", func() error { return s.Folder(ctx).(*folderRepository).purgeEmpty() }),
|
trace(ctx, "purge empty folders", func() error { return s.Folder(ctx).(*folderRepository).purgeEmpty() }),
|
||||||
trace(ctx, "clean album annotations", func() error { return s.Album(ctx).(*albumRepository).cleanAnnotations() }),
|
trace(ctx, "clean album annotations", func() error { return s.Album(ctx).(*albumRepository).cleanAnnotations() }),
|
||||||
trace(ctx, "clean artist annotations", func() error { return s.Artist(ctx).(*artistRepository).cleanAnnotations() }),
|
trace(ctx, "clean artist annotations", func() error { return s.Artist(ctx).(*artistRepository).cleanAnnotations() }),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (api *Router) getArtist(r *http.Request, libId int, ifModifiedSince time.Ti
|
|||||||
|
|
||||||
var indexes model.ArtistIndexes
|
var indexes model.ArtistIndexes
|
||||||
if lib.LastScanAt.After(ifModifiedSince) {
|
if lib.LastScanAt.After(ifModifiedSince) {
|
||||||
indexes, err = api.ds.Artist(ctx).GetIndex(model.RoleAlbumArtist)
|
indexes, err = api.ds.Artist(ctx).GetIndex(false, model.RoleAlbumArtist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error retrieving Indexes", err)
|
log.Error(ctx, "Error retrieving Indexes", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import FavoriteIcon from '@material-ui/icons/Favorite'
|
|||||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { useDrag } from 'react-dnd'
|
import { useDrag } from 'react-dnd'
|
||||||
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
ArtistContextMenu,
|
ArtistContextMenu,
|
||||||
List,
|
List,
|
||||||
@@ -49,6 +50,9 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
missingRow: {
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
@@ -95,7 +99,15 @@ const ArtistDatagridRow = (props) => {
|
|||||||
}),
|
}),
|
||||||
[record],
|
[record],
|
||||||
)
|
)
|
||||||
return <DatagridRow ref={dragArtistRef} {...props} />
|
const classes = useStyles()
|
||||||
|
const computedClasses = clsx(
|
||||||
|
props.className,
|
||||||
|
classes.row,
|
||||||
|
record?.missing && classes.missingRow,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<DatagridRow ref={dragArtistRef} {...props} className={computedClasses} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArtistDatagridBody = (props) => (
|
const ArtistDatagridBody = (props) => (
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const mapResource = (resource, params) => {
|
|||||||
return [`playlist/${plsId}/tracks`, params]
|
return [`playlist/${plsId}/tracks`, params]
|
||||||
}
|
}
|
||||||
case 'album':
|
case 'album':
|
||||||
case 'song': {
|
case 'song':
|
||||||
|
case 'artist': {
|
||||||
if (params.filter && !isAdmin()) {
|
if (params.filter && !isAdmin()) {
|
||||||
params.filter.missing = false
|
params.filter.missing = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user