Add multiple genres to Albums

This commit is contained in:
Deluan 2021-07-16 17:15:34 -04:00 committed by Deluan Quintão
parent 39da741a80
commit 5e54925520
15 changed files with 102 additions and 79 deletions

View File

@ -22,6 +22,7 @@ type Album struct {
Duration float32 `json:"duration"` Duration float32 `json:"duration"`
Size int64 `json:"size"` Size int64 `json:"size"`
Genre string `json:"genre"` Genre string `json:"genre"`
Genres Genres `json:"genres"`
FullText string `json:"fullText"` FullText string `json:"fullText"`
SortAlbumName string `json:"sortAlbumName,omitempty"` SortAlbumName string `json:"sortAlbumName,omitempty"`
SortArtistName string `json:"sortArtistName,omitempty"` SortArtistName string `json:"sortArtistName,omitempty"`
@ -42,11 +43,11 @@ type Albums []Album
type AlbumRepository interface { type AlbumRepository interface {
CountAll(...QueryOptions) (int64, error) CountAll(...QueryOptions) (int64, error)
Exists(id string) (bool, error) Exists(id string) (bool, error)
Put(*Album) error
Get(id string) (*Album, error) Get(id string) (*Album, error)
FindByArtist(albumArtistId string) (Albums, error) FindByArtist(albumArtistId string) (Albums, error)
GetAll(...QueryOptions) (Albums, error) GetAll(...QueryOptions) (Albums, error)
GetRandom(...QueryOptions) (Albums, error) GetRandom(...QueryOptions) (Albums, error)
GetStarred(options ...QueryOptions) (Albums, error)
Search(q string, offset int, size int) (Albums, error) Search(q string, offset int, size int) (Albums, error)
Refresh(ids ...string) error Refresh(ids ...string) error
AnnotatedRepository AnnotatedRepository

View File

@ -47,7 +47,6 @@ type ArtistRepository interface {
Put(m *Artist) error Put(m *Artist) error
Get(id string) (*Artist, error) Get(id string) (*Artist, error)
GetAll(options ...QueryOptions) (Artists, error) GetAll(options ...QueryOptions) (Artists, error)
GetStarred(options ...QueryOptions) (Artists, error)
Search(q string, offset int, size int) (Artists, error) Search(q string, offset int, size int) (Artists, error)
Refresh(ids ...string) error Refresh(ids ...string) error
GetIndex() (ArtistIndexes, error) GetIndex() (ArtistIndexes, error)

View File

@ -68,7 +68,6 @@ type MediaFileRepository interface {
FindAllByPath(path string) (MediaFiles, error) FindAllByPath(path string) (MediaFiles, error)
FindByPath(path string) (*MediaFile, error) FindByPath(path string) (*MediaFile, error)
FindPathsRecursively(basePath string) ([]string, error) FindPathsRecursively(basePath string) ([]string, error)
GetStarred(options ...QueryOptions) (MediaFiles, error)
GetRandom(options ...QueryOptions) (MediaFiles, error) GetRandom(options ...QueryOptions) (MediaFiles, error)
Search(q string, offset int, size int) (MediaFiles, error) Search(q string, offset int, size int) (MediaFiles, error)
Delete(id string) error Delete(id string) error

View File

@ -89,7 +89,7 @@ func (r *albumRepository) Exists(id string) (bool, error) {
} }
func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder { func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation("album.id", options...).Columns("*") return r.newSelectWithAnnotation("album.id", options...).Columns("album.*")
} }
func (r *albumRepository) Get(id string) (*model.Album, error) { func (r *albumRepository) Get(id string) (*model.Album, error) {
@ -101,30 +101,51 @@ func (r *albumRepository) Get(id string) (*model.Album, error) {
if len(res) == 0 { if len(res) == 0 {
return nil, model.ErrNotFound return nil, model.ErrNotFound
} }
return &res[0], nil err := r.loadAlbumGenres(&res)
return &res[0], err
}
func (r *albumRepository) Put(m *model.Album) error {
genres := m.Genres
m.Genres = nil
defer func() { m.Genres = genres }()
_, err := r.put(m.ID, m)
if err != nil {
return err
}
return r.updateGenres(m.ID, r.tableName, genres)
} }
func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) { func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) {
sq := r.selectAlbum().Where(Eq{"album_artist_id": artistId}).OrderBy("max_year") options := model.QueryOptions{
res := model.Albums{} Sort: "max_year",
err := r.queryAll(sq, &res) Filters: Eq{"album_artist_id": artistId},
return res, err }
return r.GetAll(options)
} }
func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, error) { func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...) sq := r.selectAlbum(options...).
LeftJoin("album_genres ag on album.id = ag.album_id").
LeftJoin("genre on ag.genre_id = genre.id").
GroupBy("album.id")
res := model.Albums{} res := model.Albums{}
err := r.queryAll(sq, &res) err := r.queryAll(sq, &res)
if err != nil {
return nil, err
}
err = r.loadAlbumGenres(&res)
return res, err return res, err
} }
// TODO Keep order when paginating // TODO Keep order when paginating
func (r *albumRepository) GetRandom(options ...model.QueryOptions) (model.Albums, error) { func (r *albumRepository) GetRandom(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...) if len(options) == 0 {
sq = sq.OrderBy("RANDOM()") options = []model.QueryOptions{{}}
results := model.Albums{} }
err := r.queryAll(sq, &results) options[0].Sort = "random()"
return results, err return r.GetAll(options...)
} }
// Return a map of mediafiles that have embedded covers for the given album ids // Return a map of mediafiles that have embedded covers for the given album ids
@ -164,6 +185,7 @@ type refreshAlbum struct {
SongArtists string SongArtists string
SongArtistIds string SongArtistIds string
AlbumArtistIds string AlbumArtistIds string
GenreIds string
Years string Years string
DiscSubtitles string DiscSubtitles string
Comments string Comments string
@ -190,9 +212,11 @@ func (r *albumRepository) refresh(ids ...string) error {
group_concat(f.artist, ' ') as song_artists, group_concat(f.artist, ' ') as song_artists,
group_concat(f.artist_id, ' ') as song_artist_ids, group_concat(f.artist_id, ' ') as song_artist_ids,
group_concat(f.album_artist_id, ' ') as album_artist_ids, group_concat(f.album_artist_id, ' ') as album_artist_ids,
group_concat(f.year, ' ') as years`). group_concat(f.year, ' ') as years,
group_concat(mg.genre_id, ' ') as genre_ids`).
From("media_file f"). From("media_file f").
LeftJoin("album a on f.album_id = a.id"). LeftJoin("album a on f.album_id = a.id").
LeftJoin("media_file_genres mg on mg.media_file_id = f.id").
Where(Eq{"f.album_id": ids}).GroupBy("f.album_id") Where(Eq{"f.album_id": ids}).GroupBy("f.album_id")
err := r.queryAll(sel, &albums) err := r.queryAll(sel, &albums)
if err != nil { if err != nil {
@ -246,7 +270,8 @@ func (r *albumRepository) refresh(ids ...string) error {
al.AllArtistIDs = utils.SanitizeStrings(al.SongArtistIds, al.AlbumArtistID, al.ArtistID) al.AllArtistIDs = utils.SanitizeStrings(al.SongArtistIds, al.AlbumArtistID, al.ArtistID)
al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists, al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists,
al.SortAlbumName, al.SortArtistName, al.SortAlbumArtistName, al.DiscSubtitles) al.SortAlbumName, al.SortArtistName, al.SortAlbumArtistName, al.DiscSubtitles)
_, err := r.put(al.ID, al.Album) al.Genres = getGenres(al.GenreIds)
err := r.Put(&al.Album)
if err != nil { if err != nil {
return err return err
} }
@ -260,6 +285,20 @@ func (r *albumRepository) refresh(ids ...string) error {
return err return err
} }
func getGenres(genreIds string) model.Genres {
ids := strings.Fields(genreIds)
var genres model.Genres
unique := map[string]struct{}{}
for _, id := range ids {
if _, ok := unique[id]; ok {
continue
}
genres = append(genres, model.Genre{ID: id})
unique[id] = struct{}{}
}
return genres
}
func getAlbumArtist(al refreshAlbum) (id, name string) { func getAlbumArtist(al refreshAlbum) (id, name string) {
if !al.Compilation { if !al.Compilation {
if al.AlbumArtist != "" { if al.AlbumArtist != "" {
@ -358,13 +397,6 @@ func (r *albumRepository) purgeEmpty() error {
return err return err
} }
func (r *albumRepository) GetStarred(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...).Where("starred = true")
starred := model.Albums{}
err := r.queryAll(sq, &starred)
return starred, err
}
func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) { func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) {
results := model.Albums{} results := model.Albums{}
err := r.doSearch(q, offset, size, &results, "name") err := r.doSearch(q, offset, size, &results, "name")

View File

@ -62,14 +62,6 @@ var _ = Describe("AlbumRepository", func() {
}) })
}) })
Describe("GetStarred", func() {
It("returns all starred records", func() {
Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Albums{
albumRadioactivity,
}))
})
})
Describe("FindByArtist", func() { Describe("FindByArtist", func() {
It("returns all records from a given ArtistID", func() { It("returns all records from a given ArtistID", func() {
Expect(repo.FindByArtist("3")).To(Equal(model.Albums{ Expect(repo.FindByArtist("3")).To(Equal(model.Albums{

View File

@ -213,14 +213,6 @@ func (r *artistRepository) refresh(ids ...string) error {
return err return err
} }
func (r *artistRepository) GetStarred(options ...model.QueryOptions) (model.Artists, error) {
sq := r.selectArtist(options...).Where("starred = true")
var dba []dbArtist
err := r.queryAll(sq, &dba)
starred := r.toModels(dba)
return starred, err
}
func (r *artistRepository) purgeEmpty() error { func (r *artistRepository) purgeEmpty() error {
del := Delete(r.tableName).Where("id not in (select distinct(album_artist_id) from album)") del := Delete(r.tableName).Where("id not in (select distinct(album_artist_id) from album)")
c, err := r.executeSQL(del) c, err := r.executeSQL(del)

View File

@ -42,14 +42,6 @@ var _ = Describe("ArtistRepository", func() {
}) })
}) })
Describe("GetStarred", func() {
It("returns all starred records", func() {
Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Artists{
artistBeatles,
}))
})
})
Describe("GetIndex", func() { Describe("GetIndex", func() {
It("returns the index", func() { It("returns the index", func() {
idx, err := repo.GetIndex() idx, err := repo.GetIndex()

View File

@ -25,11 +25,10 @@ func NewGenreRepository(ctx context.Context, o orm.Ormer) model.GenreRepository
func (r *genreRepository) GetAll() (model.Genres, error) { func (r *genreRepository) GetAll() (model.Genres, error) {
sq := Select("*", sq := Select("*",
"(select count(1) from album where album.genre = genre.name) as album_count", "count(distinct a.album_id) as album_count",
"count(distinct f.media_file_id) as song_count"). "count(distinct f.media_file_id) as song_count").
From(r.tableName). From(r.tableName).
// TODO Use relation table LeftJoin("album_genres a on a.genre_id = genre.id").
// LeftJoin("album_genres a on a.genre_id = genre.id").
LeftJoin("media_file_genres f on f.genre_id = genre.id"). LeftJoin("media_file_genres f on f.genre_id = genre.id").
GroupBy("genre.id") GroupBy("genre.id")
res := model.Genres{} res := model.Genres{}

View File

@ -23,7 +23,7 @@ var _ = Describe("GenreRepository", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(genres).To(ConsistOf( Expect(genres).To(ConsistOf(
model.Genre{ID: "gn-1", Name: "Electronic", AlbumCount: 1, SongCount: 2}, model.Genre{ID: "gn-1", Name: "Electronic", AlbumCount: 1, SongCount: 2},
model.Genre{ID: "gn-2", Name: "Rock", AlbumCount: 2, SongCount: 3}, model.Genre{ID: "gn-2", Name: "Rock", AlbumCount: 3, SongCount: 3},
)) ))
}) })
}) })

View File

@ -161,14 +161,6 @@ func (r *mediaFileRepository) deleteNotInPath(basePath string) error {
return err return err
} }
func (r *mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.MediaFiles, error) {
if len(options) == 0 {
options = []model.QueryOptions{{}}
}
options[0].Filters = Eq{"starred": true}
return r.GetAll(options...)
}
// TODO Keep order when paginating // TODO Keep order when paginating
func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) { func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
if len(options) == 0 { if len(options) == 0 {

View File

@ -86,12 +86,6 @@ var _ = Describe("MediaRepository", func() {
Expect(found[0].ID).To(Equal("7004")) Expect(found[0].ID).To(Equal("7004"))
}) })
It("returns starred tracks", func() {
Expect(mr.GetStarred()).To(Equal(model.MediaFiles{
songComeTogether,
}))
})
It("delete tracks by id", func() { It("delete tracks by id", func() {
id := uuid.NewString() id := uuid.NewString()
Expect(mr.Put(&model.MediaFile{ID: id})).To(BeNil()) Expect(mr.Put(&model.MediaFile{ID: id})).To(BeNil())

View File

@ -46,9 +46,9 @@ var (
) )
var ( var (
albumSgtPeppers = model.Album{ID: "101", Name: "Sgt Peppers", Artist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: " beatles peppers sgt the"} albumSgtPeppers = model.Album{ID: "101", Name: "Sgt Peppers", Artist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: " beatles peppers sgt the"}
albumAbbeyRoad = model.Album{ID: "102", Name: "Abbey Road", Artist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: " abbey beatles road the"} albumAbbeyRoad = model.Album{ID: "102", Name: "Abbey Road", Artist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: " abbey beatles road the"}
albumRadioactivity = model.Album{ID: "103", Name: "Radioactivity", Artist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: " kraftwerk radioactivity"} albumRadioactivity = model.Album{ID: "103", Name: "Radioactivity", Artist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", Genre: "Electronic", Genres: model.Genres{genreElectronic, genreRock}, CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: " kraftwerk radioactivity"}
testAlbums = model.Albums{ testAlbums = model.Albums{
albumSgtPeppers, albumSgtPeppers,
albumAbbeyRoad, albumAbbeyRoad,
@ -115,7 +115,7 @@ var _ = Describe("Initialize test DB", func() {
alr := NewAlbumRepository(ctx, o).(*albumRepository) alr := NewAlbumRepository(ctx, o).(*albumRepository)
for i := range testAlbums { for i := range testAlbums {
a := testAlbums[i] a := testAlbums[i]
_, err := alr.put(a.ID, &a) err := alr.Put(&a)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -54,3 +54,30 @@ func (r *sqlRepository) loadMediaFileGenres(mfs *model.MediaFiles) error {
} }
return nil return nil
} }
func (r *sqlRepository) loadAlbumGenres(mfs *model.Albums) error {
var ids []string
m := map[string]*model.Album{}
for i := range *mfs {
mf := &(*mfs)[i]
ids = append(ids, mf.ID)
m[mf.ID] = mf
}
sql := Select("g.*", "ag.album_id").From("genre g").Join("album_genres ag on ag.genre_id = g.id").
Where(Eq{"ag.album_id": ids}).OrderBy("ag.album_id", "ag.rowid")
var genres []struct {
model.Genre
AlbumId string
}
err := r.queryAll(sql, &genres)
if err != nil {
return err
}
for _, g := range genres {
mf := m[g.AlbumId]
mf.Genres = append(mf.Genres, g.Genre)
}
return nil
}

View File

@ -62,7 +62,7 @@ func (c *AlbumListController) getAlbumList(r *http.Request) (model.Albums, error
opts.Offset = utils.ParamInt(r, "offset", 0) opts.Offset = utils.ParamInt(r, "offset", 0)
opts.Max = utils.MinInt(utils.ParamInt(r, "size", 10), 500) opts.Max = utils.MinInt(utils.ParamInt(r, "size", 10), 500)
albums, err := c.ds.Album(r.Context()).GetAll(model.QueryOptions(opts)) albums, err := c.ds.Album(r.Context()).GetAll(opts)
if err != nil { if err != nil {
log.Error(r, "Error retrieving albums", "error", err) log.Error(r, "Error retrieving albums", "error", err)
@ -96,18 +96,18 @@ func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Reque
func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
options := model.QueryOptions{Sort: "starred_at", Order: "desc"} options := filter.Starred()
artists, err := c.ds.Artist(ctx).GetStarred(options) artists, err := c.ds.Artist(ctx).GetAll(options)
if err != nil { if err != nil {
log.Error(r, "Error retrieving starred artists", "error", err) log.Error(r, "Error retrieving starred artists", "error", err)
return nil, err return nil, err
} }
albums, err := c.ds.Album(ctx).GetStarred(options) albums, err := c.ds.Album(ctx).GetAll(options)
if err != nil { if err != nil {
log.Error(r, "Error retrieving starred albums", "error", err) log.Error(r, "Error retrieving starred albums", "error", err)
return nil, err return nil, err
} }
mediaFiles, err := c.ds.MediaFile(ctx).GetStarred(options) mediaFiles, err := c.ds.MediaFile(ctx).GetAll(options)
if err != nil { if err != nil {
log.Error(r, "Error retrieving starred mediaFiles", "error", err) log.Error(r, "Error retrieving starred mediaFiles", "error", err)
return nil, err return nil, err
@ -196,5 +196,5 @@ func (c *AlbumListController) GetSongsByGenre(w http.ResponseWriter, r *http.Req
func (c *AlbumListController) getSongs(ctx context.Context, offset, size int, opts filter.Options) (model.MediaFiles, error) { func (c *AlbumListController) getSongs(ctx context.Context, offset, size int, opts filter.Options) (model.MediaFiles, error) {
opts.Offset = offset opts.Offset = offset
opts.Max = size opts.Max = size
return c.ds.MediaFile(ctx).GetAll(model.QueryOptions(opts)) return c.ds.MediaFile(ctx).GetAll(opts)
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
) )
type Options model.QueryOptions type Options = model.QueryOptions
func AlbumsByNewest() Options { func AlbumsByNewest() Options {
return Options{Sort: "recently_added", Order: "desc"} return Options{Sort: "recently_added", Order: "desc"}
@ -43,8 +43,8 @@ func AlbumsByRating() Options {
func AlbumsByGenre(genre string) Options { func AlbumsByGenre(genre string) Options {
return Options{ return Options{
Sort: "genre asc, name asc", Sort: "genre.name asc, name asc",
Filters: squirrel.Eq{"genre": genre}, Filters: squirrel.Eq{"genre.name": genre},
} }
} }
@ -93,3 +93,7 @@ func SongsByRandom(genre string, fromYear, toYear int) Options {
options.Filters = ff options.Filters = ff
return options return options
} }
func Starred() Options {
return Options{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
}