feat: add multi folder support

closes #50
This commit is contained in:
sentriz
2021-11-03 23:15:09 +00:00
parent fa587fc7de
commit 40cd031b05
32 changed files with 744 additions and 606 deletions

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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).

View File

@@ -46,7 +46,6 @@ func statusToBlock(code int) string {
type Controller struct {
DB *db.DB
MusicPath string
Scanner *scanner.Scanner
ProxyPrefix string
}

View File

@@ -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())
}
})
}
}

View File

@@ -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

View File

@@ -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},
})
}

View File

@@ -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)),

View File

@@ -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},
})
}

View File

@@ -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{}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}
]
}

View File

@@ -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

View File

@@ -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 }
]
}
]

View 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 }
]
}
]
}
}
}

View 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 }
]
}
]
}
}
}

View File

@@ -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 }
]
}
]

View 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 }
]
}
]
}
}
}

View 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 }
]
}
]
}
}
}

View File

@@ -5,6 +5,7 @@
"type": "gonic",
"directory": {
"id": "al-3",
"parent": "al-1",
"name": "album-1",
"child": [
{

View File

@@ -5,6 +5,7 @@
"type": "gonic",
"directory": {
"id": "al-2",
"parent": "al-1",
"name": "album-0",
"child": [
{

View File

@@ -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",

View File

@@ -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" }
]
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,9 +1,11 @@
package scanner_test
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
"github.com/jinzhu/gorm"

View File

@@ -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
}