add some endpoints
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/model"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/jinzhu/gorm"
|
||||
@@ -84,7 +83,7 @@ func handleFolderCompletion(fullPath string, info *godirwalk.Dirent) error {
|
||||
if cLastAlbum.isEmpty() {
|
||||
return nil
|
||||
}
|
||||
cover := model.Cover{
|
||||
cover := db.Cover{
|
||||
Path: cLastAlbum.coverPath,
|
||||
}
|
||||
err := tx.Where(cover).First(&cover).Error
|
||||
@@ -122,7 +121,7 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
|
||||
return nil
|
||||
}
|
||||
// set track basics
|
||||
track := model.Track{
|
||||
track := db.Track{
|
||||
Path: fullPath,
|
||||
}
|
||||
err = tx.Where(track).First(&track).Error
|
||||
@@ -131,6 +130,8 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
|
||||
return nil
|
||||
}
|
||||
tags, err := readTags(fullPath)
|
||||
fmt.Println(tags.Raw())
|
||||
os.Exit(0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("when reading tags: %v", err)
|
||||
}
|
||||
@@ -138,13 +139,13 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
|
||||
discNumber, TotalDiscs := tags.Disc()
|
||||
track.Path = fullPath
|
||||
track.Title = tags.Title()
|
||||
track.DiscNumber = discNumber
|
||||
track.TotalDiscs = TotalDiscs
|
||||
track.TotalTracks = totalTracks
|
||||
track.TrackNumber = trackNumber
|
||||
track.Year = tags.Year()
|
||||
// set artist
|
||||
artist := model.Artist{
|
||||
track.DiscNumber = uint(discNumber)
|
||||
track.TotalDiscs = uint(TotalDiscs)
|
||||
track.TotalTracks = uint(totalTracks)
|
||||
track.TrackNumber = uint(trackNumber)
|
||||
track.Year = uint(tags.Year())
|
||||
// set artist {
|
||||
artist := db.Artist{
|
||||
Name: tags.AlbumArtist(),
|
||||
}
|
||||
err = tx.Where(artist).First(&artist).Error
|
||||
@@ -154,7 +155,7 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
|
||||
}
|
||||
track.ArtistID = artist.ID
|
||||
// set album
|
||||
album := model.Album{
|
||||
album := db.Album{
|
||||
ArtistID: artist.ID,
|
||||
Title: tags.Album(),
|
||||
}
|
||||
@@ -180,10 +181,11 @@ func main() {
|
||||
orm = db.New()
|
||||
orm.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
orm.AutoMigrate(
|
||||
&model.Album{},
|
||||
&model.Artist{},
|
||||
&model.Track{},
|
||||
&model.Cover{},
|
||||
&db.Album{},
|
||||
&db.Artist{},
|
||||
&db.Track{},
|
||||
&db.Cover{},
|
||||
&db.User{},
|
||||
)
|
||||
// 🤫🤫🤫
|
||||
orm.Exec(`
|
||||
@@ -191,6 +193,11 @@ func main() {
|
||||
SELECT 'albums', 500000
|
||||
WHERE NOT EXISTS (SELECT * FROM sqlite_sequence)
|
||||
`)
|
||||
orm.Exec(`
|
||||
INSERT INTO users(username, password)
|
||||
SELECT 'admin', 'admin'
|
||||
WHERE NOT EXISTS (SELECT * FROM users)
|
||||
`)
|
||||
startTime := time.Now()
|
||||
tx = orm.Begin()
|
||||
err := godirwalk.Walk(os.Args[1], &godirwalk.Options{
|
||||
|
||||
@@ -1,78 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sentriz/gonic/context"
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/handler"
|
||||
"github.com/sentriz/gonic/router"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
var (
|
||||
username = "senan"
|
||||
password = "howdy"
|
||||
requiredParameters = []string{
|
||||
"u", "t", "s", "v", "c",
|
||||
}
|
||||
)
|
||||
type middleware func(next http.HandlerFunc) http.HandlerFunc
|
||||
|
||||
func checkCredentials(token, salt string) bool {
|
||||
toHash := fmt.Sprintf("%s%s", password, salt)
|
||||
hash := md5.Sum([]byte(toHash))
|
||||
expToken := hex.EncodeToString(hash[:])
|
||||
return token == expToken
|
||||
}
|
||||
|
||||
func contextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return next(&context.Subsonic{c})
|
||||
}
|
||||
}
|
||||
|
||||
func validationMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
cc := c.(*context.Subsonic)
|
||||
for _, req := range requiredParameters {
|
||||
param := cc.QueryParams().Get(req)
|
||||
if param != "" {
|
||||
continue
|
||||
func newChain(wares ...middleware) middleware {
|
||||
return func(final http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
last := final
|
||||
for i := len(wares) - 1; i >= 0; i-- {
|
||||
last = wares[i](last)
|
||||
}
|
||||
return cc.Respond(http.StatusBadRequest, subsonic.NewError(
|
||||
10, fmt.Sprintf("please provide a `%s` parameter", req),
|
||||
))
|
||||
last(w, r)
|
||||
}
|
||||
credsOk := checkCredentials(
|
||||
cc.QueryParams().Get("t"), // token
|
||||
cc.QueryParams().Get("s"), // salt
|
||||
)
|
||||
if !credsOk {
|
||||
return cc.Respond(http.StatusBadRequest, subsonic.NewError(
|
||||
40, "invalid username or password",
|
||||
))
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
d := db.New()
|
||||
r := router.New()
|
||||
r.Use(contextMiddleware)
|
||||
r.Use(validationMiddleware)
|
||||
h := &handler.Handler{
|
||||
DB: d,
|
||||
Router: r,
|
||||
address := ":5000"
|
||||
cont := handler.Controller{
|
||||
DB: db.New(),
|
||||
}
|
||||
withWare := newChain(
|
||||
cont.LogConnection,
|
||||
cont.EnableCORS,
|
||||
cont.CheckParameters,
|
||||
)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
|
||||
mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes))
|
||||
mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
|
||||
mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt))
|
||||
mux.HandleFunc("/rest/getMusicFolders.view", withWare(cont.GetMusicFolders))
|
||||
mux.HandleFunc("/rest/getPlaylists.view", withWare(cont.GetPlaylists))
|
||||
mux.HandleFunc("/rest/getGenres.view", withWare(cont.GetGenres))
|
||||
mux.HandleFunc("/rest/getPodcasts.view", withWare(cont.GetPodcasts))
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
log.Printf("starting server at `%s`\n", address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Printf("when starting server: %v\n", err)
|
||||
}
|
||||
rest := r.Group("/rest")
|
||||
rest.GET("", h.GetTest)
|
||||
log.Fatal(r.Start("127.0.0.1:5001"))
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type Subsonic struct {
|
||||
echo.Context
|
||||
}
|
||||
|
||||
func (c *Subsonic) Respond(code int, r *subsonic.Response) error {
|
||||
format := c.QueryParams().Get("f")
|
||||
switch format {
|
||||
case "json":
|
||||
return c.JSON(code, r)
|
||||
case "jsonp":
|
||||
callback := c.QueryParams().Get("callback")
|
||||
return c.JSONP(code, callback, r)
|
||||
default:
|
||||
return c.XML(code, r)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -13,16 +13,20 @@ type CrudBase struct {
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
type IDBase struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
}
|
||||
|
||||
// Base is the base model with an auto incrementing primary key
|
||||
type Base struct {
|
||||
IDBase
|
||||
CrudBase
|
||||
ID uint `gorm:"primary_key"`
|
||||
}
|
||||
|
||||
// BaseWithUUID is the base model with an UUIDv4 primary key
|
||||
type BaseWithUUID struct {
|
||||
IDBase
|
||||
CrudBase
|
||||
ID string `gorm:"primary_key"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called by GORM to set the UUID primary key
|
||||
52
db/model.go
Normal file
52
db/model.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package db
|
||||
|
||||
// Album represents the albums table
|
||||
type Album struct {
|
||||
Base
|
||||
Artist Artist
|
||||
ArtistID uint
|
||||
Title string `gorm:"not null;index"`
|
||||
Tracks []Track
|
||||
}
|
||||
|
||||
// Artist represents the artists table
|
||||
type Artist struct {
|
||||
Base
|
||||
Albums []Album
|
||||
Name string `gorm:"not null;unique_index"`
|
||||
}
|
||||
|
||||
// Track represents the tracks table
|
||||
type Track struct {
|
||||
Base
|
||||
Album Album
|
||||
AlbumID uint
|
||||
Artist Artist
|
||||
ArtistID uint
|
||||
Bitrate uint
|
||||
Codec string
|
||||
DiscNumber uint
|
||||
Duration uint
|
||||
Title string
|
||||
TotalDiscs uint
|
||||
TotalTracks uint
|
||||
TrackNumber uint
|
||||
Year uint
|
||||
Path string `gorm:"not null;unique_index"`
|
||||
}
|
||||
|
||||
// Cover represents the covers table
|
||||
type Cover struct {
|
||||
Base
|
||||
Album Album
|
||||
AlbumID uint
|
||||
Image []byte
|
||||
Path string `gorm:"not null;unique_index"`
|
||||
}
|
||||
|
||||
// User represents the users table
|
||||
type User struct {
|
||||
IDBase
|
||||
Username string `gorm:"not null;unique_index"`
|
||||
Password string
|
||||
}
|
||||
7
go.mod
7
go.mod
@@ -3,7 +3,6 @@ module github.com/sentriz/gonic
|
||||
require (
|
||||
cloud.google.com/go v0.37.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/dhowden/tag v0.0.0-20181104225729-a9f04c2798ca
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||
@@ -12,16 +11,10 @@ require (
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
|
||||
github.com/jinzhu/now v1.0.0 // indirect
|
||||
github.com/karrick/godirwalk v1.8.0
|
||||
github.com/labstack/echo v3.3.10+incompatible
|
||||
github.com/labstack/gommon v0.2.8 // indirect
|
||||
github.com/lib/pq v1.0.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
||||
github.com/myesui/uuid v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/twinj/uuid v1.0.0
|
||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 // indirect
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
)
|
||||
|
||||
23
go.sum
23
go.sum
@@ -13,14 +13,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 h1:tTngnoO/B6HQnJ+pK8tN7kEAhmhIfaJOutqq/A4/JTM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dhowden/tag v0.0.0-20181104225729-a9f04c2798ca h1:EsPh1VImRZ6OOhWtz/zzwTjxVQKcKIiqS5tYNdx2eCg=
|
||||
github.com/dhowden/tag v0.0.0-20181104225729-a9f04c2798ca/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
@@ -70,17 +66,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -103,18 +90,11 @@ github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
@@ -155,9 +135,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sentriz/gonic/context"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
// GetTest doesn't do anything
|
||||
func (h *Handler) GetTest(c echo.Context) error {
|
||||
cc := c.(*context.Subsonic)
|
||||
resp := subsonic.NewResponse()
|
||||
return cc.Respond(http.StatusOK, resp)
|
||||
}
|
||||
@@ -1,13 +1,58 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
)
|
||||
|
||||
// Handler is passed to the handler functions so
|
||||
// they can access the database
|
||||
type Handler struct {
|
||||
DB *gorm.DB
|
||||
Router *echo.Echo
|
||||
type Controller struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.Response) {
|
||||
format := r.URL.Query().Get("f")
|
||||
switch format {
|
||||
case "json":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, err := json.Marshal(sub)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
}
|
||||
w.Write([]byte(`{"subsonic-response":`))
|
||||
w.Write(data)
|
||||
w.Write([]byte("}"))
|
||||
case "jsonp":
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
data, err := json.Marshal(sub)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
}
|
||||
callback := r.URL.Query().Get("callback")
|
||||
w.Write([]byte(fmt.Sprintf("%s(", callback)))
|
||||
w.Write(data)
|
||||
w.Write([]byte(");"))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
data, err := xml.Marshal(sub)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to xml: %v\n", err)
|
||||
}
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request, sub *subsonic.Response) {
|
||||
respondRaw(w, r, http.StatusOK, sub)
|
||||
}
|
||||
|
||||
func respondError(w http.ResponseWriter, r *http.Request, code uint64, message string) {
|
||||
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
|
||||
code, message,
|
||||
))
|
||||
}
|
||||
|
||||
149
handler/media.go
Normal file
149
handler/media.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
)
|
||||
|
||||
func (c *Controller) Ping(w http.ResponseWriter, req *http.Request) {
|
||||
sub := subsonic.NewResponse()
|
||||
respond(w, req, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) GetIndexes(w http.ResponseWriter, req *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, 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,
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
c.DB.First(&cover, id)
|
||||
w.Write(cover.Image)
|
||||
}
|
||||
|
||||
func (c *Controller) GetMusicFolders(w http.ResponseWriter, req *http.Request) {}
|
||||
func (c *Controller) GetPlaylists(w http.ResponseWriter, req *http.Request) {}
|
||||
func (c *Controller) GetGenres(w http.ResponseWriter, req *http.Request) {}
|
||||
func (c *Controller) GetPodcasts(w http.ResponseWriter, req *http.Request) {}
|
||||
96
handler/middleware.go
Normal file
96
handler/middleware.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sentriz/gonic/db"
|
||||
)
|
||||
|
||||
var requiredParameters = []string{
|
||||
"u", "v", "c",
|
||||
}
|
||||
|
||||
func checkCredentialsNewWay(password, token, salt string) bool {
|
||||
toHash := fmt.Sprintf("%s%s", password, salt)
|
||||
hash := md5.Sum([]byte(toHash))
|
||||
expToken := hex.EncodeToString(hash[:])
|
||||
return token == expToken
|
||||
}
|
||||
|
||||
func checkCredentialsOldWay(password, givenPassword string) bool {
|
||||
if givenPassword[:4] == "enc:" {
|
||||
bytes, _ := hex.DecodeString(givenPassword[4:])
|
||||
givenPassword = string(bytes)
|
||||
}
|
||||
return password == givenPassword
|
||||
}
|
||||
|
||||
func (c *Controller) LogConnection(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("connection from %s", r.RemoteAddr)
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) CheckParameters(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, req := range requiredParameters {
|
||||
param := r.URL.Query().Get(req)
|
||||
if param != "" {
|
||||
continue
|
||||
}
|
||||
respondError(w, r, 10, fmt.Sprintf("please provide a `%s` parameter", req))
|
||||
return
|
||||
}
|
||||
username := r.URL.Query().Get("u")
|
||||
password := r.URL.Query().Get("p")
|
||||
token := r.URL.Query().Get("t")
|
||||
salt := r.URL.Query().Get("s")
|
||||
passwordAuth := token == "" && salt == ""
|
||||
tokenAuth := password == ""
|
||||
if tokenAuth == passwordAuth {
|
||||
respondError(w, r, 10, "please provide parameters `t` and `s`, or just `p`")
|
||||
return
|
||||
}
|
||||
user := db.User{
|
||||
Username: username,
|
||||
}
|
||||
err := c.DB.Where(user).First(&user).Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
respondError(w, r, 40, "invalid username")
|
||||
return
|
||||
}
|
||||
var credsOk bool
|
||||
if tokenAuth {
|
||||
credsOk = checkCredentialsNewWay(user.Password, token, salt)
|
||||
} else {
|
||||
credsOk = checkCredentialsOldWay(user.Password, password)
|
||||
}
|
||||
if !credsOk {
|
||||
respondError(w, r, 40, "invalid password")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) EnableCORS(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods",
|
||||
"POST, GET, OPTIONS, PUT, DELETE",
|
||||
)
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization",
|
||||
)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
// Album represents the albums table
|
||||
type Album struct {
|
||||
Base
|
||||
Artist Artist
|
||||
ArtistID uint
|
||||
Title string `gorm:"not null;index"`
|
||||
Tracks []Track
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package model
|
||||
|
||||
// Artist represents the artists table
|
||||
type Artist struct {
|
||||
Base
|
||||
Albums []Album
|
||||
Name string `gorm:"not null;unique_index"`
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
// Cover represents the covers table
|
||||
type Cover struct {
|
||||
Base
|
||||
Album Album
|
||||
AlbumID uint
|
||||
Image []byte
|
||||
Path string `gorm:"not null;unique_index"`
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package model
|
||||
|
||||
// Track represents the tracks table
|
||||
type Track struct {
|
||||
Base
|
||||
Album Album
|
||||
AlbumID uint
|
||||
Artist Artist
|
||||
ArtistID uint
|
||||
Bitrate int
|
||||
Codec string
|
||||
DiscNumber int
|
||||
Duration int
|
||||
Title string
|
||||
TotalDiscs int
|
||||
TotalTracks int
|
||||
TrackNumber int
|
||||
Year int
|
||||
Path string `gorm:"not null;unique_index"`
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
)
|
||||
|
||||
// New creates a new Echo instance
|
||||
func New() *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.Pre(middleware.RemoveTrailingSlash())
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowHeaders: []string{
|
||||
echo.HeaderOrigin,
|
||||
echo.HeaderContentType,
|
||||
echo.HeaderAccept,
|
||||
echo.HeaderAuthorization,
|
||||
},
|
||||
AllowMethods: []string{
|
||||
echo.GET,
|
||||
echo.HEAD,
|
||||
echo.PUT,
|
||||
echo.PATCH,
|
||||
echo.POST,
|
||||
echo.DELETE,
|
||||
},
|
||||
}))
|
||||
return e
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import "encoding/xml"
|
||||
|
||||
type Album struct {
|
||||
XMLName xml.Name `xml:"album" json:"-"`
|
||||
Id uint64 `xml:"id,attr" json:"id"`
|
||||
ID uint `xml:"id,attr" json:"id"`
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
ArtistId uint64 `xml:"artistId,attr" json:"artistId"`
|
||||
ArtistID uint `xml:"artistId,attr" json:"artistId"`
|
||||
ArtistName string `xml:"artist,attr" json:"artist"`
|
||||
SongCount uint64 `xml:"songCount,attr" json:"songCount"`
|
||||
Duration uint64 `xml:"duration,attr" json:"duration"`
|
||||
SongCount uint `xml:"songCount,attr" json:"songCount"`
|
||||
Duration uint `xml:"duration,attr" json:"duration"`
|
||||
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
|
||||
Created string `xml:"created,attr" json:"created"`
|
||||
Songs *[]*Song `xml:"song" json:"song,omitempty"`
|
||||
@@ -22,39 +22,39 @@ type RandomSongs struct {
|
||||
|
||||
type Song struct {
|
||||
XMLName xml.Name `xml:"song" json:"-"`
|
||||
Id uint64 `xml:"id,attr" json:"id"`
|
||||
Parent uint64 `xml:"parent,attr" json:"parent"`
|
||||
ID uint `xml:"id,attr" json:"id"`
|
||||
Parent uint `xml:"parent,attr" json:"parent"`
|
||||
Title string `xml:"title,attr" json:"title"`
|
||||
Album string `xml:"album,attr" json:"album"`
|
||||
Artist string `xml:"artist,attr" json:"artist"`
|
||||
IsDir bool `xml:"isDir,attr" json:"isDir"`
|
||||
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
|
||||
Created string `xml:"created,attr" json:"created"`
|
||||
Duration uint64 `xml:"duration,attr" json:"duration"`
|
||||
Duration uint `xml:"duration,attr" json:"duration"`
|
||||
Genre string `xml:"genre,attr" json:"genre"`
|
||||
BitRate uint64 `xml:"bitRate,attr" json:"bitRate"`
|
||||
Size uint64 `xml:"size,attr" json:"size"`
|
||||
BitRate uint `xml:"bitRate,attr" json:"bitRate"`
|
||||
Size uint `xml:"size,attr" json:"size"`
|
||||
Suffix string `xml:"suffix,attr" json:"suffix"`
|
||||
ContentType string `xml:"contentType,attr" json:"contentType"`
|
||||
IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
|
||||
Path string `xml:"path,attr" json:"path"`
|
||||
AlbumId uint64 `xml:"albumId,attr" json:"albumId"`
|
||||
ArtistId uint64 `xml:"artistId,attr" json:"artistId"`
|
||||
TrackNo uint64 `xml:"track,attr" json:"track"`
|
||||
AlbumID uint `xml:"albumId,attr" json:"albumId"`
|
||||
ArtistID uint `xml:"artistId,attr" json:"artistId"`
|
||||
TrackNo uint `xml:"track,attr" json:"track"`
|
||||
Type string `xml:"type,attr" json:"type"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
XMLName xml.Name `xml:"artist" json:"-"`
|
||||
Id uint64 `xml:"id,attr" json:"id"`
|
||||
ID uint `xml:"id,attr" json:"id"`
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
|
||||
AlbumCount uint64 `xml:"albumCount,attr" json:"albumCount"`
|
||||
CoverArt string `xml:"coverArt,attr" json:"coverArt,omitempty"`
|
||||
AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"`
|
||||
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
|
||||
}
|
||||
|
||||
type Indexes struct {
|
||||
LastModified uint64 `xml:"lastModified,attr" json:"lastModified"`
|
||||
LastModified uint `xml:"lastModified,attr" json:"lastModified"`
|
||||
Index *[]*Index `xml:"index" json:"index"`
|
||||
}
|
||||
|
||||
@@ -66,29 +66,31 @@ type Index struct {
|
||||
|
||||
type Directory struct {
|
||||
XMLName xml.Name `xml:"directory" json:"-"`
|
||||
Id string `xml:"id,attr" json:"id"`
|
||||
Parent string `xml:"parent,attr" json:"parent"`
|
||||
ID uint `xml:"id,attr" json:"id"`
|
||||
Parent uint `xml:"parent,attr" json:"parent"`
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
|
||||
Children []Child `xml:"child" json:"child"`
|
||||
}
|
||||
|
||||
type Child struct {
|
||||
XMLName xml.Name `xml:child`
|
||||
Id string `xml:"id,attr"`
|
||||
Parent string `xml:"parent,attr"`
|
||||
Title string `xml:"title,attr"`
|
||||
IsDir bool `xml:"isDir,attr"`
|
||||
Album string `xml:"album,attr,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty"`
|
||||
Track uint64 `xml:"track,attr,omitempty"`
|
||||
Year uint64 `xml:"year,attr,omitempty"`
|
||||
Genre string `xml:"genre,attr,omitempty"`
|
||||
CoverArt uint64 `xml:"coverart,attr"`
|
||||
Size uint64 `xml:"size,attr,omitempty"`
|
||||
ContentType string `xml:"contentType,attr,omitempty"`
|
||||
Suffix string `xml:"suffix,attr,omitempty"`
|
||||
Duration uint64 `xml:"duration,attr,omitempty"`
|
||||
BitRate uint64 `xml:"bitRate,attr,omitempty"`
|
||||
Path string `xml:"path,attr,omitempty"`
|
||||
XMLName xml.Name `xml:"child" json:"-"`
|
||||
ID uint `xml:"id,attr" json:"id,omitempty"`
|
||||
Parent uint `xml:"parent,attr" json:"parent,omitempty"`
|
||||
Title string `xml:"title,attr" json:"title,omitempty"`
|
||||
IsDir bool `xml:"isDir,attr" json:"isDir,omitempty"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
AlbumID uint `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
ArtistID uint `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Track uint `xml:"track,attr,omitempty" json:"track,omitempty"`
|
||||
Year uint `xml:"year,attr,omitempty" json:"year,omitempty"`
|
||||
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
|
||||
CoverArt uint `xml:"coverart,attr" json:"coverArt,omitempty"`
|
||||
Size uint `xml:"size,attr,omitempty" json:"size,omitempty"`
|
||||
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
|
||||
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
|
||||
Duration uint `xml:"duration,attr,omitempty" json:"duration,omitempty"`
|
||||
BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
|
||||
Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
apiVersion = "1.10.0"
|
||||
apiVersion = "1.16.1"
|
||||
xmlns = "http://subsonic.org/restapi"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ type Response struct {
|
||||
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
||||
Status string `xml:"status,attr" json:"status"`
|
||||
Version string `xml:"version,attr" json:"version"`
|
||||
XMLNS string `xml:"xmlns,attr" json:"xmlns"`
|
||||
XMLNS string `xml:"xmlns,attr" json:"-"`
|
||||
Error *Error `xml:"error" json:"error,omitempty"`
|
||||
AlbumList2 *[]*Album `xml:"albumList2>album" json:"album,omitempty"`
|
||||
Album *Album `xml:"album" json:"album,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user