seperate routes, provide robust handler types, use mux
This commit is contained in:
@@ -9,11 +9,12 @@ if ! test -e "$embed_bin_path"; then
|
|||||||
cmd/gonicembed/main.go
|
cmd/gonicembed/main.go
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find server/assets/ \
|
find assets/ \
|
||||||
-type f \
|
-type f \
|
||||||
|
! -name '*.go' \
|
||||||
-exec "$embed_bin_path" \
|
-exec "$embed_bin_path" \
|
||||||
-out-path server/assets_bytes.go \
|
-out-path assets/assets_gen.go \
|
||||||
-package-name server \
|
-package-name assets \
|
||||||
-assets-var-name assetBytes \
|
-assets-var-name Bytes \
|
||||||
-asset-path-prefix server/assets/ \
|
-asset-path-prefix assets/ \
|
||||||
{} +
|
{} +
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
test_data_path=server/handler/test_data
|
test_data_path=server/ctrlsubsonic/testdata
|
||||||
test_listen_addr=localhost:9353
|
test_listen_addr=localhost:9353
|
||||||
test_music_path=~/music
|
test_music_path=~/music
|
||||||
test_db_path=$test_data_path/db
|
test_db_path=$test_data_path/db
|
||||||
|
|||||||
13
assets/assets.go
Normal file
13
assets/assets.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// PrefixDo runs a given callback for every path in our assets with
|
||||||
|
// the given prefix
|
||||||
|
func PrefixDo(pre string, cb func(path string, asset *EmbeddedAsset)) {
|
||||||
|
for path, asset := range Bytes {
|
||||||
|
if strings.HasPrefix(path, pre) {
|
||||||
|
cb(path, asset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10809
assets/assets_gen.go
Normal file
10809
assets/assets_gen.go
Normal file
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"senan.xyz/g/gonic/db"
|
"senan.xyz/g/gonic/db"
|
||||||
"senan.xyz/g/gonic/server"
|
"senan.xyz/g/gonic/server"
|
||||||
"senan.xyz/g/gonic/server/handler"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,7 +47,6 @@ func main() {
|
|||||||
log.Fatalf("error opening database: %v\n", err)
|
log.Fatalf("error opening database: %v\n", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
gob.Register(&handler.Flash{})
|
|
||||||
s := server.New(
|
s := server.New(
|
||||||
db,
|
db,
|
||||||
*musicPath,
|
*musicPath,
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -7,19 +7,21 @@ require (
|
|||||||
github.com/Masterminds/sprig v2.20.0+incompatible
|
github.com/Masterminds/sprig v2.20.0+incompatible
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/google/uuid v1.1.1 // indirect
|
github.com/google/uuid v1.1.1 // indirect
|
||||||
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/gorilla/sessions v1.1.3
|
github.com/gorilla/sessions v1.1.3
|
||||||
github.com/huandu/xstrings v1.2.0 // indirect
|
github.com/huandu/xstrings v1.2.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.7 // indirect
|
github.com/imdario/mergo v0.3.7 // indirect
|
||||||
github.com/jinzhu/gorm v1.9.9
|
github.com/jinzhu/gorm v1.9.9
|
||||||
github.com/josephburnett/jd v0.0.0-20190531151850-1f9071c800e7
|
github.com/josephburnett/jd v0.0.0-20190531151850-1f9071c800e7
|
||||||
|
github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da
|
||||||
github.com/karrick/godirwalk v1.10.12
|
github.com/karrick/godirwalk v1.10.12
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||||
github.com/peterbourgon/ff v1.2.0
|
github.com/peterbourgon/ff v1.2.0
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||||
github.com/stretchr/testify v1.3.0 // indirect
|
|
||||||
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755
|
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755
|
||||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 // indirect
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 // indirect
|
||||||
google.golang.org/appengine v1.6.1 // indirect
|
google.golang.org/appengine v1.6.1 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -62,6 +62,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
|||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||||
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||||
@@ -83,6 +85,8 @@ github.com/josephburnett/jd v0.0.0-20190531151850-1f9071c800e7 h1:/sW5BavO4uIUSq
|
|||||||
github.com/josephburnett/jd v0.0.0-20190531151850-1f9071c800e7/go.mod h1:aeV+6oc13ogwzcRNHBe4vbyLmoQxMfEDoqyqCU9oE30=
|
github.com/josephburnett/jd v0.0.0-20190531151850-1f9071c800e7/go.mod h1:aeV+6oc13ogwzcRNHBe4vbyLmoQxMfEDoqyqCU9oE30=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A=
|
||||||
|
github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8=
|
||||||
github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
|
github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
|
||||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||||
@@ -106,6 +110,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
|||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
github.com/peterbourgon/ff v1.2.0 h1:wGn2NwdHk8MTlRQpnXnO91UKegxt5DvlwR/bTK/L2hc=
|
github.com/peterbourgon/ff v1.2.0 h1:wGn2NwdHk8MTlRQpnXnO91UKegxt5DvlwR/bTK/L2hc=
|
||||||
github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40=
|
github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ var coverFilenames = map[string]struct{}{
|
|||||||
"front.jpeg": {},
|
"front.jpeg": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ## begin callbacks
|
||||||
|
// ## begin callbacks
|
||||||
// ## begin callbacks
|
// ## begin callbacks
|
||||||
|
|
||||||
func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
|
func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
|
||||||
@@ -237,6 +239,8 @@ func decoded(in string) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ## begin handlers
|
||||||
|
// ## begin handlers
|
||||||
// ## begin handlers
|
// ## begin handlers
|
||||||
|
|
||||||
func (s *Scanner) handleFolder(it *item) error {
|
func (s *Scanner) handleFolder(it *item) error {
|
||||||
|
|||||||
233
server/ctrladmin/ctrl.go
Normal file
233
server/ctrladmin/ctrl.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package ctrladmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/sprig"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/oxtoacart/bpool"
|
||||||
|
"github.com/wader/gormstore"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/assets"
|
||||||
|
"senan.xyz/g/gonic/model"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlbase"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(&Flash{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extendFromPaths /extends/ the given template for every asset
|
||||||
|
// with given prefix
|
||||||
|
func extendFromPaths(b *template.Template, p string) *template.Template {
|
||||||
|
assets.PrefixDo(p, func(_ string, asset *assets.EmbeddedAsset) {
|
||||||
|
tmplStr := string(asset.Bytes)
|
||||||
|
b = template.Must(b.Parse(tmplStr))
|
||||||
|
})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// extendFromPaths /clones/ the given template for every asset
|
||||||
|
// with given prefix, extends it, and insert it into a new map
|
||||||
|
func pagesFromPaths(b *template.Template, p string) map[string]*template.Template {
|
||||||
|
ret := map[string]*template.Template{}
|
||||||
|
assets.PrefixDo(p, func(path string, asset *assets.EmbeddedAsset) {
|
||||||
|
tmplKey := filepath.Base(path)
|
||||||
|
clone := template.Must(b.Clone())
|
||||||
|
tmplStr := string(asset.Bytes)
|
||||||
|
ret[tmplKey] = template.Must(clone.Parse(tmplStr))
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
prefixPartials = "partials"
|
||||||
|
prefixLayouts = "layouts"
|
||||||
|
prefixPages = "pages"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
*ctrlbase.Controller
|
||||||
|
buffPool *bpool.BufferPool
|
||||||
|
templates map[string]*template.Template
|
||||||
|
sessDB *gormstore.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(base *ctrlbase.Controller) *Controller {
|
||||||
|
sessionKey := []byte(base.DB.GetSetting("session_key"))
|
||||||
|
if len(sessionKey) == 0 {
|
||||||
|
sessionKey = securecookie.GenerateRandomKey(32)
|
||||||
|
base.DB.SetSetting("session_key", string(sessionKey))
|
||||||
|
}
|
||||||
|
tmplBase := template.
|
||||||
|
New("layout").
|
||||||
|
Funcs(sprig.FuncMap()).
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"humanDate": humanize.Time,
|
||||||
|
})
|
||||||
|
tmplBase = extendFromPaths(tmplBase, prefixPartials)
|
||||||
|
tmplBase = extendFromPaths(tmplBase, prefixLayouts)
|
||||||
|
return &Controller{
|
||||||
|
Controller: base,
|
||||||
|
buffPool: bpool.NewBufferPool(64),
|
||||||
|
templates: pagesFromPaths(tmplBase, prefixPages),
|
||||||
|
sessDB: gormstore.New(base.DB.DB, sessionKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type templateData struct {
|
||||||
|
// common
|
||||||
|
Flashes []interface{}
|
||||||
|
User *model.User
|
||||||
|
// home
|
||||||
|
AlbumCount int
|
||||||
|
ArtistCount int
|
||||||
|
TrackCount int
|
||||||
|
RequestRoot string
|
||||||
|
RecentFolders []*model.Album
|
||||||
|
AllUsers []*model.User
|
||||||
|
LastScanTime time.Time
|
||||||
|
IsScanning bool
|
||||||
|
//
|
||||||
|
CurrentLastFMAPIKey string
|
||||||
|
CurrentLastFMAPISecret string
|
||||||
|
SelectedUser *model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminHandler func(w http.ResponseWriter, r *http.Request) *response
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
// code is 200
|
||||||
|
template string
|
||||||
|
data *templateData
|
||||||
|
// code is 303
|
||||||
|
redirect string
|
||||||
|
// code is >= 400
|
||||||
|
code int
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) H(h adminHandler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := h(w, r)
|
||||||
|
if resp.redirect != "" {
|
||||||
|
http.Redirect(w, r, resp.redirect, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.err != "" {
|
||||||
|
http.Error(w, resp.err, resp.code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.template == "" {
|
||||||
|
http.Error(w, "useless handler return", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.data == nil {
|
||||||
|
resp.data = &templateData{}
|
||||||
|
}
|
||||||
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
|
resp.data.Flashes = session.Flashes()
|
||||||
|
if err := session.Save(r, w); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("saving session: %v", err), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.data.User, _ = r.Context().Value(key.User).(*model.User)
|
||||||
|
buff := c.buffPool.Get()
|
||||||
|
defer c.buffPool.Put(buff)
|
||||||
|
tmpl, ok := c.templates[resp.template]
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, fmt.Sprintf("finding template %q", resp.template), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(buff, resp.data); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("executing template: %v", err), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
buff.WriteTo(w)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ## begin utilities
|
||||||
|
// ## begin utilities
|
||||||
|
// ## begin utilities
|
||||||
|
|
||||||
|
func firstExisting(or string, strings ...string) string {
|
||||||
|
for _, s := range strings {
|
||||||
|
if s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return or
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessLogSave(w http.ResponseWriter, r *http.Request, s *sessions.Session) {
|
||||||
|
if err := s.Save(r, w); err != nil {
|
||||||
|
log.Printf("error saving session: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Flash struct {
|
||||||
|
Message string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessAddFlashW(message string, s *sessions.Session) {
|
||||||
|
s.AddFlash(Flash{
|
||||||
|
Message: message,
|
||||||
|
Type: "warning",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessAddFlashWf(message string, s *sessions.Session, a ...interface{}) {
|
||||||
|
sessAddFlashW(fmt.Sprintf(message, a...), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessAddFlashN(message string, s *sessions.Session) {
|
||||||
|
s.AddFlash(Flash{
|
||||||
|
Message: message,
|
||||||
|
Type: "normal",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessAddFlashNf(message string, s *sessions.Session, a ...interface{}) {
|
||||||
|
sessAddFlashN(fmt.Sprintf(message, a...), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ## begin validation
|
||||||
|
// ## begin validation
|
||||||
|
// ## begin validation
|
||||||
|
|
||||||
|
func validateUsername(username string) error {
|
||||||
|
if username == "" {
|
||||||
|
return fmt.Errorf("please enter the username")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePasswords(pOne, pTwo string) error {
|
||||||
|
if pOne == "" || pTwo == "" {
|
||||||
|
return fmt.Errorf("please enter the password twice")
|
||||||
|
}
|
||||||
|
if !(pOne == pTwo) {
|
||||||
|
return fmt.Errorf("the two passwords entered were not the same")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAPIKey(apiKey, secret string) error {
|
||||||
|
if apiKey == "" || secret == "" {
|
||||||
|
return fmt.Errorf("please enter both the api key and secret")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrladmin
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrladmin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,50 +8,48 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
"senan.xyz/g/gonic/scanner"
|
"senan.xyz/g/gonic/scanner"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
"senan.xyz/g/gonic/server/lastfm"
|
"senan.xyz/g/gonic/server/lastfm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) ServeLogin(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeLogin(w http.ResponseWriter, r *http.Request) *response {
|
||||||
renderTemplate(w, r, c.Templates["login.tmpl"], nil)
|
return &response{template: "login.tmpl"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeLoginDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeLoginDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
sessAddFlashW("please provide both a username and password", session)
|
sessAddFlashW("please provide both a username and password", session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
user := c.DB.GetUserFromName(username)
|
user := c.DB.GetUserFromName(username)
|
||||||
if user == nil || password != user.Password {
|
if user == nil || password != user.Password {
|
||||||
sessAddFlashW("invalid username / password", session)
|
sessAddFlashW("invalid username / password", session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// put the user name into the session. future endpoints after this one
|
// put the user name into the session. future endpoints after this one
|
||||||
// are wrapped with WithUserSession() which will get the name from the
|
// are wrapped with WithUserSession() which will get the name from the
|
||||||
// session and put the row into the request context
|
// session and put the row into the request context
|
||||||
session.Values["user"] = user.Name
|
session.Values["user"] = user.Name
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
session.Options.MaxAge = -1
|
session.Options.MaxAge = -1
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
return &response{redirect: "/admin/login"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) *response {
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
//
|
//
|
||||||
// stats box
|
// stats box
|
||||||
@@ -89,79 +87,89 @@ func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
|
|||||||
data.LastScanTime = time.Unix(i, 0)
|
data.LastScanTime = time.Unix(i, 0)
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
renderTemplate(w, r, c.Templates["home.tmpl"], data)
|
return &response{
|
||||||
|
template: "home.tmpl",
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) *response {
|
||||||
renderTemplate(w, r, c.Templates["change_own_password.tmpl"], nil)
|
return &response{template: "change_own_password.tmpl"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeChangeOwnPasswordDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeChangeOwnPasswordDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
passwordOne := r.FormValue("password_one")
|
passwordOne := r.FormValue("password_one")
|
||||||
passwordTwo := r.FormValue("password_two")
|
passwordTwo := r.FormValue("password_two")
|
||||||
err := validatePasswords(passwordOne, passwordTwo)
|
err := validatePasswords(passwordOne, passwordTwo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
user.Password = passwordOne
|
user.Password = passwordOne
|
||||||
c.DB.Save(user)
|
c.DB.Save(user)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeLinkLastFMDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeLinkLastFMDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
token := r.URL.Query().Get("token")
|
token := r.URL.Query().Get("token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
http.Error(w, "please provide a token", 400)
|
return &response{
|
||||||
return
|
err: "please provide a token",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessionKey, err := lastfm.GetSession(
|
sessionKey, err := lastfm.GetSession(
|
||||||
c.DB.GetSetting("lastfm_api_key"),
|
c.DB.GetSetting("lastfm_api_key"),
|
||||||
c.DB.GetSetting("lastfm_secret"),
|
c.DB.GetSetting("lastfm_secret"),
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
user.LastFMSession = sessionKey
|
user.LastFMSession = sessionKey
|
||||||
c.DB.Save(&user)
|
c.DB.Save(&user)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeUnlinkLastFMDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeUnlinkLastFMDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
user.LastFMSession = ""
|
user.LastFMSession = ""
|
||||||
c.DB.Save(&user)
|
c.DB.Save(&user)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request) *response {
|
||||||
username := r.URL.Query().Get("user")
|
username := r.URL.Query().Get("user")
|
||||||
if username == "" {
|
if username == "" {
|
||||||
http.Error(w, "please provide a username", 400)
|
return &response{
|
||||||
return
|
err: "please provide a username",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user := c.DB.GetUserFromName(username)
|
user := c.DB.GetUserFromName(username)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
http.Error(w, "couldn't find a user with that name", 400)
|
return &response{
|
||||||
return
|
err: "couldn't find a user with that name",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
data.SelectedUser = user
|
data.SelectedUser = user
|
||||||
renderTemplate(w, r, c.Templates["change_password.tmpl"], data)
|
return &response{
|
||||||
|
template: "change_own_password.tmpl",
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
username := r.URL.Query().Get("user")
|
username := r.URL.Query().Get("user")
|
||||||
passwordOne := r.FormValue("password_one")
|
passwordOne := r.FormValue("password_one")
|
||||||
passwordTwo := r.FormValue("password_two")
|
passwordTwo := r.FormValue("password_two")
|
||||||
@@ -169,51 +177,56 @@ func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
user := c.DB.GetUserFromName(username)
|
user := c.DB.GetUserFromName(username)
|
||||||
user.Password = passwordOne
|
user.Password = passwordOne
|
||||||
c.DB.Save(user)
|
c.DB.Save(user)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) *response {
|
||||||
username := r.URL.Query().Get("user")
|
username := r.URL.Query().Get("user")
|
||||||
if username == "" {
|
if username == "" {
|
||||||
http.Error(w, "please provide a username", 400)
|
return &response{
|
||||||
return
|
err: "please provide a username",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user := c.DB.GetUserFromName(username)
|
user := c.DB.GetUserFromName(username)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
http.Error(w, "couldn't find a user with that name", 400)
|
return &response{
|
||||||
return
|
err: "couldn't find a user with that name",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
data.SelectedUser = user
|
data.SelectedUser = user
|
||||||
renderTemplate(w, r, c.Templates["delete_user.tmpl"], data)
|
return &response{
|
||||||
|
template: "delete_user.tmpl",
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
username := r.URL.Query().Get("user")
|
username := r.URL.Query().Get("user")
|
||||||
user := c.DB.GetUserFromName(username)
|
user := c.DB.GetUserFromName(username)
|
||||||
c.DB.Delete(user)
|
c.DB.Delete(user)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeCreateUser(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeCreateUser(w http.ResponseWriter, r *http.Request) *response {
|
||||||
renderTemplate(w, r, c.Templates["create_user.tmpl"], nil)
|
return &response{template: "create_user.tmpl"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
err := validateUsername(username)
|
err := validateUsername(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
passwordOne := r.FormValue("password_one")
|
passwordOne := r.FormValue("password_one")
|
||||||
passwordTwo := r.FormValue("password_two")
|
passwordTwo := r.FormValue("password_two")
|
||||||
@@ -221,8 +234,7 @@ func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
user := model.User{
|
user := model.User{
|
||||||
Name: username,
|
Name: username,
|
||||||
@@ -232,46 +244,49 @@ func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashWf("could not create user `%s`: %v", session, username, err)
|
sessAddFlashWf("could not create user `%s`: %v", session, username, err)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) *response {
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key")
|
data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key")
|
||||||
data.CurrentLastFMAPISecret = c.DB.GetSetting("lastfm_secret")
|
data.CurrentLastFMAPISecret = c.DB.GetSetting("lastfm_secret")
|
||||||
renderTemplate(w, r, c.Templates["update_lastfm_api_key.tmpl"], data)
|
return &response{
|
||||||
|
template: "create_user.tmpl",
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
apiKey := r.FormValue("api_key")
|
apiKey := r.FormValue("api_key")
|
||||||
secret := r.FormValue("secret")
|
secret := r.FormValue("secret")
|
||||||
err := validateAPIKey(apiKey, secret)
|
err := validateAPIKey(apiKey, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sessAddFlashW(err.Error(), session)
|
sessAddFlashW(err.Error(), session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.DB.SetSetting("lastfm_api_key", apiKey)
|
c.DB.SetSetting("lastfm_api_key", apiKey)
|
||||||
c.DB.SetSetting("lastfm_secret", secret)
|
c.DB.SetSetting("lastfm_secret", secret)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: r.Referer()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeStartScanDo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeStartScanDo(w http.ResponseWriter, r *http.Request) *response {
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
defer func() {
|
||||||
|
go func() {
|
||||||
|
err := scanner.
|
||||||
|
New(c.DB, c.MusicPath).
|
||||||
|
Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while scanning: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
sessAddFlashN("scan started. refresh for results", session)
|
sessAddFlashN("scan started. refresh for results", session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
return &response{redirect: "/admin/home"}
|
||||||
go func() {
|
|
||||||
err := scanner.
|
|
||||||
New(c.DB, c.MusicPath).
|
|
||||||
Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while scanning: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrladmin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,22 +7,21 @@ import (
|
|||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:interfacer
|
func (c *Controller) WithSession(next http.Handler) http.Handler {
|
||||||
func (c *Controller) WithSession(next http.HandlerFunc) http.HandlerFunc {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
session, _ := c.sessDB.Get(r, "gonic")
|
||||||
session, _ := c.SessDB.Get(r, "gonic")
|
withSession := context.WithValue(r.Context(), key.Session, session)
|
||||||
withSession := context.WithValue(r.Context(), contextSessionKey, session)
|
|
||||||
next.ServeHTTP(w, r.WithContext(withSession))
|
next.ServeHTTP(w, r.WithContext(withSession))
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:interfacer
|
func (c *Controller) WithUserSession(next http.Handler) http.Handler {
|
||||||
func (c *Controller) WithUserSession(next http.HandlerFunc) http.HandlerFunc {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// session exists at this point
|
// session exists at this point
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
username, ok := session.Values["user"].(string)
|
username, ok := session.Values["user"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
sessAddFlashW("you are not authenticated", session)
|
sessAddFlashW("you are not authenticated", session)
|
||||||
@@ -40,17 +39,16 @@ func (c *Controller) WithUserSession(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
withUser := context.WithValue(r.Context(), contextUserKey, user)
|
withUser := context.WithValue(r.Context(), key.User, user)
|
||||||
next.ServeHTTP(w, r.WithContext(withUser))
|
next.ServeHTTP(w, r.WithContext(withUser))
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:interfacer
|
func (c *Controller) WithAdminSession(next http.Handler) http.Handler {
|
||||||
func (c *Controller) WithAdminSession(next http.HandlerFunc) http.HandlerFunc {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// session and user exist at this point
|
// session and user exist at this point
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
session := r.Context().Value(key.Session).(*sessions.Session)
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
if !user.IsAdmin {
|
if !user.IsAdmin {
|
||||||
sessAddFlashW("you are not an admin", session)
|
sessAddFlashW("you are not an admin", session)
|
||||||
sessLogSave(w, r, session)
|
sessLogSave(w, r, session)
|
||||||
@@ -58,5 +56,5 @@ func (c *Controller) WithAdminSession(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
36
server/ctrlbase/ctrl.go
Normal file
36
server/ctrlbase/ctrl.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package ctrlbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
DB *db.DB
|
||||||
|
MusicPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) WithLogging(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("connection from `%s` for `%s`", r.RemoteAddr, r.URL)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) WithCORS(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(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)
|
||||||
|
})
|
||||||
|
}
|
||||||
98
server/ctrlsubsonic/ctrl.go
Normal file
98
server/ctrlsubsonic/ctrl.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package ctrlsubsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/server/ctrlbase"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
*ctrlbase.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(base *ctrlbase.Controller) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
Controller: base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type metaResponse struct {
|
||||||
|
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
||||||
|
*spec.Response `json:"subsonic-response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type errWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ew *errWriter) write(buf []byte) {
|
||||||
|
if ew.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ew.err = ew.w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResp(w http.ResponseWriter, r *http.Request, resp *spec.Response) {
|
||||||
|
res := metaResponse{Response: resp}
|
||||||
|
ew := &errWriter{w: w}
|
||||||
|
switch parsing.GetStrParam(r, "f") {
|
||||||
|
case "json":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
data, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not marshall to json: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ew.write(data)
|
||||||
|
case "jsonp":
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
data, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not marshall to json: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ew.write([]byte(parsing.GetStrParamOr(r, "callback", "cb")))
|
||||||
|
ew.write([]byte("("))
|
||||||
|
ew.write(data)
|
||||||
|
ew.write([]byte(");"))
|
||||||
|
default:
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
data, err := xml.MarshalIndent(res, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not marshall to xml: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ew.write(data)
|
||||||
|
}
|
||||||
|
if ew.err != nil {
|
||||||
|
log.Printf("error writing to response: %v\n", ew.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type subsonicHandler func(r *http.Request) *spec.Response
|
||||||
|
|
||||||
|
func (c *Controller) H(h subsonicHandler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: write a non 200 if has err
|
||||||
|
response := h(r)
|
||||||
|
writeResp(w, r, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type subsonicHandlerRaw func(w http.ResponseWriter, r *http.Request) *spec.Response
|
||||||
|
|
||||||
|
func (c *Controller) HR(h subsonicHandlerRaw) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: write a non 200 if has err
|
||||||
|
// TODO: ensure no mixed return/writer
|
||||||
|
response := h(w, r)
|
||||||
|
writeResp(w, r, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
@@ -13,10 +13,11 @@ import (
|
|||||||
jd "github.com/josephburnett/jd/lib"
|
jd "github.com/josephburnett/jd/lib"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/db"
|
"senan.xyz/g/gonic/db"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlbase"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testDataDir = "test_data"
|
testDataDir = "testdata"
|
||||||
testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
testDBPath = path.Join(testDataDir, "db")
|
testDBPath = path.Join(testDataDir, "db")
|
||||||
testController *Controller
|
testController *Controller
|
||||||
@@ -27,9 +28,7 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error opening database: %v\n", err)
|
log.Fatalf("error opening database: %v\n", err)
|
||||||
}
|
}
|
||||||
testController = &Controller{
|
testController = New(&ctrlbase.Controller{DB: db})
|
||||||
DB: db,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryCase struct {
|
type queryCase struct {
|
||||||
@@ -38,7 +37,7 @@ type queryCase struct {
|
|||||||
listSet bool
|
listSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runQueryCases(t *testing.T, handler http.HandlerFunc, cases []*queryCase) {
|
func runQueryCases(t *testing.T, h subsonicHandler, cases []*queryCase) {
|
||||||
for _, qc := range cases {
|
for _, qc := range cases {
|
||||||
qc := qc // pin
|
qc := qc // pin
|
||||||
t.Run(qc.expectPath, func(t *testing.T) {
|
t.Run(qc.expectPath, func(t *testing.T) {
|
||||||
@@ -50,7 +49,7 @@ func runQueryCases(t *testing.T, handler http.HandlerFunc, cases []*queryCase) {
|
|||||||
// request from the handler in question
|
// request from the handler in question
|
||||||
req, _ := http.NewRequest("", "?"+qc.params.Encode(), nil)
|
req, _ := http.NewRequest("", "?"+qc.params.Encode(), nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
testController.H(h).ServeHTTP(rr, req)
|
||||||
body := rr.Body.String()
|
body := rr.Body.String()
|
||||||
if status := rr.Code; status != http.StatusOK {
|
if status := rr.Code; status != http.StatusOK {
|
||||||
t.Fatalf("didn't give a 200\n%s", body)
|
t.Fatalf("didn't give a 200\n%s", body)
|
||||||
@@ -81,8 +80,7 @@ func runQueryCases(t *testing.T, handler http.HandlerFunc, cases []*queryCase) {
|
|||||||
if len(diff) == 0 {
|
if len(diff) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Errorf("\u001b[31;1mdiffering json\u001b[0m\n%s",
|
t.Errorf("\u001b[31;1mdiffering json\u001b[0m\n%s", diff.Render())
|
||||||
diff.Render())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the subsonic spec metions "artist" a lot when talking about the
|
// the subsonic spec metions "artist" a lot when talking about the
|
||||||
@@ -18,7 +20,7 @@ import (
|
|||||||
// an track to be the it's respective folder that comes directly
|
// an track to be the it's respective folder that comes directly
|
||||||
// under the root directory
|
// under the root directory
|
||||||
|
|
||||||
func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
|
||||||
var folders []*model.Album
|
var folders []*model.Album
|
||||||
c.DB.
|
c.DB.
|
||||||
Select("*, count(sub.id) as child_count").
|
Select("*, count(sub.id) as child_count").
|
||||||
@@ -30,40 +32,39 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
|
|||||||
Group("albums.id").
|
Group("albums.id").
|
||||||
Find(&folders)
|
Find(&folders)
|
||||||
// [a-z#] -> 27
|
// [a-z#] -> 27
|
||||||
indexMap := make(map[string]*subsonic.Index, 27)
|
indexMap := make(map[string]*spec.Index, 27)
|
||||||
resp := make([]*subsonic.Index, 0, 27)
|
resp := make([]*spec.Index, 0, 27)
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
i := lowerUDecOrHash(folder.IndexRightPath())
|
i := lowerUDecOrHash(folder.IndexRightPath())
|
||||||
index, ok := indexMap[i]
|
index, ok := indexMap[i]
|
||||||
if !ok {
|
if !ok {
|
||||||
index = &subsonic.Index{
|
index = &spec.Index{
|
||||||
Name: i,
|
Name: i,
|
||||||
Artists: []*subsonic.Artist{},
|
Artists: []*spec.Artist{},
|
||||||
}
|
}
|
||||||
indexMap[i] = index
|
indexMap[i] = index
|
||||||
resp = append(resp, index)
|
resp = append(resp, index)
|
||||||
}
|
}
|
||||||
index.Artists = append(index.Artists,
|
index.Artists = append(index.Artists,
|
||||||
newArtistByFolder(folder))
|
spec.NewArtistByFolder(folder))
|
||||||
}
|
}
|
||||||
sort.Slice(resp, func(i, j int) bool {
|
sort.Slice(resp, func(i, j int) bool {
|
||||||
return resp[i].Name < resp[j].Name
|
return resp[i].Name < resp[j].Name
|
||||||
})
|
})
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Indexes = &subsonic.Indexes{
|
sub.Indexes = &spec.Indexes{
|
||||||
LastModified: 0,
|
LastModified: 0,
|
||||||
Index: resp,
|
Index: resp,
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
||||||
id, err := getIntParam(r, "id")
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
childrenObj := []*subsonic.TrackChild{}
|
childrenObj := []*spec.TrackChild{}
|
||||||
folder := &model.Album{}
|
folder := &model.Album{}
|
||||||
c.DB.First(folder, id)
|
c.DB.First(folder, id)
|
||||||
//
|
//
|
||||||
@@ -73,7 +74,7 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
|||||||
Where("parent_id = ?", id).
|
Where("parent_id = ?", id).
|
||||||
Find(&childFolders)
|
Find(&childFolders)
|
||||||
for _, c := range childFolders {
|
for _, c := range childFolders {
|
||||||
childrenObj = append(childrenObj, newTCAlbumByFolder(c))
|
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(c))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// start looking for child childTracks in the current dir
|
// start looking for child childTracks in the current dir
|
||||||
@@ -84,8 +85,8 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
|||||||
Order("filename").
|
Order("filename").
|
||||||
Find(&childTracks)
|
Find(&childTracks)
|
||||||
for _, c := range childTracks {
|
for _, c := range childTracks {
|
||||||
toAppend := newTCTrackByFolder(c, folder)
|
toAppend := spec.NewTCTrackByFolder(c, folder)
|
||||||
if getStrParam(r, "c") == "Jamstash" {
|
if parsing.GetStrParam(r, "c") == "Jamstash" {
|
||||||
// jamstash thinks it can't play flacs
|
// jamstash thinks it can't play flacs
|
||||||
toAppend.ContentType = "audio/mpeg"
|
toAppend.ContentType = "audio/mpeg"
|
||||||
toAppend.Suffix = "mp3"
|
toAppend.Suffix = "mp3"
|
||||||
@@ -94,18 +95,17 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
// respond section
|
// respond section
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Directory = newDirectoryByFolder(folder, childrenObj)
|
sub.Directory = spec.NewDirectoryByFolder(folder, childrenObj)
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
// changes to this function should be reflected in in _by_tags.go's
|
// changes to this function should be reflected in in _by_tags.go's
|
||||||
// getAlbumListTwo() function
|
// getAlbumListTwo() function
|
||||||
func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
|
||||||
listType := getStrParam(r, "type")
|
listType := parsing.GetStrParam(r, "type")
|
||||||
if listType == "" {
|
if listType == "" {
|
||||||
respondError(w, r, 10, "please provide a `type` parameter")
|
return spec.NewError(10, "please provide a `type` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
q := c.DB.DB
|
q := c.DB.DB
|
||||||
switch listType {
|
switch listType {
|
||||||
@@ -117,7 +117,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "alphabeticalByName":
|
case "alphabeticalByName":
|
||||||
q = q.Order("right_path")
|
q = q.Order("right_path")
|
||||||
case "frequent":
|
case "frequent":
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
q = q.Joins(`
|
q = q.Joins(`
|
||||||
JOIN plays
|
JOIN plays
|
||||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||||
@@ -128,43 +128,40 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "random":
|
case "random":
|
||||||
q = q.Order(gorm.Expr("random()"))
|
q = q.Order(gorm.Expr("random()"))
|
||||||
case "recent":
|
case "recent":
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
q = q.Joins(`
|
q = q.Joins(`
|
||||||
JOIN plays
|
JOIN plays
|
||||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||||
user.ID)
|
user.ID)
|
||||||
q = q.Order("plays.time DESC")
|
q = q.Order("plays.time DESC")
|
||||||
default:
|
default:
|
||||||
respondError(w, r, 10,
|
return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType)
|
||||||
"unknown value `%s` for parameter 'type'", listType)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var folders []*model.Album
|
var folders []*model.Album
|
||||||
q.
|
q.
|
||||||
Where("albums.tag_artist_id IS NOT NULL").
|
Where("albums.tag_artist_id IS NOT NULL").
|
||||||
Offset(getIntParamOr(r, "offset", 0)).
|
Offset(parsing.GetIntParamOr(r, "offset", 0)).
|
||||||
Limit(getIntParamOr(r, "size", 10)).
|
Limit(parsing.GetIntParamOr(r, "size", 10)).
|
||||||
Preload("Parent").
|
Preload("Parent").
|
||||||
Find(&folders)
|
Find(&folders)
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Albums = &subsonic.Albums{
|
sub.Albums = &spec.Albums{
|
||||||
List: make([]*subsonic.Album, len(folders)),
|
List: make([]*spec.Album, len(folders)),
|
||||||
}
|
}
|
||||||
for i, folder := range folders {
|
for i, folder := range folders {
|
||||||
sub.Albums.List[i] = newAlbumByFolder(folder)
|
sub.Albums.List[i] = spec.NewAlbumByFolder(folder)
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
||||||
query := getStrParam(r, "query")
|
query := parsing.GetStrParam(r, "query")
|
||||||
if query == "" {
|
if query == "" {
|
||||||
respondError(w, r, 10, "please provide a `query` parameter")
|
return spec.NewError(10, "please provide a `query` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
query = fmt.Sprintf("%%%s%%",
|
query = fmt.Sprintf("%%%s%%",
|
||||||
strings.TrimSuffix(query, "*"))
|
strings.TrimSuffix(query, "*"))
|
||||||
results := &subsonic.SearchResultTwo{}
|
results := &spec.SearchResultTwo{}
|
||||||
//
|
//
|
||||||
// search "artists"
|
// search "artists"
|
||||||
var artists []*model.Album
|
var artists []*model.Album
|
||||||
@@ -174,12 +171,12 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) {
|
|||||||
AND (right_path LIKE ? OR
|
AND (right_path LIKE ? OR
|
||||||
right_path_u_dec LIKE ?)
|
right_path_u_dec LIKE ?)
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "artistOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "artistCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
|
||||||
Find(&artists)
|
Find(&artists)
|
||||||
for _, a := range artists {
|
for _, a := range artists {
|
||||||
results.Artists = append(results.Artists,
|
results.Artists = append(results.Artists,
|
||||||
newDirectoryByFolder(a, nil))
|
spec.NewDirectoryByFolder(a, nil))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// search "albums"
|
// search "albums"
|
||||||
@@ -190,11 +187,11 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) {
|
|||||||
AND (right_path LIKE ? OR
|
AND (right_path LIKE ? OR
|
||||||
right_path_u_dec LIKE ?)
|
right_path_u_dec LIKE ?)
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "albumOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "albumCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
|
||||||
Find(&albums)
|
Find(&albums)
|
||||||
for _, a := range albums {
|
for _, a := range albums {
|
||||||
results.Albums = append(results.Albums, newTCAlbumByFolder(a))
|
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// search tracks
|
// search tracks
|
||||||
@@ -205,15 +202,15 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) {
|
|||||||
filename LIKE ? OR
|
filename LIKE ? OR
|
||||||
filename_u_dec LIKE ?
|
filename_u_dec LIKE ?
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "songOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "songCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
|
||||||
Find(&tracks)
|
Find(&tracks)
|
||||||
for _, t := range tracks {
|
for _, t := range tracks {
|
||||||
results.Tracks = append(results.Tracks,
|
results.Tracks = append(results.Tracks,
|
||||||
newTCTrackByFolder(t, t.Album))
|
spec.NewTCTrackByFolder(t, t.Album))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.SearchResultTwo = results
|
sub.SearchResultTwo = results
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -8,20 +8,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetIndexes(t *testing.T) {
|
func TestGetIndexes(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetIndexes, []*queryCase{
|
runQueryCases(t, testController.ServeGetIndexes, []*queryCase{
|
||||||
{url.Values{}, "no_args", false},
|
{url.Values{}, "no_args", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMusicDirectory(t *testing.T) {
|
func TestGetMusicDirectory(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetMusicDirectory, []*queryCase{
|
runQueryCases(t, testController.ServeGetMusicDirectory, []*queryCase{
|
||||||
{url.Values{"id": []string{"2"}}, "without_tracks", false},
|
{url.Values{"id": []string{"2"}}, "without_tracks", false},
|
||||||
{url.Values{"id": []string{"3"}}, "with_tracks", false},
|
{url.Values{"id": []string{"3"}}, "with_tracks", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbumList(t *testing.T) {
|
func TestGetAlbumList(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetAlbumList, []*queryCase{
|
runQueryCases(t, testController.ServeGetAlbumList, []*queryCase{
|
||||||
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||||
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
||||||
{url.Values{"type": []string{"newest"}}, "newest", false},
|
{url.Values{"type": []string{"newest"}}, "newest", false},
|
||||||
@@ -30,7 +30,7 @@ func TestGetAlbumList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchTwo(t *testing.T) {
|
func TestSearchTwo(t *testing.T) {
|
||||||
runQueryCases(t, testController.SearchTwo, []*queryCase{
|
runQueryCases(t, testController.ServeSearchTwo, []*queryCase{
|
||||||
{url.Values{"query": []string{"13"}}, "q_13", false},
|
{url.Values{"query": []string{"13"}}, "q_13", false},
|
||||||
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
||||||
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
||||||
var artists []*model.Artist
|
var artists []*model.Artist
|
||||||
c.DB.
|
c.DB.
|
||||||
Select("*, count(sub.id) as album_count").
|
Select("*, count(sub.id) as album_count").
|
||||||
@@ -23,56 +25,54 @@ func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
|
|||||||
Group("artists.id").
|
Group("artists.id").
|
||||||
Find(&artists)
|
Find(&artists)
|
||||||
// [a-z#] -> 27
|
// [a-z#] -> 27
|
||||||
indexMap := make(map[string]*subsonic.Index, 27)
|
indexMap := make(map[string]*spec.Index, 27)
|
||||||
resp := make([]*subsonic.Index, 0, 27)
|
resp := make([]*spec.Index, 0, 27)
|
||||||
for _, artist := range artists {
|
for _, artist := range artists {
|
||||||
i := lowerUDecOrHash(artist.IndexName())
|
i := lowerUDecOrHash(artist.IndexName())
|
||||||
index, ok := indexMap[i]
|
index, ok := indexMap[i]
|
||||||
if !ok {
|
if !ok {
|
||||||
index = &subsonic.Index{
|
index = &spec.Index{
|
||||||
Name: i,
|
Name: i,
|
||||||
Artists: []*subsonic.Artist{},
|
Artists: []*spec.Artist{},
|
||||||
}
|
}
|
||||||
indexMap[i] = index
|
indexMap[i] = index
|
||||||
resp = append(resp, index)
|
resp = append(resp, index)
|
||||||
}
|
}
|
||||||
index.Artists = append(index.Artists,
|
index.Artists = append(index.Artists,
|
||||||
newArtistByTags(artist))
|
spec.NewArtistByTags(artist))
|
||||||
}
|
}
|
||||||
sort.Slice(resp, func(i, j int) bool {
|
sort.Slice(resp, func(i, j int) bool {
|
||||||
return resp[i].Name < resp[j].Name
|
return resp[i].Name < resp[j].Name
|
||||||
})
|
})
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Artists = &subsonic.Artists{
|
sub.Artists = &spec.Artists{
|
||||||
List: resp,
|
List: resp,
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
|
||||||
id, err := getIntParam(r, "id")
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
artist := &model.Artist{}
|
artist := &model.Artist{}
|
||||||
c.DB.
|
c.DB.
|
||||||
Preload("Albums").
|
Preload("Albums").
|
||||||
First(artist, id)
|
First(artist, id)
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Artist = newArtistByTags(artist)
|
sub.Artist = spec.NewArtistByTags(artist)
|
||||||
sub.Artist.Albums = make([]*subsonic.Album, len(artist.Albums))
|
sub.Artist.Albums = make([]*spec.Album, len(artist.Albums))
|
||||||
for i, album := range artist.Albums {
|
for i, album := range artist.Albums {
|
||||||
sub.Artist.Albums[i] = newAlbumByTags(album, artist)
|
sub.Artist.Albums[i] = spec.NewAlbumByTags(album, artist)
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
||||||
id, err := getIntParam(r, "id")
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
album := &model.Album{}
|
album := &model.Album{}
|
||||||
err = c.DB.
|
err = c.DB.
|
||||||
@@ -83,25 +83,23 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
|
|||||||
First(album, id).
|
First(album, id).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if gorm.IsRecordNotFoundError(err) {
|
||||||
respondError(w, r, 10, "couldn't find an album with that id")
|
return spec.NewError(10, "couldn't find an album with that id")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Album = newAlbumByTags(album, album.TagArtist)
|
sub.Album = spec.NewAlbumByTags(album, album.TagArtist)
|
||||||
sub.Album.Tracks = make([]*subsonic.TrackChild, len(album.Tracks))
|
sub.Album.Tracks = make([]*spec.TrackChild, len(album.Tracks))
|
||||||
for i, track := range album.Tracks {
|
for i, track := range album.Tracks {
|
||||||
sub.Album.Tracks[i] = newTrackByTags(track, album)
|
sub.Album.Tracks[i] = spec.NewTrackByTags(track, album)
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
// changes to this function should be reflected in in _by_folder.go's
|
// changes to this function should be reflected in in _by_folder.go's
|
||||||
// getAlbumList() function
|
// getAlbumList() function
|
||||||
func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||||
listType := getStrParam(r, "type")
|
listType := parsing.GetStrParam(r, "type")
|
||||||
if listType == "" {
|
if listType == "" {
|
||||||
respondError(w, r, 10, "please provide a `type` parameter")
|
return spec.NewError(10, "please provide a `type` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
q := c.DB.DB
|
q := c.DB.DB
|
||||||
switch listType {
|
switch listType {
|
||||||
@@ -115,11 +113,11 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "byYear":
|
case "byYear":
|
||||||
q = q.Where(
|
q = q.Where(
|
||||||
"tag_year BETWEEN ? AND ?",
|
"tag_year BETWEEN ? AND ?",
|
||||||
getIntParamOr(r, "fromYear", 1800),
|
parsing.GetIntParamOr(r, "fromYear", 1800),
|
||||||
getIntParamOr(r, "toYear", 2200))
|
parsing.GetIntParamOr(r, "toYear", 2200))
|
||||||
q = q.Order("tag_year")
|
q = q.Order("tag_year")
|
||||||
case "frequent":
|
case "frequent":
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
q = q.Joins(`
|
q = q.Joins(`
|
||||||
JOIN plays
|
JOIN plays
|
||||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||||
@@ -130,43 +128,40 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "random":
|
case "random":
|
||||||
q = q.Order(gorm.Expr("random()"))
|
q = q.Order(gorm.Expr("random()"))
|
||||||
case "recent":
|
case "recent":
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
q = q.Joins(`
|
q = q.Joins(`
|
||||||
JOIN plays
|
JOIN plays
|
||||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||||
user.ID)
|
user.ID)
|
||||||
q = q.Order("plays.time DESC")
|
q = q.Order("plays.time DESC")
|
||||||
default:
|
default:
|
||||||
respondError(w, r, 10,
|
return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType)
|
||||||
"unknown value `%s` for parameter 'type'", listType)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var albums []*model.Album
|
var albums []*model.Album
|
||||||
q.
|
q.
|
||||||
Where("albums.tag_artist_id IS NOT NULL").
|
Where("albums.tag_artist_id IS NOT NULL").
|
||||||
Offset(getIntParamOr(r, "offset", 0)).
|
Offset(parsing.GetIntParamOr(r, "offset", 0)).
|
||||||
Limit(getIntParamOr(r, "size", 10)).
|
Limit(parsing.GetIntParamOr(r, "size", 10)).
|
||||||
Preload("TagArtist").
|
Preload("TagArtist").
|
||||||
Find(&albums)
|
Find(&albums)
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.AlbumsTwo = &subsonic.Albums{
|
sub.AlbumsTwo = &spec.Albums{
|
||||||
List: make([]*subsonic.Album, len(albums)),
|
List: make([]*spec.Album, len(albums)),
|
||||||
}
|
}
|
||||||
for i, album := range albums {
|
for i, album := range albums {
|
||||||
sub.AlbumsTwo.List[i] = newAlbumByTags(album, album.TagArtist)
|
sub.AlbumsTwo.List[i] = spec.NewAlbumByTags(album, album.TagArtist)
|
||||||
}
|
}
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||||
query := getStrParam(r, "query")
|
query := parsing.GetStrParam(r, "query")
|
||||||
if query == "" {
|
if query == "" {
|
||||||
respondError(w, r, 10, "please provide a `query` parameter")
|
return spec.NewError(10, "please provide a `query` parameter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
query = fmt.Sprintf("%%%s%%",
|
query = fmt.Sprintf("%%%s%%",
|
||||||
strings.TrimSuffix(query, "*"))
|
strings.TrimSuffix(query, "*"))
|
||||||
results := &subsonic.SearchResultThree{}
|
results := &spec.SearchResultThree{}
|
||||||
//
|
//
|
||||||
// search "artists"
|
// search "artists"
|
||||||
var artists []*model.Artist
|
var artists []*model.Artist
|
||||||
@@ -175,12 +170,12 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
|
|||||||
name LIKE ? OR
|
name LIKE ? OR
|
||||||
name_u_dec LIKE ?
|
name_u_dec LIKE ?
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "artistOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "artistCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
|
||||||
Find(&artists)
|
Find(&artists)
|
||||||
for _, a := range artists {
|
for _, a := range artists {
|
||||||
results.Artists = append(results.Artists,
|
results.Artists = append(results.Artists,
|
||||||
newArtistByTags(a))
|
spec.NewArtistByTags(a))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// search "albums"
|
// search "albums"
|
||||||
@@ -191,12 +186,12 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
|
|||||||
tag_title LIKE ? OR
|
tag_title LIKE ? OR
|
||||||
tag_title_u_dec LIKE ?
|
tag_title_u_dec LIKE ?
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "albumOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "albumCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
|
||||||
Find(&albums)
|
Find(&albums)
|
||||||
for _, a := range albums {
|
for _, a := range albums {
|
||||||
results.Albums = append(results.Albums,
|
results.Albums = append(results.Albums,
|
||||||
newAlbumByTags(a, a.TagArtist))
|
spec.NewAlbumByTags(a, a.TagArtist))
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// search tracks
|
// search tracks
|
||||||
@@ -207,14 +202,14 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) {
|
|||||||
tag_title LIKE ? OR
|
tag_title LIKE ? OR
|
||||||
tag_title_u_dec LIKE ?
|
tag_title_u_dec LIKE ?
|
||||||
`, query, query).
|
`, query, query).
|
||||||
Offset(getIntParamOr(r, "songOffset", 0)).
|
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
|
||||||
Limit(getIntParamOr(r, "songCount", 20)).
|
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
|
||||||
Find(&tracks)
|
Find(&tracks)
|
||||||
for _, t := range tracks {
|
for _, t := range tracks {
|
||||||
results.Tracks = append(results.Tracks,
|
results.Tracks = append(results.Tracks,
|
||||||
newTrackByTags(t, t.Album))
|
spec.NewTrackByTags(t, t.Album))
|
||||||
}
|
}
|
||||||
sub := subsonic.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.SearchResultThree = results
|
sub.SearchResultThree = results
|
||||||
respond(w, r, sub)
|
return sub
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetArtists(t *testing.T) {
|
func TestGetArtists(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetArtists, []*queryCase{
|
runQueryCases(t, testController.ServeGetArtists, []*queryCase{
|
||||||
{url.Values{}, "no_args", false},
|
{url.Values{}, "no_args", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetArtist(t *testing.T) {
|
func TestGetArtist(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetArtist, []*queryCase{
|
runQueryCases(t, testController.ServeGetArtist, []*queryCase{
|
||||||
{url.Values{"id": []string{"1"}}, "id_one", false},
|
{url.Values{"id": []string{"1"}}, "id_one", false},
|
||||||
{url.Values{"id": []string{"2"}}, "id_two", false},
|
{url.Values{"id": []string{"2"}}, "id_two", false},
|
||||||
{url.Values{"id": []string{"3"}}, "id_three", false},
|
{url.Values{"id": []string{"3"}}, "id_three", false},
|
||||||
@@ -20,14 +20,14 @@ func TestGetArtist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbum(t *testing.T) {
|
func TestGetAlbum(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetAlbum, []*queryCase{
|
runQueryCases(t, testController.ServeGetAlbum, []*queryCase{
|
||||||
{url.Values{"id": []string{"2"}}, "without_cover", false},
|
{url.Values{"id": []string{"2"}}, "without_cover", false},
|
||||||
{url.Values{"id": []string{"3"}}, "with_cover", false},
|
{url.Values{"id": []string{"3"}}, "with_cover", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbumListTwo(t *testing.T) {
|
func TestGetAlbumListTwo(t *testing.T) {
|
||||||
runQueryCases(t, testController.GetAlbumListTwo, []*queryCase{
|
runQueryCases(t, testController.ServeGetAlbumListTwo, []*queryCase{
|
||||||
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||||
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
||||||
{url.Values{"type": []string{"newest"}}, "newest", false},
|
{url.Values{"type": []string{"newest"}}, "newest", false},
|
||||||
@@ -36,7 +36,7 @@ func TestGetAlbumListTwo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchThree(t *testing.T) {
|
func TestSearchThree(t *testing.T) {
|
||||||
runQueryCases(t, testController.SearchThree, []*queryCase{
|
runQueryCases(t, testController.ServeSearchThree, []*queryCase{
|
||||||
{url.Values{"query": []string{"13"}}, "q_13", false},
|
{url.Values{"query": []string{"13"}}, "q_13", false},
|
||||||
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
||||||
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
||||||
107
server/ctrlsubsonic/handlers_common.go
Normal file
107
server/ctrlsubsonic/handlers_common.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package ctrlsubsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/model"
|
||||||
|
"senan.xyz/g/gonic/scanner"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
"senan.xyz/g/gonic/server/lastfm"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lowerUDecOrHash(in string) string {
|
||||||
|
lower := unicode.ToLower(rune(in[0]))
|
||||||
|
if !unicode.IsLetter(lower) {
|
||||||
|
return "#"
|
||||||
|
}
|
||||||
|
return string(lower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetLicence(r *http.Request) *spec.Response {
|
||||||
|
sub := spec.NewResponse()
|
||||||
|
sub.Licence = &spec.Licence{
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServePing(r *http.Request) *spec.Response {
|
||||||
|
return spec.NewResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
||||||
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
|
}
|
||||||
|
// fetch user to get lastfm session
|
||||||
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
|
if user.LastFMSession == "" {
|
||||||
|
return spec.NewError(0, "you don't have a last.fm session")
|
||||||
|
}
|
||||||
|
// fetch track for getting info to send to last.fm function
|
||||||
|
track := &model.Track{}
|
||||||
|
c.DB.
|
||||||
|
Preload("Album").
|
||||||
|
Preload("Artist").
|
||||||
|
First(track, id)
|
||||||
|
// scrobble with above info
|
||||||
|
err = lastfm.Scrobble(
|
||||||
|
c.DB.GetSetting("lastfm_api_key"),
|
||||||
|
c.DB.GetSetting("lastfm_secret"),
|
||||||
|
user.LastFMSession,
|
||||||
|
track,
|
||||||
|
// clients will provide time in miliseconds, so use that or
|
||||||
|
// instead convert UnixNano to miliseconds
|
||||||
|
parsing.GetIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)),
|
||||||
|
parsing.GetStrParamOr(r, "submission", "true") != "false",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(0, "error when submitting: %v", err)
|
||||||
|
}
|
||||||
|
return spec.NewResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response {
|
||||||
|
folders := &spec.MusicFolders{}
|
||||||
|
folders.List = []*spec.MusicFolder{
|
||||||
|
{ID: 1, Name: "music"},
|
||||||
|
}
|
||||||
|
sub := spec.NewResponse()
|
||||||
|
sub.MusicFolders = folders
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeStartScan(r *http.Request) *spec.Response {
|
||||||
|
go func() {
|
||||||
|
err := scanner.
|
||||||
|
New(c.DB, c.MusicPath).
|
||||||
|
Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while scanning: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return c.ServeGetScanStatus(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetScanStatus(r *http.Request) *spec.Response {
|
||||||
|
var trackCount int
|
||||||
|
c.DB.
|
||||||
|
Model(model.Track{}).
|
||||||
|
Count(&trackCount)
|
||||||
|
sub := spec.NewResponse()
|
||||||
|
sub.ScanStatus = &spec.ScanStatus{
|
||||||
|
Scanning: scanner.IsScanning(),
|
||||||
|
Count: trackCount,
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeNotFound(r *http.Request) *spec.Response {
|
||||||
|
return spec.NewError(70, "view not found")
|
||||||
|
}
|
||||||
88
server/ctrlsubsonic/handlers_raw.go
Normal file
88
server/ctrlsubsonic/handlers_raw.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package ctrlsubsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/model"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// "raw" handlers are ones that don't always return a spec response.
|
||||||
|
// it could be a file, stream, etc. so you must either
|
||||||
|
// a) write to response writer
|
||||||
|
// b) return a non-nil spec.Response
|
||||||
|
// _but not both_
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||||
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
|
}
|
||||||
|
folder := &model.Album{}
|
||||||
|
err = c.DB.
|
||||||
|
Select("id, left_path, right_path, cover").
|
||||||
|
First(folder, id).
|
||||||
|
Error
|
||||||
|
if gorm.IsRecordNotFoundError(err) {
|
||||||
|
return spec.NewError(10, "could not find a cover with that id")
|
||||||
|
}
|
||||||
|
if folder.Cover == "" {
|
||||||
|
return spec.NewError(10, "no cover found for that folder")
|
||||||
|
}
|
||||||
|
absPath := path.Join(
|
||||||
|
c.MusicPath,
|
||||||
|
folder.LeftPath,
|
||||||
|
folder.RightPath,
|
||||||
|
folder.Cover,
|
||||||
|
)
|
||||||
|
http.ServeFile(w, r, absPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||||
|
id, err := parsing.GetIntParam(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
|
}
|
||||||
|
track := &model.Track{}
|
||||||
|
err = c.DB.
|
||||||
|
Preload("Album").
|
||||||
|
First(track, id).
|
||||||
|
Error
|
||||||
|
if gorm.IsRecordNotFoundError(err) {
|
||||||
|
return spec.NewError(70, "media with id `%d` was not found", id)
|
||||||
|
}
|
||||||
|
absPath := path.Join(
|
||||||
|
c.MusicPath,
|
||||||
|
track.Album.LeftPath,
|
||||||
|
track.Album.RightPath,
|
||||||
|
track.Filename,
|
||||||
|
)
|
||||||
|
file, err := os.Open(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(0, "error while streaming media: %v", err)
|
||||||
|
}
|
||||||
|
stat, _ := file.Stat()
|
||||||
|
http.ServeContent(w, r, absPath, stat.ModTime(), file)
|
||||||
|
//
|
||||||
|
// after we've served the file, mark the album as played
|
||||||
|
user := r.Context().Value(key.User).(*model.User)
|
||||||
|
play := model.Play{
|
||||||
|
AlbumID: track.Album.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
}
|
||||||
|
c.DB.
|
||||||
|
Where(play).
|
||||||
|
First(&play)
|
||||||
|
play.Time = time.Now() // for getAlbumList?type=recent
|
||||||
|
play.Count++ // for getAlbumList?type=frequent
|
||||||
|
c.DB.Save(&play)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
server/ctrlsubsonic/middleware.go
Normal file
80
server/ctrlsubsonic/middleware.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package ctrlsubsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"senan.xyz/g/gonic/server/key"
|
||||||
|
"senan.xyz/g/gonic/server/parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var requiredParameters = []string{
|
||||||
|
"u", "v", "c",
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHasAllParams(params url.Values) error {
|
||||||
|
for _, req := range requiredParameters {
|
||||||
|
param := params.Get(req)
|
||||||
|
if param != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("please provide a `%s` parameter", req)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCredsToken(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 checkCredsBasic(password, given string) bool {
|
||||||
|
if len(given) >= 4 && given[:4] == "enc:" {
|
||||||
|
bytes, _ := hex.DecodeString(given[4:])
|
||||||
|
given = string(bytes)
|
||||||
|
}
|
||||||
|
return password == given
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) WithValidSubsonicArgs(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := checkHasAllParams(r.URL.Query()); err != nil {
|
||||||
|
writeResp(w, r, spec.NewError(10, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := parsing.GetStrParam(r, "u")
|
||||||
|
password := parsing.GetStrParam(r, "p")
|
||||||
|
token := parsing.GetStrParam(r, "t")
|
||||||
|
salt := parsing.GetStrParam(r, "s")
|
||||||
|
passwordAuth := token == "" && salt == ""
|
||||||
|
tokenAuth := password == ""
|
||||||
|
if tokenAuth == passwordAuth {
|
||||||
|
writeResp(w, r, spec.NewError(10, "please provide `t` and `s`, or just `p`"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := c.DB.GetUserFromName(username)
|
||||||
|
if user == nil {
|
||||||
|
writeResp(w, r, spec.NewError(40, "invalid username `%s`", username))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var credsOk bool
|
||||||
|
if tokenAuth {
|
||||||
|
credsOk = checkCredsToken(user.Password, token, salt)
|
||||||
|
} else {
|
||||||
|
credsOk = checkCredsBasic(user.Password, password)
|
||||||
|
}
|
||||||
|
if !credsOk {
|
||||||
|
writeResp(w, r, spec.NewError(40, "invalid password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
withUser := context.WithValue(r.Context(), key.User, user)
|
||||||
|
next.ServeHTTP(w, r.WithContext(withUser))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
package handler
|
package spec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAlbumByFolder(f *model.Album) *subsonic.Album {
|
func NewAlbumByFolder(f *model.Album) *Album {
|
||||||
return &subsonic.Album{
|
return &Album{
|
||||||
Artist: f.Parent.RightPath,
|
Artist: f.Parent.RightPath,
|
||||||
CoverID: f.ID,
|
CoverID: f.ID,
|
||||||
ID: f.ID,
|
ID: f.ID,
|
||||||
@@ -18,8 +17,8 @@ func newAlbumByFolder(f *model.Album) *subsonic.Album {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTCAlbumByFolder(f *model.Album) *subsonic.TrackChild {
|
func NewTCAlbumByFolder(f *model.Album) *TrackChild {
|
||||||
trCh := &subsonic.TrackChild{
|
trCh := &TrackChild{
|
||||||
ID: f.ID,
|
ID: f.ID,
|
||||||
IsDir: true,
|
IsDir: true,
|
||||||
Title: f.RightPath,
|
Title: f.RightPath,
|
||||||
@@ -31,8 +30,8 @@ func newTCAlbumByFolder(f *model.Album) *subsonic.TrackChild {
|
|||||||
return trCh
|
return trCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTCTrackByFolder(t *model.Track, parent *model.Album) *subsonic.TrackChild {
|
func NewTCTrackByFolder(t *model.Track, parent *model.Album) *TrackChild {
|
||||||
trCh := &subsonic.TrackChild{
|
trCh := &TrackChild{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
Album: t.Album.RightPath,
|
Album: t.Album.RightPath,
|
||||||
ContentType: t.MIME(),
|
ContentType: t.MIME(),
|
||||||
@@ -59,16 +58,16 @@ func newTCTrackByFolder(t *model.Track, parent *model.Album) *subsonic.TrackChil
|
|||||||
return trCh
|
return trCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArtistByFolder(f *model.Album) *subsonic.Artist {
|
func NewArtistByFolder(f *model.Album) *Artist {
|
||||||
return &subsonic.Artist{
|
return &Artist{
|
||||||
ID: f.ID,
|
ID: f.ID,
|
||||||
Name: f.RightPath,
|
Name: f.RightPath,
|
||||||
AlbumCount: f.ChildCount,
|
AlbumCount: f.ChildCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDirectoryByFolder(f *model.Album, children []*subsonic.TrackChild) *subsonic.Directory {
|
func NewDirectoryByFolder(f *model.Album, children []*TrackChild) *Directory {
|
||||||
return &subsonic.Directory{
|
return &Directory{
|
||||||
ID: f.ID,
|
ID: f.ID,
|
||||||
Parent: f.ParentID,
|
Parent: f.ParentID,
|
||||||
Name: f.RightPath,
|
Name: f.RightPath,
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
package handler
|
package spec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
"senan.xyz/g/gonic/model"
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAlbumByTags(a *model.Album, artist *model.Artist) *subsonic.Album {
|
func NewAlbumByTags(a *model.Album, artist *model.Artist) *Album {
|
||||||
ret := &subsonic.Album{
|
ret := &Album{
|
||||||
Created: a.ModifiedAt,
|
Created: a.ModifiedAt,
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Name: a.TagTitle,
|
Name: a.TagTitle,
|
||||||
@@ -23,8 +22,8 @@ func newAlbumByTags(a *model.Album, artist *model.Artist) *subsonic.Album {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTrackByTags(t *model.Track, album *model.Album) *subsonic.TrackChild {
|
func NewTrackByTags(t *model.Track, album *model.Album) *TrackChild {
|
||||||
ret := &subsonic.TrackChild{
|
ret := &TrackChild{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
ContentType: t.MIME(),
|
ContentType: t.MIME(),
|
||||||
Suffix: t.Ext(),
|
Suffix: t.Ext(),
|
||||||
@@ -53,8 +52,8 @@ func newTrackByTags(t *model.Track, album *model.Album) *subsonic.TrackChild {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArtistByTags(a *model.Artist) *subsonic.Artist {
|
func NewArtistByTags(a *model.Artist) *Artist {
|
||||||
return &subsonic.Artist{
|
return &Artist{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Name: a.Name,
|
Name: a.Name,
|
||||||
AlbumCount: a.AlbumCount,
|
AlbumCount: a.AlbumCount,
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package subsonic
|
package spec
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
apiVersion = "1.9.0"
|
apiVersion = "1.9.0"
|
||||||
@@ -36,19 +39,30 @@ func NewResponse() *Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spec errors:
|
||||||
|
// 0 a generic error
|
||||||
|
// 10 required parameter is missing
|
||||||
|
// 20 incompatible subsonic rest protocol version. client must upgrade
|
||||||
|
// 30 incompatible subsonic rest protocol version. server must upgrade
|
||||||
|
// 40 wrong username or password
|
||||||
|
// 41 token authentication not supported for ldap users
|
||||||
|
// 50 user is not authorized for the given operation
|
||||||
|
// 60 the trial period for the subsonic server is over
|
||||||
|
// 70 the requested data was not found
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Code int `xml:"code,attr" json:"code"`
|
Code int `xml:"code,attr" json:"code"`
|
||||||
Message string `xml:"message,attr" json:"message"`
|
Message string `xml:"message,attr" json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewError(code int, message string) *Response {
|
func NewError(code int, message string, a ...interface{}) *Response {
|
||||||
return &Response{
|
return &Response{
|
||||||
Status: "failed",
|
Status: "failed",
|
||||||
XMLNS: xmlns,
|
XMLNS: xmlns,
|
||||||
Version: apiVersion,
|
Version: apiVersion,
|
||||||
Error: &Error{
|
Error: &Error{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: message,
|
Message: fmt.Sprintf(message, a...),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
|
|
||||||
"github.com/wader/gormstore"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextUserKey contextKey = iota
|
|
||||||
contextSessionKey
|
|
||||||
)
|
|
||||||
|
|
||||||
type Controller struct {
|
|
||||||
DB *db.DB
|
|
||||||
SessDB *gormstore.Store
|
|
||||||
Templates map[string]*template.Template
|
|
||||||
MusicPath string
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func firstExisting(or string, strings ...string) string {
|
|
||||||
for _, s := range strings {
|
|
||||||
if s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return or
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessLogSave(w http.ResponseWriter, r *http.Request, s *sessions.Session) {
|
|
||||||
if err := s.Save(r, w); err != nil {
|
|
||||||
log.Printf("error saving session: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Flash struct {
|
|
||||||
Message string
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessAddFlashW(message string, s *sessions.Session) {
|
|
||||||
s.AddFlash(Flash{
|
|
||||||
Message: message,
|
|
||||||
Type: "warning",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessAddFlashWf(message string, s *sessions.Session, a ...interface{}) {
|
|
||||||
sessAddFlashW(fmt.Sprintf(message, a...), s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessAddFlashN(message string, s *sessions.Session) {
|
|
||||||
s.AddFlash(Flash{
|
|
||||||
Message: message,
|
|
||||||
Type: "normal",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessAddFlashNf(message string, s *sessions.Session, a ...interface{}) {
|
|
||||||
sessAddFlashN(fmt.Sprintf(message, a...), s)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func validateUsername(username string) error {
|
|
||||||
if username == "" {
|
|
||||||
return fmt.Errorf("please enter the username")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePasswords(pOne, pTwo string) error {
|
|
||||||
if pOne == "" || pTwo == "" {
|
|
||||||
return fmt.Errorf("please enter the password twice")
|
|
||||||
}
|
|
||||||
if !(pOne == pTwo) {
|
|
||||||
return fmt.Errorf("the two passwords entered were not the same")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateAPIKey(apiKey, secret string) error {
|
|
||||||
if apiKey == "" || secret == "" {
|
|
||||||
return fmt.Errorf("please enter both the api key and secret")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
|
||||||
"senan.xyz/g/gonic/scanner"
|
|
||||||
"senan.xyz/g/gonic/server/lastfm"
|
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
|
||||||
)
|
|
||||||
|
|
||||||
func lowerUDecOrHash(in string) string {
|
|
||||||
lower := unicode.ToLower(rune(in[0]))
|
|
||||||
if !unicode.IsLetter(lower) {
|
|
||||||
return "#"
|
|
||||||
}
|
|
||||||
return string(lower)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := getIntParam(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
track := &model.Track{}
|
|
||||||
err = c.DB.
|
|
||||||
Preload("Album").
|
|
||||||
First(track, id).
|
|
||||||
Error
|
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
|
||||||
respondError(w, r, 70, "media with id `%d` was not found", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
absPath := path.Join(
|
|
||||||
c.MusicPath,
|
|
||||||
track.Album.LeftPath,
|
|
||||||
track.Album.RightPath,
|
|
||||||
track.Filename,
|
|
||||||
)
|
|
||||||
file, err := os.Open(absPath)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 0, "error while streaming media: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stat, _ := file.Stat()
|
|
||||||
http.ServeContent(w, r, absPath, stat.ModTime(), file)
|
|
||||||
//
|
|
||||||
// after we've served the file, mark the album as played
|
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
|
||||||
play := model.Play{
|
|
||||||
AlbumID: track.Album.ID,
|
|
||||||
UserID: user.ID,
|
|
||||||
}
|
|
||||||
c.DB.
|
|
||||||
Where(play).
|
|
||||||
First(&play)
|
|
||||||
play.Time = time.Now() // for getAlbumList?type=recent
|
|
||||||
play.Count++ // for getAlbumList?type=frequent
|
|
||||||
c.DB.Save(&play)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := getIntParam(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
folder := &model.Album{}
|
|
||||||
err = c.DB.
|
|
||||||
Select("id, left_path, right_path, cover").
|
|
||||||
First(folder, id).
|
|
||||||
Error
|
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
|
||||||
respondError(w, r, 10, "could not find a cover with that id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if folder.Cover == "" {
|
|
||||||
respondError(w, r, 10, "no cover found for that folder")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
absPath := path.Join(
|
|
||||||
c.MusicPath,
|
|
||||||
folder.LeftPath,
|
|
||||||
folder.RightPath,
|
|
||||||
folder.Cover,
|
|
||||||
)
|
|
||||||
http.ServeFile(w, r, absPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sub := subsonic.NewResponse()
|
|
||||||
sub.Licence = &subsonic.Licence{
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
respond(w, r, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) Ping(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sub := subsonic.NewResponse()
|
|
||||||
respond(w, r, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id, err := getIntParam(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 10, "please provide an `id` parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// fetch user to get lastfm session
|
|
||||||
user := r.Context().Value(contextUserKey).(*model.User)
|
|
||||||
if user.LastFMSession == "" {
|
|
||||||
respondError(w, r, 0, "you don't have a last.fm session")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// fetch track for getting info to send to last.fm function
|
|
||||||
track := &model.Track{}
|
|
||||||
c.DB.
|
|
||||||
Preload("Album").
|
|
||||||
Preload("Artist").
|
|
||||||
First(track, id)
|
|
||||||
// scrobble with above info
|
|
||||||
err = lastfm.Scrobble(
|
|
||||||
c.DB.GetSetting("lastfm_api_key"),
|
|
||||||
c.DB.GetSetting("lastfm_secret"),
|
|
||||||
user.LastFMSession,
|
|
||||||
track,
|
|
||||||
// clients will provide time in miliseconds, so use that or
|
|
||||||
// instead convert UnixNano to miliseconds
|
|
||||||
getIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)),
|
|
||||||
getStrParamOr(r, "submission", "true") != "false",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 0, "error when submitting: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sub := subsonic.NewResponse()
|
|
||||||
respond(w, r, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
|
|
||||||
folders := &subsonic.MusicFolders{}
|
|
||||||
folders.List = []*subsonic.MusicFolder{
|
|
||||||
{ID: 1, Name: "music"},
|
|
||||||
}
|
|
||||||
sub := subsonic.NewResponse()
|
|
||||||
sub.MusicFolders = folders
|
|
||||||
respond(w, r, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) StartScan(w http.ResponseWriter, r *http.Request) {
|
|
||||||
go func() {
|
|
||||||
err := scanner.
|
|
||||||
New(c.DB, c.MusicPath).
|
|
||||||
Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while scanning: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
c.GetScanStatus(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetScanStatus(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var trackCount int
|
|
||||||
c.DB.
|
|
||||||
Model(model.Track{}).
|
|
||||||
Count(&trackCount)
|
|
||||||
sub := subsonic.NewResponse()
|
|
||||||
sub.ScanStatus = &subsonic.ScanStatus{
|
|
||||||
Scanning: scanner.IsScanning(),
|
|
||||||
Count: trackCount,
|
|
||||||
}
|
|
||||||
respond(w, r, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
respondError(w, r, 0, "unknown route")
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:interfacer
|
|
||||||
func (c *Controller) WithLogging(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("connection from `%s` for `%s`", r.RemoteAddr, r.URL)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
var requiredParameters = []string{
|
|
||||||
"u", "v", "c",
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkHasAllParams(params url.Values) error {
|
|
||||||
for _, req := range requiredParameters {
|
|
||||||
param := params.Get(req)
|
|
||||||
if param != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("please provide a `%s` parameter", req)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCredentialsToken(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 checkCredentialsBasic(password, given string) bool {
|
|
||||||
if given[:4] == "enc:" {
|
|
||||||
bytes, _ := hex.DecodeString(given[4:])
|
|
||||||
given = string(bytes)
|
|
||||||
}
|
|
||||||
return password == given
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:interfacer
|
|
||||||
func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := checkHasAllParams(r.URL.Query())
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, r, 10, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
username, password := r.URL.Query().Get("u"),
|
|
||||||
r.URL.Query().Get("p")
|
|
||||||
token, salt := r.URL.Query().Get("t"),
|
|
||||||
r.URL.Query().Get("s")
|
|
||||||
passwordAuth, tokenAuth := token == "" && salt == "",
|
|
||||||
password == ""
|
|
||||||
if tokenAuth == passwordAuth {
|
|
||||||
respondError(w, r, 10,
|
|
||||||
"please provide parameters `t` and `s`, or just `p`")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user := c.DB.GetUserFromName(username)
|
|
||||||
if user == nil {
|
|
||||||
respondError(w, r, 40, "invalid username `%s`", username)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var credsOk bool
|
|
||||||
if tokenAuth {
|
|
||||||
credsOk = checkCredentialsToken(user.Password, token, salt)
|
|
||||||
} else {
|
|
||||||
credsOk = checkCredentialsBasic(user.Password, password)
|
|
||||||
}
|
|
||||||
if !credsOk {
|
|
||||||
respondError(w, r, 40, "invalid password")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
withUser := context.WithValue(r.Context(), contextUserKey, user)
|
|
||||||
next.ServeHTTP(w, r.WithContext(withUser))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:interfacer
|
|
||||||
func (c *Controller) WithCORS(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,52 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type templateData struct {
|
|
||||||
// common
|
|
||||||
Flashes []interface{}
|
|
||||||
User *model.User
|
|
||||||
// home
|
|
||||||
AlbumCount int
|
|
||||||
ArtistCount int
|
|
||||||
TrackCount int
|
|
||||||
RequestRoot string
|
|
||||||
RecentFolders []*model.Album
|
|
||||||
AllUsers []*model.User
|
|
||||||
LastScanTime time.Time
|
|
||||||
IsScanning bool
|
|
||||||
//
|
|
||||||
CurrentLastFMAPIKey string
|
|
||||||
CurrentLastFMAPISecret string
|
|
||||||
SelectedUser *model.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderTemplate(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
tmpl *template.Template,
|
|
||||||
data *templateData,
|
|
||||||
) {
|
|
||||||
if data == nil {
|
|
||||||
data = &templateData{}
|
|
||||||
}
|
|
||||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
|
||||||
data.Flashes = session.Flashes()
|
|
||||||
sessLogSave(w, r, session)
|
|
||||||
data.User, _ = r.Context().Value(contextUserKey).(*model.User)
|
|
||||||
err := tmpl.Execute(w, data)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error executing template: %v\n", err)
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/server/subsonic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type metaResponse struct {
|
|
||||||
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
|
||||||
*subsonic.Response `json:"subsonic-response"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type errWriter struct {
|
|
||||||
w io.Writer
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ew *errWriter) write(buf []byte) {
|
|
||||||
if ew.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, ew.err = ew.w.Write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func respondRaw(w http.ResponseWriter, r *http.Request,
|
|
||||||
code int, sub *subsonic.Response) {
|
|
||||||
w.WriteHeader(code)
|
|
||||||
res := metaResponse{
|
|
||||||
Response: sub,
|
|
||||||
}
|
|
||||||
ew := &errWriter{w: w}
|
|
||||||
switch getStrParam(r, "f") {
|
|
||||||
case "json":
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
data, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not marshall to json: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ew.write(data)
|
|
||||||
case "jsonp":
|
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
|
||||||
data, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not marshall to json: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ew.write([]byte(getStrParamOr(r, "callback", "cb")))
|
|
||||||
ew.write([]byte("("))
|
|
||||||
ew.write(data)
|
|
||||||
ew.write([]byte(");"))
|
|
||||||
default:
|
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
|
||||||
data, err := xml.MarshalIndent(res, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not marshall to xml: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ew.write(data)
|
|
||||||
}
|
|
||||||
if ew.err != nil {
|
|
||||||
log.Printf("error writing to response: %v\n", ew.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 int, message string, a ...interface{}) {
|
|
||||||
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
|
|
||||||
code, fmt.Sprintf(message, a...),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
8
server/key/key.go
Normal file
8
server/key/key.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package key
|
||||||
|
|
||||||
|
type Key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
User Key = iota
|
||||||
|
Session
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package parsing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -6,19 +6,19 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getStrParam(r *http.Request, key string) string {
|
func GetStrParam(r *http.Request, key string) string {
|
||||||
return r.URL.Query().Get(key)
|
return r.URL.Query().Get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStrParamOr(r *http.Request, key, or string) string {
|
func GetStrParamOr(r *http.Request, key, or string) string {
|
||||||
val := getStrParam(r, key)
|
val := GetStrParam(r, key)
|
||||||
if val == "" {
|
if val == "" {
|
||||||
return or
|
return or
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIntParam(r *http.Request, key string) (int, error) {
|
func GetIntParam(r *http.Request, key string) (int, error) {
|
||||||
strVal := r.URL.Query().Get(key)
|
strVal := r.URL.Query().Get(key)
|
||||||
if strVal == "" {
|
if strVal == "" {
|
||||||
return 0, fmt.Errorf("no param with key `%s`", key)
|
return 0, fmt.Errorf("no param with key `%s`", key)
|
||||||
@@ -30,8 +30,8 @@ func getIntParam(r *http.Request, key string) (int, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIntParamOr(r *http.Request, key string, or int) int {
|
func GetIntParamOr(r *http.Request, key string, or int) int {
|
||||||
val, err := getIntParam(r, key)
|
val, err := GetIntParam(r, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return or
|
return or
|
||||||
}
|
}
|
||||||
128
server/server.go
128
server/server.go
@@ -1,53 +1,123 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"senan.xyz/g/gonic/assets"
|
||||||
"senan.xyz/g/gonic/db"
|
"senan.xyz/g/gonic/db"
|
||||||
"senan.xyz/g/gonic/server/handler"
|
"senan.xyz/g/gonic/server/ctrladmin"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlbase"
|
||||||
|
"senan.xyz/g/gonic/server/ctrlsubsonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
mux *http.ServeMux
|
|
||||||
*handler.Controller
|
|
||||||
*http.Server
|
*http.Server
|
||||||
|
router *mux.Router
|
||||||
|
ctrlBase *ctrlbase.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(db *db.DB, musicPath string, listenAddr string) *Server {
|
||||||
db *db.DB,
|
ctrlBase := &ctrlbase.Controller{
|
||||||
musicPath string,
|
DB: db,
|
||||||
listenAddr string,
|
MusicPath: musicPath,
|
||||||
) *Server {
|
}
|
||||||
mux := http.NewServeMux()
|
router := mux.NewRouter()
|
||||||
|
// jamstash seems to call "musicFolderSettings.view" to start a scan. notice
|
||||||
|
// that there is no "/rest/" prefix, so i doesn't fit in with the nice router,
|
||||||
|
// custom handler, middleware. etc setup that we've got in `SetupSubsonic()`.
|
||||||
|
// instead lets redirect to down there and use the scan endpoint
|
||||||
|
router.HandleFunc("/musicFolderSettings.view", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
oldParams := r.URL.Query().Encode()
|
||||||
|
redirectTo := "/rest/startScan.view?" + oldParams
|
||||||
|
http.Redirect(w, r, redirectTo, http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
router.Use(ctrlBase.WithLogging)
|
||||||
|
router.Use(ctrlBase.WithCORS)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: listenAddr,
|
Addr: listenAddr,
|
||||||
Handler: mux,
|
Handler: router,
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
IdleTimeout: 15 * time.Second,
|
IdleTimeout: 15 * time.Second,
|
||||||
}
|
}
|
||||||
controller := &handler.Controller{
|
|
||||||
DB: db,
|
|
||||||
MusicPath: musicPath,
|
|
||||||
}
|
|
||||||
return &Server{
|
return &Server{
|
||||||
mux: mux,
|
Server: server,
|
||||||
Server: server,
|
router: router,
|
||||||
Controller: controller,
|
ctrlBase: ctrlBase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type middleware func(next http.HandlerFunc) http.HandlerFunc
|
func (s *Server) SetupAdmin() error {
|
||||||
|
ctrl := ctrladmin.New(s.ctrlBase)
|
||||||
func newChain(wares ...middleware) middleware {
|
// TODO: remove all the H()s
|
||||||
return func(final http.HandlerFunc) http.HandlerFunc {
|
routPublic := s.router.PathPrefix("/admin").Subrouter()
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
routPublic.Use(ctrl.WithSession)
|
||||||
last := final
|
routPublic.Handle("/login", ctrl.H(ctrl.ServeLogin))
|
||||||
for i := len(wares) - 1; i >= 0; i-- {
|
routPublic.Handle("/login_do", ctrl.H(ctrl.ServeLoginDo))
|
||||||
last = wares[i](last)
|
assets.PrefixDo("static", func(path string, asset *assets.EmbeddedAsset) {
|
||||||
}
|
_, name := filepath.Split(path)
|
||||||
last(w, r)
|
route := filepath.Join("/static", name)
|
||||||
}
|
reader := bytes.NewReader(asset.Bytes)
|
||||||
}
|
routPublic.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeContent(w, r, name, asset.ModTime, reader)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// begin user routes (if session is valid)
|
||||||
|
routUser := routPublic.NewRoute().Subrouter()
|
||||||
|
routUser.Use(ctrl.WithUserSession)
|
||||||
|
routUser.Handle("/logout", ctrl.H(ctrl.ServeLogout))
|
||||||
|
routUser.Handle("/home", ctrl.H(ctrl.ServeHome))
|
||||||
|
routUser.Handle("/change_own_password", ctrl.H(ctrl.ServeChangeOwnPassword))
|
||||||
|
routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo))
|
||||||
|
routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo))
|
||||||
|
routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo))
|
||||||
|
// begin admin routes (if session is valid, and is admin)
|
||||||
|
routAdmin := routUser.NewRoute().Subrouter()
|
||||||
|
routAdmin.Use(ctrl.WithAdminSession)
|
||||||
|
routAdmin.Handle("/change_password", ctrl.H(ctrl.ServeChangePassword))
|
||||||
|
routAdmin.Handle("/change_password_do", ctrl.H(ctrl.ServeChangePasswordDo))
|
||||||
|
routAdmin.Handle("/delete_user", ctrl.H(ctrl.ServeDeleteUser))
|
||||||
|
routAdmin.Handle("/delete_user_do", ctrl.H(ctrl.ServeDeleteUserDo))
|
||||||
|
routAdmin.Handle("/create_user", ctrl.H(ctrl.ServeCreateUser))
|
||||||
|
routAdmin.Handle("/create_user_do", ctrl.H(ctrl.ServeCreateUserDo))
|
||||||
|
routAdmin.Handle("/update_lastfm_api_key", ctrl.H(ctrl.ServeUpdateLastFMAPIKey))
|
||||||
|
routAdmin.Handle("/update_lastfm_api_key_do", ctrl.H(ctrl.ServeUpdateLastFMAPIKeyDo))
|
||||||
|
routAdmin.Handle("/start_scan_do", ctrl.H(ctrl.ServeStartScanDo))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) SetupSubsonic() error {
|
||||||
|
ctrl := ctrlsubsonic.New(s.ctrlBase)
|
||||||
|
rout := s.router.PathPrefix("/rest").Subrouter()
|
||||||
|
rout.Use(ctrl.WithValidSubsonicArgs)
|
||||||
|
rout.NotFoundHandler = ctrl.H(ctrl.ServeNotFound)
|
||||||
|
// common
|
||||||
|
rout.Handle("/getLicense{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetLicence))
|
||||||
|
rout.Handle("/getMusicFolders{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicFolders))
|
||||||
|
rout.Handle("/getScanStatus{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetScanStatus))
|
||||||
|
rout.Handle("/ping{_:(?:\\.view)?}", ctrl.H(ctrl.ServePing))
|
||||||
|
rout.Handle("/scrobble{_:(?:\\.view)?}", ctrl.H(ctrl.ServeScrobble))
|
||||||
|
rout.Handle("/startScan{_:(?:\\.view)?}", ctrl.H(ctrl.ServeStartScan))
|
||||||
|
// raw
|
||||||
|
rout.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
||||||
|
rout.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
||||||
|
rout.Handle("/stream{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
||||||
|
// browse by tag
|
||||||
|
rout.Handle("/getAlbum{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbum))
|
||||||
|
rout.Handle("/getAlbumList2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumListTwo))
|
||||||
|
rout.Handle("/getArtist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtist))
|
||||||
|
rout.Handle("/getArtists{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtists))
|
||||||
|
rout.Handle("/search3{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchThree))
|
||||||
|
// browse by folder
|
||||||
|
rout.Handle("/getIndexes{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetIndexes))
|
||||||
|
rout.Handle("/getMusicDirectory{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicDirectory))
|
||||||
|
rout.Handle("/getAlbumList{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumList))
|
||||||
|
rout.Handle("/search2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchTwo))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Masterminds/sprig" //nolint:typecheck
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/wader/gormstore"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
prefixLayouts = "layouts"
|
|
||||||
prefixPages = "pages"
|
|
||||||
prefixPartials = "partials"
|
|
||||||
prefixStatic = "static"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tmplMap map[string]*template.Template
|
|
||||||
|
|
||||||
// prefixDo runs a given callback for every path in our assets with
|
|
||||||
// the given prefix
|
|
||||||
func prefixDo(pre string, cb func(path string, asset *EmbeddedAsset)) {
|
|
||||||
for path, asset := range assetBytes {
|
|
||||||
if strings.HasPrefix(path, pre) {
|
|
||||||
cb(path, asset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extendFromPaths /extends/ the given template for every asset
|
|
||||||
// with given prefix
|
|
||||||
func extendFromPaths(b *template.Template, p string) *template.Template {
|
|
||||||
prefixDo(p, func(_ string, asset *EmbeddedAsset) {
|
|
||||||
tmplStr := string(asset.Bytes)
|
|
||||||
b = template.Must(b.Parse(tmplStr))
|
|
||||||
})
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// extendFromPaths /clones/ the given template for every asset
|
|
||||||
// with given prefix, extends it, and insert it into a new tmplMap
|
|
||||||
func pagesFromPaths(b *template.Template, p string) tmplMap {
|
|
||||||
ret := tmplMap{}
|
|
||||||
prefixDo(p, func(path string, asset *EmbeddedAsset) {
|
|
||||||
tmplKey := filepath.Base(path)
|
|
||||||
clone := template.Must(b.Clone())
|
|
||||||
tmplStr := string(asset.Bytes)
|
|
||||||
ret[tmplKey] = template.Must(clone.Parse(tmplStr))
|
|
||||||
})
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) SetupAdmin() error {
|
|
||||||
sessionKey := []byte(s.DB.GetSetting("session_key"))
|
|
||||||
if len(sessionKey) == 0 {
|
|
||||||
sessionKey = securecookie.GenerateRandomKey(32)
|
|
||||||
s.DB.SetSetting("session_key", string(sessionKey))
|
|
||||||
}
|
|
||||||
s.SessDB = gormstore.New(s.DB.DB, sessionKey)
|
|
||||||
go s.SessDB.PeriodicCleanup(time.Hour, nil)
|
|
||||||
//
|
|
||||||
tmplBase := template.
|
|
||||||
New("layout").
|
|
||||||
Funcs(sprig.FuncMap()).
|
|
||||||
Funcs(template.FuncMap{
|
|
||||||
"humanDate": humanize.Time,
|
|
||||||
})
|
|
||||||
tmplBase = extendFromPaths(tmplBase, prefixPartials)
|
|
||||||
tmplBase = extendFromPaths(tmplBase, prefixLayouts)
|
|
||||||
s.Templates = pagesFromPaths(tmplBase, prefixPages)
|
|
||||||
// setup static server
|
|
||||||
prefixDo(prefixStatic, func(path string, asset *EmbeddedAsset) {
|
|
||||||
_, name := filepath.Split(path)
|
|
||||||
route := filepath.Join("/admin/static", name)
|
|
||||||
reader := bytes.NewReader(asset.Bytes)
|
|
||||||
s.mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeContent(w, r, name, asset.ModTime, reader)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
//
|
|
||||||
withPublicWare := newChain(
|
|
||||||
s.WithLogging,
|
|
||||||
s.WithSession,
|
|
||||||
)
|
|
||||||
withUserWare := newChain(
|
|
||||||
withPublicWare,
|
|
||||||
s.WithUserSession,
|
|
||||||
)
|
|
||||||
withAdminWare := newChain(
|
|
||||||
withUserWare,
|
|
||||||
s.WithAdminSession,
|
|
||||||
)
|
|
||||||
// begin public routes (creates new session)
|
|
||||||
s.mux.HandleFunc("/admin/login", withPublicWare(s.ServeLogin))
|
|
||||||
s.mux.HandleFunc("/admin/login_do", withPublicWare(s.ServeLoginDo))
|
|
||||||
// begin user routes (if session is valid)
|
|
||||||
s.mux.HandleFunc("/admin/logout", withUserWare(s.ServeLogout))
|
|
||||||
s.mux.HandleFunc("/admin/home", withUserWare(s.ServeHome))
|
|
||||||
s.mux.HandleFunc("/admin/change_own_password", withUserWare(s.ServeChangeOwnPassword))
|
|
||||||
s.mux.HandleFunc("/admin/change_own_password_do", withUserWare(s.ServeChangeOwnPasswordDo))
|
|
||||||
s.mux.HandleFunc("/admin/link_lastfm_do", withUserWare(s.ServeLinkLastFMDo))
|
|
||||||
s.mux.HandleFunc("/admin/unlink_lastfm_do", withUserWare(s.ServeUnlinkLastFMDo))
|
|
||||||
// begin admin routes (if session is valid, and is admin)
|
|
||||||
s.mux.HandleFunc("/admin/change_password", withAdminWare(s.ServeChangePassword))
|
|
||||||
s.mux.HandleFunc("/admin/change_password_do", withAdminWare(s.ServeChangePasswordDo))
|
|
||||||
s.mux.HandleFunc("/admin/delete_user", withAdminWare(s.ServeDeleteUser))
|
|
||||||
s.mux.HandleFunc("/admin/delete_user_do", withAdminWare(s.ServeDeleteUserDo))
|
|
||||||
s.mux.HandleFunc("/admin/create_user", withAdminWare(s.ServeCreateUser))
|
|
||||||
s.mux.HandleFunc("/admin/create_user_do", withAdminWare(s.ServeCreateUserDo))
|
|
||||||
s.mux.HandleFunc("/admin/update_lastfm_api_key", withAdminWare(s.ServeUpdateLastFMAPIKey))
|
|
||||||
s.mux.HandleFunc("/admin/update_lastfm_api_key_do", withAdminWare(s.ServeUpdateLastFMAPIKeyDo))
|
|
||||||
s.mux.HandleFunc("/admin/start_scan_do", withAdminWare(s.ServeStartScanDo))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
func (s *Server) SetupSubsonic() error {
|
|
||||||
withWare := newChain(
|
|
||||||
s.WithLogging,
|
|
||||||
s.WithCORS,
|
|
||||||
s.WithValidSubsonicArgs,
|
|
||||||
)
|
|
||||||
// common
|
|
||||||
s.mux.HandleFunc("/rest/download", withWare(s.Stream))
|
|
||||||
s.mux.HandleFunc("/rest/download.view", withWare(s.Stream))
|
|
||||||
s.mux.HandleFunc("/rest/stream", withWare(s.Stream))
|
|
||||||
s.mux.HandleFunc("/rest/stream.view", withWare(s.Stream))
|
|
||||||
s.mux.HandleFunc("/rest/getCoverArt", withWare(s.GetCoverArt))
|
|
||||||
s.mux.HandleFunc("/rest/getCoverArt.view", withWare(s.GetCoverArt))
|
|
||||||
s.mux.HandleFunc("/rest/getLicense", withWare(s.GetLicence))
|
|
||||||
s.mux.HandleFunc("/rest/getLicense.view", withWare(s.GetLicence))
|
|
||||||
s.mux.HandleFunc("/rest/ping", withWare(s.Ping))
|
|
||||||
s.mux.HandleFunc("/rest/ping.view", withWare(s.Ping))
|
|
||||||
s.mux.HandleFunc("/rest/scrobble", withWare(s.Scrobble))
|
|
||||||
s.mux.HandleFunc("/rest/scrobble.view", withWare(s.Scrobble))
|
|
||||||
s.mux.HandleFunc("/rest/getMusicFolders", withWare(s.GetMusicFolders))
|
|
||||||
s.mux.HandleFunc("/rest/getMusicFolders.view", withWare(s.GetMusicFolders))
|
|
||||||
s.mux.HandleFunc("/rest/startScan", withWare(s.StartScan))
|
|
||||||
s.mux.HandleFunc("/rest/startScan.view", withWare(s.StartScan))
|
|
||||||
s.mux.HandleFunc("/rest/getScanStatus", withWare(s.GetScanStatus))
|
|
||||||
s.mux.HandleFunc("/rest/getScanStatus.view", withWare(s.GetScanStatus))
|
|
||||||
// browse by tag
|
|
||||||
s.mux.HandleFunc("/rest/getAlbum", withWare(s.GetAlbum))
|
|
||||||
s.mux.HandleFunc("/rest/getAlbum.view", withWare(s.GetAlbum))
|
|
||||||
s.mux.HandleFunc("/rest/getAlbumList2", withWare(s.GetAlbumListTwo))
|
|
||||||
s.mux.HandleFunc("/rest/getAlbumList2.view", withWare(s.GetAlbumListTwo))
|
|
||||||
s.mux.HandleFunc("/rest/getArtist", withWare(s.GetArtist))
|
|
||||||
s.mux.HandleFunc("/rest/getArtist.view", withWare(s.GetArtist))
|
|
||||||
s.mux.HandleFunc("/rest/getArtists", withWare(s.GetArtists))
|
|
||||||
s.mux.HandleFunc("/rest/getArtists.view", withWare(s.GetArtists))
|
|
||||||
s.mux.HandleFunc("/rest/search3", withWare(s.SearchThree))
|
|
||||||
s.mux.HandleFunc("/rest/search3.view", withWare(s.SearchThree))
|
|
||||||
// browse by folder
|
|
||||||
s.mux.HandleFunc("/rest/getIndexes", withWare(s.GetIndexes))
|
|
||||||
s.mux.HandleFunc("/rest/getIndexes.view", withWare(s.GetIndexes))
|
|
||||||
s.mux.HandleFunc("/rest/getMusicDirectory", withWare(s.GetMusicDirectory))
|
|
||||||
s.mux.HandleFunc("/rest/getMusicDirectory.view", withWare(s.GetMusicDirectory))
|
|
||||||
s.mux.HandleFunc("/rest/getAlbumList", withWare(s.GetAlbumList))
|
|
||||||
s.mux.HandleFunc("/rest/getAlbumList.view", withWare(s.GetAlbumList))
|
|
||||||
s.mux.HandleFunc("/rest/search2", withWare(s.SearchTwo))
|
|
||||||
s.mux.HandleFunc("/rest/search2.view", withWare(s.SearchTwo))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user