From f5aa05abc331b030ad933c65252f6fbe2109b014 Mon Sep 17 00:00:00 2001 From: sentriz Date: Mon, 1 Apr 2019 13:53:21 +0100 Subject: [PATCH] add some endpoints --- cmd/scanner/main.go | 37 ++++++----- cmd/server/main.go | 95 +++++++++++---------------- context/context.go | 24 ------- {model => db}/base.go | 10 ++- db/model.go | 52 +++++++++++++++ go.mod | 7 -- go.sum | 23 +------ handler/article.go | 17 ----- handler/handler.go | 57 ++++++++++++++-- handler/media.go | 149 ++++++++++++++++++++++++++++++++++++++++++ handler/middleware.go | 96 +++++++++++++++++++++++++++ model/album.go | 10 --- model/artist.go | 8 --- model/cover.go | 10 --- model/track.go | 20 ------ router/router.go | 31 --------- subsonic/media.go | 72 ++++++++++---------- subsonic/response.go | 4 +- 18 files changed, 455 insertions(+), 267 deletions(-) delete mode 100644 context/context.go rename {model => db}/base.go (91%) create mode 100644 db/model.go delete mode 100644 handler/article.go create mode 100644 handler/media.go create mode 100644 handler/middleware.go delete mode 100644 model/album.go delete mode 100644 model/artist.go delete mode 100644 model/cover.go delete mode 100644 model/track.go delete mode 100644 router/router.go diff --git a/cmd/scanner/main.go b/cmd/scanner/main.go index 7ee8f62..8dad51f 100644 --- a/cmd/scanner/main.go +++ b/cmd/scanner/main.go @@ -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{ diff --git a/cmd/server/main.go b/cmd/server/main.go index aed0cac..98aae34 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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")) } diff --git a/context/context.go b/context/context.go deleted file mode 100644 index f8f5d2d..0000000 --- a/context/context.go +++ /dev/null @@ -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) - } -} diff --git a/model/base.go b/db/base.go similarity index 91% rename from model/base.go rename to db/base.go index 442e52f..b2abec4 100644 --- a/model/base.go +++ b/db/base.go @@ -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 diff --git a/db/model.go b/db/model.go new file mode 100644 index 0000000..ce46d70 --- /dev/null +++ b/db/model.go @@ -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 +} diff --git a/go.mod b/go.mod index fb50a41..c27dac5 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index d0f1d98..471d4a0 100644 --- a/go.sum +++ b/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= diff --git a/handler/article.go b/handler/article.go deleted file mode 100644 index 30a113e..0000000 --- a/handler/article.go +++ /dev/null @@ -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) -} diff --git a/handler/handler.go b/handler/handler.go index 0f1a553..0ae7d06 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -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, + )) } diff --git a/handler/media.go b/handler/media.go new file mode 100644 index 0000000..cd56c2c --- /dev/null +++ b/handler/media.go @@ -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) {} diff --git a/handler/middleware.go b/handler/middleware.go new file mode 100644 index 0000000..53f75eb --- /dev/null +++ b/handler/middleware.go @@ -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) + } +} diff --git a/model/album.go b/model/album.go deleted file mode 100644 index 0ab8a4b..0000000 --- a/model/album.go +++ /dev/null @@ -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 -} diff --git a/model/artist.go b/model/artist.go deleted file mode 100644 index d012f14..0000000 --- a/model/artist.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -// Artist represents the artists table -type Artist struct { - Base - Albums []Album - Name string `gorm:"not null;unique_index"` -} diff --git a/model/cover.go b/model/cover.go deleted file mode 100644 index 988ef4a..0000000 --- a/model/cover.go +++ /dev/null @@ -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"` -} diff --git a/model/track.go b/model/track.go deleted file mode 100644 index fd2f03d..0000000 --- a/model/track.go +++ /dev/null @@ -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"` -} diff --git a/router/router.go b/router/router.go deleted file mode 100644 index b904306..0000000 --- a/router/router.go +++ /dev/null @@ -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 -} diff --git a/subsonic/media.go b/subsonic/media.go index ff5244f..ccc9a78 100644 --- a/subsonic/media.go +++ b/subsonic/media.go @@ -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"` } diff --git a/subsonic/response.go b/subsonic/response.go index 202f90d..f543b68 100644 --- a/subsonic/response.go +++ b/subsonic/response.go @@ -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"`