Merge branch 'albmerge'

This commit is contained in:
sentriz
2019-06-05 17:57:43 +01:00
48 changed files with 1095 additions and 1101 deletions

29
.golangci.yml Normal file
View File

@@ -0,0 +1,29 @@
linters:
enable-all: true
disable:
- gochecknoglobals
- gochecknoinits
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- text: "weak cryptographic primitive"
linters:
- gosec
- path: model/model\.go
linters:
- lll
- path: server/handler/
source: "next http.HandlerFunc"
linters:
- interfacer
- path: server/handler/
source: "session.Save"
linters:
- errcheck
- path: server/handler/
source: "w.Write"
linters:
- errcheck

View File

@@ -14,7 +14,6 @@ import (
const (
programName = "gonic"
programVar = "GONIC"
)
func main() {

View File

@@ -52,6 +52,8 @@ func main() {
*musicPath,
*listenAddr,
)
s.SetupAdmin()
s.SetupSubsonic()
log.Printf("starting server at %s", *listenAddr)
if err := s.ListenAndServe(); err != nil {
log.Fatalf("error starting server: %v\n", err)

29
gen_handler_tests Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
# by folder
curl "http://localhost:6969/rest/getAlbumList.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=alphabeticalByArtist" | jq > server/handler/test_data/test_get_album_list_alpha_artist
curl "http://localhost:6969/rest/getAlbumList.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=alphabeticalByName" | jq > server/handler/test_data/test_get_album_list_alpha_name
curl "http://localhost:6969/rest/getAlbumList.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=alphabeticalByName" | jq > server/handler/test_data/test_get_album_list_two_alpha_name
curl "http://localhost:6969/rest/getAlbumList.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=newest" | jq > server/handler/test_data/test_get_album_list_newest
curl "http://localhost:6969/rest/getAlbumList.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=random" | jq > server/handler/test_data/test_get_album_list_random
curl "http://localhost:6969/rest/search2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=13" | jq > server/handler/test_data/test_search_two_q_13
curl "http://localhost:6969/rest/search2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=ani" | jq > server/handler/test_data/test_search_two_q_ani
curl "http://localhost:6969/rest/search2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=cert" | jq > server/handler/test_data/test_search_two_q_cert
curl 'http://localhost:6969/rest/getIndexes.view?c=Jamstash&p=admin&u=admin&v=1.9.0&f=json' | jq > server/handler/test_data/test_get_indexes_no_args
curl 'http://localhost:6969/rest/getMusicDirectory.view?c=Jamsstash&id=2&p=admin&u=admin&v=1.9.0&f=json' | jq > server/handler/test_data/test_get_music_directory_without_tracks
curl 'http://localhost:6969/rest/getMusicDirectory.view?c=Jamsstash&id=3&p=admin&u=admin&v=1.9.0&f=json' | jq > server/handler/test_data/test_get_music_directory_with_tracks
# by tags
curl "http://localhost:6969/rest/getAlbum.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&id=2" | jq > server/handler/test_data/test_get_album_without_cover
curl "http://localhost:6969/rest/getAlbum.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&id=3" | jq > server/handler/test_data/test_get_album_with_cover
curl "http://localhost:6969/rest/getAlbumList2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=alphabeticalByArtist" | jq > server/handler/test_data/test_get_album_list_two_alpha_artist
curl "http://localhost:6969/rest/getAlbumList2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=alphabeticalByName" | jq > server/handler/test_data/test_get_album_list_two_alpha_name
curl "http://localhost:6969/rest/getAlbumList2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=newest" | jq > server/handler/test_data/test_get_album_list_two_newest
curl "http://localhost:6969/rest/getAlbumList2.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&type=random" | jq > server/handler/test_data/test_get_album_list_two_random
curl "http://localhost:6969/rest/getArtist.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&id=1" | jq > server/handler/test_data/test_get_artist_id_one
curl "http://localhost:6969/rest/getArtist.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&id=2" | jq > server/handler/test_data/test_get_artist_id_two
curl "http://localhost:6969/rest/getArtist.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&id=3" | jq > server/handler/test_data/test_get_artist_id_three
curl "http://localhost:6969/rest/getArtists.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0" | jq > server/handler/test_data/test_get_artists_no_args
curl "http://localhost:6969/rest/search3.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=13" | jq > server/handler/test_data/test_search_three_q_13
curl "http://localhost:6969/rest/search3.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=ani" | jq > server/handler/test_data/test_search_three_q_ani
curl "http://localhost:6969/rest/search3.view?c=Jamstash&f=json&p=admin&u=admin&v=1.9.0&query=cert" | jq > server/handler/test_data/test_search_three_q_cert

3
go.mod
View File

@@ -28,4 +28,7 @@ require (
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b // indirect
google.golang.org/appengine v1.5.0 // indirect
gopkg.in/axiomzen/null.v3 v3.2.4
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
gopkg.in/pg.v4 v4.9.5 // indirect
)

6
go.sum
View File

@@ -263,10 +263,16 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/axiomzen/null.v3 v3.2.4 h1:5VmJ9lSU0dBJjisXhuhRnGiIolhTjkmQQ0EBNE9Z5QY=
gopkg.in/axiomzen/null.v3 v3.2.4/go.mod h1:Vq8/79AVvSZVg5PdN4kNROnTff7WtSh0HOiBHfOvVNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/pg.v4 v4.9.5 h1:bs21aaMPPPcUPhNtqGxN8EeYUFU10MsNrC7U9m/lJgU=
gopkg.in/pg.v4 v4.9.5/go.mod h1:cSUPtzgofjgARAbFCE5u6WDHGPgbR1sjUYcWQlKvpec=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

9
mime/mime.go Normal file
View File

@@ -0,0 +1,9 @@
package mime
var Types = map[string]string{
"mp3": "audio/mpeg",
"flac": "audio/x-flac",
"aac": "audio/x-aac",
"m4a": "audio/m4a",
"ogg": "audio/ogg",
}

View File

@@ -1,116 +1,88 @@
package model
import "time"
import (
"path"
"time"
// q: what in tarnation are the `IsNew`s for?
// a: it's a bit of a hack - but we set a models IsNew to true if
// we just filled it in for the first time, so when it comes
// time to insert them (post children callback) we can check for
// that bool being true - since it won't be true if it was already
// in the db
"github.com/sentriz/gonic/mime"
)
// Album represents the albums table
type Album struct {
IDBase
CrudBase
Artist Artist
ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Title string `gorm:"not null; index"`
// an Album having a `Path` is a little weird when browsing by tags
// (for the most part - the library's folder structure is treated as
// if it were flat), but this solves the "American Football problem"
// https://en.wikipedia.org/wiki/American_Football_(band)#Discography
Path string `gorm:"not null; unique_index"`
CoverID int `sql:"default: null; type:int REFERENCES covers(id)"`
Cover Cover
Year int
Tracks []Track
IsNew bool `gorm:"-"`
}
// Artist represents the Artists table
type Artist struct {
IDBase
CrudBase
Name string `gorm:"not null; unique_index"`
Albums []Album
Name string `gorm:"not null; unique_index"`
Albums []*Album `gorm:"foreignkey:TagArtistID"`
}
// Track represents the tracks table
type Track struct {
IDBase
CrudBase
Album Album
AlbumID int `gorm:"index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Artist Artist
ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
TrackArtist string
Bitrate int
Codec string
DiscNumber int
Duration int
Title string
TotalDiscs int
TotalTracks int
TrackNumber int
Year int
Suffix string
ContentType string
Size int
Folder Folder
FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"`
Path string `gorm:"not null; unique_index"`
Album *Album
AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"`
Artist *Artist
ArtistID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Duration int `gorm:"not null" sql:"default: null"`
Size int `gorm:"not null" sql:"default: null"`
Bitrate int `gorm:"not null" sql:"default: null"`
TagDiscNumber int `sql:"default: null"`
TagTitle string `sql:"default: null"`
TagTotalDiscs int `sql:"default: null"`
TagTotalTracks int `sql:"default: null"`
TagTrackArtist string `sql:"default: null"`
TagTrackNumber int `sql:"default: null"`
TagYear int `sql:"default: null"`
}
// Cover represents the covers table
type Cover struct {
IDBase
CrudBase
Image []byte
Path string `gorm:"not null; unique_index"`
IsNew bool `gorm:"-"`
func (t *Track) Ext() string {
longExt := path.Ext(t.Filename)
if len(longExt) < 1 {
return ""
}
return longExt[1:]
}
func (t *Track) MIME() string {
ext := t.Ext()
return mime.Types[ext]
}
// User represents the users table
type User struct {
IDBase
CrudBase
Name string `gorm:"not null; unique_index"`
Password string
LastFMSession string
IsAdmin bool
Name string `gorm:"not null; unique_index" sql:"default: null"`
Password string `gorm:"not null" sql:"default: null"`
LastFMSession string `sql:"default: null"`
IsAdmin bool `sql:"default: null"`
}
// Setting represents the settings table
type Setting struct {
CrudBase
Key string `gorm:"primary_key; auto_increment:false"`
Value string
Key string `gorm:"not null; primary_key; auto_increment:false" sql:"default: null"`
Value string `sql:"default: null"`
}
// Play represents the settings table
type Play struct {
IDBase
User User
UserID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
Album Album
AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Folder Folder
FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"`
Time time.Time
Count int
User *User
UserID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
Album *Album
AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Time time.Time `sql:"default: null"`
Count int
}
// Folder represents the settings table
type Folder struct {
type Album struct {
IDBase
CrudBase
Name string
Path string `gorm:"not null; unique_index"`
Parent *Folder
ParentID int `sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"`
CoverID int `sql:"default: null; type:int REFERENCES covers(id)"`
HasTracks bool `gorm:"not null; index"`
Cover Cover
IsNew bool `gorm:"-"`
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"`
Parent *Album
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Cover string `sql:"default: null"`
TagArtist *Artist
TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
TagTitle string `gorm:"index" sql:"default: null"`
TagYear int `sql:"default: null"`
Tracks []*Track
IsNew bool `gorm:"-"`
}

View File

@@ -1,35 +0,0 @@
package scanner
import "github.com/sentriz/gonic/model"
type folderStack []model.Folder
func (s *folderStack) Push(v model.Folder) {
*s = append(*s, v)
}
func (s *folderStack) Pop() model.Folder {
l := len(*s)
if l == 0 {
return model.Folder{}
}
r := (*s)[l-1]
*s = (*s)[:l-1]
return r
}
func (s *folderStack) Peek() model.Folder {
l := len(*s)
if l == 0 {
return model.Folder{}
}
return (*s)[l-1]
}
func (s *folderStack) PeekID() int {
l := len(*s)
if l == 0 {
return 0
}
return (*s)[l-1].ID
}

View File

@@ -1,21 +1,20 @@
package scanner
// Album -> needs a CoverID
// -> needs a FolderID (American Football)
// Folder -> needs a CoverID
// -> needs a ParentID
// Track -> needs an AlbumID
// -> needs a FolderID
import (
"log"
"os"
"path"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/dhowden/tag"
"github.com/jinzhu/gorm"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/sentriz/gonic/mime"
"github.com/sentriz/gonic/model"
)
@@ -23,30 +22,70 @@ var (
IsScanning int32
)
var coverFilenames = map[string]struct{}{
"cover.png": {},
"cover.jpg": {},
"cover.jpeg": {},
"folder.png": {},
"folder.jpg": {},
"folder.jpeg": {},
"album.png": {},
"album.jpg": {},
"album.jpeg": {},
"front.png": {},
"front.jpg": {},
"front.jpeg": {},
}
type Scanner struct {
db, tx *gorm.DB
musicPath string
seenTracks map[string]bool
seenTracks map[int]struct{}
curFolders folderStack
curTracks []model.Track
curCover model.Cover
curAlbum model.Album
curAArtist model.Artist
curCover string
}
func New(db *gorm.DB, musicPath string) *Scanner {
return &Scanner{
db: db,
musicPath: musicPath,
seenTracks: make(map[string]bool),
seenTracks: make(map[int]struct{}),
curFolders: make(folderStack, 0),
curTracks: make([]model.Track, 0),
curCover: model.Cover{},
curAlbum: model.Album{},
curAArtist: model.Artist{},
}
}
func (s *Scanner) curFolder() *model.Album {
return s.curFolders.Peek()
}
func (s *Scanner) curFolderID() int {
peek := s.curFolders.Peek()
if peek == nil {
return 0
}
return peek.ID
}
func (s *Scanner) MigrateDB() error {
defer logElapsed(time.Now(), "migrating database")
s.tx = s.db.Begin()
defer s.tx.Commit()
s.tx.AutoMigrate(
model.Artist{},
model.Track{},
model.User{},
model.Setting{},
model.Play{},
model.Album{},
)
s.tx.FirstOrCreate(&model.User{}, model.User{
Name: "admin",
Password: "admin",
IsAdmin: true,
})
return nil
}
func (s *Scanner) Start() error {
if atomic.LoadInt32(&IsScanning) == 1 {
return errors.New("already scanning")
@@ -84,39 +123,172 @@ func (s *Scanner) startScan() error {
func (s *Scanner) startClean() error {
defer logElapsed(time.Now(), "cleaning database")
var tracks []model.Track
s.tx.
Select("id, path").
Find(&tracks)
var tracks []*model.Track
err := s.tx.
Select("id").
Find(&tracks).
Error
if err != nil {
return errors.Wrap(err, "scanning tracks")
}
var deleted int
for _, track := range tracks {
_, ok := s.seenTracks[track.Path]
if ok {
continue
_, ok := s.seenTracks[track.ID]
if !ok {
s.tx.Delete(track)
deleted++
}
s.tx.Delete(&track)
log.Println("removed track", track.Path)
}
log.Printf("removed %d tracks\n", deleted)
return nil
}
type item struct {
fullPath string
relPath string
directory string
filename string
stat os.FileInfo
}
func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
stat, err := os.Stat(fullPath)
if err != nil {
return errors.Wrap(err, "stating")
}
relPath, err := filepath.Rel(s.musicPath, fullPath)
if err != nil {
return errors.Wrap(err, "getting relative path")
}
directory, filename := path.Split(relPath)
it := &item{
fullPath: fullPath,
relPath: relPath,
directory: directory,
filename: filename,
stat: stat,
}
if info.IsDir() {
return s.handleFolder(it)
}
lowerFilename := strings.ToLower(filename)
if _, ok := coverFilenames[lowerFilename]; ok {
s.curCover = filename
return nil
}
ext := path.Ext(filename)[1:]
if _, ok := mime.Types[ext]; ok {
return s.handleTrack(it)
}
return nil
}
func (s *Scanner) MigrateDB() error {
defer logElapsed(time.Now(), "migrating database")
s.tx = s.db.Begin()
defer s.tx.Commit()
s.tx.AutoMigrate(
model.Album{},
model.Artist{},
model.Track{},
model.Cover{},
model.User{},
model.Setting{},
model.Play{},
model.Folder{},
)
s.tx.FirstOrCreate(&model.User{}, model.User{
Name: "admin",
Password: "admin",
IsAdmin: true,
})
func (s *Scanner) callbackPost(fullPath string, info *godirwalk.Dirent) error {
folder := s.curFolders.Pop()
if folder.IsNew {
folder.ParentID = s.curFolderID()
folder.Cover = s.curCover
s.tx.Save(folder)
}
s.curCover = ""
log.Printf("processed folder `%s`\n", fullPath)
return nil
}
func readTags(path string) (tag.Metadata, error) {
trackData, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "reading track from disk")
}
defer trackData.Close()
tags, err := tag.ReadFrom(trackData)
if err != nil {
return nil, errors.Wrap(err, "reading tags from track")
}
return tags, nil
}
func (s *Scanner) handleFolder(it *item) error {
folder := &model.Album{}
defer s.curFolders.Push(folder)
err := s.tx.
Where(model.Album{
LeftPath: it.directory,
RightPath: it.filename,
}).
First(folder).
Error
if !gorm.IsRecordNotFoundError(err) &&
it.stat.ModTime().Before(folder.UpdatedAt) {
// we found the record but it hasn't changed
return nil
}
folder.LeftPath = it.directory
folder.RightPath = it.filename
s.tx.Save(folder)
folder.IsNew = true
return nil
}
func (s *Scanner) handleTrack(it *item) error {
//
// set track basics
track := &model.Track{}
defer func() {
// id will will be found (the first early return)
// or created the tx.Save(track)
s.seenTracks[track.ID] = struct{}{}
}()
err := s.tx.
Where(model.Track{
AlbumID: s.curFolderID(),
Filename: it.filename,
}).
First(track).
Error
if !gorm.IsRecordNotFoundError(err) &&
it.stat.ModTime().Before(track.UpdatedAt) {
// we found the record but it hasn't changed
return nil
}
track.Filename = it.filename
track.Size = int(it.stat.Size())
track.AlbumID = s.curFolderID()
track.Duration = -1
track.Bitrate = -1
tags, err := readTags(it.fullPath)
if err != nil {
return errors.Wrap(err, "reading tags")
}
trackNumber, totalTracks := tags.Track()
discNumber, totalDiscs := tags.Disc()
track.TagDiscNumber = discNumber
track.TagTotalDiscs = totalDiscs
track.TagTotalTracks = totalTracks
track.TagTrackNumber = trackNumber
track.TagTitle = tags.Title()
track.TagTrackArtist = tags.Artist()
track.TagYear = tags.Year()
//
// set album artist basics
artist := &model.Artist{}
err = s.tx.
Where("name = ?", tags.AlbumArtist()).
First(artist).
Error
if gorm.IsRecordNotFoundError(err) {
artist.Name = tags.AlbumArtist()
s.tx.Save(artist)
}
track.ArtistID = artist.ID
s.tx.Save(track)
//
// set album if this is the first track in the folder
if !s.curFolder().IsNew {
return nil
}
s.curFolder().TagTitle = tags.Album()
s.curFolder().TagYear = tags.Year()
s.curFolder().TagArtistID = artist.ID
return nil
}

40
scanner/stack.go Normal file
View File

@@ -0,0 +1,40 @@
package scanner
import (
"fmt"
"strings"
"github.com/sentriz/gonic/model"
)
type folderStack []*model.Album
func (s *folderStack) Push(v *model.Album) {
*s = append(*s, v)
}
func (s *folderStack) Pop() *model.Album {
l := len(*s)
if l == 0 {
return nil
}
r := (*s)[l-1]
*s = (*s)[:l-1]
return r
}
func (s *folderStack) Peek() *model.Album {
l := len(*s)
if l == 0 {
return nil
}
return (*s)[l-1]
}
func (s *folderStack) String() string {
paths := make([]string, len(*s))
for i, folder := range *s {
paths[i] = folder.LeftPath
}
return fmt.Sprintf("[%s]", strings.Join(paths, " "))
}

View File

@@ -1,62 +0,0 @@
package scanner
import (
"os"
"path"
"path/filepath"
"strings"
"github.com/dhowden/tag"
"github.com/pkg/errors"
)
var trackExtensions = map[string]string{
"mp3": "audio/mpeg",
"flac": "audio/x-flac",
"aac": "audio/x-aac",
"m4a": "audio/m4a",
"ogg": "audio/ogg",
}
func isTrack(fullPath string) (string, string, bool) {
ext := filepath.Ext(fullPath)[1:]
mine, ok := trackExtensions[ext]
if !ok {
return "", "", false
}
return mine, ext, true
}
var coverFilenames = map[string]struct{}{
"cover.png": struct{}{},
"cover.jpg": struct{}{},
"cover.jpeg": struct{}{},
"folder.png": struct{}{},
"folder.jpg": struct{}{},
"folder.jpeg": struct{}{},
"album.png": struct{}{},
"album.jpg": struct{}{},
"album.jpeg": struct{}{},
"front.png": struct{}{},
"front.jpg": struct{}{},
"front.jpeg": struct{}{},
}
func isCover(fullPath string) bool {
_, filename := path.Split(fullPath)
_, ok := coverFilenames[strings.ToLower(filename)]
return ok
}
func readTags(path string) (tag.Metadata, error) {
trackData, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "reading track from disk")
}
defer trackData.Close()
tags, err := tag.ReadFrom(trackData)
if err != nil {
return nil, errors.Wrap(err, "reading tags from track")
}
return tags, nil
}

View File

@@ -1,195 +0,0 @@
package scanner
import (
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"github.com/jinzhu/gorm"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/sentriz/gonic/model"
)
type trackItem struct {
mime string
ext string
}
type item struct {
path string
relPath string
stat os.FileInfo
track *trackItem
}
func (s *Scanner) callbackItem(path string, info *godirwalk.Dirent) error {
stat, err := os.Stat(path)
if err != nil {
return errors.Wrap(err, "stating")
}
relPath, err := filepath.Rel(s.musicPath, path)
if err != nil {
return errors.Wrap(err, "getting relative path")
}
it := &item{
path: path,
relPath: relPath,
stat: stat,
}
if info.IsDir() {
return s.handleFolder(it)
}
if isCover(path) {
return s.handleCover(it)
}
if mime, ext, ok := isTrack(path); ok {
s.seenTracks[relPath] = true
it.track = &trackItem{mime: mime, ext: ext}
return s.handleTrack(it)
}
return nil
}
func (s *Scanner) callbackPost(path string, info *godirwalk.Dirent) error {
// in general in this function - if a model is not nil, then it
// has at least been looked up. if it has a id of 0, then it is
// a new record and needs to be inserted
if s.curCover.IsNew {
s.tx.Save(&s.curCover)
}
if s.curAlbum.IsNew {
s.curAlbum.CoverID = s.curCover.ID
s.tx.Save(&s.curAlbum)
}
folder := s.curFolders.Pop()
if folder.IsNew {
folder.ParentID = s.curFolders.PeekID()
folder.CoverID = s.curCover.ID
folder.HasTracks = len(s.curTracks) > 1
s.tx.Save(&folder)
}
for _, t := range s.curTracks {
t.FolderID = folder.ID
t.AlbumID = s.curAlbum.ID
s.tx.Save(&t)
}
//
s.curTracks = make([]model.Track, 0)
s.curCover = model.Cover{}
s.curAlbum = model.Album{}
s.curAArtist = model.Artist{}
//
log.Printf("processed folder `%s`\n", path)
return nil
}
func (s *Scanner) handleFolder(it *item) error {
// TODO:
var folder model.Folder
err := s.tx.
Where("path = ?", it.relPath).
First(&folder).
Error
if !gorm.IsRecordNotFoundError(err) &&
it.stat.ModTime().Before(folder.UpdatedAt) {
// we found the record but it hasn't changed
s.curFolders.Push(folder)
return nil
}
folder.Path = it.relPath
folder.Name = it.stat.Name()
s.tx.Save(&folder)
folder.IsNew = true
s.curFolders.Push(folder)
return nil
}
func (s *Scanner) handleCover(it *item) error {
err := s.tx.
Where("path = ?", it.relPath).
First(&s.curCover).
Error
if !gorm.IsRecordNotFoundError(err) &&
it.stat.ModTime().Before(s.curCover.UpdatedAt) {
// we found the record but it hasn't changed
return nil
}
s.curCover.Path = it.relPath
image, err := ioutil.ReadFile(it.path)
if err != nil {
return errors.Wrap(err, "reading cover")
}
s.curCover.Image = image
s.curCover.IsNew = true
return nil
}
func (s *Scanner) handleTrack(it *item) error {
//
// set track basics
track := model.Track{}
err := s.tx.
Where("path = ?", it.relPath).
First(&track).
Error
if !gorm.IsRecordNotFoundError(err) &&
it.stat.ModTime().Before(track.UpdatedAt) {
// we found the record but it hasn't changed
return nil
}
tags, err := readTags(it.path)
if err != nil {
return errors.Wrap(err, "reading tags")
}
trackNumber, totalTracks := tags.Track()
discNumber, totalDiscs := tags.Disc()
track.DiscNumber = discNumber
track.TotalDiscs = totalDiscs
track.TotalTracks = totalTracks
track.TrackNumber = trackNumber
track.Path = it.relPath
track.Suffix = it.track.ext
track.ContentType = it.track.mime
track.Size = int(it.stat.Size())
track.Title = tags.Title()
track.TrackArtist = tags.Artist()
track.Year = tags.Year()
track.FolderID = s.curFolders.PeekID()
//
// set album artist basics
err = s.tx.Where("name = ?", tags.AlbumArtist()).
First(&s.curAArtist).
Error
if gorm.IsRecordNotFoundError(err) {
s.curAArtist.Name = tags.AlbumArtist()
s.tx.Save(&s.curAArtist)
}
track.ArtistID = s.curAArtist.ID
//
// set album if this is the first track in the folder
if len(s.curTracks) > 0 {
s.curTracks = append(s.curTracks, track)
return nil
}
s.curTracks = append(s.curTracks, track)
//
directory, _ := path.Split(it.relPath)
err = s.tx.
Where("path = ?", directory).
First(&s.curAlbum).
Error
if !gorm.IsRecordNotFoundError(err) {
// we found the record
return nil
}
s.curAlbum.Path = directory
s.curAlbum.Title = tags.Album()
s.curAlbum.Year = tags.Year()
s.curAlbum.ArtistID = s.curAArtist.ID
s.curAlbum.IsNew = true
return nil
}

View File

@@ -1,15 +1,17 @@
package handler
import (
"path"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/server/subsonic"
)
func makeChildFromFolder(f *model.Folder, parent *model.Folder) *subsonic.Track {
func makeChildFromFolder(f *model.Album, parent *model.Album) *subsonic.Track {
child := &subsonic.Track{
ID: f.ID,
Title: f.Name,
CoverID: f.CoverID,
CoverID: f.ID,
Title: f.RightPath,
IsDir: true,
}
if parent != nil {
@@ -18,48 +20,52 @@ func makeChildFromFolder(f *model.Folder, parent *model.Folder) *subsonic.Track
return child
}
func makeChildFromTrack(t *model.Track, parent *model.Folder) *subsonic.Track {
func makeChildFromTrack(t *model.Track, parent *model.Album) *subsonic.Track {
return &subsonic.Track{
ID: t.ID,
Album: t.Album.Title,
Artist: t.TrackArtist,
ContentType: t.ContentType,
Path: t.Path,
Album: t.Album.RightPath,
ContentType: t.MIME(),
Suffix: t.Ext(),
Size: t.Size,
Suffix: t.Suffix,
Title: t.Title,
TrackNumber: t.TrackNumber,
ParentID: parent.ID,
CoverID: parent.CoverID,
Duration: 0,
IsDir: false,
Type: "music",
Artist: t.TagTrackArtist,
Title: t.TagTitle,
TrackNumber: t.TagTrackNumber,
Path: path.Join(
parent.LeftPath,
parent.RightPath,
t.Filename,
),
ParentID: parent.ID,
CoverID: parent.ID,
Duration: 0,
IsDir: false,
Type: "music",
}
}
func makeAlbumFromFolder(f *model.Folder) *subsonic.Album {
func makeAlbumFromFolder(f *model.Album) *subsonic.Album {
return &subsonic.Album{
ID: f.ID,
Title: f.Name,
CoverID: f.CoverID,
Title: f.RightPath,
CoverID: f.ID,
ParentID: f.ParentID,
Artist: f.Parent.Name,
Artist: f.Parent.RightPath,
IsDir: true,
}
}
func makeArtistFromFolder(f *model.Folder) *subsonic.Artist {
func makeArtistFromFolder(f *model.Album) *subsonic.Artist {
return &subsonic.Artist{
ID: f.ID,
Name: f.Name,
Name: f.RightPath,
}
}
func makeDirFromFolder(f *model.Folder, children []*subsonic.Track) *subsonic.Directory {
func makeDirFromFolder(f *model.Album, children []*subsonic.Track) *subsonic.Directory {
return &subsonic.Directory{
ID: f.ID,
Parent: f.ParentID,
Name: f.Name,
Name: f.RightPath,
Children: children,
}
}

View File

@@ -1,38 +1,47 @@
package handler
import (
"path"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/server/subsonic"
)
func makeAlbumFromAlbum(a *model.Album, artist *model.Artist) *subsonic.Album {
return &subsonic.Album{
ID: a.ID,
Name: a.Title,
Created: a.CreatedAt,
CoverID: a.CoverID,
Artist: artist.Name,
ArtistID: artist.ID,
ret := &subsonic.Album{
ID: a.ID,
Name: a.TagTitle,
Created: a.CreatedAt,
CoverID: a.ID,
}
if artist != nil {
ret.Artist = artist.Name
ret.ArtistID = artist.ID
}
return ret
}
func makeTrackFromTrack(t *model.Track, album *model.Album) *subsonic.Track {
return &subsonic.Track{
ID: t.ID,
Title: t.Title,
Artist: t.TrackArtist,
TrackNumber: t.TrackNumber,
ContentType: t.ContentType,
Path: t.Path,
ParentID: t.FolderID,
Suffix: t.Suffix,
ContentType: t.MIME(),
Suffix: t.Ext(),
ParentID: t.AlbumID,
CreatedAt: t.CreatedAt,
Size: t.Size,
Album: album.Title,
AlbumID: album.ID,
ArtistID: album.Artist.ID,
CoverID: album.CoverID,
Type: "music",
Title: t.TagTitle,
Artist: t.TagTrackArtist,
TrackNumber: t.TagTrackNumber,
Path: path.Join(
album.LeftPath,
album.RightPath,
t.Filename,
),
Album: album.TagTitle,
AlbumID: album.ID,
ArtistID: album.TagArtist.ID,
CoverID: album.ID,
Type: "music",
}
}

View File

@@ -24,8 +24,10 @@ type Controller struct {
}
func (c *Controller) GetSetting(key string) string {
var setting model.Setting
c.DB.Where("key = ?", key).First(&setting)
setting := &model.Setting{}
c.DB.
Where("key = ?", key).
First(setting)
return setting.Value
}
@@ -37,13 +39,13 @@ func (c *Controller) SetSetting(key, value string) {
}
func (c *Controller) GetUserFromName(name string) *model.User {
var user model.User
user := &model.User{}
err := c.DB.
Where("name = ?", name).
First(&user).
First(user).
Error
if gorm.IsRecordNotFoundError(err) {
return nil
}
return &user
return user
}

View File

@@ -48,11 +48,11 @@ func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
}
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
var data templateData
c.DB.Table("artists").Count(&data.ArtistCount)
c.DB.Table("albums").Count(&data.AlbumCount)
c.DB.Table("tracks").Count(&data.TrackCount)
c.DB.Find(&data.AllUsers)
data := &templateData{}
c.DB.Table("artists").Count(data.ArtistCount)
c.DB.Table("albums").Count(data.AlbumCount)
c.DB.Table("tracks").Count(data.TrackCount)
c.DB.Find(data.AllUsers)
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
scheme := firstExisting(
"http", // fallback
@@ -66,7 +66,7 @@ func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
r.Host,
)
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
renderTemplate(w, r, c.Templates["home"], &data)
renderTemplate(w, r, c.Templates["home"], data)
}
func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
@@ -127,22 +127,27 @@ func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request)
http.Error(w, "please provide a username", 400)
return
}
var user model.User
err := c.DB.Where("name = ?", username).First(&user).Error
user := &model.User{}
err := c.DB.
Where("name = ?", username).
First(user).
Error
if gorm.IsRecordNotFoundError(err) {
http.Error(w, "couldn't find a user with that name", 400)
return
}
var data templateData
data.SelectedUser = &user
renderTemplate(w, r, c.Templates["change_password"], &data)
data := &templateData{}
data.SelectedUser = user
renderTemplate(w, r, c.Templates["change_password"], data)
}
func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
username := r.URL.Query().Get("user")
var user model.User
c.DB.Where("name = ?", username).First(&user)
user := &model.User{}
c.DB.
Where("name = ?", username).
First(user)
passwordOne := r.FormValue("password_one")
passwordTwo := r.FormValue("password_two")
err := validatePasswords(passwordOne, passwordTwo)
@@ -153,7 +158,7 @@ func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Reques
return
}
user.Password = passwordOne
c.DB.Save(&user)
c.DB.Save(user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
@@ -163,22 +168,27 @@ func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
http.Error(w, "please provide a username", 400)
return
}
var user model.User
err := c.DB.Where("name = ?", username).First(&user).Error
user := &model.User{}
err := c.DB.
Where("name = ?", username).
First(user).
Error
if gorm.IsRecordNotFoundError(err) {
http.Error(w, "couldn't find a user with that name", 400)
return
}
var data templateData
data.SelectedUser = &user
renderTemplate(w, r, c.Templates["delete_user"], &data)
data := &templateData{}
data.SelectedUser = user
renderTemplate(w, r, c.Templates["delete_user"], data)
}
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("user")
var user model.User
c.DB.Where("name = ?", username).First(&user)
c.DB.Delete(&user)
user := &model.User{}
c.DB.
Where("name = ?", username).
First(user)
c.DB.Delete(user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
@@ -222,10 +232,10 @@ func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
}
func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) {
var data templateData
data := &templateData{}
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
data.CurrentLastFMAPISecret = c.GetSetting("lastfm_secret")
renderTemplate(w, r, c.Templates["update_lastfm_api_key"], &data)
renderTemplate(w, r, c.Templates["update_lastfm_api_key"], data)
}
func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) {

View File

@@ -22,7 +22,9 @@ func TestFirstExisting(t *testing.T) {
"default"},
}
for _, tc := range cases {
tc := tc // pin
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actu := firstExisting(tc.or, tc.values...)
if actu != tc.exp {
t.Errorf("expected %q, got %q", tc.exp, actu)

View File

@@ -19,12 +19,14 @@ import (
// under the root directory
func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
var folders []model.Folder
c.DB.Where("parent_id = 1").Find(&folders)
var indexMap = make(map[rune]*subsonic.Index)
var indexes []*subsonic.Index
var folders []*model.Album
c.DB.
Where("parent_id = 1").
Find(&folders)
indexMap := make(map[rune]*subsonic.Index)
indexes := []*subsonic.Index{}
for _, folder := range folders {
i := indexOf(folder.Name)
i := indexOf(folder.RightPath)
index, ok := indexMap[i]
if !ok {
index = &subsonic.Index{
@@ -35,9 +37,9 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
indexes = append(indexes, index)
}
index.Artists = append(index.Artists,
makeArtistFromFolder(&folder))
makeArtistFromFolder(folder))
}
sort.Slice(indexes[:], func(i, j int) bool {
sort.Slice(indexes, func(i, j int) bool {
return indexes[i].Name < indexes[j].Name
})
sub := subsonic.NewResponse()
@@ -55,39 +57,39 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
return
}
childrenObj := []*subsonic.Track{}
var folder model.Folder
c.DB.First(&folder, id)
folder := &model.Album{}
c.DB.First(folder, id)
//
// start looking for child childFolders in the current dir
var childFolders []model.Folder
var childFolders []*model.Album
c.DB.
Where("parent_id = ?", id).
Find(&childFolders)
for _, c := range childFolders {
childrenObj = append(childrenObj,
makeChildFromFolder(&c, &folder))
makeChildFromFolder(c, folder))
}
//
// start looking for child childTracks in the current dir
var childTracks []model.Track
var childTracks []*model.Track
c.DB.
Where("folder_id = ?", id).
Where("album_id = ?", id).
Preload("Album").
Order("title").
Order("filename").
Find(&childTracks)
for _, c := range childTracks {
toAppend := makeChildFromTrack(c, folder)
if getStrParam(r, "c") == "Jamstash" {
// jamstash thinks it can't play flacs
c.ContentType = "audio/mpeg"
c.Suffix = "mp3"
toAppend.ContentType = "audio/mpeg"
toAppend.Suffix = "mp3"
}
childrenObj = append(childrenObj,
makeChildFromTrack(&c, &folder))
childrenObj = append(childrenObj, toAppend)
}
//
// respond section
sub := subsonic.NewResponse()
sub.Directory = makeDirFromFolder(&folder, childrenObj)
sub.Directory = makeDirFromFolder(folder, childrenObj)
respond(w, r, sub)
}
@@ -103,16 +105,16 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
switch listType {
case "alphabeticalByArtist":
q = q.Joins(`
JOIN folders AS parent_folders
ON folders.parent_id = parent_folders.id`)
q = q.Order("parent_folders.name")
JOIN albums AS parent_albums
ON albums.parent_id = parent_albums.id`)
q = q.Order("parent_albums.right_path")
case "alphabeticalByName":
q = q.Order("name")
q = q.Order("right_path")
case "frequent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON folders.id = plays.folder_id AND plays.user_id = ?`,
ON albums.id = plays.album_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.count DESC")
case "newest":
@@ -123,7 +125,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON folders.id = plays.folder_id AND plays.user_id = ?`,
ON albums.id = plays.album_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.time DESC")
default:
@@ -131,9 +133,9 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
"unknown value `%s` for parameter 'type'", listType)
return
}
var folders []model.Folder
var folders []*model.Album
q.
Where("folders.has_tracks = 1").
Where("albums.tag_artist_id IS NOT NULL").
Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)).
Preload("Parent").
@@ -142,7 +144,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
sub.Albums = &subsonic.Albums{}
for _, folder := range folders {
sub.Albums.List = append(sub.Albums.List,
makeAlbumFromFolder(&folder))
makeAlbumFromFolder(folder))
}
respond(w, r, sub)
}
@@ -158,41 +160,41 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) {
results := &subsonic.SearchResultTwo{}
//
// search "artists"
var artists []model.Folder
var artists []*model.Album
c.DB.
Where("parent_id = 1 AND name LIKE ?", query).
Where("parent_id = 1 AND right_path LIKE ?", query).
Offset(getIntParamOr(r, "artistOffset", 0)).
Limit(getIntParamOr(r, "artistCount", 20)).
Find(&artists)
for _, a := range artists {
results.Artists = append(results.Artists,
makeDirFromFolder(&a, nil))
makeDirFromFolder(a, nil))
}
//
// search "albums"
var albums []model.Folder
var albums []*model.Album
c.DB.
Preload("Parent").
Where("has_tracks = 1 AND name LIKE ?", query).
Where("tag_artist_id IS NOT NULL AND right_path LIKE ?", query).
Offset(getIntParamOr(r, "albumOffset", 0)).
Limit(getIntParamOr(r, "albumCount", 20)).
Find(&albums)
for _, a := range albums {
results.Albums = append(results.Albums,
makeChildFromFolder(&a, a.Parent))
makeChildFromFolder(a, a.Parent))
}
//
// search tracks
var tracks []model.Track
var tracks []*model.Track
c.DB.
Preload("Folder").
Where("title LIKE ?", query).
Preload("Album").
Where("filename LIKE ?", query).
Offset(getIntParamOr(r, "songOffset", 0)).
Limit(getIntParamOr(r, "songCount", 20)).
Find(&tracks)
for _, t := range tracks {
results.Tracks = append(results.Tracks,
makeChildFromTrack(&t, &t.Folder))
makeChildFromTrack(t, t.Album))
}
//
sub := subsonic.NewResponse()

View File

@@ -9,7 +9,7 @@ import (
func TestGetIndexes(t *testing.T) {
testQueryCases(t, testController.GetIndexes, []*queryCase{
{url.Values{"id": []string{"2"}}, "id_two", false},
{url.Values{}, "no_args", false},
})
}

View File

@@ -13,10 +13,10 @@ import (
)
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
var artists []model.Artist
var artists []*model.Artist
c.DB.Find(&artists)
var indexMap = make(map[rune]*subsonic.Index)
var indexes subsonic.Artists
indexMap := make(map[rune]*subsonic.Index)
indexes := &subsonic.Artists{}
for _, artist := range artists {
i := indexOf(artist.Name)
index, ok := indexMap[i]
@@ -29,13 +29,13 @@ func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
indexes.List = append(indexes.List, index)
}
index.Artists = append(index.Artists,
makeArtistFromArtist(&artist))
makeArtistFromArtist(artist))
}
sort.Slice(indexes.List[:], func(i, j int) bool {
sort.Slice(indexes.List, func(i, j int) bool {
return indexes.List[i].Name < indexes.List[j].Name
})
sub := subsonic.NewResponse()
sub.Artists = &indexes
sub.Artists = indexes
respond(w, r, sub)
}
@@ -45,15 +45,15 @@ func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var artist model.Artist
artist := &model.Artist{}
c.DB.
Preload("Albums").
First(&artist, id)
First(artist, id)
sub := subsonic.NewResponse()
sub.Artist = makeArtistFromArtist(&artist)
sub.Artist = makeArtistFromArtist(artist)
for _, album := range artist.Albums {
sub.Artist.Albums = append(sub.Artist.Albums,
makeAlbumFromAlbum(&album, &artist))
makeAlbumFromAlbum(album, artist))
}
respond(w, r, sub)
}
@@ -64,23 +64,23 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var album model.Album
album := &model.Album{}
err = c.DB.
Preload("Artist").
Preload("TagArtist").
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
return db.Order("tracks.track_number")
return db.Order("tracks.tag_track_number")
}).
First(&album, id).
First(album, id).
Error
if gorm.IsRecordNotFoundError(err) {
respondError(w, r, 10, "couldn't find an album with that id")
return
}
sub := subsonic.NewResponse()
sub.Album = makeAlbumFromAlbum(&album, &album.Artist)
sub.Album = makeAlbumFromAlbum(album, album.TagArtist)
for _, track := range album.Tracks {
sub.Album.Tracks = append(sub.Album.Tracks,
makeTrackFromTrack(&track, &album))
makeTrackFromTrack(track, album))
}
respond(w, r, sub)
}
@@ -98,16 +98,16 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
case "alphabeticalByArtist":
q = q.Joins(`
JOIN artists
ON albums.artist_id = artists.id`)
ON albums.tag_artist_id = artists.id`)
q = q.Order("artists.name")
case "alphabeticalByName":
q = q.Order("title")
q = q.Order("tag_title")
case "byYear":
q = q.Where(
"year BETWEEN ? AND ?",
"tag_year BETWEEN ? AND ?",
getIntParamOr(r, "fromYear", 1800),
getIntParamOr(r, "toYear", 2200))
q = q.Order("year")
q = q.Order("tag_year")
case "frequent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
@@ -131,17 +131,18 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
"unknown value `%s` for parameter 'type'", listType)
return
}
var albums []model.Album
var albums []*model.Album
q.
Where("albums.tag_artist_id IS NOT NULL").
Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)).
Preload("Artist").
Preload("TagArtist").
Find(&albums)
sub := subsonic.NewResponse()
sub.AlbumsTwo = &subsonic.Albums{}
for _, album := range albums {
sub.AlbumsTwo.List = append(sub.AlbumsTwo.List,
makeAlbumFromAlbum(&album, &album.Artist))
makeAlbumFromAlbum(album, album.TagArtist))
}
respond(w, r, sub)
}
@@ -157,7 +158,7 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
results := &subsonic.SearchResultThree{}
//
// search "artists"
var artists []model.Artist
var artists []*model.Artist
c.DB.
Where("name LIKE ?", query).
Offset(getIntParamOr(r, "artistOffset", 0)).
@@ -165,33 +166,33 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
Find(&artists)
for _, a := range artists {
results.Artists = append(results.Artists,
makeArtistFromArtist(&a))
makeArtistFromArtist(a))
}
//
// search "albums"
var albums []model.Album
var albums []*model.Album
c.DB.
Preload("Artist").
Where("title LIKE ?", query).
Preload("TagArtist").
Where("tag_title LIKE ?", query).
Offset(getIntParamOr(r, "albumOffset", 0)).
Limit(getIntParamOr(r, "albumCount", 20)).
Find(&albums)
for _, a := range albums {
results.Albums = append(results.Albums,
makeAlbumFromAlbum(&a, &a.Artist))
makeAlbumFromAlbum(a, a.TagArtist))
}
//
// search tracks
var tracks []model.Track
var tracks []*model.Track
c.DB.
Preload("Album").
Where("title LIKE ?", query).
Where("tag_title LIKE ?", query).
Offset(getIntParamOr(r, "songOffset", 0)).
Limit(getIntParamOr(r, "songCount", 20)).
Find(&tracks)
for _, t := range tracks {
results.Tracks = append(results.Tracks,
makeTrackFromTrack(&t, &t.Album))
makeTrackFromTrack(t, t.Album))
}
sub := subsonic.NewResponse()
sub.SearchResultThree = results

View File

@@ -1,6 +1,7 @@
package handler
import (
"log"
"net/http"
"os"
"path"
@@ -8,6 +9,7 @@ import (
"time"
"unicode"
"github.com/jinzhu/gorm"
"github.com/rainycape/unidecode"
"github.com/sentriz/gonic/model"
@@ -31,16 +33,21 @@ func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var track model.Track
c.DB.
track := &model.Track{}
err = c.DB.
Preload("Album").
Preload("Folder").
First(&track, id)
if track.Path == "" {
First(track, id).
Error
if gorm.IsRecordNotFoundError(err) {
respondError(w, r, 70, "media with id `%d` was not found", id)
return
}
absPath := path.Join(c.MusicPath, track.Path)
absPath := path.Join(
c.MusicPath,
track.Album.LeftPath,
track.Album.RightPath,
track.Filename,
)
file, err := os.Open(absPath)
if err != nil {
respondError(w, r, 0, "error while streaming media: %v", err)
@@ -52,11 +59,12 @@ func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
// after we've served the file, mark the album as played
user := r.Context().Value(contextUserKey).(*model.User)
play := model.Play{
AlbumID: track.Album.ID,
FolderID: track.Folder.ID,
UserID: user.ID,
AlbumID: track.Album.ID,
UserID: user.ID,
}
c.DB.Where(play).First(&play)
c.DB.
Where(play).
First(&play)
play.Time = time.Now() // for getAlbumList?type=recent
play.Count++ // for getAlbumList?type=frequent
c.DB.Save(&play)
@@ -68,9 +76,26 @@ func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var cover model.Cover
c.DB.First(&cover, id)
w.Write(cover.Image)
folder := &model.Album{}
err = c.DB.
Select("id, path, cover").
First(folder, id).
Error
if gorm.IsRecordNotFoundError(err) {
respondError(w, r, 10, "could not find a cover with that id")
return
}
if folder.Cover == "" {
respondError(w, r, 10, "no cover found for that folder")
return
}
absPath := path.Join(
c.MusicPath,
folder.RightPath,
folder.LeftPath,
folder.Cover,
)
http.ServeFile(w, r, absPath)
}
func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
@@ -99,17 +124,17 @@ func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
return
}
// fetch track for getting info to send to last.fm function
var track model.Track
track := &model.Track{}
c.DB.
Preload("Album").
Preload("Artist").
First(&track, id)
First(track, id)
// scrobble with above info
err = lastfm.Scrobble(
c.GetSetting("lastfm_api_key"),
c.GetSetting("lastfm_secret"),
user.LastFMSession,
&track,
track,
// clients will provide time in miliseconds, so use that or
// instead convert UnixNano to miliseconds
getIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)),
@@ -134,14 +159,22 @@ func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
}
func (c *Controller) StartScan(w http.ResponseWriter, r *http.Request) {
scanC := scanner.New(c.DB, c.MusicPath)
go scanC.Start()
go func() {
err := scanner.
New(c.DB, c.MusicPath).
Start()
if err != nil {
log.Printf("error while scanning: %v\n", err)
}
}()
c.GetScanStatus(w, r)
}
func (c *Controller) GetScanStatus(w http.ResponseWriter, r *http.Request) {
var trackCount int
c.DB.Model(&model.Track{}).Count(&trackCount)
c.DB.
Model(model.Track{}).
Count(&trackCount)
sub := subsonic.NewResponse()
sub.ScanStatus = &subsonic.ScanStatus{
Scanning: atomic.LoadInt32(&scanner.IsScanning) == 1,

View File

@@ -44,7 +44,9 @@ func testNameToPath(name string) string {
func testQueryCases(t *testing.T, handler http.HandlerFunc, cases []*queryCase) {
for _, qc := range cases {
qc := qc // pin
t.Run(qc.expectPath, func(t *testing.T) {
t.Parallel()
// ensure the handlers give us json
qc.params.Add("f", "json")
req, _ := http.NewRequest("", "?"+qc.params.Encode(), nil)

Binary file not shown.

View File

@@ -6,7 +6,7 @@
"album": [
{
"id": 6,
"coverArt": 2,
"coverArt": 6,
"artist": "13th Floor Lowervators",
"title": "(1967) Easter Nowhere",
"parent": 5,
@@ -15,7 +15,7 @@
},
{
"id": 7,
"coverArt": 3,
"coverArt": 7,
"artist": "13th Floor Lowervators",
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
"parent": 5,
@@ -24,7 +24,7 @@
},
{
"id": 3,
"coverArt": 1,
"coverArt": 3,
"artist": "A Certain Ratio",
"title": "(1994) The Graveyard and the Ballroom",
"parent": 2,
@@ -33,6 +33,7 @@
},
{
"id": 4,
"coverArt": 4,
"artist": "A Certain Ratio",
"title": "(1981) To Each.",
"parent": 2,
@@ -41,7 +42,7 @@
},
{
"id": 11,
"coverArt": 4,
"coverArt": 11,
"artist": "There",
"title": "(2010) Anika",
"parent": 10,

View File

@@ -6,7 +6,7 @@
"album": [
{
"id": 7,
"coverArt": 3,
"coverArt": 7,
"artist": "13th Floor Lowervators",
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
"parent": 5,
@@ -15,7 +15,7 @@
},
{
"id": 6,
"coverArt": 2,
"coverArt": 6,
"artist": "13th Floor Lowervators",
"title": "(1967) Easter Nowhere",
"parent": 5,
@@ -24,6 +24,7 @@
},
{
"id": 4,
"coverArt": 4,
"artist": "A Certain Ratio",
"title": "(1981) To Each.",
"parent": 2,
@@ -32,7 +33,7 @@
},
{
"id": 3,
"coverArt": 1,
"coverArt": 3,
"artist": "A Certain Ratio",
"title": "(1994) The Graveyard and the Ballroom",
"parent": 2,
@@ -41,7 +42,7 @@
},
{
"id": 11,
"coverArt": 4,
"coverArt": 11,
"artist": "There",
"title": "(2010) Anika",
"parent": 10,

View File

@@ -6,7 +6,7 @@
"album": [
{
"id": 11,
"coverArt": 4,
"coverArt": 11,
"artist": "There",
"title": "(2010) Anika",
"parent": 10,
@@ -15,7 +15,7 @@
},
{
"id": 7,
"coverArt": 3,
"coverArt": 7,
"artist": "13th Floor Lowervators",
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
"parent": 5,
@@ -24,7 +24,7 @@
},
{
"id": 6,
"coverArt": 2,
"coverArt": 6,
"artist": "13th Floor Lowervators",
"title": "(1967) Easter Nowhere",
"parent": 5,
@@ -33,6 +33,7 @@
},
{
"id": 4,
"coverArt": 4,
"artist": "A Certain Ratio",
"title": "(1981) To Each.",
"parent": 2,
@@ -41,7 +42,7 @@
},
{
"id": 3,
"coverArt": 1,
"coverArt": 3,
"artist": "A Certain Ratio",
"title": "(1994) The Graveyard and the Ballroom",
"parent": 2,

View File

@@ -4,44 +4,45 @@
"version": "1.9.0",
"albumList": {
"album": [
{
"id": 11,
"coverArt": 4,
"artist": "There",
"title": "(2010) Anika",
"parent": 10,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 6,
"coverArt": 2,
"artist": "13th Floor Lowervators",
"title": "(1967) Easter Nowhere",
"parent": 5,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 4,
"artist": "A Certain Ratio",
"title": "(1981) To Each.",
"parent": 2,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 3,
"coverArt": 1,
"coverArt": 3,
"artist": "A Certain Ratio",
"title": "(1994) The Graveyard and the Ballroom",
"parent": 2,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 11,
"coverArt": 11,
"artist": "There",
"title": "(2010) Anika",
"parent": 10,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 4,
"coverArt": 4,
"artist": "A Certain Ratio",
"title": "(1981) To Each.",
"parent": 2,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 6,
"coverArt": 6,
"artist": "13th Floor Lowervators",
"title": "(1967) Easter Nowhere",
"parent": 5,
"isDir": true,
"created": "0001-01-01T00:00:00Z"
},
{
"id": 7,
"coverArt": 3,
"coverArt": 7,
"artist": "13th Floor Lowervators",
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
"parent": 5,

View File

@@ -5,43 +5,44 @@
"albumList2": {
"album": [
{
"id": 3,
"coverArt": 2,
"id": 6,
"coverArt": 6,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00"
"created": "2019-06-05T16:00:10.556862345+01:00"
},
{
"id": 4,
"coverArt": 3,
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
"created": "2019-06-05T16:00:10.560355528+01:00"
},
{
"id": 1,
"coverArt": 1,
"id": 3,
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-05-28T20:59:02.988372626+01:00"
"created": "2019-06-05T16:00:10.54747823+01:00"
},
{
"id": 2,
"id": 4,
"coverArt": 4,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00"
"created": "2019-06-05T16:00:10.553065063+01:00"
},
{
"id": 5,
"coverArt": 4,
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
"created": "2019-06-05T16:00:10.565661506+01:00"
}
]
}

View File

@@ -5,43 +5,44 @@
"albumList2": {
"album": [
{
"id": 5,
"coverArt": 4,
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
"created": "2019-06-05T16:00:10.565661506+01:00"
},
{
"id": 3,
"coverArt": 2,
"id": 6,
"coverArt": 6,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00"
"created": "2019-06-05T16:00:10.556862345+01:00"
},
{
"id": 1,
"coverArt": 1,
"id": 3,
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-05-28T20:59:02.988372626+01:00"
"created": "2019-06-05T16:00:10.54747823+01:00"
},
{
"id": 4,
"coverArt": 3,
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
"created": "2019-06-05T16:00:10.560355528+01:00"
},
{
"id": 2,
"id": 4,
"coverArt": 4,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00"
"created": "2019-06-05T16:00:10.553065063+01:00"
}
]
}

View File

@@ -5,43 +5,44 @@
"albumList2": {
"album": [
{
"id": 5,
"coverArt": 4,
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
"created": "2019-06-05T16:00:10.565661506+01:00"
},
{
"id": 4,
"coverArt": 3,
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
"created": "2019-06-05T16:00:10.560355528+01:00"
},
{
"id": 3,
"coverArt": 2,
"id": 6,
"coverArt": 6,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00"
"created": "2019-06-05T16:00:10.556862345+01:00"
},
{
"id": 2,
"id": 4,
"coverArt": 4,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00"
"created": "2019-06-05T16:00:10.553065063+01:00"
},
{
"id": 1,
"coverArt": 1,
"id": 3,
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-05-28T20:59:02.988372626+01:00"
"created": "2019-06-05T16:00:10.54747823+01:00"
}
]
}

View File

@@ -6,42 +6,43 @@
"album": [
{
"id": 4,
"coverArt": 3,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
},
{
"id": 5,
"coverArt": 4,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
},
{
"id": 1,
"coverArt": 1,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-05-28T20:59:02.988372626+01:00"
},
{
"id": 3,
"coverArt": 2,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00"
},
{
"id": 2,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00"
"created": "2019-06-05T16:00:10.553065063+01:00"
},
{
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-06-05T16:00:10.565661506+01:00"
},
{
"id": 3,
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-06-05T16:00:10.54747823+01:00"
},
{
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-06-05T16:00:10.560355528+01:00"
},
{
"id": 6,
"coverArt": 6,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-06-05T16:00:10.556862345+01:00"
}
]
}

View File

@@ -4,181 +4,249 @@
"version": "1.9.0",
"album": {
"id": 3,
"coverArt": 2,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00",
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-06-05T16:00:10.54747823+01:00",
"song": [
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.01067974+01:00",
"id": 25,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/01.10 Slip Inside This House.flac",
"size": 52229000,
"coverArt": 3,
"created": "2019-06-05T16:00:10.552108331+01:00",
"id": 12,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/01.14 Do the Du (casse).flac",
"size": 20545509,
"suffix": "flac",
"title": "Slip Inside This House",
"title": "Do the Du (casse)",
"track": 1,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.010814449+01:00",
"id": 27,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/02.10 Slide Machine.flac",
"size": 22964562,
"coverArt": 3,
"created": "2019-06-05T16:00:10.550632503+01:00",
"id": 8,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/02.14 Faceless.flac",
"size": 16657561,
"suffix": "flac",
"title": "Slide Machine",
"title": "Faceless",
"track": 2,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.010880444+01:00",
"id": 28,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/03.10 She Lives (In a Time of Her Own).flac",
"size": 18474888,
"coverArt": 3,
"created": "2019-06-05T16:00:10.549864008+01:00",
"id": 6,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/03.14 Crippled Child.flac",
"size": 21325811,
"suffix": "flac",
"title": "She Lives (In a Time of Her Own)",
"title": "Crippled Child",
"track": 3,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.011044654+01:00",
"id": 30,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/04.10 Nobody to Love.flac",
"size": 18067448,
"coverArt": 3,
"created": "2019-06-05T16:00:10.551742258+01:00",
"id": 11,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/04.14 Choir.flac",
"size": 24728976,
"suffix": "flac",
"title": "Nobody to Love",
"title": "Choir",
"track": 4,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.011106774+01:00",
"id": 31,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/05.10 Baby Blue.flac",
"size": 31828836,
"coverArt": 3,
"created": "2019-06-05T16:00:10.548393601+01:00",
"id": 2,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/05.14 Flight.flac",
"size": 24860635,
"suffix": "flac",
"title": "Baby Blue",
"title": "Flight",
"track": 5,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.011174899+01:00",
"id": 32,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/06.10 Earthquake.flac",
"size": 29066645,
"coverArt": 3,
"created": "2019-06-05T16:00:10.552469878+01:00",
"id": 13,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/06.14 I Feel.flac",
"size": 16118749,
"suffix": "flac",
"title": "Earthquake",
"title": "I Feel",
"track": 6,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.010597689+01:00",
"id": 24,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/07.10 Dust.flac",
"size": 22652796,
"coverArt": 3,
"created": "2019-06-05T16:00:10.552842449+01:00",
"id": 14,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/07.14 Strain.flac",
"size": 17608752,
"suffix": "flac",
"title": "Dust",
"title": "Strain",
"track": 7,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.010749083+01:00",
"id": 26,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/08.10 Levitation.flac",
"size": 16354677,
"coverArt": 3,
"created": "2019-06-05T16:00:10.549504916+01:00",
"id": 5,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/08.14 All Night Party.flac",
"size": 24960016,
"suffix": "flac",
"title": "Levitation",
"title": "All Night Party",
"track": 8,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.011238025+01:00",
"id": 33,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/09.10 I Had to Tell You.flac",
"size": 14261007,
"coverArt": 3,
"created": "2019-06-05T16:00:10.549134887+01:00",
"id": 4,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/09.14 Oceans.flac",
"size": 26401567,
"suffix": "flac",
"title": "I Had to Tell You",
"title": "Oceans",
"track": 9,
"type": "music"
},
{
"album": "Easter Everywhere",
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "13th Floor Elevators",
"artistId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 2,
"created": "2019-05-28T20:59:03.010949903+01:00",
"id": 29,
"parent": 6,
"path": "13th Floor Lowervators/(1967) Easter Nowhere/10.10 Pictures (Leave Your Body Behind).flac",
"size": 39529576,
"coverArt": 3,
"created": "2019-06-05T16:00:10.551373999+01:00",
"id": 10,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/10.14 The Choir.flac",
"size": 24106680,
"suffix": "flac",
"title": "Pictures (Leave Your Body Behind)",
"title": "The Choir",
"track": 10,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "2019-06-05T16:00:10.550996621+01:00",
"id": 9,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/11.14 The Fox.flac",
"size": 24054498,
"suffix": "flac",
"title": "The Fox",
"track": 11,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "2019-06-05T16:00:10.55027053+01:00",
"id": 7,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/12.14 Suspect.flac",
"size": 16592296,
"suffix": "flac",
"title": "Suspect",
"track": 12,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "2019-06-05T16:00:10.547986038+01:00",
"id": 1,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
"size": 37302417,
"suffix": "flac",
"title": "Flight",
"track": 13,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"albumId": 3,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "2019-06-05T16:00:10.548763765+01:00",
"id": 3,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/14.14 Genotype_Phenotype.flac",
"size": 24349252,
"suffix": "flac",
"title": "Genotype/Phenotype",
"track": 14,
"type": "music"
}
]
}

View File

@@ -4,156 +4,8 @@
"version": "1.9.0",
"album": {
"id": 2,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00",
"song": [
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.99562727+01:00",
"id": 16,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./01.09 Felch.flac",
"size": 24708838,
"suffix": "flac",
"title": "Felch",
"track": 1,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.995889638+01:00",
"id": 20,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./02.09 My Spirit.flac",
"size": 17102404,
"suffix": "flac",
"title": "My Spirit",
"track": 2,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.99609285+01:00",
"id": 23,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./03.09 Forced Laugh.flac",
"size": 37924980,
"suffix": "flac",
"title": "Forced Laugh",
"track": 3,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.995953401+01:00",
"id": 21,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./04.09 Choir.flac",
"size": 21205583,
"suffix": "flac",
"title": "Choir",
"track": 4,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.99569934+01:00",
"id": 17,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./05.09 Back to the Start.flac",
"size": 56733069,
"suffix": "flac",
"title": "Back to the Start",
"track": 5,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.995826449+01:00",
"id": 19,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./06.09 The Fox.flac",
"size": 26835335,
"suffix": "flac",
"title": "The Fox",
"track": 6,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.995544892+01:00",
"id": 15,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./07.09 Loss.flac",
"size": 20494369,
"suffix": "flac",
"title": "Loss",
"track": 7,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.995761233+01:00",
"id": 18,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./08.09 Oceans.flac",
"size": 25233096,
"suffix": "flac",
"title": "Oceans",
"track": 8,
"type": "music"
},
{
"album": "To Each...",
"albumId": 2,
"artist": "A Certain Ratio",
"artistId": 1,
"contentType": "audio/x-flac",
"created": "2019-05-28T20:59:02.99603217+01:00",
"id": 22,
"parent": 4,
"path": "A Certain Ratio/(1981) To Each./09.09 Winter Hill.flac",
"size": 89483446,
"suffix": "flac",
"title": "Winter Hill",
"track": 9,
"type": "music"
}
]
"coverArt": 2,
"created": "2019-06-05T16:00:10.547172057+01:00"
}
}
}

View File

@@ -7,19 +7,20 @@
"name": "A Certain Ratio",
"album": [
{
"id": 1,
"coverArt": 1,
"id": 3,
"coverArt": 3,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "The Graveyard and the Ballroom",
"created": "2019-05-28T20:59:02.988372626+01:00"
"created": "2019-06-05T16:00:10.54747823+01:00"
},
{
"id": 2,
"id": 4,
"coverArt": 4,
"artistId": 1,
"artist": "A Certain Ratio",
"name": "To Each...",
"created": "2019-05-28T20:59:02.995320471+01:00"
"created": "2019-06-05T16:00:10.553065063+01:00"
}
]
}

View File

@@ -7,12 +7,12 @@
"name": "Anikas",
"album": [
{
"id": 5,
"coverArt": 4,
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
"created": "2019-06-05T16:00:10.565661506+01:00"
}
]
}

View File

@@ -7,20 +7,20 @@
"name": "13th Floor Elevators",
"album": [
{
"id": 3,
"coverArt": 2,
"id": 6,
"coverArt": 6,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "Easter Everywhere",
"created": "2019-05-28T20:59:03.010415595+01:00"
"created": "2019-06-05T16:00:10.556862345+01:00"
},
{
"id": 4,
"coverArt": 3,
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
"created": "2019-06-05T16:00:10.560355528+01:00"
}
]
}

View File

@@ -11,6 +11,10 @@
{
"id": 5,
"name": "13th Floor Lowervators"
},
{
"id": 8,
"name": "___Anika"
}
]
},
@@ -20,10 +24,6 @@
{
"id": 2,
"name": "A Certain Ratio"
},
{
"id": 8,
"name": "Anika"
}
]
}

View File

@@ -8,55 +8,10 @@
"name": "(1994) The Graveyard and the Ballroom",
"child": [
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"created": "0001-01-01T00:00:00Z",
"id": 5,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/08.14 All Night Party.flac",
"size": 24960016,
"suffix": "flac",
"title": "All Night Party",
"track": 8,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"created": "0001-01-01T00:00:00Z",
"id": 11,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/04.14 Choir.flac",
"size": 24728976,
"suffix": "flac",
"title": "Choir",
"track": 4,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"created": "0001-01-01T00:00:00Z",
"id": 6,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/03.14 Crippled Child.flac",
"size": 21325811,
"suffix": "flac",
"title": "Crippled Child",
"track": 3,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 12,
"parent": 3,
@@ -68,10 +23,10 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 8,
"parent": 3,
@@ -83,25 +38,40 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 1,
"id": 6,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
"size": 37302417,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/03.14 Crippled Child.flac",
"size": 21325811,
"suffix": "flac",
"title": "Flight",
"track": 13,
"title": "Crippled Child",
"track": 3,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 11,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/04.14 Choir.flac",
"size": 24728976,
"suffix": "flac",
"title": "Choir",
"track": 4,
"type": "music"
},
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 2,
"parent": 3,
@@ -113,25 +83,10 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"created": "0001-01-01T00:00:00Z",
"id": 3,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/14.14 Genotype_Phenotype.flac",
"size": 24349252,
"suffix": "flac",
"title": "Genotype/Phenotype",
"track": 14,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 13,
"parent": 3,
@@ -143,25 +98,10 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"created": "0001-01-01T00:00:00Z",
"id": 4,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/09.14 Oceans.flac",
"size": 26401567,
"suffix": "flac",
"title": "Oceans",
"track": 9,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 14,
"parent": 3,
@@ -173,25 +113,40 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 7,
"id": 5,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/12.14 Suspect.flac",
"size": 16592296,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/08.14 All Night Party.flac",
"size": 24960016,
"suffix": "flac",
"title": "Suspect",
"track": 12,
"title": "All Night Party",
"track": 8,
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 4,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/09.14 Oceans.flac",
"size": 26401567,
"suffix": "flac",
"title": "Oceans",
"track": 9,
"type": "music"
},
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 10,
"parent": 3,
@@ -203,10 +158,10 @@
"type": "music"
},
{
"album": "The Graveyard and the Ballroom",
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 9,
"parent": 3,
@@ -216,6 +171,51 @@
"title": "The Fox",
"track": 11,
"type": "music"
},
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 7,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/12.14 Suspect.flac",
"size": 16592296,
"suffix": "flac",
"title": "Suspect",
"track": 12,
"type": "music"
},
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 1,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
"size": 37302417,
"suffix": "flac",
"title": "Flight",
"track": 13,
"type": "music"
},
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 3,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/14.14 Genotype_Phenotype.flac",
"size": 24349252,
"suffix": "flac",
"title": "Genotype/Phenotype",
"track": 14,
"type": "music"
}
]
}

View File

@@ -8,7 +8,7 @@
"name": "A Certain Ratio",
"child": [
{
"coverArt": 1,
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 3,
"isDir": true,
@@ -16,9 +16,10 @@
"title": "(1994) The Graveyard and the Ballroom"
},
{
"coverArt": 4,
"created": "0001-01-01T00:00:00Z",
"isDir": true,
"id": 4,
"isDir": true,
"parent": 2,
"title": "(1981) To Each."
}

View File

@@ -11,12 +11,12 @@
],
"album": [
{
"id": 4,
"coverArt": 3,
"id": 7,
"coverArt": 7,
"artistId": 2,
"artist": "13th Floor Elevators",
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
"created": "2019-05-28T20:59:03.022922683+01:00"
"created": "2019-06-05T16:00:10.560355528+01:00"
}
]
}

View File

@@ -11,12 +11,12 @@
],
"album": [
{
"id": 5,
"coverArt": 4,
"id": 11,
"coverArt": 11,
"artistId": 3,
"artist": "Anikas",
"name": "Anika",
"created": "2019-05-28T20:59:03.035442597+01:00"
"created": "2019-06-05T16:00:10.565661506+01:00"
}
]
}

View File

@@ -12,13 +12,45 @@
],
"album": [
{
"coverArt": 3,
"coverArt": 7,
"created": "0001-01-01T00:00:00Z",
"id": 7,
"isDir": true,
"parent": 5,
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators"
}
],
"song": [
{
"album": "(1994) The Graveyard and the Ballroom",
"artist": "A Certain Ratio",
"contentType": "audio/x-flac",
"coverArt": 3,
"created": "0001-01-01T00:00:00Z",
"id": 1,
"parent": 3,
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
"size": 37302417,
"suffix": "flac",
"title": "Flight",
"track": 13,
"type": "music"
},
{
"album": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
"artist": "13th Floor Elevators",
"contentType": "audio/mpeg",
"coverArt": 7,
"created": "0001-01-01T00:00:00Z",
"id": 35,
"parent": 7,
"path": "13th Floor Lowervators/(1966) The Psychedelic Sounds of the 13th Floor Elevators/13.21 Before You Accuse Me.mp3",
"size": 4722688,
"suffix": "mp3",
"title": "Before You Accuse Me",
"track": 13,
"type": "music"
}
]
}
}

View File

@@ -7,12 +7,12 @@
{
"id": 8,
"parent": 1,
"name": "Anika"
"name": "___Anika"
}
],
"album": [
{
"coverArt": 4,
"coverArt": 11,
"created": "0001-01-01T00:00:00Z",
"id": 11,
"isDir": true,

View File

@@ -48,11 +48,11 @@ func Scrobble(apiKey, secret, session string, track *model.Track,
}
params.Add("api_key", apiKey)
params.Add("sk", session)
params.Add("artist", track.TrackArtist)
params.Add("track", track.Title)
params.Add("album", track.Album.Title)
params.Add("artist", track.TagTrackArtist)
params.Add("track", track.TagTitle)
params.Add("album", track.Album.TagTitle)
params.Add("albumArtist", track.Artist.Name)
params.Add("trackNumber", strconv.Itoa(track.TrackNumber))
params.Add("trackNumber", strconv.Itoa(track.TagTrackNumber))
params.Add("api_sig", getParamSignature(params, secret))
_, err := makeRequest("POST", params)
return err
@@ -84,13 +84,13 @@ func makeRequest(method string, params url.Values) (*LastFM, error) {
}
defer resp.Body.Close()
decoder := xml.NewDecoder(resp.Body)
var lastfm LastFM
err = decoder.Decode(&lastfm)
lastfm := &LastFM{}
err = decoder.Decode(lastfm)
if err != nil {
return nil, errors.Wrap(err, "decoding")
}
if lastfm.Error != nil {
return nil, fmt.Errorf("parsing: %v", lastfm.Error.Value)
}
return &lastfm, nil
return lastfm, nil
}

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/sentriz/gonic/server/handler"
)
@@ -43,12 +42,9 @@ func New(db *gorm.DB, musicPath string, listenAddr string) *Server {
DB: db,
MusicPath: musicPath,
}
ret := &Server{
return &Server{
mux: mux,
Server: server,
Controller: controller,
}
ret.setupAdmin()
ret.setupSubsonic()
return ret
}

View File

@@ -28,14 +28,14 @@ func extendFromBox(tmpl *template.Template, box *packr.Box, key string) *templat
return newT
}
func (s *Server) setupAdmin() {
func (s *Server) SetupAdmin() {
sessionKey := []byte(s.GetSetting("session_key"))
if len(sessionKey) == 0 {
sessionKey = securecookie.GenerateRandomKey(32)
s.SetSetting("session_key", string(sessionKey))
}
// create gormstore (and cleanup) for backend sessions
s.SessDB = gormstore.New(s.DB, []byte(sessionKey))
s.SessDB = gormstore.New(s.DB, sessionKey)
go s.SessDB.PeriodicCleanup(1*time.Hour, nil)
// using packr to bundle templates and static files
box := packr.New("templates", "./templates")

View File

@@ -1,6 +1,6 @@
package server
func (s *Server) setupSubsonic() {
func (s *Server) SetupSubsonic() {
withWare := newChain(
s.WithLogging,
s.WithCORS,