mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-10-16 22:07:30 +02:00
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>
This commit is contained in:
5 files changed
+936
-800
No files matched your search
@@ -0,0 +1,281 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"streamrip-go/internal/app"
|
||||
"streamrip-go/internal/config"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// Commands that discover media before ripping it: search and lastfm.
|
||||
|
||||
func cmdSearch(ctx context.Context, cfg *config.Config, gopts globalOptions, args []string) int {
|
||||
var err error
|
||||
var source, mediaType string
|
||||
var sopts searchOptions
|
||||
if len(args) < 3 {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
printCommandHelp(os.Stdout, "search")
|
||||
return 2
|
||||
}
|
||||
source, mediaType, sopts, err = promptSearchInteractive(cfg.Session.CLI.MaxSearchResults)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "search prompt error: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
} else {
|
||||
source = strings.ToLower(strings.TrimSpace(args[0]))
|
||||
mediaType = strings.ToLower(strings.TrimSpace(args[1]))
|
||||
sopts, err = parseSearchArgs(args[2:], cfg.Session.CLI.MaxSearchResults)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "search option error: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if sopts.first && sopts.outputFile != "" {
|
||||
fmt.Fprintln(os.Stderr, "cannot choose --first and --output-file together")
|
||||
return 2
|
||||
}
|
||||
if !isAllowedSearchSource(source) {
|
||||
fmt.Fprintf(os.Stderr, "unsupported search source %q\n", source)
|
||||
return 2
|
||||
}
|
||||
if !isAllowedMediaType(mediaType) {
|
||||
fmt.Fprintf(os.Stderr, "unsupported media type %q\n", mediaType)
|
||||
return 2
|
||||
}
|
||||
if source == "soundcloud" && mediaType != "track" && mediaType != "playlist" {
|
||||
fmt.Fprintln(os.Stderr, "soundcloud search currently supports media types track and playlist")
|
||||
return 2
|
||||
}
|
||||
if source == "yandex" && mediaType != "track" && mediaType != "album" && mediaType != "playlist" && mediaType != "artist" {
|
||||
fmt.Fprintln(os.Stderr, "yandex search currently supports media types track, album, playlist, and artist")
|
||||
return 2
|
||||
}
|
||||
if source == "beatport" && mediaType != "track" && mediaType != "album" && mediaType != "label" {
|
||||
fmt.Fprintln(os.Stderr, "beatport search currently supports media types track, album, and label")
|
||||
return 2
|
||||
}
|
||||
if sopts.query == "" {
|
||||
fmt.Fprintln(os.Stderr, "search query cannot be empty")
|
||||
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, source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s login error: %v\n", source, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
pages, err := provider.Search(ctx, mediaType, sopts.query, sopts.limit)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
results := normalizeSearchResults(source, mediaType, pages)
|
||||
if len(results) == 0 {
|
||||
fmt.Println("no results")
|
||||
return 0
|
||||
}
|
||||
if sopts.outputFile != "" {
|
||||
fmt.Printf("results: %d\n", len(results))
|
||||
for i, result := range results {
|
||||
fmt.Printf("%2d. id=%s | %s\n", i+1, result.ID, result.Title)
|
||||
}
|
||||
if err = writeSearchResultsToFile(source, mediaType, results, sopts.outputFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "write results error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("wrote %d results to %s\n", len(results), sopts.outputFile)
|
||||
return 0
|
||||
}
|
||||
if sopts.first {
|
||||
fmt.Printf("results: %d\n", len(results))
|
||||
fmt.Printf(" 1. id=%s | %s\n", results[0].ID, results[0].Title)
|
||||
results = results[:1]
|
||||
}
|
||||
if sopts.noDownload {
|
||||
fmt.Printf("results: %d\n", len(results))
|
||||
for i, result := range results {
|
||||
fmt.Printf("%2d. id=%s | %s\n", i+1, result.ID, result.Title)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if !sopts.first {
|
||||
fmt.Printf("results: %d\n", len(results))
|
||||
}
|
||||
|
||||
if sopts.first {
|
||||
selection := []int{0}
|
||||
mainApp.IgnoreDB = sopts.ignoreDB || gopts.noDB
|
||||
skippedDownloaded := 0
|
||||
added := 0
|
||||
for _, idx := range selection {
|
||||
item := results[idx]
|
||||
if !sopts.ignoreDB {
|
||||
already, checkErr := mainApp.Store.IsDownloaded(ctx, source, item.ID)
|
||||
if checkErr == nil && already {
|
||||
skippedDownloaded++
|
||||
fmt.Printf("skip (already downloaded): id=%s | %s\n", item.ID, item.Title)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err = mainApp.AddByID(ctx, source, mediaType, item.ID); err != nil {
|
||||
fmt.Printf("add failed: id=%s err=%v\n", item.ID, err)
|
||||
continue
|
||||
}
|
||||
added++
|
||||
}
|
||||
if added == 0 {
|
||||
if skippedDownloaded > 0 {
|
||||
fmt.Println("selected item was already downloaded (use --force to redownload)")
|
||||
} else {
|
||||
fmt.Println("nothing selected to download")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if err = mainApp.Resolve(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
||||
return 1
|
||||
}
|
||||
if err = mainApp.Rip(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("search download complete (%d item(s))\n", added)
|
||||
return 0
|
||||
}
|
||||
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
fmt.Println("non-interactive input; use `rip id` to download specific results")
|
||||
return 0
|
||||
}
|
||||
|
||||
selection, err := promptSearchSelectionMenu(source, mediaType, sopts.query, results)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "selection error: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if len(selection) == 0 {
|
||||
fmt.Println("download cancelled")
|
||||
return 0
|
||||
}
|
||||
|
||||
mainApp.IgnoreDB = sopts.ignoreDB || gopts.noDB
|
||||
skippedDownloaded := 0
|
||||
added := 0
|
||||
for _, idx := range selection {
|
||||
item := results[idx]
|
||||
if !sopts.ignoreDB {
|
||||
already, checkErr := mainApp.Store.IsDownloaded(ctx, source, item.ID)
|
||||
if checkErr == nil && already {
|
||||
skippedDownloaded++
|
||||
fmt.Printf("skip (already downloaded): id=%s | %s\n", item.ID, item.Title)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err = mainApp.AddByID(ctx, source, mediaType, item.ID); err != nil {
|
||||
fmt.Printf("add failed: id=%s err=%v\n", item.ID, err)
|
||||
continue
|
||||
}
|
||||
added++
|
||||
}
|
||||
if added == 0 {
|
||||
if skippedDownloaded > 0 {
|
||||
fmt.Println("all selected items were already downloaded (use --force to redownload)")
|
||||
} else {
|
||||
fmt.Println("nothing selected to download")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if err = mainApp.Resolve(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
||||
return 1
|
||||
}
|
||||
if err = mainApp.Rip(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("search download complete (%d item(s))\n", added)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func cmdLastFM(ctx context.Context, cfg *config.Config, gopts globalOptions, args []string) int {
|
||||
opts, parseErr := parseLastFMArgs(args, cfg.Session.LastFM.Source, cfg.Session.LastFM.FallbackSource)
|
||||
if parseErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "lastfm option error: %v\n", parseErr)
|
||||
printCommandHelp(os.Stdout, "lastfm")
|
||||
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() }()
|
||||
mainApp.IgnoreDB = gopts.noDB
|
||||
|
||||
title, tracks, err := fetchLastFMPlaylist(ctx, cfg.Session.Downloads.VerifySSL, opts.PlaylistURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "lastfm parse error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if len(tracks) == 0 {
|
||||
fmt.Println("no tracks found in playlist")
|
||||
return 0
|
||||
}
|
||||
fmt.Printf("lastfm playlist: %s (%d tracks)\n", title, len(tracks))
|
||||
|
||||
resolvedTracks, err := resolveLastFMTracks(ctx, mainApp, opts, tracks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "lastfm resolve error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if len(resolvedTracks) == 0 {
|
||||
fmt.Println("no lastfm tracks resolved")
|
||||
return 0
|
||||
}
|
||||
|
||||
playlistID := fmt.Sprintf("lastfm:%s", strings.ToLower(strings.ReplaceAll(title, " ", "_")))
|
||||
refs := make([]app.PlaylistTrackRef, 0, len(resolvedTracks))
|
||||
for _, item := range resolvedTracks {
|
||||
refs = append(refs, app.PlaylistTrackRef{Source: item.Source, ID: item.ID})
|
||||
}
|
||||
if addErr := mainApp.AddMixedPlaylistByTrackRefs(ctx, playlistID, title, refs); addErr != nil {
|
||||
fmt.Printf("playlist queue failed: err=%v\n", addErr)
|
||||
fmt.Println("no lastfm playlists queued")
|
||||
return 0
|
||||
}
|
||||
fmt.Printf("queued lastfm playlist: %s (%d tracks)\n", title, len(refs))
|
||||
if len(refs) == 0 {
|
||||
fmt.Println("no lastfm playlists queued")
|
||||
return 0
|
||||
}
|
||||
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("lastfm rip complete (%d track(s) across 1 playlist)\n", len(resolvedTracks))
|
||||
|
||||
return 0
|
||||
}
|
||||
Reference in new issue
Block a user