package server import ( "context" "errors" "fmt" "net/http" "time" "music-server/internal/backend" "music-server/internal/config" "music-server/internal/db" "music-server/internal/db/repository" "music-server/internal/logging" "go.uber.org/zap" ) // Database is the database connection used by the server. type Database interface { HealthChecker Close() } type Server struct { cfg config.Config db Database q repository.Querier music *backend.MusicService library *backend.LibraryService sync *backend.SyncService characters *backend.CharacterService releases *backend.ReleaseService stats *backend.StatisticsService search *backend.SearchCache httpServer *http.Server } // New creates a Server with all services wired up. func New(cfg config.Config, q repository.Querier, database Database) (*Server, error) { characters, err := backend.NewCharacterService(cfg.CharactersPath) if err != nil { return nil, fmt.Errorf("characters path: %w", err) } s := &Server{ cfg: cfg, db: database, q: q, music: backend.NewMusicService(q, cfg.SoundCheckPath), library: backend.NewLibraryService(q), characters: characters, releases: backend.NewReleaseService(cfg.ReleasesURL), stats: backend.NewStatisticsService(q), } s.search = backend.NewSearchCache(s.music.SoundtrackNames) s.sync = backend.NewSyncService(q, cfg.MusicPath, s.afterSync) s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: s.RegisterRoutes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } return s, nil } // afterSync refreshes cached data once a sync has finished. func (s *Server) afterSync(ctx context.Context) { if err := s.music.Reset(ctx); err != nil { logging.GetLogger().Error("Failed to reset music after sync", zap.Error(err)) } if err := s.search.Reload(ctx); err != nil { logging.GetLogger().Error("Failed to reload search cache after sync", zap.Error(err)) } } // NewServerInstance loads the configuration, connects to and migrates the database and creates the Server. // It exits the process if any of these steps fail. func NewServerInstance() *Server { cfg, err := config.Load() logging.Init(cfg.LogLevel, cfg.LogJSON) logger := logging.GetLogger() if err != nil { logger.Fatal("Invalid settings", zap.Error(err)) } logger.Info("Starting server", zap.String("host", cfg.DBHost), zap.String("dbPort", cfg.DBPort), zap.String("username", cfg.DBUser), zap.String("dbName", cfg.DBName), zap.String("musicPath", cfg.MusicPath), zap.String("charactersPath", cfg.CharactersPath), ) ctx := context.Background() database, err := db.NewDatabase(ctx, db.ConnString(cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName)) if err != nil { logger.Fatal("Failed to initialize database", zap.Error(err)) } if err := database.RunMigrations(); err != nil { logger.Fatal("Migration failed", zap.Error(err)) } s, err := New(cfg, repository.New(database.Pool), database) if err != nil { logger.Fatal("Failed to create server", zap.Error(err)) } return s } // ListenAndServe starts the HTTP server. It returns nil after a graceful shutdown. func (s *Server) ListenAndServe() error { logging.GetLogger().Info("Server starting", zap.String("address", s.httpServer.Addr)) if err := s.httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { return err } return nil } // Shutdown stops accepting requests, waits for running requests and then closes the database. func (s *Server) Shutdown(ctx context.Context) error { err := s.httpServer.Shutdown(ctx) s.sync.Wait() s.characters.Close() s.db.Close() return err } // HTTPServer returns the underlying http.Server. func (s *Server) HTTPServer() *http.Server { return s.httpServer }