Skip to content

Commit

Permalink
Add multiple genres to MediaFile
Browse files Browse the repository at this point in the history
  • Loading branch information
deluan committed Jul 20, 2021
1 parent 7cd3a8b commit 39da741
Show file tree
Hide file tree
Showing 21 changed files with 309 additions and 72 deletions.
2 changes: 1 addition & 1 deletion conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func init() {
viper.SetDefault("reverseproxywhitelist", "")

viper.SetDefault("scanner.extractor", "taglib")
viper.SetDefault("scanner.genreseparators", ";/")
viper.SetDefault("scanner.genreseparators", ";/,")

viper.SetDefault("agents", "lastfm,spotify")
viper.SetDefault("lastfm.enabled", true)
Expand Down
4 changes: 2 additions & 2 deletions model/genres.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package model
type Genre struct {
ID string `json:"id" orm:"column(id)"`
Name string
SongCount int
AlbumCount int
SongCount int `json:"-"`
AlbumCount int `json:"-"`
}

type Genres []Genre
Expand Down
1 change: 1 addition & 0 deletions model/mediafile.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type MediaFile struct {
Duration float32 `json:"duration"`
BitRate int `json:"bitRate"`
Genre string `json:"genre"`
Genres Genres `json:"genres"`
FullText string `json:"fullText"`
SortTitle string `json:"sortTitle,omitempty"`
SortAlbumName string `json:"sortAlbumName,omitempty"`
Expand Down
13 changes: 10 additions & 3 deletions persistence/genre_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,22 @@ func NewGenreRepository(ctx context.Context, o orm.Ormer) model.GenreRepository
}

func (r *genreRepository) GetAll() (model.Genres, error) {
sq := Select("genre as name", "count(distinct album_id) as album_count", "count(distinct id) as song_count").
From("media_file").GroupBy("genre")
sq := Select("*",
"(select count(1) from album where album.genre = genre.name) as album_count",
"count(distinct f.media_file_id) as song_count").
From(r.tableName).
// TODO Use relation table
// LeftJoin("album_genres a on a.genre_id = genre.id").
LeftJoin("media_file_genres f on f.genre_id = genre.id").
GroupBy("genre.id")
res := model.Genres{}
err := r.queryAll(sq, &res)
return res, err
}

func (r *genreRepository) Put(m *model.Genre) error {
_, err := r.put(m.ID, m)
id, err := r.put(m.ID, m)
m.ID = id
return err
}

Expand Down
6 changes: 4 additions & 2 deletions persistence/genre_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ var _ = Describe("GenreRepository", func() {
It("returns all records", func() {
genres, err := repo.GetAll()
Expect(err).To(BeNil())
Expect(genres).To(ContainElement(model.Genre{Name: "Rock", AlbumCount: 2, SongCount: 2}))
Expect(genres).To(ContainElement(model.Genre{Name: "Electronic", AlbumCount: 1, SongCount: 2}))
Expect(genres).To(ConsistOf(
model.Genre{ID: "gn-1", Name: "Electronic", AlbumCount: 1, SongCount: 2},
model.Genre{ID: "gn-2", Name: "Rock", AlbumCount: 2, SongCount: 3},
))
})
})
96 changes: 56 additions & 40 deletions persistence/mediafile_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,53 +37,68 @@ func NewMediaFileRepository(ctx context.Context, o orm.Ormer) *mediaFileReposito
return r
}

func (r mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
return r.count(r.newSelectWithAnnotation("media_file.id"), options...)
}

func (r mediaFileRepository) Exists(id string) (bool, error) {
func (r *mediaFileRepository) Exists(id string) (bool, error) {
return r.exists(Select().Where(Eq{"id": id}))
}

func (r mediaFileRepository) Put(m *model.MediaFile) error {
func (r *mediaFileRepository) Put(m *model.MediaFile) error {
m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist,
m.SortTitle, m.SortAlbumName, m.SortArtistName, m.SortAlbumArtistName, m.DiscSubtitle)
genres := m.Genres
m.Genres = nil
defer func() { m.Genres = genres }()
_, err := r.put(m.ID, m)
return err
if err != nil {
return err
}
return r.updateGenres(m.ID, r.tableName, genres)
}

func (r mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
sql := r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
return r.withBookmark(sql, "media_file.id")
}

func (r mediaFileRepository) Get(id string) (*model.MediaFile, error) {
sel := r.selectMediaFile().Where(Eq{"id": id})
func (r *mediaFileRepository) Get(id string) (*model.MediaFile, error) {
sel := r.selectMediaFile().Where(Eq{"media_file.id": id})
var res model.MediaFiles
if err := r.queryAll(sel, &res); err != nil {
return nil, err
}
if len(res) == 0 {
return nil, model.ErrNotFound
}
return &res[0], nil
err := r.loadMediaFileGenres(&res)
return &res[0], err
}

func (r mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.selectMediaFile(options...)
func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.selectMediaFile(options...).
LeftJoin("media_file_genres mfg on media_file.id = mfg.media_file_id").
LeftJoin("genre on mfg.genre_id = genre.id").
GroupBy("media_file.id")
res := model.MediaFiles{}
err := r.queryAll(sq, &res)
if err != nil {
return nil, err
}
err = r.loadMediaFileGenres(&res)
return res, err
}

func (r mediaFileRepository) FindByAlbum(albumId string) (model.MediaFiles, error) {
sel := r.selectMediaFile(model.QueryOptions{Sort: "album"}).Where(Eq{"album_id": albumId})
res := model.MediaFiles{}
err := r.queryAll(sel, &res)
return res, err
func (r *mediaFileRepository) FindByAlbum(albumId string) (model.MediaFiles, error) {
options := model.QueryOptions{
Filters: Eq{"album_id": albumId},
Sort: "album",
}
return r.GetAll(options)
}

func (r mediaFileRepository) FindByPath(path string) (*model.MediaFile, error) {
func (r *mediaFileRepository) FindByPath(path string) (*model.MediaFile, error) {
sel := r.selectMediaFile().Where(Eq{"path": path})
var res model.MediaFiles
if err := r.queryAll(sel, &res); err != nil {
Expand All @@ -109,7 +124,7 @@ func pathStartsWith(path string) Eq {
}

// FindAllByPath only return mediafiles that are direct children of requested path
func (r mediaFileRepository) FindAllByPath(path string) (model.MediaFiles, error) {
func (r *mediaFileRepository) FindAllByPath(path string) (model.MediaFiles, error) {
// Query by path based on https://stackoverflow.com/a/13911906/653632
path = cleanPath(path)
pathLen := utf8.RuneCountInString(path)
Expand All @@ -124,7 +139,7 @@ func (r mediaFileRepository) FindAllByPath(path string) (model.MediaFiles, error
}

// FindPathsRecursively returns a list of all subfolders of basePath, recursively
func (r mediaFileRepository) FindPathsRecursively(basePath string) ([]string, error) {
func (r *mediaFileRepository) FindPathsRecursively(basePath string) ([]string, error) {
path := cleanPath(basePath)
// Query based on https://stackoverflow.com/a/38330814/653632
sel := r.newSelect().Columns(fmt.Sprintf("distinct rtrim(path, replace(path, '%s', ''))", string(os.PathSeparator))).
Expand All @@ -134,7 +149,7 @@ func (r mediaFileRepository) FindPathsRecursively(basePath string) ([]string, er
return res, err
}

func (r mediaFileRepository) deleteNotInPath(basePath string) error {
func (r *mediaFileRepository) deleteNotInPath(basePath string) error {
path := cleanPath(basePath)
sel := Delete(r.tableName).Where(NotEq(pathStartsWith(path)))
c, err := r.executeSQL(sel)
Expand All @@ -146,28 +161,29 @@ func (r mediaFileRepository) deleteNotInPath(basePath string) error {
return err
}

func (r mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.selectMediaFile(options...).Where("starred = true")
starred := model.MediaFiles{}
err := r.queryAll(sq, &starred)
return starred, 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
func (r mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.selectMediaFile(options...)
sq = sq.OrderBy("RANDOM()")
results := model.MediaFiles{}
err := r.queryAll(sq, &results)
return results, err
func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
if len(options) == 0 {
options = []model.QueryOptions{{}}
}
options[0].Sort = "random()"
return r.GetAll(options...)
}

func (r mediaFileRepository) Delete(id string) error {
func (r *mediaFileRepository) Delete(id string) error {
return r.delete(Eq{"id": id})
}

// DeleteByPath delete from the DB all mediafiles that are direct children of path
func (r mediaFileRepository) DeleteByPath(basePath string) (int64, error) {
func (r *mediaFileRepository) DeleteByPath(basePath string) (int64, error) {
path := cleanPath(basePath)
pathLen := utf8.RuneCountInString(path)
del := Delete(r.tableName).
Expand All @@ -177,39 +193,39 @@ func (r mediaFileRepository) DeleteByPath(basePath string) (int64, error) {
return r.executeSQL(del)
}

func (r mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) {
func (r *mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) {
results := model.MediaFiles{}
err := r.doSearch(q, offset, size, &results, "title")
return results, err
}

func (r mediaFileRepository) Count(options ...rest.QueryOptions) (int64, error) {
func (r *mediaFileRepository) Count(options ...rest.QueryOptions) (int64, error) {
return r.CountAll(r.parseRestOptions(options...))
}

func (r mediaFileRepository) Read(id string) (interface{}, error) {
func (r *mediaFileRepository) Read(id string) (interface{}, error) {
return r.Get(id)
}

func (r mediaFileRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
func (r *mediaFileRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
return r.GetAll(r.parseRestOptions(options...))
}

func (r mediaFileRepository) EntityName() string {
func (r *mediaFileRepository) EntityName() string {
return "mediafile"
}

func (r mediaFileRepository) NewInstance() interface{} {
func (r *mediaFileRepository) NewInstance() interface{} {
return &model.MediaFile{}
}

func (r mediaFileRepository) Save(entity interface{}) (string, error) {
func (r *mediaFileRepository) Save(entity interface{}) (string, error) {
mf := entity.(*model.MediaFile)
err := r.Put(mf)
return mf.ID, err
}

func (r mediaFileRepository) Update(entity interface{}, cols ...string) error {
func (r *mediaFileRepository) Update(entity interface{}, cols ...string) error {
mf := entity.(*model.MediaFile)
return r.Put(mf)
}
Expand Down
12 changes: 12 additions & 0 deletions persistence/mediafile_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"time"

"github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/google/uuid"
"github.com/navidrome/navidrome/log"
Expand Down Expand Up @@ -149,6 +150,17 @@ var _ = Describe("MediaRepository", func() {
Expect(mr.FindAllByPath(P("/music/overlap"))).To(HaveLen(1))
})

It("filters by genre", func() {
Expect(mr.GetAll(model.QueryOptions{
Sort: "genre.name asc, title asc",
Filters: squirrel.Eq{"genre.name": "Rock"},
})).To(Equal(model.MediaFiles{
songDayInALife,
songAntenna,
songComeTogether,
}))
})

Context("Annotations", func() {
It("increments play count when the tracks does not have annotations", func() {
id := "incplay.firsttime"
Expand Down
26 changes: 21 additions & 5 deletions persistence/persistence_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ func TestPersistence(t *testing.T) {
conf.Server.DbPath = "file::memory:?cache=shared"
_ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
db.EnsureLatestVersion()
log.SetLevel(log.LevelCritical)
log.SetLevel(log.LevelError)
RegisterFailHandler(Fail)
RunSpecs(t, "Persistence Suite")
}

var (
genreElectronic = model.Genre{ID: "gn-1", Name: "Electronic"}
genreRock = model.Genre{ID: "gn-2", Name: "Rock"}
testGenres = model.Genres{genreElectronic, genreRock}
)

var (
artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", AlbumCount: 1, FullText: " kraftwerk"}
artistBeatles = model.Artist{ID: "3", Name: "The Beatles", AlbumCount: 2, FullText: " beatles the"}
Expand All @@ -51,10 +57,10 @@ var (
)

var (
songDayInALife = model.MediaFile{ID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3"), FullText: " a beatles day in life peppers sgt the"}
songComeTogether = model.MediaFile{ID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Genre: "Rock", Path: P("/beatles/1/come together.mp3"), FullText: " abbey beatles come road the together"}
songRadioactivity = model.MediaFile{ID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3"), FullText: " kraftwerk radioactivity"}
songAntenna = model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Genre: "Electronic", Path: P("/kraft/radio/antenna.mp3"), FullText: " antenna kraftwerk"}
songDayInALife = model.MediaFile{ID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/sgt/a day.mp3"), FullText: " a beatles day in life peppers sgt the"}
songComeTogether = model.MediaFile{ID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Genre: "Rock", Genres: model.Genres{genreRock}, Path: P("/beatles/1/come together.mp3"), FullText: " abbey beatles come road the together"}
songRadioactivity = model.MediaFile{ID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Genre: "Electronic", Genres: model.Genres{genreElectronic}, Path: P("/kraft/radio/radio.mp3"), FullText: " kraftwerk radioactivity"}
songAntenna = model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Genre: "Electronic", Genres: model.Genres{genreElectronic, genreRock}, Path: P("/kraft/radio/antenna.mp3"), FullText: " antenna kraftwerk"}
testSongs = model.MediaFiles{
songDayInALife,
songComeTogether,
Expand Down Expand Up @@ -87,6 +93,16 @@ var _ = Describe("Initialize test DB", func() {
o := orm.NewOrm()
ctx := log.NewContext(context.TODO())
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid"})

gr := NewGenreRepository(ctx, o)
for i := range testGenres {
g := testGenres[i]
err := gr.Put(&g)
if err != nil {
panic(err)
}
}

mr := NewMediaFileRepository(ctx, o)
for i := range testSongs {
s := testSongs[i]
Expand Down
1 change: 1 addition & 0 deletions persistence/playlist_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func (r *playlistRepository) loadTracks(pls *model.Playlist) error {
if err != nil {
log.Error("Error loading playlist tracks", "playlist", pls.Name, "id", pls.ID)
}
err = r.loadMediaFileGenres(&pls.Tracks)
return err
}

Expand Down
2 changes: 1 addition & 1 deletion persistence/playqueue_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (r *playQueueRepository) loadTracks(tracks model.MediaFiles) model.MediaFil
mfRepo := NewMediaFileRepository(r.ctx, r.ormer)
trackMap := map[string]model.MediaFile{}
for i := range chunks {
idsFilter := Eq{"id": chunks[i]}
idsFilter := Eq{"media_file.id": chunks[i]}
tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter})
if err != nil {
u := loggedUser(r.ctx)
Expand Down
6 changes: 3 additions & 3 deletions persistence/playqueue_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ var _ = Describe("PlayQueueRepository", func() {

By("Storing a new playqueue for the same user")

new := aPlayQueue("user1", songRadioactivity.ID, 321, songAntenna, songRadioactivity)
Expect(repo.Store(new)).To(BeNil())
another := aPlayQueue("user1", songRadioactivity.ID, 321, songAntenna, songRadioactivity)
Expect(repo.Store(another)).To(BeNil())

actual, err = repo.Retrieve("user1")
Expect(err).To(BeNil())

AssertPlayQueue(new, actual)
AssertPlayQueue(another, actual)
Expect(countPlayQueues(repo, "user1")).To(Equal(1))
})
})
Expand Down

0 comments on commit 39da741

Please sign in to comment.