mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
1190 lines
34 KiB
Go
1190 lines
34 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/term"
|
|
|
|
"streamrip-go/internal/app"
|
|
"streamrip-go/internal/config"
|
|
"streamrip-go/internal/provider"
|
|
"streamrip-go/internal/verbose"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func main() {
|
|
gopts, err := parseGlobalArgs(os.Args[1:])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if gopts.command == "" {
|
|
fmt.Println("usage: rip <command>")
|
|
fmt.Println("commands: url, file, config, database, id, search, lastfm")
|
|
fmt.Println("tip: run `rip dev-help` to list developer smoke commands")
|
|
os.Exit(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)
|
|
os.Exit(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)
|
|
os.Exit(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)")
|
|
}
|
|
|
|
os.Args = append([]string{os.Args[0], gopts.command}, gopts.commandArgs...)
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
switch os.Args[1] {
|
|
case "dev-help":
|
|
fmt.Println("developer smoke commands:")
|
|
fmt.Println(" soundcloud-smoke")
|
|
fmt.Println(" qobuz-smoke, qobuz-rip-smoke, qobuz-convert-rip-smoke")
|
|
fmt.Println(" qobuz-album-rip-smoke, qobuz-playlist-rip-smoke, qobuz-artist-rip-smoke, qobuz-label-rip-smoke")
|
|
fmt.Println(" qobuz-search-smoke")
|
|
fmt.Println(" tidal-search-smoke, tidal-metadata-smoke, tidal-video-smoke")
|
|
fmt.Println(" tidal-rip-smoke, tidal-album-rip-smoke, tidal-playlist-rip-smoke, tidal-artist-rip-smoke")
|
|
return
|
|
case "url":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip url <url...> [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
rawArgs := make([]string, 0, len(os.Args[2:]))
|
|
ignoreDB := false
|
|
for _, arg := range os.Args[2:] {
|
|
if arg == "--force" || arg == "--ignore-db" {
|
|
ignoreDB = true
|
|
continue
|
|
}
|
|
rawArgs = append(rawArgs, arg)
|
|
}
|
|
mainApp.IgnoreDB = ignoreDB || gopts.noDB
|
|
|
|
added := 0
|
|
for _, raw := range rawArgs {
|
|
if addURLToQueue(ctx, mainApp, raw) {
|
|
added++
|
|
}
|
|
}
|
|
|
|
if added == 0 {
|
|
fmt.Println("nothing to rip")
|
|
return
|
|
}
|
|
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("url rip complete (%d item(s))\n", added)
|
|
case "file":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip file <path> [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
ignoreDB := false
|
|
for _, arg := range os.Args[3:] {
|
|
switch arg {
|
|
case "--force", "--ignore-db":
|
|
ignoreDB = true
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "option error: unknown option %q\n", arg)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
content, err := os.ReadFile(os.Args[2])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "read file error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
idItems, urls, repeated, jsonInput, err := parseFileInput(content)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "file parse error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = ignoreDB || gopts.noDB
|
|
|
|
added := 0
|
|
if jsonInput {
|
|
fmt.Printf("detected json file. loading %d item(s)\n", len(idItems))
|
|
for _, item := range idItems {
|
|
if err = mainApp.AddByID(ctx, item.Source, item.MediaType, item.ID); err != nil {
|
|
fmt.Printf("add failed: source=%s type=%s id=%s err=%v\n", item.Source, item.MediaType, item.ID, err)
|
|
continue
|
|
}
|
|
added++
|
|
}
|
|
} else {
|
|
if repeated > 0 {
|
|
fmt.Printf("found %d repeated url(s)\n", repeated)
|
|
}
|
|
fmt.Printf("detected list of urls. loading %d item(s)\n", len(urls))
|
|
for _, raw := range urls {
|
|
if addURLToQueue(ctx, mainApp, raw) {
|
|
added++
|
|
}
|
|
}
|
|
}
|
|
|
|
if added == 0 {
|
|
fmt.Println("nothing to rip")
|
|
return
|
|
}
|
|
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("file rip complete (%d item(s))\n", added)
|
|
case "config":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip config <open|reset|path> [options]")
|
|
os.Exit(2)
|
|
}
|
|
switch os.Args[2] {
|
|
case "open":
|
|
vim := false
|
|
for _, arg := range os.Args[3:] {
|
|
switch arg {
|
|
case "-v", "--vim":
|
|
vim = true
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "option error: unknown option %q\n", arg)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
fmt.Printf("opening file at %s\n", cfg.Path)
|
|
if err = openConfigInEditor(cfg.Path, vim); err != nil {
|
|
fmt.Fprintf(os.Stderr, "open config error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
case "reset":
|
|
yes := false
|
|
for _, arg := range os.Args[3:] {
|
|
switch arg {
|
|
case "-y", "--yes":
|
|
yes = true
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "option error: unknown option %q\n", arg)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
if !yes {
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
fmt.Fprintln(os.Stderr, "reset requires --yes in non-interactive mode")
|
|
os.Exit(2)
|
|
}
|
|
ok, askErr := promptYesNo(fmt.Sprintf("Are you sure you want to reset the config file at %s? [y/N]: ", cfg.Path))
|
|
if askErr != nil {
|
|
fmt.Fprintf(os.Stderr, "prompt error: %v\n", askErr)
|
|
os.Exit(1)
|
|
}
|
|
if !ok {
|
|
fmt.Println("reset aborted")
|
|
return
|
|
}
|
|
}
|
|
def := config.DefaultConfigData()
|
|
cfg.File = def
|
|
cfg.Session = def
|
|
if err = cfg.SaveFile(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "reset config error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("reset the config file at %s\n", cfg.Path)
|
|
case "path":
|
|
if len(os.Args) > 3 {
|
|
fmt.Fprintf(os.Stderr, "option error: unexpected argument %q\n", os.Args[3])
|
|
os.Exit(2)
|
|
}
|
|
fmt.Printf("config path: '%s'\n", cfg.Path)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown config command: %s\n", os.Args[2])
|
|
os.Exit(2)
|
|
}
|
|
case "database":
|
|
if len(os.Args) < 4 || os.Args[2] != "browse" {
|
|
fmt.Println("usage: rip database browse <downloads|failed>")
|
|
os.Exit(2)
|
|
}
|
|
table := strings.ToLower(strings.TrimSpace(os.Args[3]))
|
|
switch table {
|
|
case "downloads":
|
|
rows, listErr := listDownloadsRows(cfg.Session.Database.DownloadsPath)
|
|
if listErr != nil {
|
|
fmt.Fprintf(os.Stderr, "database browse error: %v\n", listErr)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("downloads database")
|
|
fmt.Println("row id")
|
|
for i, id := range rows {
|
|
fmt.Printf("%02d %s\n", i, id)
|
|
}
|
|
case "failed":
|
|
rows, listErr := listFailedRows(cfg.Session.Database.FailedDownloadsPath)
|
|
if listErr != nil && isNoSuchTableErr(listErr) && cfg.Session.Database.FailedDownloadsPath != cfg.Session.Database.DownloadsPath {
|
|
rows, listErr = listFailedRows(cfg.Session.Database.DownloadsPath)
|
|
}
|
|
if listErr != nil {
|
|
fmt.Fprintf(os.Stderr, "database browse error: %v\n", listErr)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("failed downloads database")
|
|
fmt.Println("row source media_type id")
|
|
for i, row := range rows {
|
|
fmt.Printf("%02d %s %s %s\n", i, row.Source, row.MediaType, row.ID)
|
|
}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "invalid database %q. choose downloads or failed\n", table)
|
|
os.Exit(2)
|
|
}
|
|
case "id":
|
|
if len(os.Args) < 5 {
|
|
fmt.Println("usage: rip id <source> <track|album|playlist|artist|label|video> <id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
source := strings.ToLower(strings.TrimSpace(os.Args[2]))
|
|
mediaType := strings.ToLower(strings.TrimSpace(os.Args[3]))
|
|
itemID := strings.TrimSpace(os.Args[4])
|
|
|
|
opts, err := parseSmokeOptions(os.Args[5:], 0, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
switch source {
|
|
case "qobuz":
|
|
if opts.quality < 1 {
|
|
fmt.Fprintf(os.Stderr, "quality error: qobuz quality must be 1-4\n")
|
|
os.Exit(2)
|
|
}
|
|
cfg.Session.Qobuz.Quality = opts.quality
|
|
case "tidal":
|
|
cfg.Session.Tidal.Quality = opts.quality
|
|
case "yandex":
|
|
cfg.Session.Yandex.Quality = opts.quality
|
|
}
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
if err = mainApp.AddByID(ctx, source, mediaType, itemID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("id rip complete: source=%s type=%s id=%s\n", source, mediaType, itemID)
|
|
case "search":
|
|
var source, mediaType string
|
|
var sopts searchOptions
|
|
if len(os.Args) < 5 {
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
fmt.Println("usage: rip search <qobuz|tidal|deezer|yandex|soundcloud> <track|album|playlist|artist|label|video> <query...> [--limit N] [--force|--ignore-db] [--no-download]")
|
|
os.Exit(2)
|
|
}
|
|
source, mediaType, sopts, err = promptSearchInteractive(cfg.Session.CLI.MaxSearchResults)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "search prompt error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
} else {
|
|
source = strings.ToLower(strings.TrimSpace(os.Args[2]))
|
|
mediaType = strings.ToLower(strings.TrimSpace(os.Args[3]))
|
|
sopts, err = parseSearchArgs(os.Args[4:], cfg.Session.CLI.MaxSearchResults)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "search option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if sopts.first && sopts.outputFile != "" {
|
|
fmt.Fprintln(os.Stderr, "cannot choose --first and --output-file together")
|
|
os.Exit(2)
|
|
}
|
|
if !isAllowedSearchSource(source) {
|
|
fmt.Fprintf(os.Stderr, "unsupported search source %q\n", source)
|
|
os.Exit(2)
|
|
}
|
|
if !isAllowedMediaType(mediaType) {
|
|
fmt.Fprintf(os.Stderr, "unsupported media type %q\n", mediaType)
|
|
os.Exit(2)
|
|
}
|
|
if source == "soundcloud" && mediaType != "track" && mediaType != "playlist" {
|
|
fmt.Fprintln(os.Stderr, "soundcloud search currently supports media types track and playlist")
|
|
os.Exit(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")
|
|
os.Exit(2)
|
|
}
|
|
if sopts.query == "" {
|
|
fmt.Fprintln(os.Stderr, "search query cannot be empty")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
|
|
pages, err := provider.Search(ctx, mediaType, sopts.query, sopts.limit)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
results := normalizeSearchResults(source, mediaType, pages)
|
|
if len(results) == 0 {
|
|
fmt.Println("no results")
|
|
return
|
|
}
|
|
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)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("wrote %d results to %s\n", len(results), sopts.outputFile)
|
|
return
|
|
}
|
|
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
|
|
}
|
|
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
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("search download complete (%d item(s))\n", added)
|
|
return
|
|
}
|
|
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
fmt.Println("non-interactive input; use `rip id` to download specific results")
|
|
return
|
|
}
|
|
|
|
selection, err := promptSearchSelectionMenu(source, mediaType, sopts.query, results)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "selection error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if len(selection) == 0 {
|
|
fmt.Println("download cancelled")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %s\n", errorWithActionableHint(err, gopts))
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("search download complete (%d item(s))\n", added)
|
|
case "lastfm":
|
|
opts, parseErr := parseLastFMArgs(os.Args[2:], cfg.Session.LastFM.Source, cfg.Session.LastFM.FallbackSource)
|
|
if parseErr != nil {
|
|
fmt.Fprintf(os.Stderr, "lastfm option error: %v\n", parseErr)
|
|
fmt.Println("usage: rip lastfm [--source SOURCE] [--fallback-source SOURCE] <playlist_url>")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
if len(tracks) == 0 {
|
|
fmt.Println("no tracks found in playlist")
|
|
return
|
|
}
|
|
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)
|
|
os.Exit(1)
|
|
}
|
|
if len(resolvedTracks) == 0 {
|
|
fmt.Println("no lastfm tracks resolved")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
fmt.Printf("queued lastfm playlist: %s (%d tracks)\n", title, len(refs))
|
|
if len(refs) == 0 {
|
|
fmt.Println("no lastfm playlists queued")
|
|
return
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("lastfm rip complete (%d track(s) across 1 playlist)\n", len(resolvedTracks))
|
|
case "soundcloud-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip soundcloud-smoke <soundcloud_url>")
|
|
os.Exit(2)
|
|
}
|
|
meta, err := fetchSoundcloudOEmbed(ctx, cfg.Session.Downloads.VerifySSL, strings.TrimSpace(os.Args[2]))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "soundcloud smoke error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("soundcloud oembed ok: title=%q author=%q provider=%q\n", asString(meta["title"]), asString(meta["author_name"]), asString(meta["provider_name"]))
|
|
case "qobuz-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-smoke <track_id> [quality]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
provider, err := mainApp.GetLoggedInProvider(ctx, "qobuz")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "qobuz login error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
trackID := os.Args[2]
|
|
meta, err := provider.GetMetadata(ctx, trackID, "track")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "metadata error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(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)
|
|
case "qobuz-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-rip-smoke <track_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
trackID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "track", trackID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("qobuz rip smoke complete")
|
|
case "qobuz-convert-rip-smoke":
|
|
if len(os.Args) < 4 {
|
|
fmt.Println("usage: rip qobuz-convert-rip-smoke <track_id> <codec> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[4:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
cfg.Session.Qobuz.Quality = opts.quality
|
|
}
|
|
cfg.Session.Conversion.Enabled = true
|
|
cfg.Session.Conversion.Codec = strings.ToUpper(strings.TrimSpace(os.Args[3]))
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
trackID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "track", trackID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("qobuz convert rip smoke complete (codec=%s)\n", cfg.Session.Conversion.Codec)
|
|
case "qobuz-album-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-album-rip-smoke <album_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
albumID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "album", albumID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("qobuz album rip smoke complete")
|
|
case "qobuz-playlist-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-playlist-rip-smoke <playlist_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
playlistID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "playlist", playlistID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("qobuz playlist rip smoke complete")
|
|
case "qobuz-artist-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-artist-rip-smoke <artist_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
artistID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "artist", artistID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("qobuz artist rip smoke complete")
|
|
case "qobuz-label-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-label-rip-smoke <label_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 1, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(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)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
labelID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "qobuz", "label", labelID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("qobuz label rip smoke complete")
|
|
case "qobuz-search-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip qobuz-search-smoke <query>")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
provider, err := mainApp.GetLoggedInProvider(ctx, "qobuz")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "qobuz login error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
query := strings.Join(os.Args[2:], " ")
|
|
pages, err := provider.Search(ctx, "album", query, 10)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
|
|
os.Exit(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)
|
|
}
|
|
}
|
|
case "tidal-search-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-search-smoke <query>")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
provider, err := mainApp.GetLoggedInProvider(ctx, "tidal")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
query := strings.Join(os.Args[2:], " ")
|
|
pages, err := provider.Search(ctx, "album", query, 10)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "search error: %v\n", err)
|
|
os.Exit(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"]))
|
|
}
|
|
}
|
|
case "tidal-metadata-smoke":
|
|
if len(os.Args) < 4 {
|
|
fmt.Println("usage: rip tidal-metadata-smoke <track|album|playlist|artist> <id>")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
provider, err := mainApp.GetLoggedInProvider(ctx, "tidal")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
mediaType := os.Args[2]
|
|
itemID := os.Args[3]
|
|
meta, err := provider.GetMetadata(ctx, itemID, mediaType)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "metadata error: %v\n", err)
|
|
os.Exit(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)
|
|
case "tidal-video-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-video-smoke <video_id>")
|
|
os.Exit(2)
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
|
|
providerClient, err := mainApp.GetLoggedInProvider(ctx, "tidal")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tidal login error: %v\n", err)
|
|
os.Exit(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")
|
|
os.Exit(1)
|
|
}
|
|
|
|
videoID := strings.TrimSpace(os.Args[2])
|
|
meta, err := providerClient.GetMetadata(ctx, videoID, "video")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "video metadata error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
d, err := videoProvider.GetVideoDownloadable(ctx, videoID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "video downloadable error: %v\n", err)
|
|
os.Exit(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)
|
|
case "tidal-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-rip-smoke <track_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 0, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
cfg.Session.Tidal.Quality = opts.quality
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
trackID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "tidal", "track", trackID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("tidal rip smoke complete")
|
|
case "tidal-album-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-album-rip-smoke <album_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 0, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
cfg.Session.Tidal.Quality = opts.quality
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
albumID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "tidal", "album", albumID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("tidal album rip smoke complete")
|
|
case "tidal-playlist-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-playlist-rip-smoke <playlist_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 0, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
cfg.Session.Tidal.Quality = opts.quality
|
|
}
|
|
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
|
|
playlistID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "tidal", "playlist", playlistID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("tidal playlist rip smoke complete")
|
|
case "tidal-artist-rip-smoke":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("usage: rip tidal-artist-rip-smoke <artist_id> [quality] [--force|--ignore-db]")
|
|
os.Exit(2)
|
|
}
|
|
opts, err := parseSmokeOptions(os.Args[3:], 0, 4)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "option error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if opts.qualitySet {
|
|
cfg.Session.Tidal.Quality = opts.quality
|
|
}
|
|
mainApp, err := app.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "app init error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = mainApp.Close() }()
|
|
mainApp.IgnoreDB = opts.ignoreDB || gopts.noDB
|
|
artistID := os.Args[2]
|
|
if err = mainApp.AddByID(ctx, "tidal", "artist", artistID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "add error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Resolve(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "resolve error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err = mainApp.Rip(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "rip error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("tidal artist rip smoke complete")
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
|
os.Exit(2)
|
|
}
|
|
}
|