Files
2026-07-13 13:39:47 +08:00

164 lines
4.9 KiB
Go

package server
import (
"os"
"path/filepath"
"caorushizi.cn/mediago/assets"
"caorushizi.cn/mediago/internal/api/handler"
"caorushizi.cn/mediago/internal/api/middleware"
"caorushizi.cn/mediago/internal/api/sse"
"caorushizi.cn/mediago/internal/core"
"caorushizi.cn/mediago/internal/db"
"caorushizi.cn/mediago/internal/db/repo"
"caorushizi.cn/mediago/internal/i18n"
"caorushizi.cn/mediago/internal/service"
"caorushizi.cn/mediago/internal/tasklog"
"caorushizi.cn/mediago/internal/video"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// Server wraps the Gin Engine with its business dependencies.
type Server struct {
queue *core.TaskQueue
hub *sse.Hub
engine *gin.Engine
logs *tasklog.Manager
taskHandler *handler.TaskHandler
configHandler *handler.ConfigHandler
eventHandler *handler.EventHandler
healthHandler *handler.HealthHandler
authHandler *handler.AuthHandler
utilHandler *handler.UtilHandler
// Database persistence handlers (available when database is non-nil)
downloadHandler *handler.DownloadHandler
favoriteHandler *handler.FavoriteHandler
conversionHandler *handler.ConversionHandler
// Download task service (used by queue callbacks to update database state)
downloadService *service.DownloadTaskService
// Video player handler (available when video-root is configured)
videoHandler *video.Handler
}
// Options holds optional configuration for the server.
type Options struct {
EnableAuth bool
StaticDir string
FFmpegBin string
VideoRoot string
EnvPaths handler.EnvPaths
}
// New creates an HTTP server instance.
func New(queue *core.TaskQueue, logs *tasklog.Manager, database *db.Database, confStore handler.ConfigStore, opts ...Options) *Server {
var opt Options
if len(opts) > 0 {
opt = opts[0]
}
engine := gin.New()
engine.Use(gin.Logger(), gin.Recovery())
engine.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-API-Key"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
// i18n middleware — resolve language before auth so error messages are translated
engine.Use(i18n.Middleware(func() string {
lang, _ := confStore.Get("language").(string)
return lang
}))
// Optional auth middleware
if opt.EnableAuth {
engine.Use(middleware.AuthMiddleware(confStore))
}
hub := sse.New()
srv := &Server{
queue: queue,
hub: hub,
engine: engine,
logs: logs,
taskHandler: handler.NewTaskHandler(queue, logs),
configHandler: handler.NewConfigHandler(confStore, hub),
eventHandler: handler.NewEventHandler(hub),
healthHandler: handler.NewHealthHandler(),
authHandler: handler.NewAuthHandler(confStore),
utilHandler: handler.NewUtilHandler(opt.EnvPaths),
}
// Initialize persistence-related components when a database is provided
if database != nil {
videoRepo := repo.NewVideoRepository(database)
favoriteRepo := repo.NewFavoriteRepository(database)
conversionRepo := repo.NewConversionRepository(database)
downloadSvc := service.NewDownloadTaskService(videoRepo, queue, logs)
favoriteSvc := service.NewFavoriteService(favoriteRepo)
converter := service.NewConverter(opt.FFmpegBin)
conversionSvc := service.NewConversionService(conversionRepo, converter, hub)
srv.downloadHandler = handler.NewDownloadHandler(downloadSvc, confStore, hub)
srv.favoriteHandler = handler.NewFavoriteHandler(favoriteSvc)
srv.conversionHandler = handler.NewConversionHandler(conversionSvc)
srv.downloadService = downloadSvc
// Video player service (requires video root directory)
if opt.VideoRoot != "" {
videoSvc := video.NewService(videoRepo, opt.VideoRoot)
srv.videoHandler = video.NewHandler(videoSvc)
}
}
srv.registerRoutes()
srv.setupQueueCallbacks()
// Static file serving for SPA (optional)
if opt.StaticDir != "" {
srv.serveStatic(opt.StaticDir)
}
// Serve embedded player UI at /player/
srv.engine.Use(newPlayerSPAHandler(playerSPAConfig{
FS: assets.PlayerFS,
Root: "player",
PathPrefix: "/player",
}))
return srv
}
// serveStatic configures the engine to serve static files and SPA fallback.
func (s *Server) serveStatic(dir string) {
s.engine.Static("/assets", filepath.Join(dir, "assets"))
s.engine.StaticFile("/favicon.ico", filepath.Join(dir, "favicon.ico"))
// SPA fallback: serve index.html for any unmatched route
indexPath := filepath.Join(dir, "index.html")
s.engine.NoRoute(func(c *gin.Context) {
if _, err := os.Stat(indexPath); err == nil {
c.File(indexPath)
}
})
}
// Run starts the HTTP server on the given address.
func (s *Server) Run(addr string) error {
return s.engine.Run(addr)
}
// Engine returns the underlying Gin Engine (primarily used for testing).
func (s *Server) Engine() *gin.Engine {
return s.engine
}