main.go
⎇
Raw
1package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "os"
8 "os/exec"
9 "os/signal"
10 "path/filepath"
11 "syscall"
12 "time"
13
14 "vidarchive/internal/config"
15 "vidarchive/internal/database"
16 "vidarchive/internal/handler"
17 "vidarchive/internal/repository"
18 "vidarchive/internal/server"
19 "vidarchive/internal/service"
20 "vidarchive/internal/worker"
21)
22
23func main() {
24 // All configuration is environment variables (see README). An argument is
25 // a mistake, so fail instead of silently starting the server.
26 if len(os.Args) > 1 {
27 fmt.Fprintf(os.Stderr, "vidarchive takes no arguments; configure it with VIDARCHIVE_* environment variables (got %q)\n", os.Args[1:])
28 os.Exit(2)
29 }
30
31 // Cancelled on SIGINT/SIGTERM so the server drains, workers stop, running
32 // yt-dlp children are killed and the database is checkpointed and closed.
33 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
34 defer stop()
35
36 cfg := config.New()
37
38 checkDependencies(cfg)
39
40 if err := os.MkdirAll(cfg.DataDir, 0o755); err != nil {
41 log.Fatalf("Failed to create data dir: %v", err)
42 }
43 if err := os.MkdirAll(cfg.LibraryDir, 0o755); err != nil {
44 log.Fatalf("Failed to create library dir: %v", err)
45 }
46 if err := os.MkdirAll(cfg.TempDir, 0o755); err != nil {
47 log.Fatalf("Failed to create temp dir: %v", err)
48 }
49 if err := os.MkdirAll(filepath.Join(cfg.DataDir, "archives"), 0o755); err != nil {
50 log.Fatalf("Failed to create archives dir: %v", err)
51 }
52
53 db, err := database.New(cfg)
54 if err != nil {
55 log.Fatalf("Failed to initialize database: %v", err)
56 }
57
58 presetRepo := repository.NewPresetRepository(db)
59 downloadRepo := repository.NewDownloadRepository(db)
60 settingsRepo := repository.NewSettingsRepository(db)
61 subscriptionRepo := repository.NewSubscriptionRepository(db)
62
63 presetSvc := service.NewPresetService(presetRepo)
64 librarySvc := service.NewLibraryService(cfg.LibraryDir, cfg.FFmpegPath, cfg.FFprobePath)
65 settingsSvc := service.NewSettingsService(settingsRepo)
66 subscriptionSvc := service.NewSubscriptionService(subscriptionRepo, cfg)
67 downloadSvc := service.NewDownloadService(downloadRepo, librarySvc, presetSvc, settingsSvc, subscriptionSvc, cfg)
68
69 workerPool := worker.New(downloadSvc, cfg.Workers)
70 workerPool.Start()
71
72 scheduler := worker.NewScheduler(subscriptionSvc, downloadSvc, workerPool, time.Duration(cfg.SchedulerInterval)*time.Second)
73 scheduler.Start()
74
75 h, err := handler.New(cfg, presetSvc, downloadSvc, librarySvc, settingsSvc, subscriptionSvc, workerPool)
76 if err != nil {
77 log.Fatalf("Failed to initialize handler: %v", err)
78 }
79
80 srv := server.New(cfg, h)
81
82 fmt.Printf("VidArchive starting...\n")
83 fmt.Printf("Data directory: %s\n", cfg.DataDir)
84 fmt.Printf("Library directory: %s\n", cfg.LibraryDir)
85 fmt.Printf("Temp directory: %s\n", cfg.TempDir)
86
87 srvErr := srv.Start(ctx)
88
89 // Shut down in dependency order: stop scheduling new runs, then stop the
90 // workers (killing any running yt-dlp), then checkpoint and close the DB.
91 // This runs on both a signal and a server error, so nothing is left orphaned.
92 log.Printf("Shutting down...")
93 scheduler.Stop()
94 workerPool.Stop()
95 if err := database.Checkpoint(db); err != nil {
96 log.Printf("Warning: %v", err)
97 }
98 if err := db.Close(); err != nil {
99 log.Printf("Warning: failed to close database: %v", err)
100 }
101 log.Printf("Shutdown complete")
102
103 if srvErr != nil {
104 log.Fatalf("Server error: %v", srvErr)
105 }
106}
107
108// checkDependencies warns (loudly, but without aborting) when an external binary
109// VidArchive relies on isn't on PATH, so a missing yt-dlp/ffmpeg surfaces as a
110// clear startup message rather than an opaque per-download failure later.
111func checkDependencies(cfg *config.Config) {
112 deps := []struct{ label, path string }{
113 {"yt-dlp", cfg.YTDLPPath},
114 {"ffmpeg", cfg.FFmpegPath},
115 {"ffprobe", cfg.FFprobePath},
116 }
117 for _, dep := range deps {
118 if _, err := exec.LookPath(dep.path); err != nil {
119 log.Printf("WARNING: %s (%q) not found in PATH; related features will fail until it is installed", dep.label, dep.path)
120 }
121 }
122}
123