diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..882ed26 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/cmd/scanner/main.go b/cmd/scanner/main.go index cfa0b65..54e5619 100644 --- a/cmd/scanner/main.go +++ b/cmd/scanner/main.go @@ -14,7 +14,6 @@ import ( const ( programName = "gonic" - programVar = "GONIC" ) func main() { diff --git a/cmd/server/main.go b/cmd/server/main.go index ce85ee4..5da9a3a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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) diff --git a/gen_handler_tests b/gen_handler_tests new file mode 100755 index 0000000..f965dfd --- /dev/null +++ b/gen_handler_tests @@ -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 diff --git a/go.mod b/go.mod index a9b5351..a4d9686 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index a9bc273..c74e204 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/mime/mime.go b/mime/mime.go new file mode 100644 index 0000000..bad4d30 --- /dev/null +++ b/mime/mime.go @@ -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", +} diff --git a/model/model.go b/model/model.go index 62ea379..540e2e3 100644 --- a/model/model.go +++ b/model/model.go @@ -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:"-"` } diff --git a/scanner/folder_stack.go b/scanner/folder_stack.go deleted file mode 100644 index d58585d..0000000 --- a/scanner/folder_stack.go +++ /dev/null @@ -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 -} diff --git a/scanner/scanner.go b/scanner/scanner.go index b82f5ea..ba723ca 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -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 } diff --git a/scanner/stack.go b/scanner/stack.go new file mode 100644 index 0000000..f567ae9 --- /dev/null +++ b/scanner/stack.go @@ -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, " ")) +} diff --git a/scanner/utilities.go b/scanner/utilities.go deleted file mode 100644 index 682bcf8..0000000 --- a/scanner/utilities.go +++ /dev/null @@ -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 -} diff --git a/scanner/walk.go b/scanner/walk.go deleted file mode 100644 index fb15d75..0000000 --- a/scanner/walk.go +++ /dev/null @@ -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 -} diff --git a/server/handler/construct_sub_by_folder.go b/server/handler/construct_sub_by_folder.go index 3c23eb9..883a272 100644 --- a/server/handler/construct_sub_by_folder.go +++ b/server/handler/construct_sub_by_folder.go @@ -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, } } diff --git a/server/handler/construct_sub_by_tags.go b/server/handler/construct_sub_by_tags.go index f00df7c..fda5408 100644 --- a/server/handler/construct_sub_by_tags.go +++ b/server/handler/construct_sub_by_tags.go @@ -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", } } diff --git a/server/handler/handler.go b/server/handler/handler.go index 4505cfd..3877293 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -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 } diff --git a/server/handler/handler_admin.go b/server/handler/handler_admin.go index 12a79b3..2155b72 100644 --- a/server/handler/handler_admin.go +++ b/server/handler/handler_admin.go @@ -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) { diff --git a/server/handler/handler_admin_utils_test.go b/server/handler/handler_admin_utils_test.go index bf2f086..1d08566 100644 --- a/server/handler/handler_admin_utils_test.go +++ b/server/handler/handler_admin_utils_test.go @@ -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) diff --git a/server/handler/handler_sub_by_folder.go b/server/handler/handler_sub_by_folder.go index 4b0c98c..26c798e 100644 --- a/server/handler/handler_sub_by_folder.go +++ b/server/handler/handler_sub_by_folder.go @@ -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() diff --git a/server/handler/handler_sub_by_folder_test.go b/server/handler/handler_sub_by_folder_test.go index 803be0d..01c57fe 100644 --- a/server/handler/handler_sub_by_folder_test.go +++ b/server/handler/handler_sub_by_folder_test.go @@ -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}, }) } diff --git a/server/handler/handler_sub_by_tags.go b/server/handler/handler_sub_by_tags.go index f2bbc36..1801abf 100644 --- a/server/handler/handler_sub_by_tags.go +++ b/server/handler/handler_sub_by_tags.go @@ -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 diff --git a/server/handler/handler_sub_common.go b/server/handler/handler_sub_common.go index 1cf0eb7..c6b89c0 100644 --- a/server/handler/handler_sub_common.go +++ b/server/handler/handler_sub_common.go @@ -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, diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go index 18acdb5..d7be352 100644 --- a/server/handler/handler_test.go +++ b/server/handler/handler_test.go @@ -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) diff --git a/server/handler/test_data/db b/server/handler/test_data/db index 08b6859..8249c95 100644 Binary files a/server/handler/test_data/db and b/server/handler/test_data/db differ diff --git a/server/handler/test_data/test_get_album_list_alpha_artist b/server/handler/test_data/test_get_album_list_alpha_artist index 6f839ef..0a232b9 100644 --- a/server/handler/test_data/test_get_album_list_alpha_artist +++ b/server/handler/test_data/test_get_album_list_alpha_artist @@ -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, diff --git a/server/handler/test_data/test_get_album_list_alpha_name b/server/handler/test_data/test_get_album_list_alpha_name index bf4aa19..f6a3fa6 100644 --- a/server/handler/test_data/test_get_album_list_alpha_name +++ b/server/handler/test_data/test_get_album_list_alpha_name @@ -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, diff --git a/server/handler/test_data/test_get_album_list_newest b/server/handler/test_data/test_get_album_list_newest index d862150..cdeff1a 100644 --- a/server/handler/test_data/test_get_album_list_newest +++ b/server/handler/test_data/test_get_album_list_newest @@ -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, diff --git a/server/handler/test_data/test_get_album_list_random b/server/handler/test_data/test_get_album_list_random index 4da4f24..b8427e9 100644 --- a/server/handler/test_data/test_get_album_list_random +++ b/server/handler/test_data/test_get_album_list_random @@ -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, diff --git a/server/handler/test_data/test_get_album_list_two_alpha_artist b/server/handler/test_data/test_get_album_list_two_alpha_artist index ca74905..e6e10a1 100644 --- a/server/handler/test_data/test_get_album_list_two_alpha_artist +++ b/server/handler/test_data/test_get_album_list_two_alpha_artist @@ -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" } ] } diff --git a/server/handler/test_data/test_get_album_list_two_alpha_name b/server/handler/test_data/test_get_album_list_two_alpha_name index 7e02e2f..1f0b097 100644 --- a/server/handler/test_data/test_get_album_list_two_alpha_name +++ b/server/handler/test_data/test_get_album_list_two_alpha_name @@ -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" } ] } diff --git a/server/handler/test_data/test_get_album_list_two_newest b/server/handler/test_data/test_get_album_list_two_newest index 2176136..9648a8a 100644 --- a/server/handler/test_data/test_get_album_list_two_newest +++ b/server/handler/test_data/test_get_album_list_two_newest @@ -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" } ] } diff --git a/server/handler/test_data/test_get_album_list_two_random b/server/handler/test_data/test_get_album_list_two_random index 071cef6..e5b8c87 100644 --- a/server/handler/test_data/test_get_album_list_two_random +++ b/server/handler/test_data/test_get_album_list_two_random @@ -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" } ] } diff --git a/server/handler/test_data/test_get_album_with_cover b/server/handler/test_data/test_get_album_with_cover index 3f128b4..652d725 100644 --- a/server/handler/test_data/test_get_album_with_cover +++ b/server/handler/test_data/test_get_album_with_cover @@ -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" } ] } diff --git a/server/handler/test_data/test_get_album_without_cover b/server/handler/test_data/test_get_album_without_cover index b4d8404..0b9b94f 100644 --- a/server/handler/test_data/test_get_album_without_cover +++ b/server/handler/test_data/test_get_album_without_cover @@ -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" } } } diff --git a/server/handler/test_data/test_get_artist_id_one b/server/handler/test_data/test_get_artist_id_one index 44785a5..279a8a6 100644 --- a/server/handler/test_data/test_get_artist_id_one +++ b/server/handler/test_data/test_get_artist_id_one @@ -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" } ] } diff --git a/server/handler/test_data/test_get_artist_id_three b/server/handler/test_data/test_get_artist_id_three index 24ad879..2cb1d18 100644 --- a/server/handler/test_data/test_get_artist_id_three +++ b/server/handler/test_data/test_get_artist_id_three @@ -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" } ] } diff --git a/server/handler/test_data/test_get_artist_id_two b/server/handler/test_data/test_get_artist_id_two index 9ac78c4..50ebb3b 100644 --- a/server/handler/test_data/test_get_artist_id_two +++ b/server/handler/test_data/test_get_artist_id_two @@ -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" } ] } diff --git a/server/handler/test_data/test_get_indexes_id_two b/server/handler/test_data/test_get_indexes_no_args similarity index 94% rename from server/handler/test_data/test_get_indexes_id_two rename to server/handler/test_data/test_get_indexes_no_args index e4af9c8..31976a6 100644 --- a/server/handler/test_data/test_get_indexes_id_two +++ b/server/handler/test_data/test_get_indexes_no_args @@ -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" } ] } diff --git a/server/handler/test_data/test_get_music_directory_with_tracks b/server/handler/test_data/test_get_music_directory_with_tracks index d33ef8f..b954d3a 100644 --- a/server/handler/test_data/test_get_music_directory_with_tracks +++ b/server/handler/test_data/test_get_music_directory_with_tracks @@ -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" } ] } diff --git a/server/handler/test_data/test_get_music_directory_without_tracks b/server/handler/test_data/test_get_music_directory_without_tracks index 04b8ccc..4702d84 100644 --- a/server/handler/test_data/test_get_music_directory_without_tracks +++ b/server/handler/test_data/test_get_music_directory_without_tracks @@ -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." } diff --git a/server/handler/test_data/test_search_three_q_13 b/server/handler/test_data/test_search_three_q_13 index 9689f82..5f848ed 100644 --- a/server/handler/test_data/test_search_three_q_13 +++ b/server/handler/test_data/test_search_three_q_13 @@ -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" } ] } diff --git a/server/handler/test_data/test_search_three_q_ani b/server/handler/test_data/test_search_three_q_ani index accc59d..69e491b 100644 --- a/server/handler/test_data/test_search_three_q_ani +++ b/server/handler/test_data/test_search_three_q_ani @@ -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" } ] } diff --git a/server/handler/test_data/test_search_two_q_13 b/server/handler/test_data/test_search_two_q_13 index 5de3e60..f9f8334 100644 --- a/server/handler/test_data/test_search_two_q_13 +++ b/server/handler/test_data/test_search_two_q_13 @@ -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" + } ] } } diff --git a/server/handler/test_data/test_search_two_q_ani b/server/handler/test_data/test_search_two_q_ani index f7456bf..c199c25 100644 --- a/server/handler/test_data/test_search_two_q_ani +++ b/server/handler/test_data/test_search_two_q_ani @@ -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, diff --git a/server/lastfm/lastfm.go b/server/lastfm/lastfm.go index bebfaa0..1b1a34c 100644 --- a/server/lastfm/lastfm.go +++ b/server/lastfm/lastfm.go @@ -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 } diff --git a/server/server.go b/server/server.go index 0fe3726..5ad1298 100644 --- a/server/server.go +++ b/server/server.go @@ -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 } diff --git a/server/setup_admin.go b/server/setup_admin.go index 9422768..4b1ab4b 100644 --- a/server/setup_admin.go +++ b/server/setup_admin.go @@ -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") diff --git a/server/setup_subsonic.go b/server/setup_subsonic.go index 2733ca8..2162a69 100644 --- a/server/setup_subsonic.go +++ b/server/setup_subsonic.go @@ -1,6 +1,6 @@ package server -func (s *Server) setupSubsonic() { +func (s *Server) SetupSubsonic() { withWare := newChain( s.WithLogging, s.WithCORS,