coda/cmd/server/main.go

137 lines
3.2 KiB
Go

package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"coda/internal/config"
"coda/internal/jobs"
"coda/internal/lidarr"
"coda/internal/logger"
"coda/internal/ntfy"
"coda/internal/queue"
"coda/internal/scanner"
"coda/internal/scheduler"
"coda/internal/store"
"coda/internal/transcoder"
"coda/internal/validator"
"coda/internal/web"
"coda/internal/webhook"
"coda/internal/worker"
webassets "coda/web"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
envCfg, err := config.Load()
if err != nil {
slog.Error("failed to load configuration", "error", err)
os.Exit(1)
}
st, err := store.New(envCfg.DBPath)
if err != nil {
slog.Error("failed to initialize database", "error", err)
os.Exit(1)
}
defer st.Close()
cfgSvc, err := config.NewService(context.Background(), st.Config)
if err != nil {
slog.Error("failed to initialize config service", "error", err)
os.Exit(1)
}
handler := logger.New(st.Logs, logger.Options{Level: parseLevel(cfgSvc.Get().LogLevel)})
slog.SetDefault(slog.New(handler))
defer handler.Close()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
q := queue.New(1024)
jobSvc := jobs.New(st, q)
sched := scheduler.New(cfgSvc)
tr := transcoder.New(cfgSvc)
val := validator.New(cfgSvc)
rescanner := lidarr.New(cfgSvc)
notifier := ntfy.New(cfgSvc)
pool := worker.New(cfgSvc, st, q, sched, tr, val, rescanner, notifier)
webhookHandler := webhook.New(cfgSvc, jobSvc)
sc := scanner.New(cfgSvc.Get())
if err := jobSvc.Recover(ctx); err != nil {
slog.Error("failed to recover interrupted jobs", "error", err)
}
pool.Start(ctx)
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Mount("/", web.Router(web.Deps{
Cfg: cfgSvc,
Store: st,
Queue: q,
Scanner: sc,
Enqueuer: jobSvc,
}))
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(webassets.Static()))))
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
r.Post("/webhook/lidarr", webhookHandler.Handle)
cfg := cfgSvc.Get()
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
<-ctx.Done()
slog.Info("shutdown signal received")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("server shutdown error", "error", err)
}
}()
slog.Info("server started", "port", cfg.Port, "dry_run", cfg.DryRun, "music_path", cfg.MusicPath)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server terminated with error", "error", err)
os.Exit(1)
}
pool.Wait()
slog.Info("shutdown complete")
}
func parseLevel(level string) slog.Level {
switch strings.ToUpper(level) {
case "DEBUG":
return slog.LevelDebug
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
default:
return slog.LevelInfo
}
}