137 lines
3.2 KiB
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
|
|
}
|
|
}
|