@@ -1,6 +1,6 @@
|
|||||||
// Package main is the gonic server entrypoint
|
// Package main is the gonic server entrypoint
|
||||||
//
|
//
|
||||||
//nolint:lll // flags help strings
|
//nolint:lll,gocyclo
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -16,19 +17,27 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
"github.com/oklog/run"
|
"github.com/oklog/run"
|
||||||
"github.com/peterbourgon/ff"
|
"github.com/peterbourgon/ff"
|
||||||
|
"github.com/sentriz/gormstore"
|
||||||
|
|
||||||
"go.senan.xyz/gonic"
|
"go.senan.xyz/gonic"
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
|
"go.senan.xyz/gonic/jukebox"
|
||||||
|
"go.senan.xyz/gonic/playlist"
|
||||||
|
"go.senan.xyz/gonic/podcasts"
|
||||||
"go.senan.xyz/gonic/scanner"
|
"go.senan.xyz/gonic/scanner"
|
||||||
"go.senan.xyz/gonic/server"
|
"go.senan.xyz/gonic/scanner/tags"
|
||||||
|
"go.senan.xyz/gonic/scrobble"
|
||||||
|
"go.senan.xyz/gonic/scrobble/lastfm"
|
||||||
|
"go.senan.xyz/gonic/scrobble/listenbrainz"
|
||||||
|
"go.senan.xyz/gonic/server/ctrladmin"
|
||||||
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
||||||
)
|
"go.senan.xyz/gonic/transcode"
|
||||||
|
|
||||||
const (
|
|
||||||
cleanTimeDuration = 10 * time.Minute
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -146,26 +155,6 @@ func main() {
|
|||||||
*deprecatedConfGenreSplit = "<deprecated>"
|
*deprecatedConfGenreSplit = "<deprecated>"
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := server.New(server.Options{
|
|
||||||
DB: dbc,
|
|
||||||
MusicPaths: musicPaths,
|
|
||||||
ExcludePattern: *confExcludePatterns,
|
|
||||||
CacheAudioPath: cacheDirAudio,
|
|
||||||
CoverCachePath: cacheDirCovers,
|
|
||||||
PodcastPath: *confPodcastPath,
|
|
||||||
PlaylistsPath: *confPlaylistsPath,
|
|
||||||
ProxyPrefix: *confProxyPrefix,
|
|
||||||
MultiValueSettings: map[scanner.Tag]scanner.MultiValueSetting{
|
|
||||||
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
|
|
||||||
scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist),
|
|
||||||
},
|
|
||||||
HTTPLog: *confHTTPLog,
|
|
||||||
JukeboxEnabled: *confJukeboxEnabled,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("error creating server: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("starting gonic v%s\n", gonic.Version)
|
log.Printf("starting gonic v%s\n", gonic.Version)
|
||||||
log.Printf("provided config\n")
|
log.Printf("provided config\n")
|
||||||
set.VisitAll(func(f *flag.Flag) {
|
set.VisitAll(func(f *flag.Flag) {
|
||||||
@@ -173,26 +162,177 @@ func main() {
|
|||||||
log.Printf(" %-25s %s\n", f.Name, value)
|
log.Printf(" %-25s %s\n", f.Name, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
var g run.Group
|
tagger := &tags.TagReader{}
|
||||||
g.Add(server.StartHTTP(*confListenAddr, *confTLSCert, *confTLSKey))
|
scannr := scanner.New(
|
||||||
g.Add(server.StartSessionClean(cleanTimeDuration))
|
ctrlsubsonic.PathsOf(musicPaths),
|
||||||
g.Add(server.StartPodcastRefresher(time.Hour))
|
dbc,
|
||||||
if *confScanIntervalMins > 0 {
|
map[scanner.Tag]scanner.MultiValueSetting{
|
||||||
tickerDur := time.Duration(*confScanIntervalMins) * time.Minute
|
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
|
||||||
g.Add(server.StartScanTicker(tickerDur))
|
scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist),
|
||||||
}
|
},
|
||||||
if *confScanWatcher {
|
tagger,
|
||||||
g.Add(server.StartScanWatcher())
|
*confExcludePatterns,
|
||||||
|
)
|
||||||
|
podcast := podcasts.New(dbc, *confPodcastPath, tagger)
|
||||||
|
transcoder := transcode.NewCachingTranscoder(
|
||||||
|
transcode.NewFFmpegTranscoder(),
|
||||||
|
cacheDirAudio,
|
||||||
|
)
|
||||||
|
lastfmClient := lastfm.NewClient()
|
||||||
|
playlistStore, err := playlist.NewStore(*confPlaylistsPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("error creating playlists store: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jukebx *jukebox.Jukebox
|
||||||
if *confJukeboxEnabled {
|
if *confJukeboxEnabled {
|
||||||
|
jukebx = jukebox.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
sessKey, err := dbc.GetSetting("session_key")
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("error getting session key: %v\n", err)
|
||||||
|
}
|
||||||
|
if sessKey == "" {
|
||||||
|
if err := dbc.SetSetting("session_key", string(securecookie.GenerateRandomKey(32))); err != nil {
|
||||||
|
log.Panicf("error setting session key: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessDB := gormstore.New(dbc.DB, []byte(sessKey))
|
||||||
|
sessDB.SessionOpts.HttpOnly = true
|
||||||
|
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
||||||
|
|
||||||
|
ctrlBase := &ctrlbase.Controller{
|
||||||
|
DB: dbc,
|
||||||
|
PlaylistStore: playlistStore,
|
||||||
|
ProxyPrefix: *confProxyPrefix,
|
||||||
|
Scanner: scannr,
|
||||||
|
}
|
||||||
|
ctrlAdmin, err := ctrladmin.New(ctrlBase, sessDB, podcast, lastfmClient)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("error creating admin controller: %v\n", err)
|
||||||
|
}
|
||||||
|
ctrlSubsonic := &ctrlsubsonic.Controller{
|
||||||
|
Controller: ctrlBase,
|
||||||
|
MusicPaths: musicPaths,
|
||||||
|
PodcastsPath: *confPodcastPath,
|
||||||
|
CacheAudioPath: cacheDirAudio,
|
||||||
|
CacheCoverPath: cacheDirCovers,
|
||||||
|
LastFMClient: lastfmClient,
|
||||||
|
Scrobblers: []scrobble.Scrobbler{
|
||||||
|
lastfm.NewScrobbler(dbc, lastfmClient),
|
||||||
|
listenbrainz.NewScrobbler(),
|
||||||
|
},
|
||||||
|
Podcasts: podcast,
|
||||||
|
Transcoder: transcoder,
|
||||||
|
Jukebox: jukebx,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := mux.NewRouter()
|
||||||
|
ctrlbase.AddRoutes(ctrlBase, mux, *confHTTPLog)
|
||||||
|
ctrladmin.AddRoutes(ctrlAdmin, mux.PathPrefix("/admin").Subrouter())
|
||||||
|
ctrlsubsonic.AddRoutes(ctrlSubsonic, mux.PathPrefix("/rest").Subrouter())
|
||||||
|
|
||||||
|
var g run.Group
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Print("starting job 'http'\n")
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: *confListenAddr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 80 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
if *confTLSCert != "" && *confTLSKey != "" {
|
||||||
|
return server.ListenAndServeTLS(*confTLSCert, *confTLSKey)
|
||||||
|
}
|
||||||
|
return server.ListenAndServe()
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'session clean'\n")
|
||||||
|
ticker := time.NewTicker(10 * time.Minute)
|
||||||
|
for range ticker.C {
|
||||||
|
sessDB.Cleanup()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'podcast refresher'\n")
|
||||||
|
ticker := time.NewTicker(time.Hour)
|
||||||
|
for range ticker.C {
|
||||||
|
if err := podcast.RefreshPodcasts(); err != nil {
|
||||||
|
log.Printf("failed to refresh some feeds: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'podcast purger'\n")
|
||||||
|
ticker := time.NewTicker(24 * time.Hour)
|
||||||
|
for range ticker.C {
|
||||||
|
if err := podcast.PurgeOldPodcasts(time.Duration(*confPodcastPurgeAgeDays) * 24 * time.Hour); err != nil {
|
||||||
|
log.Printf("error purging old podcasts: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if *confScanIntervalMins > 0 {
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'scan timer'\n")
|
||||||
|
ticker := time.NewTicker(time.Duration(*confScanIntervalMins) * time.Minute)
|
||||||
|
for range ticker.C {
|
||||||
|
if _, err := scannr.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
||||||
|
log.Printf("error scanning: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *confScanWatcher {
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'scan watcher'\n")
|
||||||
|
return scannr.ExecuteWatch()
|
||||||
|
}, func(_ error) {
|
||||||
|
scannr.CancelWatch()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if jukebx != nil {
|
||||||
|
var jukeboxTempDir string
|
||||||
|
g.Add(func() error {
|
||||||
|
log.Printf("starting job 'jukebox'\n")
|
||||||
extraArgs, _ := shlex.Split(*confJukeboxMPVExtraArgs)
|
extraArgs, _ := shlex.Split(*confJukeboxMPVExtraArgs)
|
||||||
g.Add(server.StartJukebox(extraArgs))
|
var err error
|
||||||
|
jukeboxTempDir, err = os.MkdirTemp("", "gonic-jukebox-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create tmp sock file: %w", err)
|
||||||
}
|
}
|
||||||
if *confPodcastPurgeAgeDays > 0 {
|
sockPath := filepath.Join(jukeboxTempDir, "sock")
|
||||||
g.Add(server.StartPodcastPurger(time.Duration(*confPodcastPurgeAgeDays) * 24 * time.Hour))
|
if err := jukebx.Start(sockPath, extraArgs); err != nil {
|
||||||
|
return fmt.Errorf("start jukebox: %w", err)
|
||||||
}
|
}
|
||||||
|
if err := jukebx.Wait(); err != nil {
|
||||||
|
return fmt.Errorf("start jukebox: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, func(_ error) {
|
||||||
|
if err := jukebx.Quit(); err != nil {
|
||||||
|
log.Printf("error quitting jukebox: %v", err)
|
||||||
|
}
|
||||||
|
_ = os.RemoveAll(jukeboxTempDir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if *confScanAtStart {
|
if *confScanAtStart {
|
||||||
server.ScanAtStart()
|
if _, err := scannr.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
||||||
|
log.Panicf("error scanning at start: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.Run(); err != nil {
|
if err := g.Run(); err != nil {
|
||||||
|
|||||||
62
server/ctrladmin/routes.go
Normal file
62
server/ctrladmin/routes.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package ctrladmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddRoutes(c *Controller, r *mux.Router) {
|
||||||
|
// public routes (creates session)
|
||||||
|
r.Use(c.WithSession)
|
||||||
|
r.Handle("/login", c.H(c.ServeLogin))
|
||||||
|
r.Handle("/login_do", c.HR(c.ServeLoginDo)) // "raw" handler, updates session
|
||||||
|
|
||||||
|
staticHandler := http.StripPrefix("/admin", http.FileServer(http.FS(adminui.StaticFS)))
|
||||||
|
r.PathPrefix("/static").Handler(staticHandler)
|
||||||
|
|
||||||
|
// user routes (if session is valid)
|
||||||
|
routUser := r.NewRoute().Subrouter()
|
||||||
|
routUser.Use(c.WithUserSession)
|
||||||
|
routUser.Handle("/logout", c.HR(c.ServeLogout)) // "raw" handler, updates session
|
||||||
|
routUser.Handle("/home", c.H(c.ServeHome))
|
||||||
|
routUser.Handle("/change_username", c.H(c.ServeChangeUsername))
|
||||||
|
routUser.Handle("/change_username_do", c.H(c.ServeChangeUsernameDo))
|
||||||
|
routUser.Handle("/change_password", c.H(c.ServeChangePassword))
|
||||||
|
routUser.Handle("/change_password_do", c.H(c.ServeChangePasswordDo))
|
||||||
|
routUser.Handle("/change_avatar", c.H(c.ServeChangeAvatar))
|
||||||
|
routUser.Handle("/change_avatar_do", c.H(c.ServeChangeAvatarDo))
|
||||||
|
routUser.Handle("/delete_avatar_do", c.H(c.ServeDeleteAvatarDo))
|
||||||
|
routUser.Handle("/delete_user", c.H(c.ServeDeleteUser))
|
||||||
|
routUser.Handle("/delete_user_do", c.H(c.ServeDeleteUserDo))
|
||||||
|
routUser.Handle("/link_lastfm_do", c.H(c.ServeLinkLastFMDo))
|
||||||
|
routUser.Handle("/unlink_lastfm_do", c.H(c.ServeUnlinkLastFMDo))
|
||||||
|
routUser.Handle("/link_listenbrainz_do", c.H(c.ServeLinkListenBrainzDo))
|
||||||
|
routUser.Handle("/unlink_listenbrainz_do", c.H(c.ServeUnlinkListenBrainzDo))
|
||||||
|
routUser.Handle("/create_transcode_pref_do", c.H(c.ServeCreateTranscodePrefDo))
|
||||||
|
routUser.Handle("/delete_transcode_pref_do", c.H(c.ServeDeleteTranscodePrefDo))
|
||||||
|
|
||||||
|
// admin routes (if session is valid, and is admin)
|
||||||
|
routAdmin := routUser.NewRoute().Subrouter()
|
||||||
|
routAdmin.Use(c.WithAdminSession)
|
||||||
|
routAdmin.Handle("/create_user", c.H(c.ServeCreateUser))
|
||||||
|
routAdmin.Handle("/create_user_do", c.H(c.ServeCreateUserDo))
|
||||||
|
routAdmin.Handle("/update_lastfm_api_key", c.H(c.ServeUpdateLastFMAPIKey))
|
||||||
|
routAdmin.Handle("/update_lastfm_api_key_do", c.H(c.ServeUpdateLastFMAPIKeyDo))
|
||||||
|
routAdmin.Handle("/start_scan_inc_do", c.H(c.ServeStartScanIncDo))
|
||||||
|
routAdmin.Handle("/start_scan_full_do", c.H(c.ServeStartScanFullDo))
|
||||||
|
routAdmin.Handle("/add_podcast_do", c.H(c.ServePodcastAddDo))
|
||||||
|
routAdmin.Handle("/delete_podcast_do", c.H(c.ServePodcastDeleteDo))
|
||||||
|
routAdmin.Handle("/download_podcast_do", c.H(c.ServePodcastDownloadDo))
|
||||||
|
routAdmin.Handle("/update_podcast_do", c.H(c.ServePodcastUpdateDo))
|
||||||
|
routAdmin.Handle("/add_internet_radio_station_do", c.H(c.ServeInternetRadioStationAddDo))
|
||||||
|
routAdmin.Handle("/delete_internet_radio_station_do", c.H(c.ServeInternetRadioStationDeleteDo))
|
||||||
|
routAdmin.Handle("/update_internet_radio_station_do", c.H(c.ServeInternetRadioStationUpdateDo))
|
||||||
|
|
||||||
|
// middlewares should be run for not found handler
|
||||||
|
// https://github.com/gorilla/mux/issues/416
|
||||||
|
notFoundHandler := c.H(c.ServeNotFound)
|
||||||
|
notFoundRoute := r.NewRoute().Handler(notFoundHandler)
|
||||||
|
r.NotFoundHandler = notFoundRoute.GetHandler()
|
||||||
|
}
|
||||||
34
server/ctrlbase/routes.go
Normal file
34
server/ctrlbase/routes.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package ctrlbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddRoutes(c *Controller, r *mux.Router, logHTTP bool) {
|
||||||
|
if logHTTP {
|
||||||
|
r.Use(c.WithLogging)
|
||||||
|
}
|
||||||
|
r.Use(c.WithCORS)
|
||||||
|
r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
|
||||||
|
|
||||||
|
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
adminHome := c.Path("/admin/home")
|
||||||
|
http.Redirect(w, r, adminHome, http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
// misc subsonic routes without /rest prefix
|
||||||
|
r.HandleFunc("/settings.view", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
adminHome := c.Path("/admin/home")
|
||||||
|
http.Redirect(w, r, adminHome, http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
r.HandleFunc("/musicFolderSettings.view", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
restScan := c.Path(fmt.Sprintf("/rest/startScan.view?%s", r.URL.Query().Encode()))
|
||||||
|
http.Redirect(w, r, restScan, http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, "OK")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ type Controller struct {
|
|||||||
MusicPaths []MusicPath
|
MusicPaths []MusicPath
|
||||||
PodcastsPath string
|
PodcastsPath string
|
||||||
CacheAudioPath string
|
CacheAudioPath string
|
||||||
CoverCachePath string
|
CacheCoverPath string
|
||||||
Jukebox *jukebox.Jukebox
|
Jukebox *jukebox.Jukebox
|
||||||
Scrobblers []scrobble.Scrobbler
|
Scrobblers []scrobble.Scrobbler
|
||||||
Podcasts *podcasts.Podcasts
|
Podcasts *podcasts.Podcasts
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
|
|||||||
}
|
}
|
||||||
size := params.GetOrInt("size", coverDefaultSize)
|
size := params.GetOrInt("size", coverDefaultSize)
|
||||||
cachePath := path.Join(
|
cachePath := path.Join(
|
||||||
c.CoverCachePath,
|
c.CacheCoverPath,
|
||||||
fmt.Sprintf("%s-%d.%s", id.String(), size, coverCacheFormat),
|
fmt.Sprintf("%s-%d.%s", id.String(), size, coverCacheFormat),
|
||||||
)
|
)
|
||||||
_, err = os.Stat(cachePath)
|
_, err = os.Stat(cachePath)
|
||||||
|
|||||||
86
server/ctrlsubsonic/routes.go
Normal file
86
server/ctrlsubsonic/routes.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package ctrlsubsonic
|
||||||
|
|
||||||
|
import "github.com/gorilla/mux"
|
||||||
|
|
||||||
|
func AddRoutes(c *Controller, r *mux.Router) {
|
||||||
|
r.Use(c.WithParams)
|
||||||
|
r.Use(c.WithRequiredParams)
|
||||||
|
r.Use(c.WithUser)
|
||||||
|
|
||||||
|
// common
|
||||||
|
r.Handle("/getLicense{_:(?:\\.view)?}", c.H(c.ServeGetLicence))
|
||||||
|
r.Handle("/getMusicFolders{_:(?:\\.view)?}", c.H(c.ServeGetMusicFolders))
|
||||||
|
r.Handle("/getScanStatus{_:(?:\\.view)?}", c.H(c.ServeGetScanStatus))
|
||||||
|
r.Handle("/ping{_:(?:\\.view)?}", c.H(c.ServePing))
|
||||||
|
r.Handle("/scrobble{_:(?:\\.view)?}", c.H(c.ServeScrobble))
|
||||||
|
r.Handle("/startScan{_:(?:\\.view)?}", c.H(c.ServeStartScan))
|
||||||
|
r.Handle("/getUser{_:(?:\\.view)?}", c.H(c.ServeGetUser))
|
||||||
|
r.Handle("/getPlaylists{_:(?:\\.view)?}", c.H(c.ServeGetPlaylists))
|
||||||
|
r.Handle("/getPlaylist{_:(?:\\.view)?}", c.H(c.ServeGetPlaylist))
|
||||||
|
r.Handle("/createPlaylist{_:(?:\\.view)?}", c.H(c.ServeCreatePlaylist))
|
||||||
|
r.Handle("/updatePlaylist{_:(?:\\.view)?}", c.H(c.ServeUpdatePlaylist))
|
||||||
|
r.Handle("/deletePlaylist{_:(?:\\.view)?}", c.H(c.ServeDeletePlaylist))
|
||||||
|
r.Handle("/savePlayQueue{_:(?:\\.view)?}", c.H(c.ServeSavePlayQueue))
|
||||||
|
r.Handle("/getPlayQueue{_:(?:\\.view)?}", c.H(c.ServeGetPlayQueue))
|
||||||
|
r.Handle("/getSong{_:(?:\\.view)?}", c.H(c.ServeGetSong))
|
||||||
|
r.Handle("/getRandomSongs{_:(?:\\.view)?}", c.H(c.ServeGetRandomSongs))
|
||||||
|
r.Handle("/getSongsByGenre{_:(?:\\.view)?}", c.H(c.ServeGetSongsByGenre))
|
||||||
|
r.Handle("/jukeboxControl{_:(?:\\.view)?}", c.H(c.ServeJukebox))
|
||||||
|
r.Handle("/getBookmarks{_:(?:\\.view)?}", c.H(c.ServeGetBookmarks))
|
||||||
|
r.Handle("/createBookmark{_:(?:\\.view)?}", c.H(c.ServeCreateBookmark))
|
||||||
|
r.Handle("/deleteBookmark{_:(?:\\.view)?}", c.H(c.ServeDeleteBookmark))
|
||||||
|
r.Handle("/getTopSongs{_:(?:\\.view)?}", c.H(c.ServeGetTopSongs))
|
||||||
|
r.Handle("/getSimilarSongs{_:(?:\\.view)?}", c.H(c.ServeGetSimilarSongs))
|
||||||
|
r.Handle("/getSimilarSongs2{_:(?:\\.view)?}", c.H(c.ServeGetSimilarSongsTwo))
|
||||||
|
r.Handle("/getLyrics{_:(?:\\.view)?}", c.H(c.ServeGetLyrics))
|
||||||
|
|
||||||
|
// raw
|
||||||
|
r.Handle("/getCoverArt{_:(?:\\.view)?}", c.HR(c.ServeGetCoverArt))
|
||||||
|
r.Handle("/stream{_:(?:\\.view)?}", c.HR(c.ServeStream))
|
||||||
|
r.Handle("/download{_:(?:\\.view)?}", c.HR(c.ServeStream))
|
||||||
|
r.Handle("/getAvatar{_:(?:\\.view)?}", c.HR(c.ServeGetAvatar))
|
||||||
|
|
||||||
|
// browse by tag
|
||||||
|
r.Handle("/getAlbum{_:(?:\\.view)?}", c.H(c.ServeGetAlbum))
|
||||||
|
r.Handle("/getAlbumList2{_:(?:\\.view)?}", c.H(c.ServeGetAlbumListTwo))
|
||||||
|
r.Handle("/getArtist{_:(?:\\.view)?}", c.H(c.ServeGetArtist))
|
||||||
|
r.Handle("/getArtists{_:(?:\\.view)?}", c.H(c.ServeGetArtists))
|
||||||
|
r.Handle("/search3{_:(?:\\.view)?}", c.H(c.ServeSearchThree))
|
||||||
|
r.Handle("/getArtistInfo2{_:(?:\\.view)?}", c.H(c.ServeGetArtistInfoTwo))
|
||||||
|
r.Handle("/getStarred2{_:(?:\\.view)?}", c.H(c.ServeGetStarredTwo))
|
||||||
|
|
||||||
|
// browse by folder
|
||||||
|
r.Handle("/getIndexes{_:(?:\\.view)?}", c.H(c.ServeGetIndexes))
|
||||||
|
r.Handle("/getMusicDirectory{_:(?:\\.view)?}", c.H(c.ServeGetMusicDirectory))
|
||||||
|
r.Handle("/getAlbumList{_:(?:\\.view)?}", c.H(c.ServeGetAlbumList))
|
||||||
|
r.Handle("/search2{_:(?:\\.view)?}", c.H(c.ServeSearchTwo))
|
||||||
|
r.Handle("/getGenres{_:(?:\\.view)?}", c.H(c.ServeGetGenres))
|
||||||
|
r.Handle("/getArtistInfo{_:(?:\\.view)?}", c.H(c.ServeGetArtistInfo))
|
||||||
|
r.Handle("/getStarred{_:(?:\\.view)?}", c.H(c.ServeGetStarred))
|
||||||
|
|
||||||
|
// star / rating
|
||||||
|
r.Handle("/star{_:(?:\\.view)?}", c.H(c.ServeStar))
|
||||||
|
r.Handle("/unstar{_:(?:\\.view)?}", c.H(c.ServeUnstar))
|
||||||
|
r.Handle("/setRating{_:(?:\\.view)?}", c.H(c.ServeSetRating))
|
||||||
|
|
||||||
|
// podcasts
|
||||||
|
r.Handle("/getPodcasts{_:(?:\\.view)?}", c.H(c.ServeGetPodcasts))
|
||||||
|
r.Handle("/getNewestPodcasts{_:(?:\\.view)?}", c.H(c.ServeGetNewestPodcasts))
|
||||||
|
r.Handle("/downloadPodcastEpisode{_:(?:\\.view)?}", c.H(c.ServeDownloadPodcastEpisode))
|
||||||
|
r.Handle("/createPodcastChannel{_:(?:\\.view)?}", c.H(c.ServeCreatePodcastChannel))
|
||||||
|
r.Handle("/refreshPodcasts{_:(?:\\.view)?}", c.H(c.ServeRefreshPodcasts))
|
||||||
|
r.Handle("/deletePodcastChannel{_:(?:\\.view)?}", c.H(c.ServeDeletePodcastChannel))
|
||||||
|
r.Handle("/deletePodcastEpisode{_:(?:\\.view)?}", c.H(c.ServeDeletePodcastEpisode))
|
||||||
|
|
||||||
|
// internet radio
|
||||||
|
r.Handle("/getInternetRadioStations{_:(?:\\.view)?}", c.H(c.ServeGetInternetRadioStations))
|
||||||
|
r.Handle("/createInternetRadioStation{_:(?:\\.view)?}", c.H(c.ServeCreateInternetRadioStation))
|
||||||
|
r.Handle("/updateInternetRadioStation{_:(?:\\.view)?}", c.H(c.ServeUpdateInternetRadioStation))
|
||||||
|
r.Handle("/deleteInternetRadioStation{_:(?:\\.view)?}", c.H(c.ServeDeleteInternetRadioStation))
|
||||||
|
|
||||||
|
// middlewares should be run for not found handler
|
||||||
|
// https://github.com/gorilla/mux/issues/416
|
||||||
|
notFoundHandler := c.H(c.ServeNotFound)
|
||||||
|
notFoundRoute := r.NewRoute().Handler(notFoundHandler)
|
||||||
|
r.NotFoundHandler = notFoundRoute.GetHandler()
|
||||||
|
}
|
||||||
465
server/server.go
465
server/server.go
@@ -1,465 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/sentriz/gormstore"
|
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
|
||||||
"go.senan.xyz/gonic/jukebox"
|
|
||||||
"go.senan.xyz/gonic/playlist"
|
|
||||||
"go.senan.xyz/gonic/podcasts"
|
|
||||||
"go.senan.xyz/gonic/scanner"
|
|
||||||
"go.senan.xyz/gonic/scanner/tags"
|
|
||||||
"go.senan.xyz/gonic/scrobble"
|
|
||||||
"go.senan.xyz/gonic/scrobble/lastfm"
|
|
||||||
"go.senan.xyz/gonic/scrobble/listenbrainz"
|
|
||||||
"go.senan.xyz/gonic/server/ctrladmin"
|
|
||||||
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
|
||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
|
||||||
"go.senan.xyz/gonic/transcode"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
DB *db.DB
|
|
||||||
MusicPaths []ctrlsubsonic.MusicPath
|
|
||||||
ExcludePattern string
|
|
||||||
PodcastPath string
|
|
||||||
CacheAudioPath string
|
|
||||||
CoverCachePath string
|
|
||||||
PlaylistsPath string
|
|
||||||
ProxyPrefix string
|
|
||||||
MultiValueSettings map[scanner.Tag]scanner.MultiValueSetting
|
|
||||||
HTTPLog bool
|
|
||||||
JukeboxEnabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
scanner *scanner.Scanner
|
|
||||||
jukebox *jukebox.Jukebox
|
|
||||||
router *mux.Router
|
|
||||||
sessDB *gormstore.Store
|
|
||||||
podcast *podcasts.Podcasts
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(opts Options) (*Server, error) {
|
|
||||||
tagger := &tags.TagReader{}
|
|
||||||
|
|
||||||
scanner := scanner.New(ctrlsubsonic.PathsOf(opts.MusicPaths), opts.DB, opts.MultiValueSettings, tagger, opts.ExcludePattern)
|
|
||||||
|
|
||||||
playlistStore, err := playlist.NewStore(opts.PlaylistsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create playlists store: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
base := &ctrlbase.Controller{
|
|
||||||
DB: opts.DB,
|
|
||||||
PlaylistStore: playlistStore,
|
|
||||||
ProxyPrefix: opts.ProxyPrefix,
|
|
||||||
Scanner: scanner,
|
|
||||||
}
|
|
||||||
|
|
||||||
// router with common wares for admin / subsonic
|
|
||||||
r := mux.NewRouter()
|
|
||||||
if opts.HTTPLog {
|
|
||||||
r.Use(base.WithLogging)
|
|
||||||
}
|
|
||||||
r.Use(base.WithCORS)
|
|
||||||
r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
|
|
||||||
|
|
||||||
sessKey, err := opts.DB.GetSetting("session_key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get session key: %w", err)
|
|
||||||
}
|
|
||||||
if sessKey == "" {
|
|
||||||
if err := opts.DB.SetSetting("session_key", string(securecookie.GenerateRandomKey(32))); err != nil {
|
|
||||||
return nil, fmt.Errorf("set session key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessDB := gormstore.New(opts.DB.DB, []byte(sessKey))
|
|
||||||
sessDB.SessionOpts.HttpOnly = true
|
|
||||||
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
|
||||||
|
|
||||||
podcast := podcasts.New(opts.DB, opts.PodcastPath, tagger)
|
|
||||||
|
|
||||||
cacheTranscoder := transcode.NewCachingTranscoder(
|
|
||||||
transcode.NewFFmpegTranscoder(),
|
|
||||||
opts.CacheAudioPath,
|
|
||||||
)
|
|
||||||
|
|
||||||
lastfmClient := lastfm.NewClient()
|
|
||||||
|
|
||||||
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast, lastfmClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create admin controller: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrlSubsonic := &ctrlsubsonic.Controller{
|
|
||||||
Controller: base,
|
|
||||||
MusicPaths: opts.MusicPaths,
|
|
||||||
PodcastsPath: opts.PodcastPath,
|
|
||||||
CacheAudioPath: opts.CacheAudioPath,
|
|
||||||
CoverCachePath: opts.CoverCachePath,
|
|
||||||
LastFMClient: lastfmClient,
|
|
||||||
Scrobblers: []scrobble.Scrobbler{
|
|
||||||
lastfm.NewScrobbler(opts.DB, lastfmClient),
|
|
||||||
listenbrainz.NewScrobbler(),
|
|
||||||
},
|
|
||||||
Podcasts: podcast,
|
|
||||||
Transcoder: cacheTranscoder,
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMisc(r, base)
|
|
||||||
setupAdmin(r.PathPrefix("/admin").Subrouter(), ctrlAdmin)
|
|
||||||
setupSubsonic(r.PathPrefix("/rest").Subrouter(), ctrlSubsonic)
|
|
||||||
|
|
||||||
server := &Server{
|
|
||||||
scanner: scanner,
|
|
||||||
router: r,
|
|
||||||
sessDB: sessDB,
|
|
||||||
podcast: podcast,
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.JukeboxEnabled {
|
|
||||||
jukebox := jukebox.New()
|
|
||||||
ctrlSubsonic.Jukebox = jukebox
|
|
||||||
server.jukebox = jukebox
|
|
||||||
}
|
|
||||||
|
|
||||||
return server, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupMisc(r *mux.Router, ctrl *ctrlbase.Controller) {
|
|
||||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
adminHome := ctrl.Path("/admin/home")
|
|
||||||
http.Redirect(w, r, adminHome, http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
// misc subsonic routes without /rest prefix
|
|
||||||
r.HandleFunc("/settings.view", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
adminHome := ctrl.Path("/admin/home")
|
|
||||||
http.Redirect(w, r, adminHome, http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
r.HandleFunc("/musicFolderSettings.view", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
restScan := ctrl.Path(fmt.Sprintf("/rest/startScan.view?%s", r.URL.Query().Encode()))
|
|
||||||
http.Redirect(w, r, restScan, http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "OK")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
|
||||||
|
|
||||||
// public routes (creates session)
|
|
||||||
r.Use(ctrl.WithSession)
|
|
||||||
r.Handle("/login", ctrl.H(ctrl.ServeLogin))
|
|
||||||
r.Handle("/login_do", ctrl.HR(ctrl.ServeLoginDo)) // "raw" handler, updates session
|
|
||||||
|
|
||||||
staticHandler := http.StripPrefix("/admin", http.FileServer(http.FS(adminui.StaticFS)))
|
|
||||||
r.PathPrefix("/static").Handler(staticHandler)
|
|
||||||
|
|
||||||
// user routes (if session is valid)
|
|
||||||
routUser := r.NewRoute().Subrouter()
|
|
||||||
routUser.Use(ctrl.WithUserSession)
|
|
||||||
routUser.Handle("/logout", ctrl.HR(ctrl.ServeLogout)) // "raw" handler, updates session
|
|
||||||
routUser.Handle("/home", ctrl.H(ctrl.ServeHome))
|
|
||||||
routUser.Handle("/change_username", ctrl.H(ctrl.ServeChangeUsername))
|
|
||||||
routUser.Handle("/change_username_do", ctrl.H(ctrl.ServeChangeUsernameDo))
|
|
||||||
routUser.Handle("/change_password", ctrl.H(ctrl.ServeChangePassword))
|
|
||||||
routUser.Handle("/change_password_do", ctrl.H(ctrl.ServeChangePasswordDo))
|
|
||||||
routUser.Handle("/change_avatar", ctrl.H(ctrl.ServeChangeAvatar))
|
|
||||||
routUser.Handle("/change_avatar_do", ctrl.H(ctrl.ServeChangeAvatarDo))
|
|
||||||
routUser.Handle("/delete_avatar_do", ctrl.H(ctrl.ServeDeleteAvatarDo))
|
|
||||||
routUser.Handle("/delete_user", ctrl.H(ctrl.ServeDeleteUser))
|
|
||||||
routUser.Handle("/delete_user_do", ctrl.H(ctrl.ServeDeleteUserDo))
|
|
||||||
routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo))
|
|
||||||
routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo))
|
|
||||||
routUser.Handle("/link_listenbrainz_do", ctrl.H(ctrl.ServeLinkListenBrainzDo))
|
|
||||||
routUser.Handle("/unlink_listenbrainz_do", ctrl.H(ctrl.ServeUnlinkListenBrainzDo))
|
|
||||||
routUser.Handle("/create_transcode_pref_do", ctrl.H(ctrl.ServeCreateTranscodePrefDo))
|
|
||||||
routUser.Handle("/delete_transcode_pref_do", ctrl.H(ctrl.ServeDeleteTranscodePrefDo))
|
|
||||||
|
|
||||||
// admin routes (if session is valid, and is admin)
|
|
||||||
routAdmin := routUser.NewRoute().Subrouter()
|
|
||||||
routAdmin.Use(ctrl.WithAdminSession)
|
|
||||||
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_inc_do", ctrl.H(ctrl.ServeStartScanIncDo))
|
|
||||||
routAdmin.Handle("/start_scan_full_do", ctrl.H(ctrl.ServeStartScanFullDo))
|
|
||||||
routAdmin.Handle("/add_podcast_do", ctrl.H(ctrl.ServePodcastAddDo))
|
|
||||||
routAdmin.Handle("/delete_podcast_do", ctrl.H(ctrl.ServePodcastDeleteDo))
|
|
||||||
routAdmin.Handle("/download_podcast_do", ctrl.H(ctrl.ServePodcastDownloadDo))
|
|
||||||
routAdmin.Handle("/update_podcast_do", ctrl.H(ctrl.ServePodcastUpdateDo))
|
|
||||||
routAdmin.Handle("/add_internet_radio_station_do", ctrl.H(ctrl.ServeInternetRadioStationAddDo))
|
|
||||||
routAdmin.Handle("/delete_internet_radio_station_do", ctrl.H(ctrl.ServeInternetRadioStationDeleteDo))
|
|
||||||
routAdmin.Handle("/update_internet_radio_station_do", ctrl.H(ctrl.ServeInternetRadioStationUpdateDo))
|
|
||||||
|
|
||||||
// middlewares should be run for not found handler
|
|
||||||
// https://github.com/gorilla/mux/issues/416
|
|
||||||
notFoundHandler := ctrl.H(ctrl.ServeNotFound)
|
|
||||||
notFoundRoute := r.NewRoute().Handler(notFoundHandler)
|
|
||||||
r.NotFoundHandler = notFoundRoute.GetHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
|
||||||
r.Use(ctrl.WithParams)
|
|
||||||
r.Use(ctrl.WithRequiredParams)
|
|
||||||
r.Use(ctrl.WithUser)
|
|
||||||
|
|
||||||
// common
|
|
||||||
r.Handle("/getLicense{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetLicence))
|
|
||||||
r.Handle("/getMusicFolders{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicFolders))
|
|
||||||
r.Handle("/getScanStatus{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetScanStatus))
|
|
||||||
r.Handle("/ping{_:(?:\\.view)?}", ctrl.H(ctrl.ServePing))
|
|
||||||
r.Handle("/scrobble{_:(?:\\.view)?}", ctrl.H(ctrl.ServeScrobble))
|
|
||||||
r.Handle("/startScan{_:(?:\\.view)?}", ctrl.H(ctrl.ServeStartScan))
|
|
||||||
r.Handle("/getUser{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetUser))
|
|
||||||
r.Handle("/getPlaylists{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlaylists))
|
|
||||||
r.Handle("/getPlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlaylist))
|
|
||||||
r.Handle("/createPlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreatePlaylist))
|
|
||||||
r.Handle("/updatePlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeUpdatePlaylist))
|
|
||||||
r.Handle("/deletePlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePlaylist))
|
|
||||||
r.Handle("/savePlayQueue{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSavePlayQueue))
|
|
||||||
r.Handle("/getPlayQueue{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlayQueue))
|
|
||||||
r.Handle("/getSong{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSong))
|
|
||||||
r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs))
|
|
||||||
r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre))
|
|
||||||
r.Handle("/jukeboxControl{_:(?:\\.view)?}", ctrl.H(ctrl.ServeJukebox))
|
|
||||||
r.Handle("/getBookmarks{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetBookmarks))
|
|
||||||
r.Handle("/createBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreateBookmark))
|
|
||||||
r.Handle("/deleteBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteBookmark))
|
|
||||||
r.Handle("/getTopSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetTopSongs))
|
|
||||||
r.Handle("/getSimilarSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSimilarSongs))
|
|
||||||
r.Handle("/getSimilarSongs2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSimilarSongsTwo))
|
|
||||||
r.Handle("/getLyrics{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetLyrics))
|
|
||||||
|
|
||||||
// raw
|
|
||||||
r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
|
||||||
r.Handle("/stream{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
|
||||||
r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
|
||||||
r.Handle("/getAvatar{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetAvatar))
|
|
||||||
|
|
||||||
// browse by tag
|
|
||||||
r.Handle("/getAlbum{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbum))
|
|
||||||
r.Handle("/getAlbumList2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumListTwo))
|
|
||||||
r.Handle("/getArtist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtist))
|
|
||||||
r.Handle("/getArtists{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtists))
|
|
||||||
r.Handle("/search3{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchThree))
|
|
||||||
r.Handle("/getArtistInfo2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfoTwo))
|
|
||||||
r.Handle("/getStarred2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetStarredTwo))
|
|
||||||
|
|
||||||
// browse by folder
|
|
||||||
r.Handle("/getIndexes{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetIndexes))
|
|
||||||
r.Handle("/getMusicDirectory{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicDirectory))
|
|
||||||
r.Handle("/getAlbumList{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumList))
|
|
||||||
r.Handle("/search2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchTwo))
|
|
||||||
r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres))
|
|
||||||
r.Handle("/getArtistInfo{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfo))
|
|
||||||
r.Handle("/getStarred{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetStarred))
|
|
||||||
|
|
||||||
// star / rating
|
|
||||||
r.Handle("/star{_:(?:\\.view)?}", ctrl.H(ctrl.ServeStar))
|
|
||||||
r.Handle("/unstar{_:(?:\\.view)?}", ctrl.H(ctrl.ServeUnstar))
|
|
||||||
r.Handle("/setRating{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSetRating))
|
|
||||||
|
|
||||||
// podcasts
|
|
||||||
r.Handle("/getPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPodcasts))
|
|
||||||
r.Handle("/getNewestPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetNewestPodcasts))
|
|
||||||
r.Handle("/downloadPodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDownloadPodcastEpisode))
|
|
||||||
r.Handle("/createPodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreatePodcastChannel))
|
|
||||||
r.Handle("/refreshPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeRefreshPodcasts))
|
|
||||||
r.Handle("/deletePodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastChannel))
|
|
||||||
r.Handle("/deletePodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastEpisode))
|
|
||||||
|
|
||||||
// internet radio
|
|
||||||
r.Handle("/getInternetRadioStations{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetInternetRadioStations))
|
|
||||||
r.Handle("/createInternetRadioStation{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreateInternetRadioStation))
|
|
||||||
r.Handle("/updateInternetRadioStation{_:(?:\\.view)?}", ctrl.H(ctrl.ServeUpdateInternetRadioStation))
|
|
||||||
r.Handle("/deleteInternetRadioStation{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteInternetRadioStation))
|
|
||||||
|
|
||||||
// middlewares should be run for not found handler
|
|
||||||
// https://github.com/gorilla/mux/issues/416
|
|
||||||
notFoundHandler := ctrl.H(ctrl.ServeNotFound)
|
|
||||||
notFoundRoute := r.NewRoute().Handler(notFoundHandler)
|
|
||||||
r.NotFoundHandler = notFoundRoute.GetHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
FuncExecute func() error
|
|
||||||
FuncInterrupt func(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) StartHTTP(listenAddr string, tlsCert string, tlsKey string) (FuncExecute, FuncInterrupt) {
|
|
||||||
list := &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
Handler: s.router,
|
|
||||||
ReadTimeout: 5 * time.Second,
|
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
|
||||||
WriteTimeout: 80 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
log.Print("starting job 'http'\n")
|
|
||||||
if tlsCert != "" && tlsKey != "" {
|
|
||||||
return list.ListenAndServeTLS(tlsCert, tlsKey)
|
|
||||||
}
|
|
||||||
return list.ListenAndServe()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
_ = list.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartScanTicker(dur time.Duration) (FuncExecute, FuncInterrupt) {
|
|
||||||
ticker := time.NewTicker(dur)
|
|
||||||
done := make(chan struct{})
|
|
||||||
waitFor := func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
go func() {
|
|
||||||
if _, err := s.scanner.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
|
||||||
log.Printf("error scanning: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'scan timer'\n")
|
|
||||||
return waitFor()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
ticker.Stop()
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ScanAtStart() {
|
|
||||||
if _, err := s.scanner.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
|
||||||
log.Printf("error scanning: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartScanWatcher() (FuncExecute, FuncInterrupt) {
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'scan watcher'\n")
|
|
||||||
return s.scanner.ExecuteWatch()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
s.scanner.CancelWatch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartJukebox(mpvExtraArgs []string) (FuncExecute, FuncInterrupt) {
|
|
||||||
var tempDir string
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'jukebox'\n")
|
|
||||||
var err error
|
|
||||||
tempDir, err = os.MkdirTemp("", "gonic-jukebox-*")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create tmp sock file: %w", err)
|
|
||||||
}
|
|
||||||
sockPath := filepath.Join(tempDir, "sock")
|
|
||||||
if err := s.jukebox.Start(sockPath, mpvExtraArgs); err != nil {
|
|
||||||
return fmt.Errorf("start jukebox: %w", err)
|
|
||||||
}
|
|
||||||
if err := s.jukebox.Wait(); err != nil {
|
|
||||||
return fmt.Errorf("start jukebox: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
if err := s.jukebox.Quit(); err != nil {
|
|
||||||
log.Printf("error quitting jukebox: %v", err)
|
|
||||||
}
|
|
||||||
_ = os.RemoveAll(tempDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartPodcastRefresher(dur time.Duration) (FuncExecute, FuncInterrupt) {
|
|
||||||
ticker := time.NewTicker(dur)
|
|
||||||
done := make(chan struct{})
|
|
||||||
waitFor := func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := s.podcast.RefreshPodcasts(); err != nil {
|
|
||||||
log.Printf("failed to refresh some feeds: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'podcast refresher'\n")
|
|
||||||
return waitFor()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
ticker.Stop()
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartPodcastPurger(maxAge time.Duration) (FuncExecute, FuncInterrupt) {
|
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
|
||||||
done := make(chan struct{})
|
|
||||||
waitFor := func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := s.podcast.PurgeOldPodcasts(maxAge); err != nil {
|
|
||||||
log.Printf("error purging old podcasts: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'podcast purger'\n")
|
|
||||||
return waitFor()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
ticker.Stop()
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) StartSessionClean(dur time.Duration) (FuncExecute, FuncInterrupt) {
|
|
||||||
ticker := time.NewTicker(dur)
|
|
||||||
done := make(chan struct{})
|
|
||||||
waitFor := func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
s.sessDB.Cleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
log.Printf("starting job 'session clean'\n")
|
|
||||||
return waitFor()
|
|
||||||
}, func(_ error) {
|
|
||||||
// stop job
|
|
||||||
ticker.Stop()
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user