Skip to content

Commit

Permalink
remove a whole pile of startup indirection
Browse files Browse the repository at this point in the history
fixes #360
  • Loading branch information
sentriz committed Sep 11, 2023
1 parent 749233d commit b3199de
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 506 deletions.
218 changes: 179 additions & 39 deletions cmd/gonic/gonic.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Package main is the gonic server entrypoint
//
//nolint:lll // flags help strings
//nolint:lll,gocyclo
package main

import (
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"path"
"path/filepath"
Expand All @@ -16,19 +17,27 @@ import (
"time"

"github.com/google/shlex"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/oklog/run"
"github.com/peterbourgon/ff"
"github.com/sentriz/gormstore"

"go.senan.xyz/gonic"
"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/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"
)

const (
cleanTimeDuration = 10 * time.Minute
"go.senan.xyz/gonic/transcode"
)

func main() {
Expand Down Expand Up @@ -146,53 +155,184 @@ func main() {
*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("provided config\n")
set.VisitAll(func(f *flag.Flag) {
value := strings.ReplaceAll(f.Value.String(), "\n", "")
log.Printf(" %-25s %s\n", f.Name, value)
})

tagger := &tags.TagReader{}
scannr := scanner.New(
ctrlsubsonic.PathsOf(musicPaths),
dbc,
map[scanner.Tag]scanner.MultiValueSetting{
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist),
},
tagger,
*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 {
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(server.StartHTTP(*confListenAddr, *confTLSCert, *confTLSKey))
g.Add(server.StartSessionClean(cleanTimeDuration))
g.Add(server.StartPodcastRefresher(time.Hour))
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 {
tickerDur := time.Duration(*confScanIntervalMins) * time.Minute
g.Add(server.StartScanTicker(tickerDur))
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(server.StartScanWatcher())
}
if *confJukeboxEnabled {
extraArgs, _ := shlex.Split(*confJukeboxMPVExtraArgs)
g.Add(server.StartJukebox(extraArgs))
g.Add(func() error {
log.Printf("starting job 'scan watcher'\n")
return scannr.ExecuteWatch()
}, func(_ error) {
scannr.CancelWatch()
})
}
if *confPodcastPurgeAgeDays > 0 {
g.Add(server.StartPodcastPurger(time.Duration(*confPodcastPurgeAgeDays) * 24 * time.Hour))

if jukebx != nil {
var jukeboxTempDir string
g.Add(func() error {
log.Printf("starting job 'jukebox'\n")
extraArgs, _ := shlex.Split(*confJukeboxMPVExtraArgs)
var err error
jukeboxTempDir, err = os.MkdirTemp("", "gonic-jukebox-*")
if err != nil {
return fmt.Errorf("create tmp sock file: %w", err)
}
sockPath := filepath.Join(jukeboxTempDir, "sock")
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 {
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 {
Expand Down
62 changes: 62 additions & 0 deletions server/ctrladmin/routes.go
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions server/ctrlbase/routes.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
2 changes: 1 addition & 1 deletion server/ctrlsubsonic/ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Controller struct {
MusicPaths []MusicPath
PodcastsPath string
CacheAudioPath string
CoverCachePath string
CacheCoverPath string
Jukebox *jukebox.Jukebox
Scrobblers []scrobble.Scrobbler
Podcasts *podcasts.Podcasts
Expand Down
2 changes: 1 addition & 1 deletion server/ctrlsubsonic/handlers_raw.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
}
size := params.GetOrInt("size", coverDefaultSize)
cachePath := path.Join(
c.CoverCachePath,
c.CacheCoverPath,
fmt.Sprintf("%s-%d.%s", id.String(), size, coverCacheFormat),
)
_, err = os.Stat(cachePath)
Expand Down
Loading

0 comments on commit b3199de

Please sign in to comment.