main.go
⎇
Raw
1package main
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
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 cfg.SetupLogging()
38
39 checkCredentials(cfg)
40 checkDependencies(cfg)
41
42 for _, dir := range []string{
43 cfg.DataDir,
44 cfg.LibraryDir,
45 cfg.TempDir,
46 filepath.Join(cfg.DataDir, "archives"),
47 } {
48 if err := os.MkdirAll(dir, 0o755); err != nil {
49 fatal("failed to create directory", "dir", dir, "err", err)
50 }
51 }
52
53 db, err := database.New(cfg)
54 if err != nil {
55 fatal("failed to initialize database", "err", 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 fatal("failed to initialize handler", "err", err)
78 }
79
80 srv := server.New(cfg, h)
81
82 slog.Info("VidArchive starting",
83 "data_dir", cfg.DataDir, "library_dir", cfg.LibraryDir, "temp_dir", cfg.TempDir,
84 "user", cfg.Username)
85
86 srvErr := srv.Start(ctx)
87
88 // Shut down in dependency order: stop scheduling new runs, then stop the
89 // workers (killing any running yt-dlp), then checkpoint and close the DB.
90 // This runs on both a signal and a server error, so nothing is left orphaned.
91 slog.Info("shutting down")
92 scheduler.Stop()
93 workerPool.Stop()
94 if err := database.Checkpoint(db); err != nil {
95 slog.Warn("database checkpoint failed", "err", err)
96 }
97 if err := db.Close(); err != nil {
98 slog.Warn("failed to close database", "err", err)
99 }
100 slog.Info("shutdown complete")
101
102 if srvErr != nil {
103 fatal("server error", "err", srvErr)
104 }
105}
106
107// fatal logs a startup or shutdown failure and exits. slog has no Fatal, and
108// log.Fatalf would report it at info level once slog owns the log package.
109func fatal(msg string, args ...any) {
110 slog.Error(msg, args...)
111 os.Exit(1)
112}
113
114// hashHint shows the operator how to generate a usable hash. htpasswd ships
115// with Apache's tools (apache2-utils on Debian, httpd-tools on Fedora).
116const hashHint = `htpasswd -bnBC 12 "" 'your-password' | tr -d ':\n'`
117
118// checkCredentials refuses to start without a usable login. A hash that only
119// fails at the login form would look like a forgotten password rather than a
120// misconfiguration.
121func checkCredentials(cfg *config.Config) {
122 if cfg.Username == "" || cfg.PasswordHash == "" {
123 fatal("VIDARCHIVE_USERNAME and VIDARCHIVE_PASSWORD_HASH are required", "hint", hashHint)
124 }
125 if err := handler.ValidatePasswordHash(cfg.PasswordHash); err != nil {
126 fatal("VIDARCHIVE_PASSWORD_HASH is not usable", "err", err, "hint", hashHint)
127 }
128}
129
130// checkDependencies warns (loudly, but without aborting) when an external binary
131// VidArchive relies on isn't on PATH, so a missing yt-dlp/ffmpeg surfaces as a
132// clear startup message rather than an opaque per-download failure later.
133func checkDependencies(cfg *config.Config) {
134 deps := []struct{ label, path string }{
135 {"yt-dlp", cfg.YTDLPPath},
136 {"ffmpeg", cfg.FFmpegPath},
137 {"ffprobe", cfg.FFprobePath},
138 }
139 for _, dep := range deps {
140 if _, err := exec.LookPath(dep.path); err != nil {
141 slog.Warn("dependency not found in PATH, related features will fail until it is installed", "dependency", dep.label, "path", dep.path)
142 }
143 }
144}
145