package main import ( "context" "flag" "log" "net/http" "os" "os/signal" "syscall" "time" "jukebox_maker/internal/api" "jukebox_maker/internal/config" "jukebox_maker/internal/copier" "jukebox_maker/internal/db" "jukebox_maker/internal/disk" "jukebox_maker/internal/task" "jukebox_maker/internal/watcher" ) func main() { configPath := flag.String("config", "/config/config.json", "path to config file") addr := flag.String("addr", ":8080", "HTTP listen address") mediaPath := flag.String("media", "/media", "path to media source directory") mountPath := flag.String("mount", "/mnt/usb", "path to USB mount point") flag.Parse() cfg, err := config.Load(*configPath) if err != nil { log.Fatalf("load config: %v", err) } taskStore := task.NewStore() cp := copier.New(taskStore) var activeDB *db.DB var activeDiskID string openDiskDB := func(info disk.DiskInfo) { if activeDiskID == info.DiskID { return // already open for this disk } if activeDB != nil { activeDB.Close() activeDB = nil activeDiskID = "" } d, err := db.Open(disk.DBPath(info.MountPath)) if err != nil { log.Printf("open disk DB: %v", err) return } activeDB = d activeDiskID = info.DiskID cp.SetDB(d) log.Printf("disk DB opened for %s", info.DiskID) } closeDiskDB := func() { if activeDB != nil { activeDB.Close() activeDB = nil activeDiskID = "" cp.SetDB(nil) log.Println("disk DB closed") } } w := watcher.New(*mountPath, 5*time.Second, func(ev watcher.DiskEvent) { log.Printf("disk: %s -> %s", ev.Prev, ev.Info.State) switch ev.Info.State { case disk.DiskKnown: openDiskDB(ev.Info) if ev.Prev != disk.DiskKnown && cfg.AutoCopy { triggerAutoCopy(cp, cfg, ev.Info, *mediaPath) } case disk.DiskAbsent: closeDiskDB() } }) // Open DB immediately if disk already connected at startup { info, _ := disk.Probe(*mountPath) if info.State == disk.DiskKnown { openDiskDB(info) } } srv, err := api.New(api.Deps{ Config: cfg, ConfigPath: *configPath, Watcher: w, Copier: cp, Tasks: taskStore, MediaPath: *mediaPath, MountPath: *mountPath, }) if err != nil { log.Fatalf("init server: %v", err) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go w.Run(ctx) httpSrv := &http.Server{Addr: *addr, Handler: srv} go func() { log.Printf("jukebox_maker listening on %s", *addr) if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("http: %v", err) } }() <-ctx.Done() log.Println("shutting down…") shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() httpSrv.Shutdown(shutCtx) closeDiskDB() } func triggerAutoCopy(cp *copier.Copier, cfg *config.Config, info disk.DiskInfo, mediaPath string) { var sources []string for _, s := range cfg.Sources { if s.Enabled { sources = append(sources, s.Path) } } if len(sources) == 0 { return } go func() { _, err := cp.Start(context.Background(), copier.Options{ DiskID: info.DiskID, MountPath: info.MountPath, MediaPath: mediaPath, EnabledSources: sources, ReserveFreeGB: cfg.ReserveFreeGB, OverwriteMode: cfg.OverwriteMode, FileSelectMode: cfg.FileSelectMode, }) if err != nil { log.Printf("auto-copy: %v", err) } }() }