package main import ( "context" "errors" "fmt" "os" "os/signal" "syscall" "streamrip-go/internal/config" "streamrip-go/internal/verbose" _ "modernc.org/sqlite" ) func main() { os.Exit(run()) } // run executes the CLI and returns the process exit code. It exists so that // deferred cleanup (SQLite handles, progress bars, artwork temp dirs) always // runs; calling os.Exit directly from the command bodies skipped every defer. func run() int { gopts, err := parseGlobalArgs(os.Args[1:]) if err != nil { fmt.Fprintf(os.Stderr, "option error: %v\n", err) return 2 } if gopts.version { printVersion(os.Stdout) return 0 } if gopts.help { printMainHelp(os.Stdout) return 0 } if gopts.command == "" { printMainHelp(os.Stdout) return 2 } if gopts.command == "help" { if len(gopts.commandArgs) == 0 || isHelpArg(gopts.commandArgs[0]) { printMainHelp(os.Stdout) return 0 } if printCommandHelp(os.Stdout, gopts.commandArgs[0]) { return 0 } fmt.Fprintf(os.Stderr, "unknown help topic: %s\n", gopts.commandArgs[0]) return 2 } if gopts.command == "dev-help" { printDeveloperHelp(os.Stdout) return 0 } if commandWantsHelp(gopts.commandArgs) { if printCommandHelp(os.Stdout, gopts.command) { return 0 } fmt.Fprintf(os.Stderr, "unknown command: %s\n", gopts.command) return 2 } cfg, err := config.Load(gopts.configPath) if err != nil { if errors.Is(err, config.ErrOutdatedConfig) { resolvedPath, upErr := config.UpgradeOutdated(gopts.configPath) if upErr != nil { fmt.Fprintf(os.Stderr, "config error: %v\n", err) fmt.Fprintf(os.Stderr, "config auto-upgrade failed: %v\n", upErr) return 1 } fmt.Fprintf(os.Stderr, "config upgraded at %s\n", resolvedPath) cfg, err = config.Load(gopts.configPath) } } if err != nil { fmt.Fprintf(os.Stderr, "config error: %v\n", err) return 1 } applyGlobalConfigOverrides(cfg, gopts) verbose.SetLevel(gopts.verbose) if gopts.verbose >= 2 { fmt.Fprintln(os.Stderr, "verbose mode enabled (level 2: downloads + http)") } else if gopts.verbose >= 1 { fmt.Fprintln(os.Stderr, "verbose mode enabled (level 1: downloads)") } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() args := gopts.commandArgs switch gopts.command { case "url": return cmdURL(ctx, cfg, gopts, args) case "file": return cmdFile(ctx, cfg, gopts, args) case "config": return cmdConfig(ctx, cfg, gopts, args) case "database": return cmdDatabase(ctx, cfg, gopts, args) case "id": return cmdID(ctx, cfg, gopts, args) case "search": return cmdSearch(ctx, cfg, gopts, args) case "lastfm": return cmdLastFM(ctx, cfg, gopts, args) case "soundcloud-smoke": return cmdSoundcloudSmoke(ctx, cfg, gopts, args) case "qobuz-smoke": return cmdQobuzSmoke(ctx, cfg, gopts, args) case "qobuz-convert-rip-smoke": return cmdQobuzConvertRipSmoke(ctx, cfg, gopts, args) case "qobuz-search-smoke": return cmdQobuzSearchSmoke(ctx, cfg, gopts, args) case "tidal-search-smoke": return cmdTidalSearchSmoke(ctx, cfg, gopts, args) case "tidal-metadata-smoke": return cmdTidalMetadataSmoke(ctx, cfg, gopts, args) case "tidal-video-smoke": return cmdTidalVideoSmoke(ctx, cfg, gopts, args) default: fmt.Fprintf(os.Stderr, "unknown command: %s\n", gopts.command) return 2 } }