main.go
3525 bytes
1package main
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "os"
9 "os/signal"
10 "sync"
11 "syscall"
12 "time"
13
14 "github.com/gin-gonic/gin"
15
16 "github.com/TheEdgeOfRage/ytrssil-api/config"
17 "github.com/TheEdgeOfRage/ytrssil-api/db"
18 "github.com/TheEdgeOfRage/ytrssil-api/feedparser"
19 "github.com/TheEdgeOfRage/ytrssil-api/handler"
20 "github.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
21 "github.com/TheEdgeOfRage/ytrssil-api/lib/clients/youtube"
22 "github.com/TheEdgeOfRage/ytrssil-api/lib/downloader"
23)
24
25func init() {
26 // always use UTC
27 time.Local = time.UTC
28}
29
30func fetcherRoutine(ctx context.Context, l *slog.Logger, h handler.Handler, fetchInterval time.Duration) {
31 ticker := time.NewTicker(fetchInterval)
32 for {
33 select {
34 case <-ctx.Done():
35 l.Info("Fetcher context done, stopping fetcher")
36 return
37 case <-ticker.C:
38 err := h.FetchVideos(context.Background())
39 if err != nil {
40 l.Error("Failed to fetch videos", "call", "handler.FetchVideos", "err", err)
41 }
42 }
43 }
44}
45
46func main() {
47 logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
48
49 cfg, err := config.Parse()
50 if err != nil {
51 logger.Error("Failed to parse config", "call", "config.Parse", "error", err)
52 return
53 }
54
55 logger.Info("Running migrations")
56 if err := db.RunMigrations(cfg.DBURI); err != nil {
57 logger.Error("Failed to run migrations", "error", err)
58 return
59 }
60
61 db, err := db.NewPostgresDB(logger, cfg.DBURI)
62 if err != nil {
63 logger.Error(
64 "Failed to create DB connection",
65 "call", "db.NewPostgresDB",
66 "error", err,
67 )
68 return
69 }
70 defer db.Close()
71
72 parser := feedparser.NewParser(logger)
73 youTubeClient := youtube.NewYouTubeClient(logger, cfg.YouTubeAPIKey)
74 downloader := downloader.NewYtdlpDownloader(logger, cfg.YTDLPCookiesFile)
75 if err := downloader.ValidateInstallation(); err != nil {
76 logger.Error("yt-dlp validation failed", "error", err)
77 return
78 }
79
80 handler := handler.New(
81 logger,
82 db,
83 parser,
84 youTubeClient,
85 downloader,
86 cfg,
87 )
88 if cfg.Dev {
89 gin.SetMode(gin.DebugMode)
90 } else {
91 gin.SetMode(gin.ReleaseMode)
92 }
93 router, err := ytrssil.SetupGinRouter(logger, cfg, handler)
94 if err != nil {
95 logger.Error(
96 "Failed to set up gin server",
97 "call", "ytrssil.SetupGinServer",
98 "error", err,
99 )
100 return
101 }
102
103 server := &http.Server{
104 Addr: fmt.Sprintf(":%v", cfg.Port),
105 Handler: router,
106 }
107
108 wg := &sync.WaitGroup{}
109 quit := make(chan os.Signal, 1)
110 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
111
112 // start periodic fetch videos routine
113 fetcherContext, cancelFetcher := context.WithCancel(context.Background())
114 if !cfg.Dev {
115 wg.Go(func() {
116 fetcherRoutine(fetcherContext, logger, handler, cfg.FetchInterval)
117 })
118 }
119
120 cleanupContext, cancelCleanup := context.WithCancel(context.Background())
121 if !cfg.Dev {
122 wg.Go(func() {
123 handler.CleanupRoutine(cleanupContext)
124 })
125 }
126
127 wg.Go(func() {
128 logger.Info("ytrssil API is starting up", "port", cfg.Port)
129 if err := server.ListenAndServe(); err != nil {
130 if err != http.ErrServerClosed {
131 logger.Error("Server crashed", "call", "server.ListenAndServe", "error", err)
132 }
133 }
134 })
135
136 s := <-quit
137 logger.Info("Received signal, shutting down", "signal", s)
138 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
139 defer cancel()
140 if err := server.Shutdown(ctx); err != nil {
141 logger.Error(
142 "Failed to shutdown server",
143 "call", "server.Shutdown",
144 "error", err,
145 )
146 }
147 cancelFetcher()
148 if !cfg.Dev {
149 cancelCleanup()
150 }
151
152 wg.Wait()
153 logger.Info("exit complete")
154}