mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
350 lines
12 KiB
Go
350 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseFileInputJSONItems(t *testing.T) {
|
|
content := []byte(`[
|
|
{"source":"qobuz","media_type":"album","id":"0066991040005"},
|
|
{"source":"tidal","media_type":"track","id":3083287}
|
|
]`)
|
|
|
|
items, urls, repeated, jsonInput, err := parseFileInput(content)
|
|
if err != nil {
|
|
t.Fatalf("parseFileInput() error = %v", err)
|
|
}
|
|
if !jsonInput {
|
|
t.Fatalf("jsonInput = false, want true")
|
|
}
|
|
if len(urls) != 0 {
|
|
t.Fatalf("urls len = %d, want 0", len(urls))
|
|
}
|
|
if repeated != 0 {
|
|
t.Fatalf("repeated = %d, want 0", repeated)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("items len = %d, want 2", len(items))
|
|
}
|
|
if items[0].Source != "qobuz" || items[0].MediaType != "album" || items[0].ID != "0066991040005" {
|
|
t.Fatalf("unexpected first item: %+v", items[0])
|
|
}
|
|
if items[1].Source != "tidal" || items[1].MediaType != "track" || items[1].ID != "3083287" {
|
|
t.Fatalf("unexpected second item: %+v", items[1])
|
|
}
|
|
}
|
|
|
|
func TestParseFileInputTextURLsDedupes(t *testing.T) {
|
|
content := []byte("https://tidal.com/browse/track/3083287\nhttps://tidal.com/browse/track/3083287\nhttps://www.qobuz.com/fr-fr/album/example/0066991040005\n")
|
|
|
|
items, urls, repeated, jsonInput, err := parseFileInput(content)
|
|
if err != nil {
|
|
t.Fatalf("parseFileInput() error = %v", err)
|
|
}
|
|
if jsonInput {
|
|
t.Fatalf("jsonInput = true, want false")
|
|
}
|
|
if len(items) != 0 {
|
|
t.Fatalf("items len = %d, want 0", len(items))
|
|
}
|
|
if repeated != 1 {
|
|
t.Fatalf("repeated = %d, want 1", repeated)
|
|
}
|
|
if len(urls) != 2 {
|
|
t.Fatalf("urls len = %d, want 2", len(urls))
|
|
}
|
|
}
|
|
|
|
func TestParseFileInputRejectsInvalidJSONShape(t *testing.T) {
|
|
content := []byte(`{"source":"qobuz","media_type":"track","id":"1"}`)
|
|
|
|
_, _, _, jsonInput, err := parseFileInput(content)
|
|
if err == nil {
|
|
t.Fatalf("expected error for non-array json")
|
|
}
|
|
if !jsonInput {
|
|
t.Fatalf("jsonInput = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestParseLastFMArgsDefaults(t *testing.T) {
|
|
opts, err := parseLastFMArgs([]string{"https://www.last.fm/user/x/playlists/123"}, "qobuz", "")
|
|
if err != nil {
|
|
t.Fatalf("parseLastFMArgs() error = %v", err)
|
|
}
|
|
if opts.Source != "qobuz" {
|
|
t.Fatalf("source = %q, want qobuz", opts.Source)
|
|
}
|
|
if opts.PlaylistURL == "" {
|
|
t.Fatalf("playlist url should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestParseLastFMArgsOptions(t *testing.T) {
|
|
opts, err := parseLastFMArgs([]string{"--source", "tidal", "--fallback-source", "qobuz", "https://www.last.fm/user/x/playlists/123"}, "qobuz", "")
|
|
if err != nil {
|
|
t.Fatalf("parseLastFMArgs() error = %v", err)
|
|
}
|
|
if opts.Source != "tidal" || opts.FallbackSource != "qobuz" {
|
|
t.Fatalf("unexpected options: %+v", opts)
|
|
}
|
|
}
|
|
|
|
func TestParseLastFMArgsRejectsNonLastFMURL(t *testing.T) {
|
|
_, err := parseLastFMArgs([]string{"https://example.com/user/x/playlists/123"}, "qobuz", "")
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "last.fm") {
|
|
t.Fatalf("expected last.fm url validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIsValidLastFMPlaylistURL(t *testing.T) {
|
|
if !isValidLastFMPlaylistURL("https://www.last.fm/user/x/playlists/123") {
|
|
t.Fatalf("expected canonical last.fm playlist url to be valid")
|
|
}
|
|
if !isValidLastFMPlaylistURL("http://last.fm/user/x/playlists/123") {
|
|
t.Fatalf("expected http last.fm playlist url to be valid")
|
|
}
|
|
if isValidLastFMPlaylistURL("ftp://last.fm/user/x/playlists/123") {
|
|
t.Fatalf("expected non-http scheme to be invalid")
|
|
}
|
|
if isValidLastFMPlaylistURL("https://example.com/user/x/playlists/123") {
|
|
t.Fatalf("expected non-last.fm host to be invalid")
|
|
}
|
|
if isValidLastFMPlaylistURL("https://www.last.fm/user/x/library") {
|
|
t.Fatalf("expected non-playlist last.fm url to be invalid")
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMPlaylistInfoAndPairs(t *testing.T) {
|
|
html := `<h1 class="playlisting-playlist-header-title">Road & Rain</h1>
|
|
<div data-playlisting-entry-count="2"></div>
|
|
<a href="/music/a" title="Dreams"></a>
|
|
<a href="/music/b" title="Fleetwood Mac"></a>
|
|
<a href="/music/c" title="Go Your Own Way"></a>
|
|
<a href="/music/d" title="Fleetwood Mac"></a>`
|
|
|
|
title, total, err := extractLastFMPlaylistInfo(html)
|
|
if err != nil {
|
|
t.Fatalf("extractLastFMPlaylistInfo() error = %v", err)
|
|
}
|
|
if title != "Road & Rain" {
|
|
t.Fatalf("title = %q, want %q", title, "Road & Rain")
|
|
}
|
|
if total != 2 {
|
|
t.Fatalf("total = %d, want 2", total)
|
|
}
|
|
|
|
pairs := extractLastFMTitleArtistPairs(html)
|
|
if len(pairs) != 2 {
|
|
t.Fatalf("pairs len = %d, want 2", len(pairs))
|
|
}
|
|
if pairs[0].Title != "Dreams" || pairs[0].Artist != "Fleetwood Mac" {
|
|
t.Fatalf("unexpected first pair: %+v", pairs[0])
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMPlaylistInfoFlexibleClass(t *testing.T) {
|
|
html := `<h1 id="x" class="foo playlisting-playlist-header-title bar">Road & Rain</h1>
|
|
<div data-playlisting-entry-count="1"></div>`
|
|
title, total, err := extractLastFMPlaylistInfo(html)
|
|
if err != nil {
|
|
t.Fatalf("extractLastFMPlaylistInfo() error = %v", err)
|
|
}
|
|
if title != "Road & Rain" || total != 1 {
|
|
t.Fatalf("unexpected parsed values: title=%q total=%d", title, total)
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMTitleArtistPairsSingleQuotes(t *testing.T) {
|
|
html := `<a href='/music/a' title='Dreams'></a>
|
|
<a href='/music/b' title='Fleetwood Mac'></a>`
|
|
pairs := extractLastFMTitleArtistPairs(html)
|
|
if len(pairs) != 1 {
|
|
t.Fatalf("pairs len = %d, want 1", len(pairs))
|
|
}
|
|
if pairs[0].Title != "Dreams" || pairs[0].Artist != "Fleetwood Mac" {
|
|
t.Fatalf("unexpected pair: %+v", pairs[0])
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMTitleArtistPairsSkipsPlayOnYouTubeNoise(t *testing.T) {
|
|
html := `<a href="https://www.youtube.com/watch?v=1" data-track-name="Won't Forget You" data-artist-name="Shouse" title="Play on YouTube">Play track</a>
|
|
<a href="/music/Shouse/_/Won%27t+Forget+You" title="Won't Forget You"></a>
|
|
<a href="/music/Shouse" title="Shouse"></a>
|
|
<a href="https://www.youtube.com/watch?v=2" data-track-name="EYES" data-artist-name="The Blaze" title="Play on YouTube">Play track</a>
|
|
<a href="/music/The+Blaze/_/EYES" title="EYES"></a>
|
|
<a href="/music/The+Blaze" title="The Blaze"></a>`
|
|
pairs := extractLastFMTitleArtistPairs(html)
|
|
if len(pairs) != 2 {
|
|
t.Fatalf("pairs len = %d, want 2", len(pairs))
|
|
}
|
|
if pairs[0].Title != "Won't Forget You" || pairs[0].Artist != "Shouse" {
|
|
t.Fatalf("unexpected first pair: %+v", pairs[0])
|
|
}
|
|
if pairs[1].Title != "EYES" || pairs[1].Artist != "The Blaze" {
|
|
t.Fatalf("unexpected second pair: %+v", pairs[1])
|
|
}
|
|
}
|
|
|
|
func TestParseGlobalArgsNoDBBeforeCommand(t *testing.T) {
|
|
opts, err := parseGlobalArgs([]string{"-ndb", "url", "https://play.qobuz.com/album/0004228000522"})
|
|
if err != nil {
|
|
t.Fatalf("parseGlobalArgs() error = %v", err)
|
|
}
|
|
if !opts.noDB {
|
|
t.Fatalf("expected noDB true")
|
|
}
|
|
if opts.command != "url" {
|
|
t.Fatalf("command = %q, want %q", opts.command, "url")
|
|
}
|
|
if len(opts.commandArgs) != 1 {
|
|
t.Fatalf("command args len = %d, want 1", len(opts.commandArgs))
|
|
}
|
|
}
|
|
|
|
func TestParseGlobalArgsAllOfficialFlags(t *testing.T) {
|
|
opts, err := parseGlobalArgs([]string{
|
|
"--config-path", "/tmp/custom.toml",
|
|
"-f", "/tmp/music",
|
|
"--no-db",
|
|
"-q", "3",
|
|
"-c", "ogg",
|
|
"--no-progress",
|
|
"--no-ssl-verify",
|
|
"-v",
|
|
"search", "tidal", "track", "dreams",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parseGlobalArgs() error = %v", err)
|
|
}
|
|
if opts.configPath != "/tmp/custom.toml" || opts.folder != "/tmp/music" {
|
|
t.Fatalf("unexpected path/folder opts: %+v", opts)
|
|
}
|
|
if !opts.noDB || !opts.qualitySet || opts.quality != 3 || !opts.codecSet || opts.codec != "VORBIS" {
|
|
t.Fatalf("unexpected quality/codec/db opts: %+v", opts)
|
|
}
|
|
if !opts.noProgress || !opts.noSSLVerify || !opts.verbose {
|
|
t.Fatalf("unexpected boolean opts: %+v", opts)
|
|
}
|
|
if opts.command != "search" {
|
|
t.Fatalf("command = %q, want search", opts.command)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCodecRejectsUnknown(t *testing.T) {
|
|
if _, err := normalizeCodec("wav"); err == nil {
|
|
t.Fatalf("expected error for unsupported codec")
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMTracksFromMirrorMarkdown(t *testing.T) {
|
|
md := `Title: My Playlist | user playlists | Last.fm
|
|
| Play | Image | Loved | Name | Artist name | Buy | Options | Duration |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| [Play track](https://x) | [img](https://i) | x | [Song A](https://a) | [Artist A](https://aa) | | | 3:00 |
|
|
| [Play track](https://x) | [img](https://i) | x | [Song B](https://b) | [Artist B](https://bb) | | | 4:00 |`
|
|
title, tracks := extractLastFMTracksFromMirrorMarkdown(md)
|
|
if title != "My Playlist" {
|
|
t.Fatalf("title = %q, want %q", title, "My Playlist")
|
|
}
|
|
if len(tracks) != 2 {
|
|
t.Fatalf("tracks len = %d, want 2", len(tracks))
|
|
}
|
|
if tracks[0].Title != "Song A" || tracks[0].Artist != "Artist A" {
|
|
t.Fatalf("unexpected first track: %+v", tracks[0])
|
|
}
|
|
}
|
|
|
|
func TestExtractLastFMTracksFromMirrorMarkdownLowercasePlayTrack(t *testing.T) {
|
|
md := `Title: My Playlist | user playlists | Last.fm
|
|
| Play | Image | Loved | Name | Artist name | Buy | Options | Duration |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| [play track](https://x) | [img](https://i) | x | [Song A](https://a) | [Artist A](https://aa) | | | 3:00 |`
|
|
_, tracks := extractLastFMTracksFromMirrorMarkdown(md)
|
|
if len(tracks) != 1 {
|
|
t.Fatalf("tracks len = %d, want 1", len(tracks))
|
|
}
|
|
}
|
|
|
|
func TestParseSearchArgsAllowsFirstAndOutputFileButCallerCanReject(t *testing.T) {
|
|
opts, err := parseSearchArgs([]string{"q", "--first", "--output-file", "/tmp/out.json"}, 20)
|
|
if err != nil {
|
|
t.Fatalf("parseSearchArgs() error = %v", err)
|
|
}
|
|
if !opts.first || opts.outputFile == "" {
|
|
t.Fatalf("expected first=true and output file set, got %+v", opts)
|
|
}
|
|
}
|
|
|
|
func TestParseSearchArgsRejectsUnknownOption(t *testing.T) {
|
|
_, err := parseSearchArgs([]string{"query", "--bogus"}, 20)
|
|
if err == nil || !strings.Contains(err.Error(), "unknown option") {
|
|
t.Fatalf("expected unknown option error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseSearchArgsSupportsDoubleDashTerminator(t *testing.T) {
|
|
opts, err := parseSearchArgs([]string{"--limit", "10", "--", "--weird", "track"}, 20)
|
|
if err != nil {
|
|
t.Fatalf("parseSearchArgs() error = %v", err)
|
|
}
|
|
if opts.limit != 10 {
|
|
t.Fatalf("limit = %d, want 10", opts.limit)
|
|
}
|
|
if opts.query != "--weird track" {
|
|
t.Fatalf("query = %q, want %q", opts.query, "--weird track")
|
|
}
|
|
}
|
|
|
|
func TestWriteSearchResultsToFileCreatesParentDirectory(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
out := filepath.Join(tmp, "nested", "search", "results.json")
|
|
results := []searchResult{{ID: "1", Title: "Dreams"}}
|
|
if err := writeSearchResultsToFile("qobuz", "track", results, out); err != nil {
|
|
t.Fatalf("writeSearchResultsToFile() error = %v", err)
|
|
}
|
|
if _, err := os.Stat(out); err != nil {
|
|
t.Fatalf("expected output file, stat error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSearchResultsDedupesByID(t *testing.T) {
|
|
pages := []map[string]any{
|
|
{"tracks": map[string]any{"items": []any{
|
|
map[string]any{"id": "1", "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}},
|
|
map[string]any{"id": "1", "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}},
|
|
}}},
|
|
{"tracks": map[string]any{"items": []any{
|
|
map[string]any{"id": "2", "title": "Go Your Own Way", "artist": map[string]any{"name": "Fleetwood Mac"}},
|
|
map[string]any{"id": "1", "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}},
|
|
}}},
|
|
}
|
|
results := normalizeSearchResults("qobuz", "track", pages)
|
|
if len(results) != 2 {
|
|
t.Fatalf("len(results)=%d want 2", len(results))
|
|
}
|
|
if results[0].ID != "1" || results[1].ID != "2" {
|
|
t.Fatalf("unexpected IDs order: %+v", results)
|
|
}
|
|
}
|
|
|
|
func TestErrorWithActionableHintForSSL(t *testing.T) {
|
|
err := errors.New("x509: certificate signed by unknown authority")
|
|
msg := errorWithActionableHint(err, globalOptions{})
|
|
if msg == err.Error() {
|
|
t.Fatalf("expected ssl hint in message")
|
|
}
|
|
}
|
|
|
|
func TestErrorWithActionableHintNoHintWhenDisabled(t *testing.T) {
|
|
err := errors.New("tls handshake failure")
|
|
msg := errorWithActionableHint(err, globalOptions{noSSLVerify: true})
|
|
if msg != err.Error() {
|
|
t.Fatalf("unexpected hint when noSSLVerify set")
|
|
}
|
|
}
|