add routes

This commit is contained in:
sentriz
2019-04-14 15:28:23 +01:00
parent 76ad2ec4eb
commit 87efb3b3c5
11 changed files with 481 additions and 285 deletions

View File

@@ -81,7 +81,7 @@ func handleFolderCompletion(fullPath string, info *godirwalk.Dirent) error {
cover := db.Cover{ cover := db.Cover{
Path: cLastAlbum.coverPath, Path: cLastAlbum.coverPath,
} }
err := tx.Where(cover).First(&cover).Error err := tx.Where(cover).First(&cover).Error // TODO: swap
if !gorm.IsRecordNotFoundError(err) && if !gorm.IsRecordNotFoundError(err) &&
!cLastAlbum.coverModTime.After(cover.UpdatedAt) { !cLastAlbum.coverModTime.After(cover.UpdatedAt) {
return nil return nil
@@ -123,7 +123,7 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
track := db.Track{ track := db.Track{
Path: fullPath, Path: fullPath,
} }
err = tx.Where(track).First(&track).Error err = tx.Where(track).First(&track).Error // TODO: swap
if !gorm.IsRecordNotFoundError(err) && if !gorm.IsRecordNotFoundError(err) &&
!modTime.After(track.UpdatedAt) { !modTime.After(track.UpdatedAt) {
return nil return nil
@@ -136,6 +136,7 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
discNumber, TotalDiscs := tags.Disc() discNumber, TotalDiscs := tags.Disc()
track.Path = fullPath track.Path = fullPath
track.Title = tags.Title() track.Title = tags.Title()
track.Artist = tags.Artist()
track.DiscNumber = uint(discNumber) track.DiscNumber = uint(discNumber)
track.TotalDiscs = uint(TotalDiscs) track.TotalDiscs = uint(TotalDiscs)
track.TotalTracks = uint(totalTracks) track.TotalTracks = uint(totalTracks)
@@ -143,25 +144,26 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
track.Year = uint(tags.Year()) track.Year = uint(tags.Year())
track.Suffix = extension track.Suffix = extension
track.ContentType = mime track.ContentType = mime
// set artist { track.Size = uint(stat.Size())
artist := db.Artist{ // set album artist {
albumArtist := db.AlbumArtist{
Name: tags.AlbumArtist(), Name: tags.AlbumArtist(),
} }
err = tx.Where(artist).First(&artist).Error err = tx.Where(albumArtist).First(&albumArtist).Error
if gorm.IsRecordNotFoundError(err) { if gorm.IsRecordNotFoundError(err) {
artist.Name = tags.AlbumArtist() albumArtist.Name = tags.AlbumArtist()
tx.Save(&artist) tx.Save(&albumArtist)
} }
track.ArtistID = artist.ID track.AlbumArtistID = albumArtist.ID
// set album // set album
album := db.Album{ album := db.Album{
ArtistID: artist.ID, AlbumArtistID: albumArtist.ID,
Title: tags.Album(), Title: tags.Album(),
} }
err = tx.Where(album).First(&album).Error err = tx.Where(album).First(&album).Error
if gorm.IsRecordNotFoundError(err) { if gorm.IsRecordNotFoundError(err) {
album.Title = tags.Album() album.Title = tags.Album()
album.ArtistID = artist.ID album.AlbumArtistID = albumArtist.ID
tx.Save(&album) tx.Save(&album)
} }
track.AlbumID = album.ID track.AlbumID = album.ID
@@ -181,7 +183,7 @@ func main() {
orm.SetLogger(log.New(os.Stdout, "gorm ", 0)) orm.SetLogger(log.New(os.Stdout, "gorm ", 0))
orm.AutoMigrate( orm.AutoMigrate(
&db.Album{}, &db.Album{},
&db.Artist{}, &db.AlbumArtist{},
&db.Track{}, &db.Track{},
&db.Cover{}, &db.Cover{},
&db.User{}, &db.User{},

View File

@@ -40,15 +40,27 @@ func main() {
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping)) mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
mux.HandleFunc("/rest/stream", withWare(cont.Stream)) mux.HandleFunc("/rest/stream", withWare(cont.Stream))
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream)) mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory)) mux.HandleFunc("/rest/download", withWare(cont.Stream))
mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory)) mux.HandleFunc("/rest/download.view", withWare(cont.Stream))
mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt))
mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt))
mux.HandleFunc("/rest/getIndexes", withWare(cont.GetIndexes)) mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists))
mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes)) mux.HandleFunc("/rest/getArtists.view", withWare(cont.GetArtists))
mux.HandleFunc("/rest/getArtist", withWare(cont.GetArtist))
mux.HandleFunc("/rest/getArtist.view", withWare(cont.GetArtist))
mux.HandleFunc("/rest/getAlbum", withWare(cont.GetAlbum))
mux.HandleFunc("/rest/getAlbum.view", withWare(cont.GetAlbum))
mux.HandleFunc("/rest/getMusicFolders", withWare(cont.GetMusicFolders))
mux.HandleFunc("/rest/getMusicFolders.view", withWare(cont.GetMusicFolders))
mux.HandleFunc("/rest/getAlbumList2", withWare(cont.GetAlbumList))
mux.HandleFunc("/rest/getAlbumList2.view", withWare(cont.GetAlbumList))
mux.HandleFunc("/rest/getLicense", withWare(cont.GetLicence)) mux.HandleFunc("/rest/getLicense", withWare(cont.GetLicence))
mux.HandleFunc("/rest/getLicense.view", withWare(cont.GetLicence)) mux.HandleFunc("/rest/getLicense.view", withWare(cont.GetLicence))
mux.HandleFunc("/", withWare(cont.NotFound)) mux.HandleFunc("/", withWare(cont.NotFound))
// mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory))
// mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
// mux.HandleFunc("/rest/getIndexes", withWare(cont.GetIndexes))
// mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes))
server := &http.Server{ server := &http.Server{
Addr: address, Addr: address,
Handler: mux, Handler: mux,

View File

@@ -3,14 +3,14 @@ package db
// Album represents the albums table // Album represents the albums table
type Album struct { type Album struct {
Base Base
Artist Artist AlbumArtist AlbumArtist
ArtistID uint AlbumArtistID uint
Title string `gorm:"not null;index"` Title string `gorm:"not null;index"`
Tracks []Track Tracks []Track
} }
// Artist represents the artists table // AlbumArtist represents the AlbumArtists table
type Artist struct { type AlbumArtist struct {
Base Base
Albums []Album Albums []Album
Name string `gorm:"not null;unique_index"` Name string `gorm:"not null;unique_index"`
@@ -21,8 +21,9 @@ type Track struct {
Base Base
Album Album Album Album
AlbumID uint AlbumID uint
Artist Artist AlbumArtist AlbumArtist
ArtistID uint AlbumArtistID uint
Artist string
Bitrate uint Bitrate uint
Codec string Codec string
DiscNumber uint DiscNumber uint
@@ -34,14 +35,15 @@ type Track struct {
Year uint Year uint
Suffix string Suffix string
ContentType string ContentType string
Size uint
Path string `gorm:"not null;unique_index"` Path string `gorm:"not null;unique_index"`
} }
// Cover represents the covers table // Cover represents the covers table
type Cover struct { type Cover struct {
Base CrudBase
AlbumID uint `gorm:"primary_key;auto_increment:false"`
Album Album Album Album
AlbumID uint
Image []byte Image []byte
Path string `gorm:"not null;unique_index"` Path string `gorm:"not null;unique_index"`
} }

3
go.mod
View File

@@ -13,8 +13,9 @@ require (
github.com/karrick/godirwalk v1.8.0 github.com/karrick/godirwalk v1.8.0
github.com/lib/pq v1.0.0 // indirect github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-sqlite3 v1.10.0 // indirect github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/mozillazg/go-unidecode v0.1.1
github.com/myesui/uuid v1.0.0 // indirect github.com/myesui/uuid v1.0.0 // indirect
github.com/twinj/uuid v1.0.0 github.com/twinj/uuid v1.0.0
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 // indirect golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
google.golang.org/appengine v1.5.0 // indirect google.golang.org/appengine v1.5.0 // indirect
) )

9
go.sum
View File

@@ -8,6 +8,7 @@ git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqbl
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -73,6 +74,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mozillazg/go-unidecode v0.1.1 h1:uiRy1s4TUqLbcROUrnCN/V85Jlli2AmDF6EeAXOeMHE=
github.com/mozillazg/go-unidecode v0.1.1/go.mod h1:fYMdhyjni9ZeEmS6OE/GJHDLsF8TQvIVDwYR/drR26Q=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
@@ -104,8 +107,8 @@ golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5a
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 h1:aUX/1G2gFSs4AsJJg2cL3HuoRhCSCz733FE5GUSuaT4= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -139,6 +142,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -173,6 +177,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strconv"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/sentriz/gonic/subsonic" "github.com/sentriz/gonic/subsonic"
@@ -15,38 +16,60 @@ type Controller struct {
DB *gorm.DB DB *gorm.DB
} }
func getStrParam(r *http.Request, key string) string {
return r.URL.Query().Get(key)
}
func getIntParam(r *http.Request, key string) (int, error) {
strVal := r.URL.Query().Get(key)
if strVal == "" {
return 0, fmt.Errorf("no param with key `%s`", key)
}
val, err := strconv.Atoi(strVal)
if err != nil {
return 0, fmt.Errorf("not an int `%s`", strVal)
}
return val, nil
}
func getIntParamOr(r *http.Request, key string, or int) int {
val, err := getIntParam(r, key)
if err != nil {
return or
}
return val
}
func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.Response) { func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.Response) {
format := r.URL.Query().Get("f") res := subsonic.MetaResponse{
switch format { Response: sub,
}
switch r.URL.Query().Get("f") {
case "json": case "json":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(sub) data, err := json.Marshal(res)
if err != nil { if err != nil {
log.Printf("could not marshall to json: %v\n", err) log.Printf("could not marshall to json: %v\n", err)
} }
w.Write([]byte(`{"subsonic-response":`))
w.Write(data) w.Write(data)
w.Write([]byte("}"))
fmt.Println("THE JSON", string(data))
case "jsonp": case "jsonp":
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
data, err := json.Marshal(sub) data, err := json.Marshal(res)
if err != nil { if err != nil {
log.Printf("could not marshall to json: %v\n", err) log.Printf("could not marshall to json: %v\n", err)
} }
callback := r.URL.Query().Get("callback") callback := r.URL.Query().Get("callback")
w.Write([]byte(fmt.Sprintf(`%s({"subsonic-response":`, callback))) w.Write([]byte(callback))
w.Write([]byte("("))
w.Write(data) w.Write(data)
w.Write([]byte("});")) w.Write([]byte(");"))
fmt.Println("THE JSONP", string(data))
default: default:
w.Header().Set("Content-Type", "application/xml") w.Header().Set("Content-Type", "application/xml")
data, err := xml.Marshal(sub) data, err := xml.Marshal(res)
if err != nil { if err != nil {
log.Printf("could not marshall to xml: %v\n", err) log.Printf("could not marshall to xml: %v\n", err)
} }
w.Write(data) w.Write(data)
fmt.Println("THE XML", string(data))
} }
} }

View File

@@ -4,179 +4,212 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strconv"
"unicode" "unicode"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/sentriz/gonic/db" "github.com/sentriz/gonic/db"
"github.com/sentriz/gonic/subsonic" "github.com/sentriz/gonic/subsonic"
"github.com/mozillazg/go-unidecode"
) )
func (c *Controller) Ping(w http.ResponseWriter, req *http.Request) { var orderExpr = map[string]interface{}{
"random": gorm.Expr("random()"),
"newest": "updated_at desc",
"alphabeticalByName": "title",
"alphabeticalByArtist": "album_artist.name",
}
func indexOf(s string) rune {
first := string(s[0])
c := rune(unidecode.Unidecode(first)[0])
if !unicode.IsLetter(c) {
return '#'
}
return c
}
func (c *Controller) Ping(w http.ResponseWriter, r *http.Request) {
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
respond(w, req, sub) respond(w, r, sub)
} }
func (c *Controller) GetIndexes(w http.ResponseWriter, req *http.Request) { func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
var artists []db.Artist id, err := getIntParam(r, "id")
c.DB.Find(&artists) if err != nil {
indexMap := make(map[byte]*subsonic.Index) respondError(w, r, 10, "please provide an `id` parameter")
for _, artist := range artists {
first := artist.Name[0]
if !unicode.IsLetter(rune(first)) {
first = 0x23 // '#'
}
_, ok := indexMap[first]
if !ok {
indexMap[first] = &subsonic.Index{
Name: string(first),
Artists: []*subsonic.Artist{},
}
}
indexMap[first].Artists = append(
indexMap[first].Artists,
&subsonic.Artist{
ID: artist.ID,
Name: artist.Name,
},
)
}
indexes := []*subsonic.Index{}
for _, v := range indexMap {
indexes = append(indexes, v)
}
sub := subsonic.NewResponse()
sub.Indexes = &subsonic.Indexes{
Index: &indexes,
}
respond(w, req, sub)
}
func browseArtist(c *gorm.DB, artist *db.Artist) *subsonic.Directory {
var cover db.Cover
var dir subsonic.Directory
dir.Name = artist.Name
dir.ID = artist.ID
dir.Parent = 0
var albums []*db.Album
c.Model(artist).Related(&albums)
dir.Children = make([]subsonic.Child, len(albums))
for i, album := range albums {
c.Model(album).Related(&cover)
dir.Children[i] = subsonic.Child{
Artist: artist.Name,
ID: album.ID,
IsDir: true,
Parent: artist.ID,
Title: album.Title,
CoverArt: cover.ID,
}
cover = db.Cover{}
}
return &dir
}
func browseAlbum(c *gorm.DB, album *db.Album) *subsonic.Directory {
var artist db.Artist
c.Model(album).Related(&artist)
var tracks []*db.Track
c.Model(album).Related(&tracks)
var cover db.Cover
c.Model(album).Related(&cover)
var dir subsonic.Directory
dir.Name = album.Title
dir.ID = album.ID
dir.Parent = artist.ID
dir.Children = make([]subsonic.Child, len(tracks))
for i, track := range tracks {
dir.Children[i] = subsonic.Child{
ID: track.ID,
Title: track.Title,
Parent: album.ID,
Artist: artist.Name,
ArtistID: artist.ID,
Album: album.Title,
AlbumID: album.ID,
IsDir: false,
Path: track.Path,
CoverArt: cover.ID,
ContentType: track.ContentType,
Suffix: track.Suffix,
Duration: 0,
}
}
return &dir
}
func (c *Controller) GetMusicDirectory(w http.ResponseWriter, req *http.Request) {
idStr := req.URL.Query().Get("id")
if idStr == "" {
respondError(w, req, 10, "please provide an `id` parameter")
return return
} }
id, _ := strconv.Atoi(idStr)
sub := subsonic.NewResponse()
var artist db.Artist
c.DB.First(&artist, id)
if artist.ID != 0 {
sub.MusicDirectory = browseArtist(c.DB, &artist)
respond(w, req, sub)
return
}
var album db.Album
c.DB.First(&album, id)
if album.ID != 0 {
sub.MusicDirectory = browseAlbum(c.DB, &album)
respond(w, req, sub)
return
}
respondError(w, req,
70, fmt.Sprintf("directory with id `%d` was not found", id),
)
}
func (c *Controller) GetCoverArt(w http.ResponseWriter, req *http.Request) {
idStr := req.URL.Query().Get("id")
if idStr == "" {
respondError(w, req, 10, "please provide an `id` parameter")
return
}
id, _ := strconv.Atoi(idStr)
var cover db.Cover var cover db.Cover
c.DB.First(&cover, id) c.DB.First(&cover, id)
w.Write(cover.Image) w.Write(cover.Image)
} }
func (c *Controller) Stream(w http.ResponseWriter, req *http.Request) { func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
idStr := req.URL.Query().Get("id") var artists []*db.AlbumArtist
if idStr == "" { c.DB.Find(&artists)
respondError(w, req, 10, "please provide an `id` parameter") var indexMap = make(map[rune]*subsonic.Index)
var indexes []*subsonic.Index
for _, artist := range artists {
i := indexOf(artist.Name)
index, ok := indexMap[i]
if !ok {
index = &subsonic.Index{
Name: string(i),
Artists: []*subsonic.Artist{},
}
indexMap[i] = index
indexes = append(indexes, index)
}
index.Artists = append(index.Artists, &subsonic.Artist{
ID: artist.ID,
Name: artist.Name,
})
}
sub := subsonic.NewResponse()
sub.Artists = indexes
respond(w, r, sub)
}
func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var artist db.AlbumArtist
c.DB.
Preload("Albums").
First(&artist, id)
sub := subsonic.NewResponse()
sub.Artist = &subsonic.Artist{
ID: artist.ID,
Name: artist.Name,
}
for _, album := range artist.Albums {
sub.Artist.Albums = append(sub.Artist.Albums, &subsonic.Album{
ID: album.ID,
Name: album.Title,
Created: album.CreatedAt,
Artist: artist.Name,
ArtistID: artist.ID,
CoverID: album.ID,
})
}
respond(w, r, sub)
}
func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var album db.Album
c.DB.
Preload("AlbumArtist").
Preload("Tracks").
First(&album, id)
sub := subsonic.NewResponse()
sub.Album = &subsonic.Album{
ID: album.ID,
Name: album.Title,
CoverID: album.ID,
Created: album.CreatedAt,
Artist: album.AlbumArtist.Name,
}
for _, track := range album.Tracks {
sub.Album.Tracks = append(sub.Album.Tracks, &subsonic.Track{
ID: track.ID,
Title: track.Title,
Artist: track.Artist, // track artist
TrackNo: track.TrackNumber,
ContentType: track.ContentType,
Path: track.Path,
Suffix: track.Suffix,
Created: track.CreatedAt,
Size: track.Size,
Album: album.Title,
AlbumID: album.ID,
ArtistID: album.AlbumArtist.ID, // album artist
CoverID: album.ID,
Type: "music",
})
}
respond(w, r, sub)
}
func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
sub := subsonic.NewResponse()
sub.MusicFolders = []*subsonic.MusicFolder{
{ID: 0, Name: "music"},
}
respond(w, r, sub)
}
func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
listType := getStrParam(r, "type")
if listType == "" {
respondError(w, r, 10, "please provide a `type` parameter")
return
}
orderType, ok := orderExpr[listType]
if !ok {
respondError(w, r, 10, fmt.Sprintf(
"unknown value `%s` for parameter 'type'", listType,
))
return
}
size := getIntParamOr(r, "size", 10)
var albums []*db.Album
c.DB.
Preload("AlbumArtist").
Order(orderType).
Limit(size).
Find(&albums)
sub := subsonic.NewResponse()
for _, album := range albums {
sub.Albums = append(sub.Albums, &subsonic.Album{
ID: album.ID,
Name: album.Title,
Created: album.CreatedAt,
CoverID: album.ID,
Artist: album.AlbumArtist.Name,
ArtistID: album.AlbumArtist.ID,
})
}
respond(w, r, sub)
}
func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return return
} }
id, _ := strconv.Atoi(idStr)
var track db.Track var track db.Track
c.DB.First(&track, id) c.DB.First(&track, id)
if track.Path == "" { if track.Path == "" {
respondError(w, req, 70, fmt.Sprintf("media with id `%d` was not found", id)) respondError(w, r, 70, fmt.Sprintf("media with id `%d` was not found", id))
return return
} }
file, err := os.Open(track.Path) file, err := os.Open(track.Path)
if err != nil { if err != nil {
respondError(w, req, 0, fmt.Sprintf("error while streaming media: %v", err)) respondError(w, r, 0, fmt.Sprintf("error while streaming media: %v", err))
return return
} }
stat, _ := file.Stat() stat, _ := file.Stat()
http.ServeContent(w, req, track.Path, stat.ModTime(), file) http.ServeContent(w, r, track.Path, stat.ModTime(), file)
} }
func (c *Controller) GetLicence(w http.ResponseWriter, req *http.Request) { func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
sub.Licence = &subsonic.Licence{ sub.Licence = &subsonic.Licence{
Valid: true, Valid: true,
} }
respond(w, req, sub) respond(w, r, sub)
} }
func (c *Controller) NotFound(w http.ResponseWriter, req *http.Request) { func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) {
respondError(w, req, 0, "unknown route") respondError(w, r, 0, "unknown route")
} }

View File

@@ -15,14 +15,14 @@ var requiredParameters = []string{
"u", "v", "c", "u", "v", "c",
} }
func checkCredentialsNewWay(password, token, salt string) bool { func checkCredentialsToken(password, token, salt string) bool {
toHash := fmt.Sprintf("%s%s", password, salt) toHash := fmt.Sprintf("%s%s", password, salt)
hash := md5.Sum([]byte(toHash)) hash := md5.Sum([]byte(toHash))
expToken := hex.EncodeToString(hash[:]) expToken := hex.EncodeToString(hash[:])
return token == expToken return token == expToken
} }
func checkCredentialsOldWay(password, givenPassword string) bool { func checkCredentialsBasic(password, givenPassword string) bool {
if givenPassword[:4] == "enc:" { if givenPassword[:4] == "enc:" {
bytes, _ := hex.DecodeString(givenPassword[4:]) bytes, _ := hex.DecodeString(givenPassword[4:])
givenPassword = string(bytes) givenPassword = string(bytes)
@@ -71,9 +71,9 @@ func (c *Controller) CheckParameters(next http.HandlerFunc) http.HandlerFunc {
} }
var credsOk bool var credsOk bool
if tokenAuth { if tokenAuth {
credsOk = checkCredentialsNewWay(user.Password, token, salt) credsOk = checkCredentialsToken(user.Password, token, salt)
} else { } else {
credsOk = checkCredentialsOldWay(user.Password, password) credsOk = checkCredentialsBasic(user.Password, password)
} }
if !credsOk { if !credsOk {
respondError(w, r, 40, "invalid password") respondError(w, r, 40, "invalid password")

119
handler/oldfolderbase Normal file
View File

@@ -0,0 +1,119 @@
// func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
// var artists []*db.Artist
// c.DB.Find(&artists)
// indexMap := make(map[byte]*subsonic.Index)
// for _, artist := range artists {
// first := artist.Name[0]
// if !unicode.IsLetter(rune(first)) {
// first = 0x23 // '#'
// }
// _, ok := indexMap[first]
// if !ok {
// indexMap[first] = &subsonic.Index{
// Name: string(first),
// Artists: []*subsonic.Artist{},
// }
// }
// indexMap[first].Artists = append(
// indexMap[first].Artists,
// &subsonic.Artist{
// ID: artist.ID,
// Name: artist.Name,
// },
// )
// }
// indexes := []*subsonic.Index{}
// for _, v := range indexMap {
// indexes = append(indexes, v)
// }
// sub := subsonic.NewResponse()
// sub.Indexes = &subsonic.Indexes{
// Index: &indexes,
// }
// respond(w, r, sub)
// }
// func browseArtist(c *gorm.DB, artist *db.Artist) *subsonic.Directory {
// var cover db.Cover
// var dir subsonic.Directory
// dir.Name = artist.Name
// dir.ID = artist.ID
// dir.Parent = 0
// var albums []*db.Album
// c.Model(artist).Related(&albums)
// dir.Children = make([]subsonic.Child, len(albums))
// for i, album := range albums {
// c.Model(album).Related(&cover)
// dir.Children[i] = subsonic.Child{
// Artist: artist.Name,
// ID: album.ID,
// IsDir: true,
// Parent: artist.ID,
// Title: album.Title,
// CoverID: cover.AlbumID,
// }
// cover = db.Cover{}
// }
// return &dir
// }
// func browseAlbum(c *gorm.DB, album *db.Album) *subsonic.Directory {
// var artist db.Artist
// c.Model(album).Related(&artist)
// var tracks []*db.Track
// c.Model(album).Related(&tracks)
// var cover db.Cover
// c.Model(album).Related(&cover)
// var dir subsonic.Directory
// dir.Name = album.Title
// dir.ID = album.ID
// dir.Parent = artist.ID
// dir.Children = make([]subsonic.Child, len(tracks))
// for i, track := range tracks {
// dir.Children[i] = subsonic.Child{
// ID: track.ID,
// Title: track.Title,
// Parent: album.ID,
// Artist: artist.Name,
// ArtistID: artist.ID,
// Album: album.Title,
// AlbumID: album.ID,
// IsDir: false,
// Path: track.Path,
// CoverArt: cover.ID,
// ContentType: track.ContentType,
// Suffix: track.Suffix,
// Duration: 0,
// }
// }
// return &dir
// }
// func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
// idStr := r.URL.Query().Get("id")
// if idStr == "" {
// respondError(w, r, 10, "please provide an `id` parameter")
// return
// }
// id, _ := strconv.Atoi(idStr)
// sub := subsonic.NewResponse()
// var artist db.Artist
// c.DB.First(&artist, id)
// if artist.ID != 0 {
// sub.Directory = browseArtist(c.DB, &artist)
// respond(w, r, sub)
// return
// }
// var album db.Album
// c.DB.First(&album, id)
// if album.ID != 0 {
// sub.Directory = browseAlbum(c.DB, &album)
// respond(w, r, sub)
// return
// }
// respondError(w, r,
// 70, fmt.Sprintf("directory with id `%d` was not found", id),
// )
// }

View File

@@ -1,35 +1,32 @@
package subsonic package subsonic
import "encoding/xml" import "time"
type Album struct { type Album struct {
XMLName xml.Name `xml:"album" json:"-"`
ID uint `xml:"id,attr" json:"id"` ID uint `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
ArtistID uint `xml:"artistId,attr" json:"artistId"` ArtistID uint `xml:"artistId,attr" json:"artistId"`
ArtistName string `xml:"artist,attr" json:"artist"` Artist string `xml:"artist,attr" json:"artist"`
SongCount uint `xml:"songCount,attr" json:"songCount"` TrackCount uint `xml:"songCount,attr" json:"songCount"`
Duration uint `xml:"duration,attr" json:"duration"` Duration uint `xml:"duration,attr" json:"duration"`
CoverArt string `xml:"coverArt,attr" json:"coverArt"` CoverID uint `xml:"coverArt,attr" json:"coverArt"`
Created string `xml:"created,attr" json:"created"` Created time.Time `xml:"created,attr" json:"created"`
Songs *[]*Song `xml:"song" json:"song,omitempty"` Tracks []*Track `xml:"song" json:"song,omitempty"`
} }
type RandomSongs struct { type RandomTracks struct {
XMLName xml.Name `xml:"randomSongs" json:"-"` Tracks []*Track `xml:"song" json:"song"`
Songs []*Song `xml:"song" json:"song"`
} }
type Song struct { type Track struct {
XMLName xml.Name `xml:"song" json:"-"`
ID uint `xml:"id,attr" json:"id"` ID uint `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"` Parent uint `xml:"parent,attr" json:"parent"`
Title string `xml:"title,attr" json:"title"` Title string `xml:"title,attr" json:"title"`
Album string `xml:"album,attr" json:"album"` Album string `xml:"album,attr" json:"album"`
Artist string `xml:"artist,attr" json:"artist"` Artist string `xml:"artist,attr" json:"artist"`
IsDir bool `xml:"isDir,attr" json:"isDir"` IsDir bool `xml:"isDir,attr" json:"isDir"`
CoverArt string `xml:"coverArt,attr" json:"coverArt"` CoverID uint `xml:"coverArt,attr" json:"coverArt"`
Created string `xml:"created,attr" json:"created"` Created time.Time `xml:"created,attr" json:"created"`
Duration uint `xml:"duration,attr" json:"duration"` Duration uint `xml:"duration,attr" json:"duration"`
Genre string `xml:"genre,attr" json:"genre"` Genre string `xml:"genre,attr" json:"genre"`
BitRate uint `xml:"bitRate,attr" json:"bitRate"` BitRate uint `xml:"bitRate,attr" json:"bitRate"`
@@ -45,27 +42,24 @@ type Song struct {
} }
type Artist struct { type Artist struct {
XMLName xml.Name `xml:"artist" json:"-"`
ID uint `xml:"id,attr" json:"id"` ID uint `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
CoverArt string `xml:"coverArt,attr" json:"coverArt,omitempty"` CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"`
AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"` AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"`
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
} }
type Indexes struct { type Indexes struct {
LastModified uint `xml:"lastModified,attr" json:"lastModified"` LastModified uint `xml:"lastModified,attr" json:"lastModified"`
Index *[]*Index `xml:"index" json:"index"` Index []*Index `xml:"index" json:"index"`
} }
type Index struct { type Index struct {
XMLName xml.Name `xml:"index" json:"-"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
Artists []*Artist `xml:"artist" json:"artist"` Artists []*Artist `xml:"artist" json:"artist"`
} }
type Directory struct { type Directory struct {
XMLName xml.Name `xml:"directory" json:"-"`
ID uint `xml:"id,attr" json:"id"` ID uint `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"` Parent uint `xml:"parent,attr" json:"parent"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
@@ -74,7 +68,6 @@ type Directory struct {
} }
type Child struct { type Child struct {
XMLName xml.Name `xml:"child" json:"-"`
ID uint `xml:"id,attr" json:"id,omitempty"` ID uint `xml:"id,attr" json:"id,omitempty"`
Parent uint `xml:"parent,attr" json:"parent,omitempty"` Parent uint `xml:"parent,attr" json:"parent,omitempty"`
Title string `xml:"title,attr" json:"title,omitempty"` Title string `xml:"title,attr" json:"title,omitempty"`
@@ -86,7 +79,7 @@ type Child struct {
Track uint `xml:"track,attr,omitempty" json:"track,omitempty"` Track uint `xml:"track,attr,omitempty" json:"track,omitempty"`
Year uint `xml:"year,attr,omitempty" json:"year,omitempty"` Year uint `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
CoverArt uint `xml:"coverart,attr" json:"coverArt,omitempty"` CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"`
Size uint `xml:"size,attr,omitempty" json:"size,omitempty"` Size uint `xml:"size,attr,omitempty" json:"size,omitempty"`
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
@@ -95,7 +88,11 @@ type Child struct {
Path string `xml:"path,attr,omitempty" json:"path,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
} }
type MusicFolder struct {
ID uint `xml:"id,attr" json:"id,omitempty"`
Name string `xml:"name,attr" json:"name,omitempty"`
}
type Licence struct { type Licence struct {
XMLName xml.Name `xml:"license" json:"-"`
Valid bool `xml:"valid,attr,omitempty" json:"valid,omitempty"` Valid bool `xml:"valid,attr,omitempty" json:"valid,omitempty"`
} }

View File

@@ -2,34 +2,36 @@
package subsonic package subsonic
import ( import "encoding/xml"
"encoding/xml"
)
var ( var (
apiVersion = "1.9.0" apiVersion = "1.9.0"
xmlns = "http://subsonic.org/restapi" xmlns = "http://subsonic.org/restapi"
) )
type Response struct { type MetaResponse struct {
XMLName xml.Name `xml:"subsonic-response" json:"-"` XMLName xml.Name `xml:"subsonic-response" json:"-"`
*Response `json:"subsonic-response"`
}
type Response struct {
Status string `xml:"status,attr" json:"status"` Status string `xml:"status,attr" json:"status"`
Version string `xml:"version,attr" json:"version"` Version string `xml:"version,attr" json:"version"`
XMLNS string `xml:"xmlns,attr" json:"-"` XMLNS string `xml:"xmlns,attr" json:"-"`
Error *Error `xml:"error" json:"error,omitempty"` Error *Error `xml:"error" json:"error,omitempty"`
AlbumList2 *[]*Album `xml:"albumList2>album" json:"album,omitempty"` Albums []*Album `xml:"albumList2>album" json:"albumList2,omitempty"`
Album *Album `xml:"album" json:"album,omitempty"` Album *Album `xml:"album" json:"album,omitempty"`
Song *Song `xml:"song" json:"song,omitempty"` Track *Track `xml:"song" json:"song,omitempty"`
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"` Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
Artists *[]*Index `xml:"artists>index" json:"artists,omitempty"` Artists []*Index `xml:"artists>index" json:"artists,omitempty"`
Artist *Artist `xml:"artist" json:"artist,omitempty"` Artist *Artist `xml:"artist" json:"artist,omitempty"`
MusicDirectory *Directory `xml:"directory" json:"directory,omitempty"` Directory *Directory `xml:"directory" json:"directory,omitempty"`
RandomSongs *RandomSongs `xml:"randomSongs" json:"randomSongs,omitempty"` RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
MusicFolders []*MusicFolder `xml:"musicFolders>musicFolder" json:"musicFolders,omitempty"`
Licence *Licence `xml:"license" json:"license,omitempty"` Licence *Licence `xml:"license" json:"license,omitempty"`
} }
type Error struct { type Error struct {
XMLName xml.Name `xml:"error" json:"-"`
Code uint64 `xml:"code,attr" json:"code"` Code uint64 `xml:"code,attr" json:"code"`
Message string `xml:"message,attr" json:"message"` Message string `xml:"message,attr" json:"message"`
} }