@@ -30,7 +30,6 @@ const (
|
||||
func main() {
|
||||
set := flag.NewFlagSet(gonic.Name, flag.ExitOnError)
|
||||
confListenAddr := set.String("listen-addr", "0.0.0.0:4747", "listen address (optional)")
|
||||
confMusicPath := set.String("music-path", "", "path to music")
|
||||
confPodcastPath := set.String("podcast-path", "", "path to podcasts")
|
||||
confCachePath := set.String("cache-path", "", "path to cache")
|
||||
confDBPath := set.String("db-path", "gonic.db", "path to database (optional)")
|
||||
@@ -40,6 +39,10 @@ func main() {
|
||||
confGenreSplit := set.String("genre-split", "\n", "character or string to split genre tag data on (optional)")
|
||||
confHTTPLog := set.Bool("http-log", true, "http request logging (optional)")
|
||||
confShowVersion := set.Bool("version", false, "show gonic version")
|
||||
|
||||
var confMusicPaths musicPaths
|
||||
set.Var(&confMusicPaths, "music-path", "path to music")
|
||||
|
||||
_ = set.String("config-path", "", "path to config (optional)")
|
||||
|
||||
if err := ff.Parse(set, os.Args[1:],
|
||||
@@ -62,8 +65,13 @@ func main() {
|
||||
log.Printf(" %-15s %s\n", f.Name, value)
|
||||
})
|
||||
|
||||
if _, err := os.Stat(*confMusicPath); os.IsNotExist(err) {
|
||||
log.Fatal("please provide a valid music directory")
|
||||
if len(confMusicPaths) == 0 {
|
||||
log.Fatalf("please provide a music directory")
|
||||
}
|
||||
for _, confMusicPath := range confMusicPaths {
|
||||
if _, err := os.Stat(confMusicPath); os.IsNotExist(err) {
|
||||
log.Fatalf("music directory %q not found", confMusicPath)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(*confPodcastPath); os.IsNotExist(err) {
|
||||
log.Fatal("please provide a valid podcast directory")
|
||||
@@ -90,13 +98,20 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("error opening database: %v\n", err)
|
||||
}
|
||||
defer db.Close()
|
||||
defer dbc.Close()
|
||||
|
||||
err = dbc.Migrate(db.MigrationContext{
|
||||
OriginalMusicPath: confMusicPaths[0],
|
||||
})
|
||||
if err != nil {
|
||||
log.Panicf("error migrating database: %v\n", err)
|
||||
}
|
||||
|
||||
proxyPrefixExpr := regexp.MustCompile(`^\/*(.*?)\/*$`)
|
||||
*confProxyPrefix = proxyPrefixExpr.ReplaceAllString(*confProxyPrefix, `/$1`)
|
||||
server, err := server.New(server.Options{
|
||||
DB: db,
|
||||
MusicPath: *confMusicPath,
|
||||
DB: dbc,
|
||||
MusicPaths: confMusicPaths,
|
||||
CachePath: cacheDirAudio,
|
||||
CoverCachePath: cacheDirCovers,
|
||||
ProxyPrefix: *confProxyPrefix,
|
||||
@@ -125,3 +140,14 @@ func main() {
|
||||
log.Panicf("error in job: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type musicPaths []string
|
||||
|
||||
func (m musicPaths) String() string {
|
||||
return strings.Join(m, ", ")
|
||||
}
|
||||
|
||||
func (m *musicPaths) Set(value string) error {
|
||||
*m = append(*m, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -139,10 +139,7 @@ func (c *Controller) ServeChangeOwnPasswordDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
return &Response{
|
||||
err: "please provide a token",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a token"}
|
||||
}
|
||||
apiKey, err := c.DB.GetSetting("lastfm_api_key")
|
||||
if err != nil {
|
||||
@@ -199,17 +196,11 @@ func (c *Controller) ServeUnlinkListenBrainzDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServeChangeUsername(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
return &Response{
|
||||
err: "please provide a username",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a username"}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
if user == nil {
|
||||
return &Response{
|
||||
err: "couldn't find a user with that name",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "couldn't find a user with that name"}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -237,17 +228,11 @@ func (c *Controller) ServeChangeUsernameDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServeChangePassword(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
return &Response{
|
||||
err: "please provide a username",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a username"}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
if user == nil {
|
||||
return &Response{
|
||||
err: "couldn't find a user with that name",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "couldn't find a user with that name"}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -276,17 +261,11 @@ func (c *Controller) ServeChangePasswordDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServeDeleteUser(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
return &Response{
|
||||
err: "please provide a username",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a username"}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
if user == nil {
|
||||
return &Response{
|
||||
err: "couldn't find a user with that name",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "couldn't find a user with that name"}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -421,10 +400,7 @@ func (c *Controller) ServeDeleteTranscodePrefDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
client := r.URL.Query().Get("client")
|
||||
if client == "" {
|
||||
return &Response{
|
||||
err: "please provide a client",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a client"}
|
||||
}
|
||||
c.DB.
|
||||
Where("user_id=? AND client=?", user.ID, client).
|
||||
@@ -459,16 +435,10 @@ func (c *Controller) ServePodcastAddDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServePodcastDownloadDo(r *http.Request) *Response {
|
||||
id, err := strconv.Atoi(r.URL.Query().Get("id"))
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid podcast id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast id"}
|
||||
}
|
||||
if err := c.Podcasts.DownloadPodcastAll(id); err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid podcast id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast id"}
|
||||
}
|
||||
return &Response{
|
||||
redirect: "/admin/home",
|
||||
@@ -479,10 +449,7 @@ func (c *Controller) ServePodcastDownloadDo(r *http.Request) *Response {
|
||||
func (c *Controller) ServePodcastUpdateDo(r *http.Request) *Response {
|
||||
id, err := strconv.Atoi(r.URL.Query().Get("id"))
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid podcast id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast id"}
|
||||
}
|
||||
setting := db.PodcastAutoDownload(r.FormValue("setting"))
|
||||
var message string
|
||||
@@ -492,10 +459,7 @@ func (c *Controller) ServePodcastUpdateDo(r *http.Request) *Response {
|
||||
case db.PodcastAutoDownloadNone:
|
||||
message = "future podcast episodes will not be downloaded"
|
||||
default:
|
||||
return &Response{
|
||||
err: "please provide a valid podcast download type",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast download type"}
|
||||
}
|
||||
if err := c.Podcasts.SetAutoDownload(id, setting); err != nil {
|
||||
return &Response{
|
||||
@@ -513,16 +477,10 @@ func (c *Controller) ServePodcastDeleteDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
id, err := strconv.Atoi(r.URL.Query().Get("id"))
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid podcast id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast id"}
|
||||
}
|
||||
if err := c.Podcasts.DeletePodcast(user.ID, id); err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid podcast id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid podcast id"}
|
||||
}
|
||||
return &Response{
|
||||
redirect: "/admin/home",
|
||||
|
||||
@@ -18,16 +18,16 @@ var (
|
||||
errPlaylistNoMatch = errors.New("couldn't match track")
|
||||
)
|
||||
|
||||
func playlistParseLine(c *Controller, path string) (int, error) {
|
||||
if strings.HasPrefix(path, "#") || strings.TrimSpace(path) == "" {
|
||||
func playlistParseLine(c *Controller, absPath string) (int, error) {
|
||||
if strings.HasPrefix(absPath, "#") || strings.TrimSpace(absPath) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var track db.Track
|
||||
query := c.DB.Raw(`
|
||||
SELECT tracks.id FROM TRACKS
|
||||
JOIN albums ON tracks.album_id=albums.id
|
||||
WHERE (? || '/' || albums.left_path || albums.right_path || '/' || tracks.filename)=?`,
|
||||
c.MusicPath, path)
|
||||
WHERE (albums.root_dir || '/' || albums.left_path || albums.right_path || '/' || tracks.filename)=?`,
|
||||
absPath)
|
||||
err := query.First(&track).Error
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
@@ -95,10 +95,7 @@ func (c *Controller) ServeUploadPlaylist(r *http.Request) *Response {
|
||||
|
||||
func (c *Controller) ServeUploadPlaylistDo(r *http.Request) *Response {
|
||||
if err := r.ParseMultipartForm((1 << 10) * 24); err != nil {
|
||||
return &Response{
|
||||
err: "couldn't parse mutlipart",
|
||||
code: 500,
|
||||
}
|
||||
return &Response{code: 500, err: "couldn't parse mutlipart"}
|
||||
}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
var playlistCount int
|
||||
@@ -123,10 +120,7 @@ func (c *Controller) ServeDeletePlaylistDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
id, err := strconv.Atoi(r.URL.Query().Get("id"))
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: "please provide a valid id",
|
||||
code: 400,
|
||||
}
|
||||
return &Response{code: 400, err: "please provide a valid id"}
|
||||
}
|
||||
c.DB.
|
||||
Where("user_id=? AND id=?", user.ID, id).
|
||||
|
||||
@@ -46,7 +46,6 @@ func statusToBlock(code int) string {
|
||||
|
||||
type Controller struct {
|
||||
DB *db.DB
|
||||
MusicPath string
|
||||
Scanner *scanner.Scanner
|
||||
ProxyPrefix string
|
||||
}
|
||||
|
||||
@@ -55,15 +55,14 @@ func makeHTTPMock(query url.Values) (*httptest.ResponseRecorder, *http.Request)
|
||||
func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*queryCase) {
|
||||
t.Helper()
|
||||
for _, qc := range cases {
|
||||
qc := qc // pin
|
||||
t.Run(qc.expectPath, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rr, req := makeHTTPMock(qc.params)
|
||||
contr.H(h).ServeHTTP(rr, req)
|
||||
body := rr.Body.String()
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Fatalf("didn't give a 200\n%s", body)
|
||||
}
|
||||
|
||||
goldenPath := makeGoldenPath(t.Name())
|
||||
goldenRegen := os.Getenv("GONIC_REGEN")
|
||||
if goldenRegen == "*" || (goldenRegen != "" && strings.HasPrefix(t.Name(), goldenRegen)) {
|
||||
@@ -86,11 +85,10 @@ func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*
|
||||
diffOpts = append(diffOpts, jd.SET)
|
||||
}
|
||||
diff := expected.Diff(actual, diffOpts...)
|
||||
// pass or fail
|
||||
if len(diff) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(diff) > 0 {
|
||||
t.Errorf("\u001b[31;1mdiffering json\u001b[0m\n%s", diff.Render())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,27 @@ import (
|
||||
"go.senan.xyz/gonic/server/db"
|
||||
)
|
||||
|
||||
// the subsonic spec metions "artist" a lot when talking about the
|
||||
// the subsonic spec mentions "artist" a lot when talking about the
|
||||
// browse by folder endpoints. but since we're not browsing by tag
|
||||
// we can't access artists. so instead we'll consider the artist of
|
||||
// an track to be the it's respective folder that comes directly
|
||||
// under the root directory
|
||||
|
||||
func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
rootQ := c.DB.
|
||||
Select("id").
|
||||
Model(&db.Album{}).
|
||||
Where("parent_id IS NULL")
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
rootQ = rootQ.
|
||||
Where("root_dir=?", m)
|
||||
}
|
||||
var folders []*db.Album
|
||||
c.DB.
|
||||
Select("*, count(sub.id) child_count").
|
||||
Joins("LEFT JOIN albums sub ON albums.id=sub.parent_id").
|
||||
Where("albums.parent_id=1").
|
||||
Where("albums.parent_id IN ?", rootQ.SubQuery()).
|
||||
Group("albums.id").
|
||||
Order("albums.right_path COLLATE NOCASE").
|
||||
Find(&folders)
|
||||
@@ -31,17 +40,15 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
|
||||
indexMap := make(map[string]*spec.Index, 27)
|
||||
resp := make([]*spec.Index, 0, 27)
|
||||
for _, folder := range folders {
|
||||
i := lowerUDecOrHash(folder.IndexRightPath())
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &spec.Index{
|
||||
Name: i,
|
||||
key := lowerUDecOrHash(folder.IndexRightPath())
|
||||
if _, ok := indexMap[key]; !ok {
|
||||
indexMap[key] = &spec.Index{
|
||||
Name: key,
|
||||
Artists: []*spec.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
resp = append(resp, index)
|
||||
resp = append(resp, indexMap[key])
|
||||
}
|
||||
index.Artists = append(index.Artists,
|
||||
indexMap[key].Artists = append(indexMap[key].Artists,
|
||||
spec.NewArtistByFolder(folder))
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
@@ -137,6 +144,10 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
|
||||
default:
|
||||
return spec.NewError(10, "unknown value `%s` for parameter 'type'", v)
|
||||
}
|
||||
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.Where("root_dir=?", m)
|
||||
}
|
||||
var folders []*db.Album
|
||||
// TODO: think about removing this extra join to count number
|
||||
// of children. it might make sense to store that in the db
|
||||
@@ -166,50 +177,65 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
||||
return spec.NewError(10, "please provide a `query` parameter")
|
||||
}
|
||||
query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*"))
|
||||
|
||||
results := &spec.SearchResultTwo{}
|
||||
|
||||
// search "artists"
|
||||
var artists []*db.Album
|
||||
c.DB.
|
||||
Where(`
|
||||
parent_id=1
|
||||
AND ( right_path LIKE ? OR
|
||||
right_path_u_dec LIKE ? )`,
|
||||
query, query).
|
||||
Offset(params.GetOrInt("artistOffset", 0)).
|
||||
Limit(params.GetOrInt("artistCount", 20)).
|
||||
Find(&artists)
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists,
|
||||
spec.NewDirectoryByFolder(a, nil))
|
||||
rootQ := c.DB.
|
||||
Select("id").
|
||||
Model(&db.Album{}).
|
||||
Where("parent_id IS NULL")
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
rootQ = rootQ.Where("root_dir=?", m)
|
||||
}
|
||||
|
||||
var artists []*db.Album
|
||||
q := c.DB.
|
||||
Where(`parent_id IN ? AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, rootQ.SubQuery(), query, query).
|
||||
Offset(params.GetOrInt("artistOffset", 0)).
|
||||
Limit(params.GetOrInt("artistCount", 20))
|
||||
if err := q.Find(&artists).Error; err != nil {
|
||||
return spec.NewError(0, "find artists: %v", err)
|
||||
}
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists, spec.NewDirectoryByFolder(a, nil))
|
||||
}
|
||||
|
||||
// search "albums"
|
||||
var albums []*db.Album
|
||||
c.DB.
|
||||
Where(`
|
||||
tag_artist_id IS NOT NULL
|
||||
AND ( right_path LIKE ? OR
|
||||
right_path_u_dec LIKE ? )`,
|
||||
query, query).
|
||||
q = c.DB.
|
||||
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)).
|
||||
Find(&albums)
|
||||
Limit(params.GetOrInt("albumCount", 20))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.Where("root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&albums).Error; err != nil {
|
||||
return spec.NewError(0, "find albums: %v", err)
|
||||
}
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
|
||||
}
|
||||
|
||||
// search tracks
|
||||
var tracks []*db.Track
|
||||
c.DB.
|
||||
q = c.DB.
|
||||
Preload("Album").
|
||||
Where("filename LIKE ? OR filename_u_dec LIKE ?",
|
||||
query, query).
|
||||
Where("filename LIKE ? OR filename_u_dec LIKE ?", query, query).
|
||||
Offset(params.GetOrInt("songOffset", 0)).
|
||||
Limit(params.GetOrInt("songCount", 20)).
|
||||
Find(&tracks)
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks,
|
||||
spec.NewTCTrackByFolder(t, t.Album))
|
||||
Limit(params.GetOrInt("songCount", 20))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.
|
||||
Joins("JOIN albums ON albums.id=tracks.album_id").
|
||||
Where("albums.root_dir=?", m)
|
||||
}
|
||||
//
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
return spec.NewError(0, "find tracks: %v", err)
|
||||
}
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks, spec.NewTCTrackByFolder(t, t.Album))
|
||||
}
|
||||
|
||||
sub := spec.NewResponse()
|
||||
sub.SearchResultTwo = results
|
||||
return sub
|
||||
|
||||
@@ -2,6 +2,7 @@ package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
@@ -13,6 +14,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},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,28 +16,32 @@ import (
|
||||
)
|
||||
|
||||
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
var artists []*db.Artist
|
||||
c.DB.
|
||||
q := c.DB.
|
||||
Select("*, count(sub.id) album_count").
|
||||
Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id").
|
||||
Group("artists.id").
|
||||
Order("artists.name COLLATE NOCASE").
|
||||
Find(&artists)
|
||||
Order("artists.name COLLATE NOCASE")
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.Where("sub.root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&artists).Error; err != nil {
|
||||
return spec.NewError(10, "error finding artists: %v", err)
|
||||
}
|
||||
// [a-z#] -> 27
|
||||
indexMap := make(map[string]*spec.Index, 27)
|
||||
resp := make([]*spec.Index, 0, 27)
|
||||
for _, artist := range artists {
|
||||
i := lowerUDecOrHash(artist.IndexName())
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &spec.Index{
|
||||
Name: i,
|
||||
key := lowerUDecOrHash(artist.IndexName())
|
||||
if _, ok := indexMap[key]; !ok {
|
||||
indexMap[key] = &spec.Index{
|
||||
Name: key,
|
||||
Artists: []*spec.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
resp = append(resp, index)
|
||||
resp = append(resp, indexMap[key])
|
||||
}
|
||||
index.Artists = append(index.Artists,
|
||||
indexMap[key].Artists = append(indexMap[key].Artists,
|
||||
spec.NewArtistByTags(artist))
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
@@ -144,6 +148,9 @@ 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 != "" {
|
||||
q = q.Where("root_dir=?", m)
|
||||
}
|
||||
var albums []*db.Album
|
||||
// TODO: think about removing this extra join to count number
|
||||
// of children. it might make sense to store that in the db
|
||||
@@ -172,47 +179,63 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide a `query` parameter")
|
||||
}
|
||||
query = fmt.Sprintf("%%%s%%",
|
||||
strings.TrimSuffix(query, "*"))
|
||||
query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*"))
|
||||
results := &spec.SearchResultThree{}
|
||||
|
||||
// search "artists"
|
||||
var artists []*db.Artist
|
||||
c.DB.
|
||||
Where("name LIKE ? OR name_u_dec LIKE ?",
|
||||
query, query).
|
||||
q := c.DB.
|
||||
Where("name LIKE ? OR name_u_dec LIKE ?", query, query).
|
||||
Offset(params.GetOrInt("artistOffset", 0)).
|
||||
Limit(params.GetOrInt("artistCount", 20)).
|
||||
Find(&artists)
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists,
|
||||
spec.NewArtistByTags(a))
|
||||
Limit(params.GetOrInt("artistCount", 20))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.
|
||||
Joins("JOIN albums ON albums.tag_artist_id=artists.id").
|
||||
Where("albums.root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&artists).Error; err != nil {
|
||||
return spec.NewError(0, "find artists: %v", err)
|
||||
}
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists, spec.NewArtistByTags(a))
|
||||
}
|
||||
|
||||
// search "albums"
|
||||
var albums []*db.Album
|
||||
c.DB.
|
||||
q = c.DB.
|
||||
Preload("TagArtist").
|
||||
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?",
|
||||
query, query).
|
||||
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query).
|
||||
Offset(params.GetOrInt("albumOffset", 0)).
|
||||
Limit(params.GetOrInt("albumCount", 20)).
|
||||
Find(&albums)
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums,
|
||||
spec.NewAlbumByTags(a, a.TagArtist))
|
||||
Limit(params.GetOrInt("albumCount", 20))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.Where("root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&albums).Error; err != nil {
|
||||
return spec.NewError(0, "find albums: %v", err)
|
||||
}
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.TagArtist))
|
||||
}
|
||||
|
||||
// search tracks
|
||||
var tracks []*db.Track
|
||||
c.DB.
|
||||
q = c.DB.
|
||||
Preload("Album").
|
||||
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?",
|
||||
query, query).
|
||||
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query).
|
||||
Offset(params.GetOrInt("songOffset", 0)).
|
||||
Limit(params.GetOrInt("songCount", 20)).
|
||||
Find(&tracks)
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks,
|
||||
spec.NewTrackByTags(t, t.Album))
|
||||
Limit(params.GetOrInt("songCount", 20))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.
|
||||
Joins("JOIN albums ON albums.id=tracks.album_id").
|
||||
Where("albums.root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
return spec.NewError(0, "find tracks: %v", err)
|
||||
}
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks, spec.NewTrackByTags(t, t.Album))
|
||||
}
|
||||
|
||||
sub := spec.NewResponse()
|
||||
sub.SearchResultThree = results
|
||||
return sub
|
||||
@@ -313,17 +336,20 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `genre` parameter")
|
||||
}
|
||||
// TODO: add musicFolderId parameter
|
||||
// (since 1.12.0) only return albums in the music folder with the given id
|
||||
var tracks []*db.Track
|
||||
c.DB.
|
||||
q := c.DB.
|
||||
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||
Joins("JOIN track_genres ON track_genres.track_id=tracks.id").
|
||||
Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
|
||||
Preload("Album").
|
||||
Offset(params.GetOrInt("offset", 0)).
|
||||
Limit(params.GetOrInt("count", 10)).
|
||||
Find(&tracks)
|
||||
Limit(params.GetOrInt("count", 10))
|
||||
if m, _ := params.Get("musicFolderId"); m != "" {
|
||||
q = q.Where("albums.root_dir=?", m)
|
||||
}
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
return spec.NewError(0, "error finding tracks: %v", err)
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.TracksByGenre = &spec.TracksByGenre{
|
||||
List: make([]*spec.TrackChild, len(tracks)),
|
||||
|
||||
@@ -2,16 +2,19 @@ package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetArtists(t *testing.T) {
|
||||
t.Parallel()
|
||||
contr, m := makeController(t)
|
||||
contr, m := makeControllerRoots(t, []string{"m-0", "m-1"})
|
||||
defer m.CleanUp()
|
||||
|
||||
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},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
@@ -70,12 +71,22 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response {
|
||||
folders := &spec.MusicFolders{}
|
||||
folders.List = []*spec.MusicFolder{
|
||||
{ID: 1, Name: "music"},
|
||||
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 = folders
|
||||
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)}
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
@@ -215,6 +226,9 @@ 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 != "" {
|
||||
q = q.Where("albums.root_dir=?", m)
|
||||
}
|
||||
q.Find(&tracks)
|
||||
sub := spec.NewResponse()
|
||||
sub.RandomTracks = &spec.RandomTracks{}
|
||||
|
||||
@@ -74,10 +74,10 @@ var (
|
||||
errCoverEmpty = errors.New("no cover found for that folder")
|
||||
)
|
||||
|
||||
func coverGetPath(dbc *db.DB, musicPath, podcastPath string, id specid.ID) (string, error) {
|
||||
func coverGetPath(dbc *db.DB, podcastPath string, id specid.ID) (string, error) {
|
||||
switch id.Type {
|
||||
case specid.Album:
|
||||
return coverGetPathAlbum(dbc, musicPath, id.Value)
|
||||
return coverGetPathAlbum(dbc, id.Value)
|
||||
case specid.Podcast:
|
||||
return coverGetPathPodcast(dbc, podcastPath, id.Value)
|
||||
case specid.PodcastEpisode:
|
||||
@@ -87,10 +87,11 @@ func coverGetPath(dbc *db.DB, musicPath, podcastPath string, id specid.ID) (stri
|
||||
}
|
||||
}
|
||||
|
||||
func coverGetPathAlbum(dbc *db.DB, musicPath string, id int) (string, error) {
|
||||
func coverGetPathAlbum(dbc *db.DB, id int) (string, error) {
|
||||
folder := &db.Album{}
|
||||
err := dbc.DB.
|
||||
Select("id, left_path, right_path, cover").
|
||||
Preload("Parent").
|
||||
Select("id, root_dir, left_path, right_path, cover").
|
||||
First(folder, id).
|
||||
Error
|
||||
if err != nil {
|
||||
@@ -100,7 +101,7 @@ func coverGetPathAlbum(dbc *db.DB, musicPath string, id int) (string, error) {
|
||||
return "", errCoverEmpty
|
||||
}
|
||||
return path.Join(
|
||||
musicPath,
|
||||
folder.RootDir,
|
||||
folder.LeftPath,
|
||||
folder.RightPath,
|
||||
folder.Cover,
|
||||
@@ -201,7 +202,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
||||
case specid.Track:
|
||||
track, _ := streamGetTrack(c.DB, id.Value)
|
||||
audioFile = track
|
||||
audioPath = path.Join(c.MusicPath, track.RelPath())
|
||||
audioPath = path.Join(track.AbsPath())
|
||||
if err != nil {
|
||||
return spec.NewError(70, "track with id `%s` was not found", id)
|
||||
}
|
||||
@@ -278,7 +279,7 @@ func (c *Controller) ServeDownload(w http.ResponseWriter, r *http.Request) *spec
|
||||
case specid.Track:
|
||||
track, _ := streamGetTrack(c.DB, id.Value)
|
||||
audioFile = track
|
||||
filePath = path.Join(c.MusicPath, track.RelPath())
|
||||
filePath = track.AbsPath()
|
||||
if err != nil {
|
||||
return spec.NewError(70, "track with id `%s` was not found", id)
|
||||
}
|
||||
|
||||
@@ -84,14 +84,10 @@ func NewArtistByFolder(f *db.Album) *Artist {
|
||||
}
|
||||
|
||||
func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory {
|
||||
dir := &Directory{
|
||||
return &Directory{
|
||||
ID: f.SID(),
|
||||
Name: f.RightPath,
|
||||
Children: children,
|
||||
ParentID: f.ParentSID(),
|
||||
}
|
||||
// don't show the root dir as a parent
|
||||
if f.ParentID != 1 {
|
||||
dir.ParentID = f.ParentSID()
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ type MusicFolders struct {
|
||||
}
|
||||
|
||||
type MusicFolder struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
ID string `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,32 +5,6 @@
|
||||
"type": "gonic",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": "al-2",
|
||||
"coverArt": "al-2",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-1",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-6",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
@@ -44,45 +18,6 @@
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-1",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-8",
|
||||
"coverArt": "al-8",
|
||||
@@ -97,11 +32,11 @@
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
"id": "al-2",
|
||||
"coverArt": "al-2",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-2",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-1",
|
||||
"isDir": true,
|
||||
@@ -109,6 +44,32 @@
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-1",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-9",
|
||||
"coverArt": "al-9",
|
||||
@@ -121,6 +82,45 @@
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-6",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-2",
|
||||
"album": "",
|
||||
"parent": "al-1",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,32 +5,6 @@
|
||||
"type": "gonic",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
@@ -44,19 +18,6 @@
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-9",
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-2",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-2",
|
||||
"coverArt": "al-2",
|
||||
@@ -70,6 +31,32 @@
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
@@ -83,19 +70,6 @@
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-8",
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
@@ -110,14 +84,40 @@
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"id": "al-8",
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-1",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-9",
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-2",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "ar-1", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "ar-2", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "ar-3", "name": "artist-2", "albumCount": 3 }
|
||||
{ "id": "ar-1", "name": "artist-0", "albumCount": 6 },
|
||||
{ "id": "ar-2", "name": "artist-1", "albumCount": 6 },
|
||||
{ "id": "ar-3", "name": "artist-2", "albumCount": 6 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
20
server/ctrlsubsonic/testdata/test_get_artists_with_music_folder_1
vendored
Normal file
20
server/ctrlsubsonic/testdata/test_get_artists_with_music_folder_1
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"artists": {
|
||||
"ignoredArticles": "",
|
||||
"index": [
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "ar-1", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "ar-2", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "ar-3", "name": "artist-2", "albumCount": 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
20
server/ctrlsubsonic/testdata/test_get_artists_with_music_folder_2
vendored
Normal file
20
server/ctrlsubsonic/testdata/test_get_artists_with_music_folder_2
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"artists": {
|
||||
"ignoredArticles": "",
|
||||
"index": [
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "ar-1", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "ar-2", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "ar-3", "name": "artist-2", "albumCount": 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,12 @@
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "al-2", "name": "album-0", "albumCount": 0 },
|
||||
{ "id": "al-3", "name": "album-1", "albumCount": 0 },
|
||||
{ "id": "al-4", "name": "album-2", "albumCount": 0 }
|
||||
{ "id": "al-1", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "al-14", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "al-6", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "al-19", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "al-10", "name": "artist-2", "albumCount": 3 },
|
||||
{ "id": "al-23", "name": "artist-2", "albumCount": 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
21
server/ctrlsubsonic/testdata/test_get_indexes_with_music_folder_1
vendored
Normal file
21
server/ctrlsubsonic/testdata/test_get_indexes_with_music_folder_1
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"indexes": {
|
||||
"lastModified": 0,
|
||||
"ignoredArticles": "",
|
||||
"index": [
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "al-1", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "al-6", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "al-10", "name": "artist-2", "albumCount": 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
server/ctrlsubsonic/testdata/test_get_indexes_with_music_folder_2
vendored
Normal file
21
server/ctrlsubsonic/testdata/test_get_indexes_with_music_folder_2
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"indexes": {
|
||||
"lastModified": 0,
|
||||
"ignoredArticles": "",
|
||||
"index": [
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{ "id": "al-14", "name": "artist-0", "albumCount": 3 },
|
||||
{ "id": "al-19", "name": "artist-1", "albumCount": 3 },
|
||||
{ "id": "al-23", "name": "artist-2", "albumCount": 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "gonic",
|
||||
"directory": {
|
||||
"id": "al-3",
|
||||
"parent": "al-1",
|
||||
"name": "album-1",
|
||||
"child": [
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "gonic",
|
||||
"directory": {
|
||||
"id": "al-2",
|
||||
"parent": "al-1",
|
||||
"name": "album-0",
|
||||
"child": [
|
||||
{
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"searchResult2": {
|
||||
"artist": [
|
||||
{ "id": "al-2", "name": "album-0" },
|
||||
{ "id": "al-3", "name": "album-1" },
|
||||
{ "id": "al-4", "name": "album-2" }
|
||||
],
|
||||
"album": [
|
||||
{
|
||||
"id": "al-2",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
"status": "ok",
|
||||
"version": "1.15.0",
|
||||
"type": "gonic",
|
||||
"searchResult2": {}
|
||||
"searchResult2": {
|
||||
"artist": [
|
||||
{ "id": "al-1", "parent": "al-5", "name": "artist-0" },
|
||||
{ "id": "al-6", "parent": "al-5", "name": "artist-1" },
|
||||
{ "id": "al-10", "parent": "al-5", "name": "artist-2" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"gopkg.in/gormigrate.v1"
|
||||
)
|
||||
|
||||
func DefaultOptions() url.Values {
|
||||
@@ -49,30 +47,6 @@ func New(path string, options url.Values) (*DB, error) {
|
||||
}
|
||||
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
db.DB().SetMaxOpenConns(1)
|
||||
migrOptions := &gormigrate.Options{
|
||||
TableName: "migrations",
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 255,
|
||||
UseTransaction: false,
|
||||
}
|
||||
migr := gormigrate.New(db, migrOptions, wrapMigrations(
|
||||
migrateInitSchema(),
|
||||
migrateCreateInitUser(),
|
||||
migrateMergePlaylist(),
|
||||
migrateCreateTranscode(),
|
||||
migrateAddGenre(),
|
||||
migrateUpdateTranscodePrefIDX(),
|
||||
migrateAddAlbumIDX(),
|
||||
migrateMultiGenre(),
|
||||
migrateListenBrainz(),
|
||||
migratePodcast(),
|
||||
migrateBookmarks(),
|
||||
migratePodcastAutoDownload(),
|
||||
migrateAlbumCreatedAt(),
|
||||
))
|
||||
if err = migr.Migrate(); err != nil {
|
||||
return nil, fmt.Errorf("migrating to latest version: %w", err)
|
||||
}
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,66 @@ package db
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"gopkg.in/gormigrate.v1"
|
||||
)
|
||||
|
||||
// $ date '+%Y%m%d%H%M'
|
||||
type MigrationContext struct {
|
||||
OriginalMusicPath string
|
||||
}
|
||||
|
||||
func migrateInitSchema() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202002192100",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func (db *DB) Migrate(ctx MigrationContext) error {
|
||||
options := &gormigrate.Options{
|
||||
TableName: "migrations",
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 255,
|
||||
UseTransaction: false,
|
||||
}
|
||||
|
||||
// $ date '+%Y%m%d%H%M'
|
||||
migrations := []*gormigrate.Migration{
|
||||
construct(ctx, "202002192100", migrateInitSchema),
|
||||
construct(ctx, "202002192019", migrateCreateInitUser),
|
||||
construct(ctx, "202002192222", migrateMergePlaylist),
|
||||
construct(ctx, "202003111222", migrateCreateTranscode),
|
||||
construct(ctx, "202003121330", migrateAddGenre),
|
||||
construct(ctx, "202003241509", migrateUpdateTranscodePrefIDX),
|
||||
construct(ctx, "202004302006", migrateAddAlbumIDX),
|
||||
construct(ctx, "202012151806", migrateMultiGenre),
|
||||
construct(ctx, "202101081149", migrateListenBrainz),
|
||||
construct(ctx, "202101111537", migratePodcast),
|
||||
construct(ctx, "202102032210", migrateBookmarks),
|
||||
construct(ctx, "202102191448", migratePodcastAutoDownload),
|
||||
construct(ctx, "202110041330", migrateAlbumCreatedAt),
|
||||
construct(ctx, "202111021951", migrateAlbumRootDir),
|
||||
}
|
||||
|
||||
return gormigrate.
|
||||
New(db.DB, options, migrations).
|
||||
Migrate()
|
||||
}
|
||||
|
||||
func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: id,
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
tx := db.Begin()
|
||||
defer tx.Commit()
|
||||
if err := f(tx, ctx); err != nil {
|
||||
return fmt.Errorf("%q: %w", id, err)
|
||||
}
|
||||
log.Printf("migration '%s' finished", id)
|
||||
return nil
|
||||
},
|
||||
Rollback: func(*gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
@@ -28,18 +77,19 @@ func migrateInitSchema() gormigrate.Migration {
|
||||
PlayQueue{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: id,
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
tx := db.Begin()
|
||||
defer tx.Commit()
|
||||
if err := f(tx, ctx); err != nil {
|
||||
return fmt.Errorf("%q: %w", id, err)
|
||||
func migrateCreateInitUser(tx *gorm.DB, _ MigrationContext) error {
|
||||
const (
|
||||
initUsername = "admin"
|
||||
initPassword = "admin"
|
||||
)
|
||||
err := tx.
|
||||
Where("name=?", initUsername).
|
||||
First(&User{}).
|
||||
Error
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.Create(&User{
|
||||
@@ -48,14 +98,9 @@ func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContex
|
||||
IsAdmin: true,
|
||||
}).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateMergePlaylist() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202002192222",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateMergePlaylist(tx *gorm.DB, _ MigrationContext) error {
|
||||
if !tx.HasTable("playlist_items") {
|
||||
return nil
|
||||
}
|
||||
@@ -71,40 +116,25 @@ func migrateMergePlaylist() gormigrate.Migration {
|
||||
DROP TABLE playlist_items;`,
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateCreateTranscode() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003111222",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateCreateTranscode(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAddGenre() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003121330",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
Album{},
|
||||
Track{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003241509",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
|
||||
var hasIDX int
|
||||
tx.
|
||||
Select("1").
|
||||
@@ -141,26 +171,16 @@ func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
|
||||
return fmt.Errorf("step copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAddAlbumIDX() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202004302006",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateAddAlbumIDX(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Album{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateMultiGenre() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202012151806",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateMultiGenre(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
@@ -202,14 +222,9 @@ func migrateMultiGenre() gormigrate.Migration {
|
||||
return fmt.Errorf("step migrate album genres: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateListenBrainz() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202101081149",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateListenBrainz(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
User{},
|
||||
)
|
||||
@@ -217,51 +232,31 @@ func migrateListenBrainz() gormigrate.Migration {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migratePodcast() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202101111537",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migratePodcast(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
PodcastEpisode{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateBookmarks() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202102032210",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateBookmarks(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Bookmark{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migratePodcastAutoDownload() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202102191448",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migratePodcastAutoDownload(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAlbumCreatedAt() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202110041330",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
func migrateAlbumCreatedAt(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
Album{},
|
||||
)
|
||||
@@ -275,6 +270,39 @@ func migrateAlbumCreatedAt() gormigrate.Migration {
|
||||
return fmt.Errorf("step migrate album created_at: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAlbumRootDir(tx *gorm.DB, ctx MigrationContext) error {
|
||||
var hasIDX int
|
||||
tx.
|
||||
Select("1").
|
||||
Table("sqlite_master").
|
||||
Where("type = ?", "index").
|
||||
Where("name = ?", "idx_album_abs_path").
|
||||
Count(&hasIDX)
|
||||
if hasIDX == 1 {
|
||||
// index already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
step := tx.AutoMigrate(
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
step = tx.Exec(`
|
||||
DROP INDEX IF EXISTS idx_left_path_right_path;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop idx: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
UPDATE albums SET root_dir=?
|
||||
`, ctx.OriginalMusicPath)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop idx: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -132,6 +132,18 @@ func (t *Track) MIME() string {
|
||||
return v
|
||||
}
|
||||
|
||||
func (t *Track) AbsPath() string {
|
||||
if t.Album == nil {
|
||||
return ""
|
||||
}
|
||||
return path.Join(
|
||||
t.Album.RootDir,
|
||||
t.Album.LeftPath,
|
||||
t.Album.RightPath,
|
||||
t.Filename,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Track) RelPath() string {
|
||||
if t.Album == nil {
|
||||
return ""
|
||||
@@ -182,11 +194,12 @@ type Album struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ModifiedAt time.Time
|
||||
LeftPath string `gorm:"unique_index:idx_left_path_right_path"`
|
||||
RightPath string `gorm:"not null; unique_index:idx_left_path_right_path" sql:"default: null"`
|
||||
LeftPath string `gorm:"unique_index:idx_album_abs_path"`
|
||||
RightPath string `gorm:"not null; unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
RightPathUDec string `sql:"default: null"`
|
||||
Parent *Album
|
||||
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
RootDir string `gorm:"unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
Genres []*Genre `gorm:"many2many:album_genres"`
|
||||
Cover string `sql:"default: null"`
|
||||
TagArtist *Artist
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -27,7 +26,6 @@ type Status struct {
|
||||
|
||||
type Jukebox struct {
|
||||
playlist []*db.Track
|
||||
musicPath string
|
||||
index int
|
||||
playing bool
|
||||
sr beep.SampleRate
|
||||
@@ -50,9 +48,8 @@ type updateSpeaker struct {
|
||||
offset int
|
||||
}
|
||||
|
||||
func New(musicPath string) *Jukebox {
|
||||
func New() *Jukebox {
|
||||
return &Jukebox{
|
||||
musicPath: musicPath,
|
||||
sr: beep.SampleRate(48000),
|
||||
speaker: make(chan updateSpeaker, 1),
|
||||
done: make(chan bool),
|
||||
@@ -89,10 +86,7 @@ func (j *Jukebox) doUpdateSpeaker(su updateSpeaker) error {
|
||||
return nil
|
||||
}
|
||||
j.index = su.index
|
||||
f, err := os.Open(path.Join(
|
||||
j.musicPath,
|
||||
j.playlist[su.index].RelPath(),
|
||||
))
|
||||
f, err := os.Open(j.playlist[su.index].AbsPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,12 +48,6 @@ func New(musicPaths []string, sorted bool, db *db.DB, genreSplit string, tagger
|
||||
}
|
||||
}
|
||||
|
||||
type ScanOptions struct {
|
||||
IsFull bool
|
||||
// TODO https://github.com/sentriz/gonic/issues/64
|
||||
Path string
|
||||
}
|
||||
|
||||
func (s *Scanner) IsScanning() bool {
|
||||
return atomic.LoadInt32(s.scanning) == 1
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package scanner_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
|
||||
type Options struct {
|
||||
DB *db.DB
|
||||
MusicPath string
|
||||
MusicPaths []string
|
||||
PodcastPath string
|
||||
CachePath string
|
||||
CoverCachePath string
|
||||
@@ -46,7 +46,9 @@ type Server struct {
|
||||
}
|
||||
|
||||
func New(opts Options) (*Server, error) {
|
||||
opts.MusicPath = filepath.Clean(opts.MusicPath)
|
||||
for i, musicPath := range opts.MusicPaths {
|
||||
opts.MusicPaths[i] = filepath.Clean(musicPath)
|
||||
}
|
||||
opts.CachePath = filepath.Clean(opts.CachePath)
|
||||
opts.PodcastPath = filepath.Clean(opts.PodcastPath)
|
||||
|
||||
@@ -55,7 +57,6 @@ func New(opts Options) (*Server, error) {
|
||||
scanner := scanner.New(opts.MusicPaths, false, opts.DB, opts.GenreSplit, tagger)
|
||||
base := &ctrlbase.Controller{
|
||||
DB: opts.DB,
|
||||
MusicPath: opts.MusicPath,
|
||||
ProxyPrefix: opts.ProxyPrefix,
|
||||
Scanner: scanner,
|
||||
}
|
||||
@@ -109,7 +110,7 @@ func New(opts Options) (*Server, error) {
|
||||
}
|
||||
|
||||
if opts.JukeboxEnabled {
|
||||
jukebox := jukebox.New(opts.MusicPath)
|
||||
jukebox := jukebox.New()
|
||||
ctrlSubsonic.Jukebox = jukebox
|
||||
server.jukebox = jukebox
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user