refactor server startup into jobs
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
run:
|
||||||
|
skip-files:
|
||||||
|
- server/assets/assets_gen.go
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ RUN apk add -U --no-cache \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
sqlite \
|
sqlite \
|
||||||
taglib-dev
|
taglib-dev \
|
||||||
|
alsa-lib-dev
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod .
|
COPY go.mod .
|
||||||
COPY go.sum .
|
COPY go.sum .
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ RUN apk add -U --no-cache \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
sqlite \
|
sqlite \
|
||||||
taglib-dev
|
taglib-dev \
|
||||||
|
alsa-lib-dev
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ the default login is **admin**/**admin**.
|
|||||||
password can then be changed from the web interface
|
password can then be changed from the web interface
|
||||||
|
|
||||||
```
|
```
|
||||||
$ apt install build-essential git sqlite libtag1-dev ffmpeg # for debian like
|
$ apt install build-essential git sqlite libtag1-dev ffmpeg libasound-dev # for debian like
|
||||||
$ pacman -S base-devel git sqlite taglib ffmpeg # for arch like
|
$ pacman -S base-devel git sqlite taglib ffmpeg alsa-lib # for arch like
|
||||||
$ go get go.senan.xyz/gonic/cmd/gonic
|
$ go get go.senan.xyz/gonic/cmd/gonic
|
||||||
$ export PATH=$PATH:$HOME/go/bin
|
$ export PATH=$PATH:$HOME/go/bin
|
||||||
$ gonic -h # or see "configuration options below"
|
$ gonic -h # or see "configuration options below"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
"github.com/oklog/run"
|
||||||
"github.com/peterbourgon/ff"
|
"github.com/peterbourgon/ff"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
@@ -33,10 +34,16 @@ func main() {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("error parsing args: %v\n", err)
|
log.Fatalf("error parsing args: %v\n", err)
|
||||||
}
|
}
|
||||||
|
//
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println(version.VERSION)
|
fmt.Println(version.VERSION)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
log.Printf("starting gonic %s\n", version.VERSION)
|
||||||
|
log.Printf("provided config:\n")
|
||||||
|
set.VisitAll(func(f *flag.Flag) {
|
||||||
|
fmt.Printf("\t%s:\t%s\n", f.Name, f.Value)
|
||||||
|
})
|
||||||
if _, err := os.Stat(*musicPath); os.IsNotExist(err) {
|
if _, err := os.Stat(*musicPath); os.IsNotExist(err) {
|
||||||
log.Fatal("please provide a valid music directory")
|
log.Fatal("please provide a valid music directory")
|
||||||
}
|
}
|
||||||
@@ -50,20 +57,24 @@ func main() {
|
|||||||
log.Fatalf("error opening database: %v\n", err)
|
log.Fatalf("error opening database: %v\n", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
//
|
||||||
proxyPrefixExpr := regexp.MustCompile(`^\/*(.*?)\/*$`)
|
proxyPrefixExpr := regexp.MustCompile(`^\/*(.*?)\/*$`)
|
||||||
*proxyPrefix = proxyPrefixExpr.ReplaceAllString(*proxyPrefix, `/$1`)
|
*proxyPrefix = proxyPrefixExpr.ReplaceAllString(*proxyPrefix, `/$1`)
|
||||||
serverOptions := server.Options{
|
//
|
||||||
DB: db,
|
server := server.New(server.Options{
|
||||||
MusicPath: *musicPath,
|
DB: db,
|
||||||
CachePath: *cachePath,
|
MusicPath: *musicPath,
|
||||||
ListenAddr: *listenAddr,
|
CachePath: *cachePath,
|
||||||
ScanInterval: time.Duration(*scanInterval) * time.Minute,
|
ProxyPrefix: *proxyPrefix,
|
||||||
ProxyPrefix: *proxyPrefix,
|
})
|
||||||
|
var g run.Group
|
||||||
|
g.Add(server.StartJukebox())
|
||||||
|
g.Add(server.StartHTTP(*listenAddr))
|
||||||
|
if *scanInterval > 0 {
|
||||||
|
tickerDur := time.Duration(*scanInterval) * time.Minute
|
||||||
|
g.Add(server.StartScanTicker(tickerDur))
|
||||||
}
|
}
|
||||||
log.Printf("using opts %+v\n", serverOptions)
|
if err := g.Run(); err != nil {
|
||||||
s := server.New(serverOptions)
|
log.Printf("error in job: %v", err)
|
||||||
log.Printf("starting server at %s", *listenAddr)
|
|
||||||
if err := s.Start(); err != nil {
|
|
||||||
log.Fatalf("error starting server: %v\n", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/karrick/godirwalk v1.15.2
|
github.com/karrick/godirwalk v1.15.2
|
||||||
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/oklog/run v1.1.0
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
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
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -138,6 +138,8 @@ github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuN
|
|||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI=
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI=
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE=
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE=
|
||||||
|
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||||
|
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
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=
|
||||||
@@ -213,6 +215,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ type Scanner struct {
|
|||||||
seenTracksErr int // n tracks we we couldn't scan
|
seenTracksErr int // n tracks we we couldn't scan
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(db *db.DB, musicPath string) *Scanner {
|
func New(musicPath string, db *db.DB) *Scanner {
|
||||||
return &Scanner{
|
return &Scanner{
|
||||||
db: db,
|
db: db,
|
||||||
musicPath: musicPath,
|
musicPath: musicPath,
|
||||||
|
|||||||
@@ -83,26 +83,26 @@ type Controller struct {
|
|||||||
sessDB *gormstore.Store
|
sessDB *gormstore.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(base *ctrlbase.Controller) *Controller {
|
func New(b *ctrlbase.Controller) *Controller {
|
||||||
sessionKey := []byte(base.DB.GetSetting("session_key"))
|
sessionKey := []byte(b.DB.GetSetting("session_key"))
|
||||||
if len(sessionKey) == 0 {
|
if len(sessionKey) == 0 {
|
||||||
sessionKey = securecookie.GenerateRandomKey(32)
|
sessionKey = securecookie.GenerateRandomKey(32)
|
||||||
base.DB.SetSetting("session_key", string(sessionKey))
|
b.DB.SetSetting("session_key", string(sessionKey))
|
||||||
}
|
}
|
||||||
tmplBase := template.
|
tmplBase := template.
|
||||||
New("layout").
|
New("layout").
|
||||||
Funcs(sprig.FuncMap()).
|
Funcs(sprig.FuncMap()).
|
||||||
Funcs(funcMap()). // static
|
Funcs(funcMap()). // static
|
||||||
Funcs(template.FuncMap{ // from base
|
Funcs(template.FuncMap{ // from base
|
||||||
"path": base.Path,
|
"path": b.Path,
|
||||||
})
|
})
|
||||||
tmplBase = extendFromPaths(tmplBase, prefixPartials)
|
tmplBase = extendFromPaths(tmplBase, prefixPartials)
|
||||||
tmplBase = extendFromPaths(tmplBase, prefixLayouts)
|
tmplBase = extendFromPaths(tmplBase, prefixLayouts)
|
||||||
sessDB := gormstore.New(base.DB.DB, sessionKey)
|
sessDB := gormstore.New(b.DB.DB, sessionKey)
|
||||||
sessDB.SessionOpts.HttpOnly = true
|
sessDB.SessionOpts.HttpOnly = true
|
||||||
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
||||||
return &Controller{
|
return &Controller{
|
||||||
Controller: base,
|
Controller: b,
|
||||||
buffPool: bpool.NewBufferPool(64),
|
buffPool: bpool.NewBufferPool(64),
|
||||||
templates: pagesFromPaths(tmplBase, prefixPages),
|
templates: pagesFromPaths(tmplBase, prefixPages),
|
||||||
sessDB: sessDB,
|
sessDB: sessDB,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/jukebox"
|
|
||||||
"go.senan.xyz/gonic/scanner"
|
"go.senan.xyz/gonic/scanner"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +49,6 @@ type Controller struct {
|
|||||||
MusicPath string
|
MusicPath string
|
||||||
Scanner *scanner.Scanner
|
Scanner *scanner.Scanner
|
||||||
ProxyPrefix string
|
ProxyPrefix string
|
||||||
Jukebox *jukebox.Jukebox
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path returns a URL path with the proxy prefix included
|
// Path returns a URL path with the proxy prefix included
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
|
"go.senan.xyz/gonic/server/jukebox"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CtxKey int
|
type CtxKey int
|
||||||
@@ -24,14 +25,8 @@ const (
|
|||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
*ctrlbase.Controller
|
*ctrlbase.Controller
|
||||||
cachePath string
|
CachePath string
|
||||||
}
|
Jukebox *jukebox.Jukebox
|
||||||
|
|
||||||
func New(base *ctrlbase.Controller, cachePath string) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
Controller: base,
|
|
||||||
cachePath: cachePath,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type metaResponse struct {
|
type metaResponse struct {
|
||||||
|
|||||||
@@ -317,22 +317,23 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
|||||||
|
|
||||||
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
|
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
|
||||||
params := r.Context().Value(CtxParams).(params.Params)
|
params := r.Context().Value(CtxParams).(params.Params)
|
||||||
switch params.Get("action") {
|
getTracks := func() []*db.Track {
|
||||||
case "set":
|
|
||||||
var tracks []*db.Track
|
var tracks []*db.Track
|
||||||
ids := params.GetFirstListInt("id")
|
ids := params.GetFirstListInt("id")
|
||||||
if len(ids) == 0 {
|
|
||||||
c.Jukebox.ClearTracks()
|
|
||||||
}
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
track := &db.Track{}
|
track := &db.Track{}
|
||||||
err := c.DB.Preload("Album").First(track, id).Error
|
c.DB.Preload("Album").First(track, id)
|
||||||
if err != nil {
|
if track.ID != 0 {
|
||||||
return spec.NewError(10, "couldn't find tracks with provided ids")
|
tracks = append(tracks, track)
|
||||||
}
|
}
|
||||||
tracks = append(tracks, track)
|
|
||||||
}
|
}
|
||||||
c.Jukebox.SetTracks(tracks)
|
return tracks
|
||||||
|
}
|
||||||
|
switch act := params.Get("action"); act {
|
||||||
|
case "set":
|
||||||
|
c.Jukebox.SetTracks(getTracks())
|
||||||
|
case "add":
|
||||||
|
c.Jukebox.AddTracks(getTracks())
|
||||||
case "clear":
|
case "clear":
|
||||||
c.Jukebox.ClearTracks()
|
c.Jukebox.ClearTracks()
|
||||||
case "remove":
|
case "remove":
|
||||||
@@ -356,8 +357,10 @@ func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
|
|||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.JukeboxPlaylist = c.Jukebox.GetTracks()
|
sub.JukeboxPlaylist = c.Jukebox.GetTracks()
|
||||||
return sub
|
return sub
|
||||||
|
default:
|
||||||
|
return spec.NewError(10, "unknown value `%s` for parameter 'action'", act)
|
||||||
}
|
}
|
||||||
// All actions except get are expected to return a status
|
// all actions except get are expected to return a status
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.JukeboxStatus = c.Jukebox.Status()
|
sub.JukeboxStatus = c.Jukebox.Status()
|
||||||
return sub
|
return sub
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
|||||||
}
|
}
|
||||||
servOpts.pref = pref
|
servOpts.pref = pref
|
||||||
servOpts.maxBitrate = params.GetIntOr("maxBitRate", 0)
|
servOpts.maxBitrate = params.GetIntOr("maxBitRate", 0)
|
||||||
servOpts.cachePath = c.cachePath
|
servOpts.cachePath = c.CachePath
|
||||||
serveTrackEncode(w, r, servOpts)
|
serveTrackEncode(w, r, servOpts)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package jukebox
|
package jukebox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"github.com/faiface/beep/flac"
|
"github.com/faiface/beep/flac"
|
||||||
"github.com/faiface/beep/mp3"
|
"github.com/faiface/beep/mp3"
|
||||||
"github.com/faiface/beep/speaker"
|
"github.com/faiface/beep/speaker"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
)
|
)
|
||||||
@@ -21,90 +23,142 @@ type strmInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Jukebox struct {
|
type Jukebox struct {
|
||||||
playlist []*db.Track
|
playlist []*db.Track
|
||||||
index int
|
musicPath string
|
||||||
playing bool
|
index int
|
||||||
|
playing bool
|
||||||
|
sr beep.SampleRate
|
||||||
// used to notify the player to re read the members
|
// used to notify the player to re read the members
|
||||||
updates chan struct{}
|
updates chan struct{}
|
||||||
|
quit chan struct{}
|
||||||
done chan bool
|
done chan bool
|
||||||
info *strmInfo
|
info *strmInfo
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Init(musicPath string) error {
|
func New(musicPath string) *Jukebox {
|
||||||
j.updates = make(chan struct{})
|
return &Jukebox{
|
||||||
sr := beep.SampleRate(48000)
|
musicPath: musicPath,
|
||||||
err := speaker.Init(sr, sr.N(time.Second/2))
|
sr: beep.SampleRate(48000),
|
||||||
if err != nil {
|
updates: make(chan struct{}),
|
||||||
return err
|
done: make(chan bool),
|
||||||
|
quit: make(chan struct{}),
|
||||||
}
|
}
|
||||||
j.done = make(chan bool)
|
}
|
||||||
go func() {
|
|
||||||
for range j.updates {
|
func (j *Jukebox) Listen() error {
|
||||||
var streamer beep.Streamer
|
if err := speaker.Init(j.sr, j.sr.N(time.Second/2)); err != nil {
|
||||||
var format beep.Format
|
return fmt.Errorf("initing speaker: %w", err)
|
||||||
f, err := os.Open(path.Join(musicPath, j.playlist[j.index].RelPath()))
|
}
|
||||||
if err != nil {
|
for {
|
||||||
j.index++
|
select {
|
||||||
continue
|
case <-j.quit:
|
||||||
}
|
return nil
|
||||||
switch j.playlist[j.index].Ext() {
|
case <-j.updates:
|
||||||
case "mp3":
|
j.doUpdate()
|
||||||
streamer, format, err = mp3.Decode(f)
|
|
||||||
case "flac":
|
|
||||||
streamer, format, err = flac.Decode(f)
|
|
||||||
default:
|
|
||||||
j.index++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
j.index++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if j.playing {
|
|
||||||
j.Lock()
|
|
||||||
{
|
|
||||||
j.info = &strmInfo{}
|
|
||||||
j.info.strm = streamer.(beep.StreamSeekCloser)
|
|
||||||
j.info.ctrlStrmr.Streamer = beep.Resample(4, format.SampleRate, sr, j.info.strm)
|
|
||||||
j.info.format = format
|
|
||||||
}
|
|
||||||
j.Unlock()
|
|
||||||
speaker.Play(beep.Seq(&j.info.ctrlStrmr, beep.Callback(func() {
|
|
||||||
j.done <- false
|
|
||||||
})))
|
|
||||||
if v := <-j.done; !v {
|
|
||||||
j.index++
|
|
||||||
j.Lock()
|
|
||||||
if j.index > len(j.playlist) {
|
|
||||||
j.index = 0
|
|
||||||
j.playing = false
|
|
||||||
}
|
|
||||||
j.Unlock()
|
|
||||||
// in a go routine as otherwise this hangs as the
|
|
||||||
go func() {
|
|
||||||
j.updates <- struct{}{}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) Quit() {
|
||||||
|
j.quit <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) doUpdate() {
|
||||||
|
var streamer beep.Streamer
|
||||||
|
var format beep.Format
|
||||||
|
if j.index >= len(j.playlist) {
|
||||||
|
j.Lock()
|
||||||
|
j.index = 0
|
||||||
|
j.playing = false
|
||||||
|
j.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.Lock()
|
||||||
|
f, err := os.Open(path.Join(
|
||||||
|
j.musicPath,
|
||||||
|
j.playlist[j.index].RelPath(),
|
||||||
|
))
|
||||||
|
j.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
j.incIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch j.playlist[j.index].Ext() {
|
||||||
|
case "mp3":
|
||||||
|
streamer, format, err = mp3.Decode(f)
|
||||||
|
case "flac":
|
||||||
|
streamer, format, err = flac.Decode(f)
|
||||||
|
default:
|
||||||
|
j.incIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
j.incIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if j.playing {
|
||||||
|
j.Lock()
|
||||||
|
{
|
||||||
|
j.info = &strmInfo{}
|
||||||
|
j.info.strm = streamer.(beep.StreamSeekCloser)
|
||||||
|
j.info.ctrlStrmr.Streamer = beep.Resample(
|
||||||
|
4, format.SampleRate,
|
||||||
|
j.sr, j.info.strm,
|
||||||
|
)
|
||||||
|
j.info.format = format
|
||||||
|
}
|
||||||
|
j.Unlock()
|
||||||
|
speaker.Play(beep.Seq(&j.info.ctrlStrmr, beep.Callback(func() {
|
||||||
|
j.done <- false
|
||||||
|
})))
|
||||||
|
if v := <-j.done; v {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.Lock()
|
||||||
|
j.index++
|
||||||
|
if j.index >= len(j.playlist) {
|
||||||
|
j.index = 0
|
||||||
|
j.playing = false
|
||||||
|
j.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.Unlock()
|
||||||
|
// in a go routine as otherwise this hangs as the
|
||||||
|
go func() {
|
||||||
|
j.updates <- struct{}{}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) incIndex() {
|
||||||
|
j.Lock()
|
||||||
|
defer j.Unlock()
|
||||||
|
j.index++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) SetTracks(tracks []*db.Track) {
|
func (j *Jukebox) SetTracks(tracks []*db.Track) {
|
||||||
j.Lock()
|
j.Lock()
|
||||||
|
defer j.Unlock()
|
||||||
j.index = 0
|
j.index = 0
|
||||||
j.playing = true
|
if len(tracks) == 0 {
|
||||||
if len(j.playlist) > 0 {
|
if j.playing {
|
||||||
j.done <- true
|
j.done <- true
|
||||||
|
}
|
||||||
|
j.playing = false
|
||||||
j.playlist = []*db.Track{}
|
j.playlist = []*db.Track{}
|
||||||
speaker.Clear()
|
speaker.Clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if j.playing {
|
||||||
|
j.playlist = tracks
|
||||||
|
j.done <- true
|
||||||
|
speaker.Clear()
|
||||||
|
j.updates <- struct{}{}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
j.playlist = tracks
|
j.playlist = tracks
|
||||||
j.Unlock()
|
j.playing = true
|
||||||
j.updates <- struct{}{}
|
j.updates <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,20 +227,25 @@ func (j *Jukebox) Stop() {
|
|||||||
|
|
||||||
func (j *Jukebox) Start() {
|
func (j *Jukebox) Start() {
|
||||||
j.Lock()
|
j.Lock()
|
||||||
j.playing = false
|
j.playing = true
|
||||||
j.info.ctrlStrmr.Paused = false
|
j.info.ctrlStrmr.Paused = false
|
||||||
j.Unlock()
|
j.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Skip(i int, skipCurrent bool) {
|
func (j *Jukebox) Skip(i int, skipCurrent bool) {
|
||||||
j.Lock()
|
j.Lock()
|
||||||
|
defer j.Unlock()
|
||||||
|
if i == j.index {
|
||||||
|
return
|
||||||
|
}
|
||||||
if skipCurrent {
|
if skipCurrent {
|
||||||
j.index++
|
j.index++
|
||||||
} else {
|
} else {
|
||||||
j.index = i
|
j.index = i
|
||||||
}
|
}
|
||||||
speaker.Clear()
|
speaker.Clear()
|
||||||
j.done <- true
|
if j.playing {
|
||||||
|
j.done <- true
|
||||||
|
}
|
||||||
j.updates <- struct{}{}
|
j.updates <- struct{}{}
|
||||||
j.Unlock()
|
|
||||||
}
|
}
|
||||||
105
server/server.go
105
server/server.go
@@ -11,27 +11,25 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/jukebox"
|
|
||||||
"go.senan.xyz/gonic/scanner"
|
"go.senan.xyz/gonic/scanner"
|
||||||
"go.senan.xyz/gonic/server/assets"
|
"go.senan.xyz/gonic/server/assets"
|
||||||
"go.senan.xyz/gonic/server/ctrladmin"
|
"go.senan.xyz/gonic/server/ctrladmin"
|
||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
||||||
|
"go.senan.xyz/gonic/server/jukebox"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
DB *db.DB
|
DB *db.DB
|
||||||
MusicPath string
|
MusicPath string
|
||||||
CachePath string
|
CachePath string
|
||||||
ListenAddr string
|
ProxyPrefix string
|
||||||
ScanInterval time.Duration
|
|
||||||
ProxyPrefix string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*http.Server
|
scanner *scanner.Scanner
|
||||||
scanner *scanner.Scanner
|
jukebox *jukebox.Jukebox
|
||||||
scanInterval time.Duration
|
router *mux.Router
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(opts Options) *Server {
|
func New(opts Options) *Server {
|
||||||
@@ -39,7 +37,8 @@ func New(opts Options) *Server {
|
|||||||
opts.MusicPath = filepath.Clean(opts.MusicPath)
|
opts.MusicPath = filepath.Clean(opts.MusicPath)
|
||||||
opts.CachePath = filepath.Clean(opts.CachePath)
|
opts.CachePath = filepath.Clean(opts.CachePath)
|
||||||
// ** begin controllers
|
// ** begin controllers
|
||||||
scanner := scanner.New(opts.DB, opts.MusicPath)
|
scanner := scanner.New(opts.MusicPath, opts.DB)
|
||||||
|
jukebox := jukebox.New(opts.MusicPath)
|
||||||
// the base controller, it's fields/middlewares are embedded/used by the
|
// the base controller, it's fields/middlewares are embedded/used by the
|
||||||
// other two admin ui and subsonic controllers
|
// other two admin ui and subsonic controllers
|
||||||
base := &ctrlbase.Controller{
|
base := &ctrlbase.Controller{
|
||||||
@@ -47,30 +46,25 @@ func New(opts Options) *Server {
|
|||||||
MusicPath: opts.MusicPath,
|
MusicPath: opts.MusicPath,
|
||||||
ProxyPrefix: opts.ProxyPrefix,
|
ProxyPrefix: opts.ProxyPrefix,
|
||||||
Scanner: scanner,
|
Scanner: scanner,
|
||||||
Jukebox: &jukebox.Jukebox{},
|
|
||||||
}
|
}
|
||||||
base.Jukebox.Init(opts.MusicPath)
|
|
||||||
// router with common wares for admin / subsonic
|
// router with common wares for admin / subsonic
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.Use(base.WithLogging)
|
r.Use(base.WithLogging)
|
||||||
r.Use(base.WithCORS)
|
r.Use(base.WithCORS)
|
||||||
setupMisc(r, base)
|
ctrlAdmin := ctrladmin.New(base)
|
||||||
setupAdminRouter := r.PathPrefix("/admin").Subrouter()
|
ctrlSubsonic := &ctrlsubsonic.Controller{
|
||||||
setupAdmin(setupAdminRouter, ctrladmin.New(base))
|
Controller: base,
|
||||||
setupSubsonicRouter := r.PathPrefix("/rest").Subrouter()
|
CachePath: opts.CachePath,
|
||||||
setupSubsonic(setupSubsonicRouter, ctrlsubsonic.New(base, opts.CachePath))
|
Jukebox: jukebox,
|
||||||
//
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: opts.ListenAddr,
|
|
||||||
Handler: r,
|
|
||||||
ReadTimeout: 5 * time.Second,
|
|
||||||
WriteTimeout: 80 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
}
|
}
|
||||||
|
setupMisc(r, base)
|
||||||
|
setupAdmin(r.PathPrefix("/admin").Subrouter(), ctrlAdmin)
|
||||||
|
setupSubsonic(r.PathPrefix("/rest").Subrouter(), ctrlSubsonic)
|
||||||
|
//
|
||||||
return &Server{
|
return &Server{
|
||||||
Server: server,
|
scanner: scanner,
|
||||||
scanner: scanner,
|
jukebox: jukebox,
|
||||||
scanInterval: opts.ScanInterval,
|
router: r,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,17 +177,54 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
|||||||
r.NotFoundHandler = notFoundRoute.GetHandler()
|
r.NotFoundHandler = notFoundRoute.GetHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
type funcExecute func() error
|
||||||
if s.scanInterval > 0 {
|
type funcInterrupt func(error)
|
||||||
log.Printf("will be scanning at intervals of %s", s.scanInterval)
|
|
||||||
ticker := time.NewTicker(s.scanInterval)
|
func (s *Server) StartHTTP(listenAddr string) (funcExecute, funcInterrupt) {
|
||||||
go func() {
|
log.Print("starting job 'http'\n")
|
||||||
for range ticker.C {
|
list := &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
Handler: s.router,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 80 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
execute := func() error {
|
||||||
|
return list.ListenAndServe()
|
||||||
|
}
|
||||||
|
return execute, func(_ error) {
|
||||||
|
list.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) StartScanTicker(dur time.Duration) (funcExecute, funcInterrupt) {
|
||||||
|
log.Printf("starting job 'scan timer'\n")
|
||||||
|
ticker := time.NewTicker(dur)
|
||||||
|
done := make(chan struct{})
|
||||||
|
execute := func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
if err := s.scanner.Start(); err != nil {
|
if err := s.scanner.Start(); err != nil {
|
||||||
log.Printf("error while scanner: %v", err)
|
log.Printf("error scanning: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
}
|
||||||
|
return execute, func(_ error) {
|
||||||
|
ticker.Stop()
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) StartJukebox() (funcExecute, funcInterrupt) {
|
||||||
|
log.Printf("starting job 'jukebox'\n")
|
||||||
|
execute := func() error {
|
||||||
|
return s.jukebox.Listen()
|
||||||
|
}
|
||||||
|
return execute, func(_ error) {
|
||||||
|
s.jukebox.Quit()
|
||||||
}
|
}
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user