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 }