Parent directory

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}