package main import ( "context" "fmt" "log" "os" "os/exec" "os/signal" "path/filepath" "syscall" "time" "vidarchive/internal/config" "vidarchive/internal/database" "vidarchive/internal/handler" "vidarchive/internal/repository" "vidarchive/internal/server" "vidarchive/internal/service" "vidarchive/internal/worker" ) func main() { // Cancelled on SIGINT/SIGTERM so the server drains, workers stop, running // yt-dlp children are killed and the database is checkpointed and closed. ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() cfg := config.New() checkDependencies(cfg) if err := os.MkdirAll(cfg.DataDir, 0755); err != nil { log.Fatalf("Failed to create data dir: %v", err) } if err := os.MkdirAll(cfg.LibraryDir, 0755); err != nil { log.Fatalf("Failed to create library dir: %v", err) } if err := os.MkdirAll(cfg.TempDir, 0755); err != nil { log.Fatalf("Failed to create temp dir: %v", err) } if err := os.MkdirAll(filepath.Join(cfg.DataDir, "archives"), 0755); err != nil { log.Fatalf("Failed to create archives dir: %v", err) } db, err := database.New(cfg) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } presetRepo := repository.NewPresetRepository(db) downloadRepo := repository.NewDownloadRepository(db) settingsRepo := repository.NewSettingsRepository(db) subscriptionRepo := repository.NewSubscriptionRepository(db) presetSvc := service.NewPresetService(presetRepo) librarySvc := service.NewLibraryService(cfg.LibraryDir, cfg.FFmpegPath, cfg.FFprobePath) settingsSvc := service.NewSettingsService(settingsRepo) subscriptionSvc := service.NewSubscriptionService(subscriptionRepo, cfg) downloadSvc := service.NewDownloadService(downloadRepo, librarySvc, presetSvc, settingsSvc, subscriptionSvc, cfg) workerPool := worker.New(downloadSvc, cfg.Workers) workerPool.Start() scheduler := worker.NewScheduler(subscriptionSvc, downloadSvc, workerPool, time.Duration(cfg.SchedulerInterval)*time.Second) scheduler.Start() h, err := handler.New(cfg, presetSvc, downloadSvc, librarySvc, settingsSvc, subscriptionSvc, workerPool) if err != nil { log.Fatalf("Failed to initialize handler: %v", err) } srv := server.New(cfg, h) fmt.Printf("VidArchive starting...\n") fmt.Printf("Data directory: %s\n", cfg.DataDir) fmt.Printf("Library directory: %s\n", cfg.LibraryDir) fmt.Printf("Temp directory: %s\n", cfg.TempDir) srvErr := srv.Start(ctx) // Shut down in dependency order: stop scheduling new runs, then stop the // workers (killing any running yt-dlp), then checkpoint and close the DB. // This runs on both a signal and a server error, so nothing is left orphaned. log.Printf("Shutting down...") scheduler.Stop() workerPool.Stop() if err := database.Checkpoint(db); err != nil { log.Printf("Warning: %v", err) } if err := db.Close(); err != nil { log.Printf("Warning: failed to close database: %v", err) } log.Printf("Shutdown complete") if srvErr != nil { log.Fatalf("Server error: %v", srvErr) } } // checkDependencies warns (loudly, but without aborting) when an external binary // VidArchive relies on isn't on PATH, so a missing yt-dlp/ffmpeg surfaces as a // clear startup message rather than an opaque per-download failure later. func checkDependencies(cfg *config.Config) { deps := []struct{ label, path string }{ {"yt-dlp", cfg.YTDLPPath}, {"ffmpeg", cfg.FFmpegPath}, {"ffprobe", cfg.FFprobePath}, } for _, dep := range deps { if _, err := exec.LookPath(dep.path); err != nil { log.Printf("WARNING: %s (%q) not found in PATH; related features will fail until it is installed", dep.label, dep.path) } } }