diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 1b544cf..340e0a8 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -30,6 +30,7 @@ type Controller struct { CachePath string CoverCachePath string PodcastsPath string + MusicPaths []string Jukebox *jukebox.Jukebox Scrobblers []scrobble.Scrobbler Podcasts *podcasts.Podcasts @@ -116,3 +117,14 @@ func (c *Controller) HR(h handlerSubsonicRaw) http.Handler { } }) } + +func (c *Controller) getMusicFolder(p params.Params) string { + idx, err := p.GetInt("musicFolderId") + if err != nil { + return "" + } + if idx < 0 || idx > len(c.MusicPaths) { + return "" + } + return c.MusicPaths[idx] +} diff --git a/server/ctrlsubsonic/ctrl_test.go b/server/ctrlsubsonic/ctrl_test.go index 862e26a..aa36bfa 100644 --- a/server/ctrlsubsonic/ctrl_test.go +++ b/server/ctrlsubsonic/ctrl_test.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "path" + "path/filepath" "regexp" "strings" "testing" @@ -110,8 +111,13 @@ func makec(t *testing.T, roots []string) (*Controller, *mockfs.MockFS) { m.ResetDates() m.LogAlbums() + var absRoots []string + for _, root := range roots { + absRoots = append(absRoots, filepath.Join(m.TmpDir(), root)) + } + base := &ctrlbase.Controller{DB: m.DB()} - return &Controller{Controller: base}, m + return &Controller{Controller: base, MusicPaths: absRoots}, m } func TestMain(m *testing.M) { diff --git a/server/ctrlsubsonic/handlers_by_folder.go b/server/ctrlsubsonic/handlers_by_folder.go index 04ccc2d..3679f30 100644 --- a/server/ctrlsubsonic/handlers_by_folder.go +++ b/server/ctrlsubsonic/handlers_by_folder.go @@ -24,7 +24,7 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response { Select("id"). Model(&db.Album{}). Where("parent_id IS NULL") - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { rootQ = rootQ. Where("root_dir=?", m) } @@ -145,7 +145,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response { return spec.NewError(10, "unknown value `%s` for parameter 'type'", v) } - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("root_dir=?", m) } var folders []*db.Album @@ -185,7 +185,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { Select("id"). Model(&db.Album{}). Where("parent_id IS NULL") - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { rootQ = rootQ.Where("root_dir=?", m) } @@ -207,7 +207,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { Where(`tag_artist_id IS NOT NULL AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, query, query). Offset(params.GetOrInt("albumOffset", 0)). Limit(params.GetOrInt("albumCount", 20)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("root_dir=?", m) } if err := q.Find(&albums).Error; err != nil { @@ -224,7 +224,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { Where("filename LIKE ? OR filename_u_dec LIKE ?", query, query). Offset(params.GetOrInt("songOffset", 0)). Limit(params.GetOrInt("songCount", 20)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q. Joins("JOIN albums ON albums.id=tracks.album_id"). Where("albums.root_dir=?", m) diff --git a/server/ctrlsubsonic/handlers_by_folder_test.go b/server/ctrlsubsonic/handlers_by_folder_test.go index 8c5c6e7..4cfa4f3 100644 --- a/server/ctrlsubsonic/handlers_by_folder_test.go +++ b/server/ctrlsubsonic/handlers_by_folder_test.go @@ -2,7 +2,6 @@ package ctrlsubsonic import ( "net/url" - "path/filepath" "testing" _ "github.com/jinzhu/gorm/dialects/sqlite" @@ -14,8 +13,8 @@ func TestGetIndexes(t *testing.T) { runQueryCases(t, contr, contr.ServeGetIndexes, []*queryCase{ {url.Values{}, "no_args", false}, - {url.Values{"musicFolderId": {filepath.Join(m.TmpDir(), "m-0")}}, "with_music_folder_1", false}, - {url.Values{"musicFolderId": {filepath.Join(m.TmpDir(), "m-1")}}, "with_music_folder_2", false}, + {url.Values{"musicFolderId": {"0"}}, "with_music_folder_1", false}, + {url.Values{"musicFolderId": {"1"}}, "with_music_folder_2", false}, }) } diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index bc80032..9f0c5ef 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -23,7 +23,7 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response { Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id"). Group("artists.id"). Order("artists.name COLLATE NOCASE") - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("sub.root_dir=?", m) } if err := q.Find(&artists).Error; err != nil { @@ -148,7 +148,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response { default: return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType) } - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("root_dir=?", m) } var albums []*db.Album @@ -188,7 +188,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { Where("name LIKE ? OR name_u_dec LIKE ?", query, query). Offset(params.GetOrInt("artistOffset", 0)). Limit(params.GetOrInt("artistCount", 20)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q. Joins("JOIN albums ON albums.tag_artist_id=artists.id"). Where("albums.root_dir=?", m) @@ -207,7 +207,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query). Offset(params.GetOrInt("albumOffset", 0)). Limit(params.GetOrInt("albumCount", 20)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("root_dir=?", m) } if err := q.Find(&albums).Error; err != nil { @@ -224,7 +224,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query). Offset(params.GetOrInt("songOffset", 0)). Limit(params.GetOrInt("songCount", 20)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q. Joins("JOIN albums ON albums.id=tracks.album_id"). Where("albums.root_dir=?", m) @@ -344,7 +344,7 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response { Preload("Album"). Offset(params.GetOrInt("offset", 0)). Limit(params.GetOrInt("count", 10)) - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("albums.root_dir=?", m) } if err := q.Find(&tracks).Error; err != nil { diff --git a/server/ctrlsubsonic/handlers_by_tags_test.go b/server/ctrlsubsonic/handlers_by_tags_test.go index 90918e2..f05cc2f 100644 --- a/server/ctrlsubsonic/handlers_by_tags_test.go +++ b/server/ctrlsubsonic/handlers_by_tags_test.go @@ -2,7 +2,6 @@ package ctrlsubsonic import ( "net/url" - "path/filepath" "testing" ) @@ -13,8 +12,8 @@ func TestGetArtists(t *testing.T) { runQueryCases(t, contr, contr.ServeGetArtists, []*queryCase{ {url.Values{}, "no_args", false}, - {url.Values{"musicFolderId": {filepath.Join(m.TmpDir(), "m-0")}}, "with_music_folder_1", false}, - {url.Values{"musicFolderId": {filepath.Join(m.TmpDir(), "m-1")}}, "with_music_folder_2", false}, + {url.Values{"musicFolderId": {"0"}}, "with_music_folder_1", false}, + {url.Values{"musicFolderId": {"1"}}, "with_music_folder_2", false}, }) } diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index c2c9ec8..e0e4188 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -71,21 +71,11 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response { } func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response { - var roots []string - err := c.DB. - Model(&db.Album{}). - Pluck("DISTINCT(root_dir)", &roots). - Where("parent_id IS NULL"). - Error - if err != nil { - return spec.NewError(0, "error getting roots: %v", err) - } - sub := spec.NewResponse() sub.MusicFolders = &spec.MusicFolders{} - sub.MusicFolders.List = make([]*spec.MusicFolder, len(roots)) - for i, root := range roots { - sub.MusicFolders.List[i] = &spec.MusicFolder{ID: root, Name: filepath.Base(root)} + sub.MusicFolders.List = make([]*spec.MusicFolder, len(c.MusicPaths)) + for i, path := range c.MusicPaths { + sub.MusicFolders.List[i] = &spec.MusicFolder{ID: i, Name: filepath.Base(path)} } return sub } @@ -226,7 +216,7 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response { q = q.Joins("JOIN track_genres ON track_genres.track_id=tracks.id") q = q.Joins("JOIN genres ON genres.id=track_genres.genre_id AND genres.name=?", genre) } - if m, _ := params.Get("musicFolderId"); m != "" { + if m := c.getMusicFolder(params); m != "" { q = q.Where("albums.root_dir=?", m) } q.Find(&tracks) diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index 05b9ee9..b94a131 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -179,7 +179,7 @@ type MusicFolders struct { } type MusicFolder struct { - ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + ID int `xml:"id,attr" json:"id"` Name string `xml:"name,attr,omitempty" json:"name,omitempty"` } diff --git a/server/mockfs/mockfs.go b/server/mockfs/mockfs.go index 093fb47..81e9d6b 100644 --- a/server/mockfs/mockfs.go +++ b/server/mockfs/mockfs.go @@ -52,6 +52,11 @@ func new(t *testing.T, dirs []string) *MockFS { for _, dir := range dirs { absDirs = append(absDirs, filepath.Join(tmpDir, dir)) } + for _, absDir := range absDirs { + if err := os.MkdirAll(absDir, os.ModePerm); err != nil { + t.Fatalf("mk abs dir: %v", err) + } + } parser := &mreader{map[string]*Tags{}} scanner := scanner.New(absDirs, true, dbc, ";", parser) @@ -128,6 +133,7 @@ func (m *MockFS) RemoveAll(path string) { } func (m *MockFS) LogItems() { + m.t.Logf("\nitems") var dirs int err := filepath.Walk(m.dir, func(path string, info fs.FileInfo, err error) error { m.t.Logf("item %q", path) @@ -139,7 +145,7 @@ func (m *MockFS) LogItems() { if err != nil { m.t.Fatalf("error logging items: %v", err) } - m.t.Logf("dirs: %d", dirs) + m.t.Logf("total %d", dirs) } func (m *MockFS) LogAlbums() { @@ -150,7 +156,7 @@ func (m *MockFS) LogAlbums() { m.t.Logf("\nalbums") for _, album := range albums { - m.t.Logf("id %-3d root %-3s %-10s %-10s pid %-3d aid %-3d cov %-10s", + m.t.Logf("id %-3d root %-3s lr %-15s %-10s pid %-3d aid %-3d cov %-10s", album.ID, album.RootDir, album.LeftPath, album.RightPath, album.ParentID, album.TagArtistID, album.Cover) } m.t.Logf("total %d", len(albums)) diff --git a/server/scanner/scanner_test.go b/server/scanner/scanner_test.go index c03884e..a255746 100644 --- a/server/scanner/scanner_test.go +++ b/server/scanner/scanner_test.go @@ -324,3 +324,49 @@ func TestNewAlbumForExistingArtist(t *testing.T) { is.NoErr(m.DB().Find(&all).Error) // still only 3? is.Equal(len(all), 3) // still only 3? } + +func TestMultiFolderWithSharedArtist(t *testing.T) { + t.Parallel() + is := is.New(t) + m := mockfs.NewWithDirs(t, []string{"m-0", "m-1"}) + defer m.CleanUp() + + const artistName = "artist-a" + + m.AddTrack(fmt.Sprintf("m-0/%s/album-a/track-1.flac", artistName)) + m.SetTags(fmt.Sprintf("m-0/%s/album-a/track-1.flac", artistName), func(tags *mockfs.Tags) { + tags.RawArtist = artistName + tags.RawAlbumArtist = artistName + tags.RawAlbum = "album-a" + tags.RawTitle = "track-1" + }) + m.ScanAndClean() + + m.AddTrack(fmt.Sprintf("m-1/%s/album-a/track-1.flac", artistName)) + m.SetTags(fmt.Sprintf("m-1/%s/album-a/track-1.flac", artistName), func(tags *mockfs.Tags) { + tags.RawArtist = artistName + tags.RawAlbumArtist = artistName + tags.RawAlbum = "album-a" + tags.RawTitle = "track-1" + }) + m.ScanAndClean() + + sq := func(db *gorm.DB) *gorm.DB { + return db. + Select("*, count(sub.id) child_count, sum(sub.length) duration"). + Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id"). + Group("albums.id") + } + + var artist db.Artist + is.NoErr(m.DB().Where("name=?", artistName).Preload("Albums", sq).First(&artist).Error) + is.Equal(artist.Name, artistName) + is.Equal(len(artist.Albums), 2) + + for _, album := range artist.Albums { + is.True(album.TagYear > 0) + is.Equal(album.TagArtistID, artist.ID) + is.True(album.ChildCount > 0) + is.True(album.Duration > 0) + } +} diff --git a/server/server.go b/server/server.go index adcff54..f1ecdb5 100644 --- a/server/server.go +++ b/server/server.go @@ -93,6 +93,7 @@ func New(opts Options) (*Server, error) { CachePath: opts.CachePath, CoverCachePath: opts.CoverCachePath, PodcastsPath: opts.PodcastPath, + MusicPaths: opts.MusicPaths, Jukebox: &jukebox.Jukebox{}, Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}}, Podcasts: podcast,