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}