Parent directory

server.go

3667 bytes
  1package ytrssil
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6	"net/http"
  7	"time"
  8
  9	"github.com/gin-gonic/gin"
 10
 11	"github.com/TheEdgeOfRage/ytrssil-api/config"
 12	"github.com/TheEdgeOfRage/ytrssil-api/handler"
 13	"github.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
 14)
 15
 16type server struct {
 17	log     *slog.Logger
 18	cfg     config.Config
 19	handler handler.Handler
 20}
 21
 22func NewServer(log *slog.Logger, cfg config.Config, handler handler.Handler) (*server, error) {
 23	return &server{log: log, cfg: cfg, handler: handler}, nil
 24}
 25
 26func ginLogFormatter(param gin.LogFormatterParams) string {
 27	return fmt.Sprintf("time=%v method=%s path=%#v status=%d ip=%s size=%v duration=%v\n",
 28		param.TimeStamp.UTC().Format(time.RFC3339Nano),
 29		param.Method,
 30		param.Path,
 31		param.StatusCode,
 32		param.ClientIP,
 33		param.BodySize,
 34		param.Latency,
 35	)
 36}
 37
 38func (srv *server) Healthz(c *gin.Context) {
 39	c.String(http.StatusOK, "healthy")
 40}
 41
 42// SetupGinRouter sets up routes for all APIs on a Gin server (aka router)
 43func SetupGinRouter(
 44	l *slog.Logger,
 45	cfg config.Config,
 46	handler handler.Handler,
 47) (*gin.Engine, error) {
 48	engine := gin.New()
 49	// Middlewares are executed top to bottom in a stack-like manner
 50	engine.Use(
 51		gin.LoggerWithFormatter(ginLogFormatter),
 52		gin.Recovery(), // Recovery needs to go before other middlewares to catch panics
 53	)
 54	engine.HandleMethodNotAllowed = true
 55	engine.NoRoute(func(c *gin.Context) {
 56		c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "URL not found"})
 57	})
 58	engine.NoMethod(func(c *gin.Context) {
 59		c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"error": "HTTP method not allowed"})
 60	})
 61
 62	srv, err := NewServer(l, cfg, handler)
 63	if err != nil {
 64		return nil, err
 65	}
 66	engine.GET("/healthz", srv.Healthz)
 67	engine.Static("/assets", "./assets")
 68
 69	engine.GET("/auth", srv.AuthPage)
 70	engine.POST("/auth", srv.HandleAuth)
 71
 72	pages := engine.Group("")
 73	pages.Use(auth.PageAuthMiddleware(cfg.AuthToken))
 74	{
 75		pages.POST("/fetch", srv.FetchVideosPage)
 76		pages.GET("/", srv.NewVideosPage)
 77		pages.GET("/watched", srv.WatchedVideosPage)
 78		pages.GET("/channels", srv.ChannelsPage)
 79		pages.POST("/subscribe", srv.SubscribeToChannelPage)
 80		pages.POST("/channels/:channel_id/unsubscribe", srv.UnsubscribeFromChannelPage)
 81		pages.POST("/channels/:channel_id/toggle-shorts", srv.ToggleChannelShortsPage)
 82		pages.POST("/videos", srv.AddVideoPage)
 83		pages.PATCH("/videos/:video_id/watch", srv.MarkVideoAsWatchedPage)
 84		pages.PATCH("/videos/:video_id/unwatch", srv.MarkVideoAsUnwatchedPage)
 85		pages.PATCH("/videos/:video_id/progress", srv.SetVideoProgressPage)
 86		pages.POST("/videos/:video_id/download", srv.DownloadVideoPage)
 87		pages.GET("/videos/:video_id/card", srv.GetVideoCardPage)
 88		pages.GET("/videos/:video_id/file", srv.ServeVideoFilePage)
 89	}
 90
 91	api := engine.Group("/api")
 92	api.Use(auth.APIAuthMiddleware(cfg.AuthToken))
 93	{
 94		api.POST("/fetch", srv.FetchVideosJSON)
 95		api.GET("channels", srv.ListChannelsJSON)
 96		api.POST("channels/subscribe", srv.SubscribeToChannelJSON)
 97		api.POST("channels/:channel_id/unsubscribe", srv.UnsubscribeFromChannelJSON)
 98		api.POST("channels/:channel_id/shorts", srv.ToggleChannelShortsJSON)
 99		api.POST("videos", srv.AddCustomVideoJSON)
100		api.GET("videos/new", srv.GetNewVideosJSON)
101		api.GET("videos/watched", srv.GetWatchedVideosJSON)
102		api.GET("videos/:video_id", srv.GetVideoJSON)
103		api.POST("videos/:video_id/watch", srv.MarkVideoAsWatchedJSON)
104		api.POST("videos/:video_id/unwatch", srv.MarkVideoAsUnwatchedJSON)
105		api.POST("videos/:video_id/progress", srv.SetVideoProgressJSON)
106		api.POST("videos/:video_id/download", srv.DownloadVideoJSON)
107		api.GET("videos/:video_id/file", srv.ServeVideoFileJSON)
108	}
109
110	return engine, nil
111}