add some endpoints

This commit is contained in:
sentriz
2019-04-01 13:53:21 +01:00
parent b7c398f1eb
commit f5aa05abc3
18 changed files with 455 additions and 267 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
package model
// Artist represents the artists table
type Artist struct {
Base
Albums []Album
Name string `gorm:"not null;unique_index"`
}

View File

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

View File

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

View File

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

View File

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

View File

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