Files
streamrip-go/cmd/rip/cmd_smoke.go
T
JorenandClaude Opus 5 78d4f0316d refactor: split the command dispatch out of main.go
Completes the restructuring started when main() became run() int. main.go goes
from 1217 lines to 130: global option handling, config loading, signal setup,
and a dispatch switch that delegates to one function per command.

The command bodies move to cmd_rip.go (url, file, id), cmd_search.go (search,
lastfm), cmd_config.go (config, database) and cmd_smoke.go (the developer smoke
commands), each taking (ctx, cfg, gopts, args) and returning an exit code.

They also stop reading the process global. Previously run() did

    os.Args = append([]string{os.Args[0], gopts.command}, gopts.commandArgs...)

and the arms then indexed back into os.Args[2:], os.Args[5:] and so on, so the
already-parsed options were round-tripped through os.Args. The arms now take
the parsed gopts.commandArgs directly, which is a uniform index shift of two
(os.Args[5:] -> args[3:], len(os.Args) < 5 -> len(args) < 3).

No behaviour change. Verified by running each command's argument validation and
help path against the built binary, plus the existing suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 17:17:24 +02:00

310 lines
8.0 KiB
Go

package main
import (
"context"
"fmt"
"os"
"strings"
"streamrip-go/internal/app"
"streamrip-go/internal/config"
"streamrip-go/internal/provider"
)
// Developer smoke commands, listed by "rip dev-help". These exercise a single
// provider call and print the result; they are not part of the supported CLI.
func cmdSoundcloudSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 1 {
fmt.Println("usage: rip soundcloud-smoke <soundcloud_url>")
return 2
}
meta, err := fetchSoundcloudOEmbed(ctx, cfg.Session.Downloads.VerifySSL, strings.TrimSpace(args[0]))
if err != nil {
fmt.Fprintf(os.Stderr, "soundcloud smoke error: %v\n", err)
return 1
}
fmt.Printf("soundcloud oembed ok: title=%q author=%q provider=%q\n", asString(meta["title"]), asString(meta["author_name"]), asString(meta["provider_name"]))
return 0
}
func cmdQobuzSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 1 {
fmt.Println("usage: rip qobuz-smoke <track_id> [quality]")
return 2
}
opts, err := parseSmokeOptions(args[1:], 1, 4)
if err != nil {
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
return 2
}
if opts.qualitySet {
cfg.Session.Qobuz.Quality = opts.quality
}
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
provider, err := mainApp.GetLoggedInProvider(ctx, "qobuz")
if err != nil {
fmt.Fprintf(os.Stderr, "qobuz login error: %v\n", err)
return 1
}
trackID := args[0]
meta, err := provider.GetMetadata(ctx, trackID, "track")
if err != nil {
fmt.Fprintf(os.Stderr, "metadata error: %v\n", err)
return 1
}
title, _ := meta["title"].(string)
d, err := provider.GetDownloadable(ctx, trackID, cfg.Session.Qobuz.Quality)
if err != nil {
fmt.Fprintf(os.Stderr, "downloadable error: %v\n", err)
return 1
}
fmt.Printf("qobuz ok: title=%q quality=%d ext=%s\n", title, cfg.Session.Qobuz.Quality, d.Extension)
fmt.Printf("stream_url=%s\n", d.URL)
return 0
}
func cmdQobuzConvertRipSmoke(ctx context.Context, cfg *config.Config, gopts globalOptions, args []string) int {
if len(args) < 2 {
fmt.Println("usage: rip qobuz-convert-rip-smoke <track_id> <codec> [quality] [--force|--ignore-db]")
return 2
}
opts, err := parseSmokeOptions(args[2:], 1, 4)
if err != nil {
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
return 2
}
if opts.qualitySet {
cfg.Session.Qobuz.Quality = opts.quality
}
cfg.Session.Conversion.Enabled = true
cfg.Session.Conversion.Codec = strings.ToUpper(strings.TrimSpace(args[1]))
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
trackID := args[0]
if err = mainApp.AddByID(ctx, "qobuz", "track", trackID); err != nil {
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
return 1
}
if err = mainApp.Resolve(ctx); err != nil {
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
return 1
}
if err = mainApp.Rip(ctx); err != nil {
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
return 1
}
fmt.Printf("qobuz convert rip smoke complete (codec=%s)\n", cfg.Session.Conversion.Codec)
return 0
}
func cmdQobuzSearchSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 1 {
fmt.Println("usage: rip qobuz-search-smoke <query>")
return 2
}
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
provider, err := mainApp.GetLoggedInProvider(ctx, "qobuz")
if err != nil {
fmt.Fprintf(os.Stderr, "qobuz login error: %v\n", err)
return 1
}
query := strings.Join(args, " ")
pages, err := provider.Search(ctx, "album", query, 10)
if err != nil {
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
return 1
}
for _, page := range pages {
albums, ok := page["albums"].(map[string]any)
if !ok {
continue
}
items, ok := albums["items"].([]any)
if !ok {
continue
}
for _, raw := range items {
item, ok := raw.(map[string]any)
if !ok {
continue
}
id := asString(item["id"])
title := asString(item["title"])
version := asString(item["version"])
bitDepth := asString(item["maximum_bit_depth"])
sr := asString(item["maximum_sampling_rate"])
if version != "" {
title = title + " (" + version + ")"
}
fmt.Printf("album_id=%s | %s | %sB-%skHz\n", id, title, bitDepth, sr)
}
}
return 0
}
func cmdTidalSearchSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 1 {
fmt.Println("usage: rip tidal-search-smoke <query>")
return 2
}
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
provider, err := mainApp.GetLoggedInProvider(ctx, "tidal")
if err != nil {
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
return 1
}
query := strings.Join(args, " ")
pages, err := provider.Search(ctx, "album", query, 10)
if err != nil {
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
return 1
}
for _, page := range pages {
items, ok := page["items"].([]any)
if !ok {
continue
}
for _, raw := range items {
wrapper, ok := raw.(map[string]any)
if !ok {
continue
}
item, ok := wrapper["item"].(map[string]any)
if !ok {
item = wrapper
}
fmt.Printf("album_id=%s | %s\n", asString(item["id"]), asString(item["title"]))
}
}
return 0
}
func cmdTidalMetadataSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 2 {
fmt.Println("usage: rip tidal-metadata-smoke <track|album|playlist|artist> <id>")
return 2
}
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
provider, err := mainApp.GetLoggedInProvider(ctx, "tidal")
if err != nil {
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
return 1
}
mediaType := args[0]
itemID := args[1]
meta, err := provider.GetMetadata(ctx, itemID, mediaType)
if err != nil {
fmt.Fprintf(os.Stderr, "metadata error: %v\n", err)
return 1
}
title := asString(meta["title"])
if title == "" {
title = asString(meta["name"])
}
trackCount := 0
if tracksMap, ok := meta["tracks"].(map[string]any); ok {
if items, ok := tracksMap["items"].([]map[string]any); ok {
trackCount = len(items)
} else if anyItems, ok := tracksMap["items"].([]any); ok {
trackCount = len(anyItems)
}
}
fmt.Printf("tidal metadata ok: type=%s id=%s title=%q tracks=%d\n", mediaType, itemID, title, trackCount)
return 0
}
func cmdTidalVideoSmoke(ctx context.Context, cfg *config.Config, _ globalOptions, args []string) int {
if len(args) < 1 {
fmt.Println("usage: rip tidal-video-smoke <video_id>")
return 2
}
mainApp, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
return 1
}
defer func() { _ = mainApp.Close() }()
providerClient, err := mainApp.GetLoggedInProvider(ctx, "tidal")
if err != nil {
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
return 1
}
videoProvider, ok := providerClient.(interface {
GetVideoDownloadable(context.Context, string) (*provider.Downloadable, error)
})
if !ok {
fmt.Fprintln(os.Stderr, "tidal provider does not support video downloadable")
return 1
}
videoID := strings.TrimSpace(args[0])
meta, err := providerClient.GetMetadata(ctx, videoID, "video")
if err != nil {
fmt.Fprintf(os.Stderr, "video metadata error: %v\n", err)
return 1
}
d, err := videoProvider.GetVideoDownloadable(ctx, videoID)
if err != nil {
fmt.Fprintf(os.Stderr, "video downloadable error: %v\n", err)
return 1
}
title := asString(meta["title"])
if title == "" {
title = asString(meta["name"])
}
fmt.Printf("tidal video ok: id=%s title=%q ext=%s\n", videoID, title, d.Extension)
fmt.Printf("stream_url=%s\n", d.URL)
return 0
}