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) activeDBs := make(map[string]*db.DB) mountToDiskID := make(map[string]string) openDiskDB := func(info disk.DiskInfo) { if info.DiskID == "" { return } if prevDiskID, ok := mountToDiskID[info.MountPath]; ok && prevDiskID != info.DiskID { if prevDB := activeDBs[prevDiskID]; prevDB != nil { prevDB.Close() delete(activeDBs, prevDiskID) cp.SetDB(prevDiskID, nil) } } mountToDiskID[info.MountPath] = info.DiskID if _, ok := activeDBs[info.DiskID]; ok { return } d, err := db.Open(disk.DBPath(info.MountPath)) if err != nil { log.Printf("open disk DB: %v", err) return } activeDBs[info.DiskID] = d cp.SetDB(info.DiskID, d) log.Printf("disk DB opened for %s", info.DiskID) } closeDiskDB := func(info disk.DiskInfo) { diskID := info.DiskID if diskID == "" { diskID = mountToDiskID[info.MountPath] } if diskID == "" { return } cp.Cancel(diskID) cp.SetDB(diskID, nil) if d := activeDBs[diskID]; d != nil { d.Close() delete(activeDBs, diskID) log.Printf("disk DB closed for %s", diskID) } delete(mountToDiskID, info.MountPath) } watcherReady := false w := watcher.New(*mountPath, 5*time.Second, func(ev watcher.DiskEvent) { log.Printf("disk: %s %s -> %s", ev.Info.MountPath, ev.Prev.State, ev.Info.State) switch ev.Info.State { case disk.DiskKnown: openDiskDB(ev.Info) if watcherReady && ev.Prev.State != disk.DiskKnown && cfg.AutoCopy { triggerAutoCopy(cp, cfg, ev.Info, *mediaPath) } case disk.DiskForeign: closeDiskDB(ev.Prev) case disk.DiskAbsent: closeDiskDB(ev.Prev) } }) w.ProbeNow() watcherReady = true srv, err := api.New(api.Deps{ Config: cfg, ConfigPath: *configPath, Watcher: w, Copier: cp, Tasks: taskStore, MediaPath: *mediaPath, MountPath: *mountPath, OnDiskInit: func(mountPath, diskID string) { openDiskDB(disk.DiskInfo{ State: disk.DiskKnown, DiskID: diskID, 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) for _, info := range w.ListDisks() { closeDiskDB(info) } } 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, DestFolder: cfg.DestFolder, EnabledSources: sources, ReserveFreeGB: cfg.ReserveFreeGB, OverwriteMode: cfg.OverwriteMode, FileSelectMode: cfg.FileSelectMode, }) if err != nil { log.Printf("auto-copy: %v", err) } }() }