Skip to content

Commit

Permalink
feat: add multi folder support
Browse files Browse the repository at this point in the history
closes #50
  • Loading branch information
sentriz committed Nov 4, 2021
1 parent 04fc5f1 commit 8fa2e5d
Show file tree
Hide file tree
Showing 28 changed files with 522 additions and 409 deletions.
38 changes: 32 additions & 6 deletions cmd/gonic/gonic.go
Expand Up @@ -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)")
Expand All @@ -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:],
Expand All @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/cespare/xxhash v1.1.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/disintegration/imaging v1.6.2
github.com/dustin/go-humanize v1.0.0
github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535
Expand Down
8 changes: 4 additions & 4 deletions server/ctrladmin/handlers_playlist.go
Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion server/ctrlbase/ctrl.go
Expand Up @@ -46,7 +46,6 @@ func statusToBlock(code int) string {

type Controller struct {
DB *db.DB
MusicPath string
Scanner *scanner.Scanner
ProxyPrefix string
}
Expand Down
10 changes: 4 additions & 6 deletions server/ctrlsubsonic/ctrl_test.go
Expand Up @@ -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)) {
Expand All @@ -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())
}
t.Errorf("\u001b[31;1mdiffering json\u001b[0m\n%s", diff.Render())
})
}
}
Expand Down
48 changes: 35 additions & 13 deletions server/ctrlsubsonic/handlers_by_folder.go
Expand Up @@ -12,36 +12,44 @@ 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)
musicFolderID, _ := params.Get("musicFolderId")
rootQ := c.DB.
Select("id").
Table("albums").
Where("parent_id IS NULL")
if musicFolderID != "" {
rootQ = rootQ.
Where("root_dir=?", musicFolderID)
}
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)
// [a-z#] -> 27
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()
Expand Down Expand Up @@ -161,27 +169,40 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {

func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
musicFolderID, _ := params.Get("musicFolderId")
query, err := params.Get("query")
if err != nil {
return spec.NewError(10, "please provide a `query` parameter")
}
query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*"))

rootQ := c.DB.
Select("id").
Table("albums").
Where("parent_id IS NULL")
if musicFolderID != "" {
rootQ = rootQ.
Where("root_dir=?", musicFolderID)
}

results := &spec.SearchResultTwo{}

// search "artists"
var artists []*db.Album
c.DB.
Where(`
parent_id=1
parent_id IN ?
AND ( right_path LIKE ? OR
right_path_u_dec LIKE ? )`,
query, query).
rootQ.SubQuery(), 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))
}

// search "albums"
var albums []*db.Album
c.DB.
Expand All @@ -196,6 +217,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
for _, a := range albums {
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
}

// search tracks
var tracks []*db.Track
c.DB.
Expand All @@ -209,7 +231,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
results.Tracks = append(results.Tracks,
spec.NewTCTrackByFolder(t, t.Album))
}
//

sub := spec.NewResponse()
sub.SearchResultTwo = results
return sub
Expand Down
3 changes: 3 additions & 0 deletions server/ctrlsubsonic/handlers_by_folder_test.go
Expand Up @@ -2,6 +2,7 @@ package ctrlsubsonic

import (
"net/url"
"path/filepath"
"testing"

_ "github.com/jinzhu/gorm/dialects/sqlite"
Expand All @@ -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},
})
}

Expand Down
14 changes: 6 additions & 8 deletions server/ctrlsubsonic/handlers_by_tags.go
Expand Up @@ -27,17 +27,15 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
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()
Expand Down
19 changes: 15 additions & 4 deletions server/ctrlsubsonic/handlers_common.go
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"path/filepath"
"time"
"unicode"

Expand Down Expand Up @@ -69,12 +70,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
}

Expand Down
15 changes: 8 additions & 7 deletions server/ctrlsubsonic/handlers_raw.go
Expand Up @@ -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:
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 8fa2e5d

Please sign in to comment.