package main import ( "fmt" "strconv" "strings" "streamrip-go/internal/config" ) type smokeOptions struct { qualitySet bool quality int ignoreDB bool } type globalOptions struct { configPath string folder string noDB bool qualitySet bool quality int codecSet bool codec string noProgress bool noSSLVerify bool verbose bool command string commandArgs []string } func parseGlobalArgs(args []string) (globalOptions, error) { opts := globalOptions{} for i := 0; i < len(args); i++ { arg := args[i] if arg == "" { continue } if !strings.HasPrefix(arg, "-") { opts.command = arg if i+1 < len(args) { opts.commandArgs = append([]string(nil), args[i+1:]...) } return opts, nil } switch { case arg == "-ndb" || arg == "--no-db": opts.noDB = true case arg == "--no-progress": opts.noProgress = true case arg == "--no-ssl-verify": opts.noSSLVerify = true case arg == "-v" || arg == "--verbose": opts.verbose = true case arg == "-f" || arg == "--folder": if i+1 >= len(args) { return globalOptions{}, fmt.Errorf("%s requires a value", arg) } opts.folder = strings.TrimSpace(args[i+1]) i++ case strings.HasPrefix(arg, "--folder="): opts.folder = strings.TrimSpace(strings.TrimPrefix(arg, "--folder=")) case arg == "--config-path": if i+1 >= len(args) { return globalOptions{}, fmt.Errorf("--config-path requires a value") } opts.configPath = strings.TrimSpace(args[i+1]) i++ case strings.HasPrefix(arg, "--config-path="): opts.configPath = strings.TrimSpace(strings.TrimPrefix(arg, "--config-path=")) case arg == "-q" || arg == "--quality": if i+1 >= len(args) { return globalOptions{}, fmt.Errorf("%s requires a value", arg) } q, err := strconv.Atoi(args[i+1]) if err != nil || q < 0 || q > 4 { return globalOptions{}, fmt.Errorf("invalid quality %q (expected 0-4)", args[i+1]) } opts.qualitySet = true opts.quality = q i++ case strings.HasPrefix(arg, "--quality="): qRaw := strings.TrimSpace(strings.TrimPrefix(arg, "--quality=")) q, err := strconv.Atoi(qRaw) if err != nil || q < 0 || q > 4 { return globalOptions{}, fmt.Errorf("invalid quality %q (expected 0-4)", qRaw) } opts.qualitySet = true opts.quality = q case arg == "-c" || arg == "--codec": if i+1 >= len(args) { return globalOptions{}, fmt.Errorf("%s requires a value", arg) } codec, err := normalizeCodec(args[i+1]) if err != nil { return globalOptions{}, err } opts.codecSet = true opts.codec = codec i++ case strings.HasPrefix(arg, "--codec="): codecRaw := strings.TrimSpace(strings.TrimPrefix(arg, "--codec=")) codec, err := normalizeCodec(codecRaw) if err != nil { return globalOptions{}, err } opts.codecSet = true opts.codec = codec default: return globalOptions{}, fmt.Errorf("unknown global option %q", arg) } } return opts, nil } func normalizeCodec(raw string) (string, error) { codec := strings.ToUpper(strings.TrimSpace(raw)) switch codec { case "ALAC", "FLAC", "MP3", "AAC", "VORBIS": return codec, nil case "OGG": return "VORBIS", nil default: return "", fmt.Errorf("unsupported codec %q (expected ALAC, FLAC, OGG, MP3, AAC)", raw) } } func applyGlobalConfigOverrides(cfg *config.Config, opts globalOptions) { if opts.folder != "" { cfg.Session.Downloads.Folder = opts.folder } if opts.noDB { cfg.Session.Database.DownloadsEnabled = false } if opts.qualitySet { cfg.Session.Qobuz.Quality = opts.quality cfg.Session.Tidal.Quality = opts.quality cfg.Session.Deezer.Quality = opts.quality cfg.Session.Soundcloud.Quality = opts.quality } if opts.codecSet { cfg.Session.Conversion.Enabled = true cfg.Session.Conversion.Codec = opts.codec } if opts.noProgress { cfg.Session.CLI.ProgressBars = false } if opts.noSSLVerify { cfg.Session.Downloads.VerifySSL = false } } func errorWithActionableHint(err error, opts globalOptions) string { if err == nil { return "" } msg := err.Error() if opts.noSSLVerify { return msg } lower := strings.ToLower(msg) if strings.Contains(lower, "x509") || strings.Contains(lower, "certificate") || strings.Contains(lower, "tls") || strings.Contains(lower, "ssl") { return msg + " (hint: try again with --no-ssl-verify)" } return msg } func parseSmokeOptions(args []string, minQuality int, maxQuality int) (smokeOptions, error) { opts := smokeOptions{} for _, arg := range args { switch arg { case "--force", "--ignore-db": opts.ignoreDB = true default: q, err := parseQuality(arg, minQuality, maxQuality) if err != nil { return smokeOptions{}, fmt.Errorf("unknown option %q", arg) } opts.quality = q opts.qualitySet = true } } return opts, nil } func parseQuality(raw string, min int, max int) (int, error) { q, err := strconv.Atoi(raw) if err != nil { return 0, err } if q < min || q > max { return 0, fmt.Errorf("quality must be %d-%d, got %d", min, max, q) } return q, nil } func asString(v any) string { switch t := v.(type) { case string: return t case int: return strconv.Itoa(t) case int64: return strconv.FormatInt(t, 10) case float64: return strconv.FormatFloat(t, 'f', -1, 64) default: return "" } }