mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
839 lines
25 KiB
Go
839 lines
25 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"streamrip-go/internal/audio/tag"
|
|
"streamrip-go/internal/config"
|
|
"streamrip-go/internal/download"
|
|
"streamrip-go/internal/provider"
|
|
"streamrip-go/internal/store"
|
|
)
|
|
|
|
type noopTagger struct{}
|
|
|
|
func (n noopTagger) TagFLAC(string, tag.Metadata, string) error { return nil }
|
|
|
|
type failingTagger struct {
|
|
err error
|
|
}
|
|
|
|
func (f failingTagger) TagFLAC(string, tag.Metadata, string) error { return f.err }
|
|
|
|
type fakeProvider struct {
|
|
url string
|
|
}
|
|
|
|
func (f *fakeProvider) Source() string { return "qobuz" }
|
|
func (f *fakeProvider) Login(context.Context) error { return nil }
|
|
func (f *fakeProvider) LoggedIn() bool { return true }
|
|
func (f *fakeProvider) Close() error { return nil }
|
|
func (f *fakeProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeProvider) GetMetadata(context.Context, string, string) (map[string]any, error) {
|
|
return map[string]any{
|
|
"title": "Dreams/Live",
|
|
"track_number": float64(3),
|
|
"performer": map[string]any{
|
|
"name": "Fleetwood Mac",
|
|
},
|
|
"album": map[string]any{
|
|
"artist": map[string]any{"name": "Fleetwood Mac"},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type fakeAlbumProvider struct {
|
|
url string
|
|
}
|
|
|
|
type fakePlaylistProvider struct {
|
|
url string
|
|
}
|
|
|
|
type fakeVideoProvider struct {
|
|
url string
|
|
}
|
|
|
|
type fakeResolvedAlbumProvider struct {
|
|
url string
|
|
downloadable provider.Downloadable
|
|
downloadableHits int
|
|
}
|
|
|
|
type fakeFailProvider struct{}
|
|
|
|
func (f *fakeAlbumProvider) Source() string { return "qobuz" }
|
|
func (f *fakePlaylistProvider) Source() string { return "qobuz" }
|
|
func (f *fakeVideoProvider) Source() string { return "tidal" }
|
|
func (f *fakeResolvedAlbumProvider) Source() string { return "qobuz" }
|
|
func (f *fakeAlbumProvider) Login(context.Context) error { return nil }
|
|
func (f *fakePlaylistProvider) Login(context.Context) error {
|
|
return nil
|
|
}
|
|
func (f *fakeVideoProvider) Login(context.Context) error { return nil }
|
|
func (f *fakeResolvedAlbumProvider) Login(context.Context) error {
|
|
return nil
|
|
}
|
|
func (f *fakeAlbumProvider) LoggedIn() bool { return true }
|
|
func (f *fakePlaylistProvider) LoggedIn() bool { return true }
|
|
func (f *fakeVideoProvider) LoggedIn() bool { return true }
|
|
func (f *fakeResolvedAlbumProvider) LoggedIn() bool { return true }
|
|
func (f *fakeAlbumProvider) Close() error { return nil }
|
|
func (f *fakePlaylistProvider) Close() error { return nil }
|
|
func (f *fakeVideoProvider) Close() error { return nil }
|
|
func (f *fakeResolvedAlbumProvider) Close() error { return nil }
|
|
func (f *fakeAlbumProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakePlaylistProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeVideoProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeResolvedAlbumProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeAlbumProvider) GetMetadata(_ context.Context, id string, mediaType string) (map[string]any, error) {
|
|
if mediaType == "album" {
|
|
return map[string]any{
|
|
"title": "Rumours",
|
|
"release_date_original": "1977-02-04",
|
|
"media_count": float64(2),
|
|
"maximum_bit_depth": float64(24),
|
|
"maximum_sampling_rate": float64(96),
|
|
"artist": map[string]any{"name": "Fleetwood Mac"},
|
|
"tracks": map[string]any{"items": []any{
|
|
map[string]any{"id": "t1"},
|
|
map[string]any{"id": "t2"},
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
tn := float64(1)
|
|
disc := float64(1)
|
|
title := "Dreams"
|
|
if id == "t2" {
|
|
tn = 2
|
|
disc = 2
|
|
title = "Go Your Own Way"
|
|
}
|
|
return map[string]any{
|
|
"title": title,
|
|
"track_number": tn,
|
|
"media_number": disc,
|
|
"performer": map[string]any{
|
|
"name": "Fleetwood Mac",
|
|
},
|
|
"album": map[string]any{
|
|
"title": "Rumours",
|
|
"artist": map[string]any{"name": "Fleetwood Mac"},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakePlaylistProvider) GetMetadata(_ context.Context, id string, mediaType string) (map[string]any, error) {
|
|
if mediaType == "playlist" {
|
|
return map[string]any{
|
|
"name": "Road Trip",
|
|
"tracks": map[string]any{
|
|
"items": []any{map[string]any{"id": "p1"}, map[string]any{"id": "p2"}},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
trackNum := float64(7)
|
|
title := "Track One"
|
|
if id == "p2" {
|
|
trackNum = 9
|
|
title = "Track Two"
|
|
}
|
|
return map[string]any{
|
|
"title": title,
|
|
"track_number": trackNum,
|
|
"performer": map[string]any{
|
|
"name": "Artist",
|
|
},
|
|
"album": map[string]any{
|
|
"title": "Original Album",
|
|
"artist": map[string]any{"name": "Artist"},
|
|
},
|
|
}, nil
|
|
}
|
|
func (f *fakeVideoProvider) GetMetadata(_ context.Context, id string, mediaType string) (map[string]any, error) {
|
|
if mediaType == "video" {
|
|
return map[string]any{"title": "Live Clip"}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (f *fakeResolvedAlbumProvider) GetMetadata(_ context.Context, id string, mediaType string) (map[string]any, error) {
|
|
if mediaType == "album" {
|
|
return map[string]any{
|
|
"title": "Fallback Album",
|
|
"release_date_original": "2020-01-01",
|
|
"artist": map[string]any{"name": "Fallback Artist"},
|
|
"tracks": map[string]any{"items": []any{map[string]any{"id": "t1"}}},
|
|
}, nil
|
|
}
|
|
return map[string]any{
|
|
"title": "Fallback Song",
|
|
"track_number": float64(1),
|
|
"performer": map[string]any{"name": "Fallback Artist"},
|
|
"album": map[string]any{"title": "Fallback Album", "artist": map[string]any{"name": "Fallback Artist"}},
|
|
}, nil
|
|
}
|
|
func (f *fakeProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
return &provider.Downloadable{URL: f.url, Extension: "flac", Source: "qobuz"}, nil
|
|
}
|
|
func (f *fakeAlbumProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
return &provider.Downloadable{URL: f.url, Extension: "flac", Source: "qobuz"}, nil
|
|
}
|
|
func (f *fakePlaylistProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
return &provider.Downloadable{URL: f.url, Extension: "flac", Source: "qobuz"}, nil
|
|
}
|
|
func (f *fakeVideoProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeResolvedAlbumProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
f.downloadableHits++
|
|
d := f.downloadable
|
|
if d.URL == "" {
|
|
d.URL = f.url
|
|
}
|
|
return &d, nil
|
|
}
|
|
func (f *fakeVideoProvider) GetVideoDownloadable(context.Context, string) (*provider.Downloadable, error) {
|
|
return &provider.Downloadable{URL: f.url, Extension: "mp4", Source: "tidal"}, nil
|
|
}
|
|
func (f *fakeFailProvider) Source() string { return "qobuz" }
|
|
func (f *fakeFailProvider) Login(context.Context) error { return nil }
|
|
func (f *fakeFailProvider) LoggedIn() bool { return true }
|
|
func (f *fakeFailProvider) Close() error { return nil }
|
|
func (f *fakeFailProvider) Search(context.Context, string, string, int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeFailProvider) GetMetadata(context.Context, string, string) (map[string]any, error) {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
func (f *fakeFailProvider) GetDownloadable(context.Context, string, int) (*provider.Downloadable, error) {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
func TestTrackRipPipeline(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakeProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.New(),
|
|
Tagger: noopTagger{},
|
|
Pending: nil,
|
|
Media: nil,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "track", "19512574"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
if _, err = os.Stat(filepath.Join(tmp, "03. Fleetwood Mac - Dreams_Live.flac")); err != nil {
|
|
t.Fatalf("expected downloaded file: %v", err)
|
|
}
|
|
|
|
ok, err := sqlite.IsDownloaded(ctx, "qobuz", "19512574")
|
|
if err != nil {
|
|
t.Fatalf("IsDownloaded() error = %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("expected track marked downloaded")
|
|
}
|
|
}
|
|
|
|
func TestTrackRipFailsWhenTaggerReportsMissingFFmpeg(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakeProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.NewWithOptions(true, false, 0),
|
|
Tagger: failingTagger{err: fmt.Errorf("ffmpeg not found: %w", exec.ErrNotFound)},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "track", "19512574"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
err = m.Rip(ctx)
|
|
if err == nil {
|
|
t.Fatalf("expected rip failure")
|
|
}
|
|
|
|
ok, err := sqlite.IsDownloaded(ctx, "qobuz", "19512574")
|
|
if err != nil {
|
|
t.Fatalf("IsDownloaded() error = %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatalf("expected track not marked downloaded")
|
|
}
|
|
}
|
|
|
|
func TestAlbumRipPipeline(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
d.Downloads.Concurrency = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakeAlbumProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.New(),
|
|
Tagger: noopTagger{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "album", "a1"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
folder := filepath.Join(tmp, "Fleetwood Mac - Rumours (1977) [FLAC] [24B-96kHz]")
|
|
if _, err = os.Stat(filepath.Join(folder, "Disc 1", "01. Fleetwood Mac - Dreams.flac")); err != nil {
|
|
t.Fatalf("missing first album track: %v", err)
|
|
}
|
|
if _, err = os.Stat(filepath.Join(folder, "Disc 2", "02. Fleetwood Mac - Go Your Own Way.flac")); err != nil {
|
|
t.Fatalf("missing second album track: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVideoRipPipeline(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("video-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"tidal": &fakeVideoProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.New(),
|
|
Tagger: noopTagger{},
|
|
Pending: nil,
|
|
Media: nil,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "tidal", "video", "v1"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
if _, err = os.Stat(filepath.Join(tmp, "Live Clip.mp4")); err != nil {
|
|
t.Fatalf("expected downloaded video file: %v", err)
|
|
}
|
|
|
|
ok, err := sqlite.IsDownloaded(ctx, "tidal", "v1")
|
|
if err != nil {
|
|
t.Fatalf("IsDownloaded() error = %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("expected video marked downloaded")
|
|
}
|
|
}
|
|
|
|
func TestResolveAllFailedReturnsError(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakeFailProvider{},
|
|
},
|
|
Store: store.NewDummy(),
|
|
DL: download.New(),
|
|
Tagger: noopTagger{},
|
|
Pending: nil,
|
|
Media: nil,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err := m.AddByID(ctx, "qobuz", "track", "x"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err := m.Resolve(ctx); err == nil {
|
|
t.Fatalf("expected Resolve() to return error when all items fail")
|
|
}
|
|
}
|
|
|
|
func TestBuildTagMetadataUsesAlbumArtistOverride(t *testing.T) {
|
|
meta := map[string]any{
|
|
"title": "One Step Too Far",
|
|
"track_number": float64(15),
|
|
"performer": map[string]any{"name": "Faithless"},
|
|
"artist": map[string]any{"name": "Faithless"},
|
|
"album": map[string]any{
|
|
"id": "23324600",
|
|
"title": "Greatest Hits (Deluxe)",
|
|
"artist": map[string]any{"name": "Faithless"},
|
|
},
|
|
}
|
|
tags := buildTagMetadata(meta, "One Step Too Far", "tidal", "23324615", ripTrackOptions{albumArtist: "Dido", albumDiscTotal: 2})
|
|
if tags.AlbumArtist != "Dido" {
|
|
t.Fatalf("album artist = %q, want Dido", tags.AlbumArtist)
|
|
}
|
|
if tags.DiscNumber != 1 {
|
|
t.Fatalf("disc number = %d, want 1", tags.DiscNumber)
|
|
}
|
|
if tags.DiscTotal != 2 {
|
|
t.Fatalf("disc total = %d, want 2", tags.DiscTotal)
|
|
}
|
|
}
|
|
|
|
func TestTrackOutputPathFallsBackToDisc1(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.DiscSubdirectories = true
|
|
d.Downloads.SourceSubdirectories = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
m := &Main{Config: cfg}
|
|
|
|
meta := map[string]any{
|
|
"title": "Song",
|
|
"track_number": float64(1),
|
|
"performer": map[string]any{"name": "Dido"},
|
|
"album": map[string]any{"id": "a", "title": "Greatest Hits", "artist": map[string]any{"name": "Dido"}},
|
|
}
|
|
path := m.trackOutputPath("tidal", "1", "Song", "flac", nil, meta, filepath.Join(tmp, "Album"), 2)
|
|
if !strings.Contains(path, string(filepath.Separator)+"Disc 1"+string(filepath.Separator)) {
|
|
t.Fatalf("expected Disc 1 subdir in path, got %q", path)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistRipPipeline(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.Concurrency = false
|
|
d.Filepaths.RestrictCharacters = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakePlaylistProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.NewWithOptions(true, false, 0),
|
|
Tagger: noopTagger{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "playlist", "pl1"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
folder := filepath.Join(tmp, "Road Trip")
|
|
if _, err = os.Stat(filepath.Join(folder, "01. Artist - Track One.flac")); err != nil {
|
|
t.Fatalf("missing first playlist track: %v", err)
|
|
}
|
|
if _, err = os.Stat(filepath.Join(folder, "02. Artist - Track Two.flac")); err != nil {
|
|
t.Fatalf("missing second playlist track: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistRipUsesSourceSubdirectory(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.Concurrency = false
|
|
d.Downloads.SourceSubdirectories = true
|
|
d.Filepaths.RestrictCharacters = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": &fakePlaylistProvider{url: ts.URL},
|
|
},
|
|
Store: sqlite,
|
|
DL: download.NewWithOptions(true, false, 0),
|
|
Tagger: noopTagger{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "playlist", "pl1"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
folder := filepath.Join(tmp, "Qobuz", "Road Trip")
|
|
if _, err = os.Stat(filepath.Join(folder, "01. Artist - Track One.flac")); err != nil {
|
|
t.Fatalf("missing first playlist track in source subdir: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRipPlaylistRequiresDeezerARL(t *testing.T) {
|
|
d := config.DefaultConfigData()
|
|
m := &Main{Config: &config.Config{File: d, Session: d}}
|
|
|
|
err := m.ripPlaylist(context.Background(), nil, "deezer", "pl1", map[string]any{
|
|
"name": "Road Trip",
|
|
"tracks": map[string]any{"items": []any{map[string]any{"id": "p1"}}},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "deezer") {
|
|
t.Fatalf("expected deezer arl error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRipAlbumRequiresDeezerARL(t *testing.T) {
|
|
d := config.DefaultConfigData()
|
|
m := &Main{Config: &config.Config{File: d, Session: d}}
|
|
|
|
err := m.ripAlbum(context.Background(), nil, "deezer", "alb1", map[string]any{})
|
|
if err == nil || !strings.Contains(err.Error(), "deezer") {
|
|
t.Fatalf("expected deezer arl error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRipPlaylistMixedRequiresDeezerAuth(t *testing.T) {
|
|
d := config.DefaultConfigData()
|
|
m := &Main{Config: &config.Config{File: d, Session: d}}
|
|
|
|
err := m.ripPlaylistMixed(context.Background(), "mix1", "Mix", []PlaylistTrackRef{{Source: "deezer", ID: "1"}})
|
|
if err == nil || !strings.Contains(err.Error(), "deezer") {
|
|
t.Fatalf("expected deezer auth error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyQobuzArtistFiltersRepeats(t *testing.T) {
|
|
albums := []collectionAlbum{
|
|
{ID: "a1", Title: "Album X", BitDepth: 16, Sampling: 44.1, Explicit: false},
|
|
{ID: "a2", Title: "Album X (Deluxe)", BitDepth: 24, Sampling: 96, Explicit: false},
|
|
{ID: "b1", Title: "Album B", BitDepth: 16, Sampling: 44.1, Explicit: false},
|
|
}
|
|
filtered := applyQobuzArtistFilters("artist", albums, config.QobuzDiscographyFilterConfig{Repeats: true})
|
|
if len(filtered) != 2 {
|
|
t.Fatalf("len(filtered)=%d want 2", len(filtered))
|
|
}
|
|
ids := map[string]bool{}
|
|
for _, a := range filtered {
|
|
ids[a.ID] = true
|
|
}
|
|
if !ids["a2"] || !ids["b1"] {
|
|
t.Fatalf("unexpected winners: %+v", ids)
|
|
}
|
|
}
|
|
|
|
func TestApplyQobuzArtistFiltersNonRemaster(t *testing.T) {
|
|
albums := []collectionAlbum{
|
|
{ID: "rm", Title: "Album X (Remastered)"},
|
|
{ID: "orig", Title: "Album X"},
|
|
}
|
|
|
|
filtered := applyQobuzArtistFilters("artist", albums, config.QobuzDiscographyFilterConfig{NonRemaster: true})
|
|
if len(filtered) != 1 {
|
|
t.Fatalf("len(filtered)=%d want 1", len(filtered))
|
|
}
|
|
if filtered[0].ID != "orig" {
|
|
t.Fatalf("unexpected album kept: %+v", filtered[0])
|
|
}
|
|
}
|
|
|
|
func TestTrackOutputPathSinglesUsesAlbumID(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
d.Filepaths.AddSinglesToFolder = true
|
|
d.Filepaths.FolderFormat = "{id}"
|
|
d.Filepaths.TrackFormat = "{title}"
|
|
d.Filepaths.RestrictCharacters = false
|
|
|
|
m := &Main{Config: &config.Config{File: d, Session: d}}
|
|
meta := map[string]any{
|
|
"album": map[string]any{
|
|
"id": "album-123",
|
|
"title": "Album",
|
|
"artist": map[string]any{"name": "Artist"},
|
|
},
|
|
"performer": map[string]any{"name": "Artist"},
|
|
}
|
|
|
|
out := m.trackOutputPath("qobuz", "track-999", "Song", "flac", nil, meta, "", 0)
|
|
if got, want := filepath.Dir(out), filepath.Join(tmp, "album-123"); got != want {
|
|
t.Fatalf("trackOutputPath() dir=%q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTrackOutputPathSinglesUsesResolvedAudioProfile(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
d.Filepaths.AddSinglesToFolder = true
|
|
d.Filepaths.FolderFormat = "{container}-{bit_depth}-{sampling_rate}"
|
|
d.Filepaths.TrackFormat = "{title}"
|
|
d.Filepaths.RestrictCharacters = false
|
|
|
|
m := &Main{Config: &config.Config{File: d, Session: d}}
|
|
meta := map[string]any{
|
|
"album": map[string]any{
|
|
"id": "album-123",
|
|
"title": "Album",
|
|
"artist": map[string]any{"name": "Artist"},
|
|
},
|
|
"performer": map[string]any{"name": "Artist"},
|
|
}
|
|
dl := &provider.Downloadable{Extension: "mp3", Audio: provider.AudioProfile{Container: "MP3", Codec: "MP3", Quality: "HIGH", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 320}}
|
|
|
|
out := m.trackOutputPath("qobuz", "track-999", "Song", "mp3", dl, meta, "", 0)
|
|
if got, want := filepath.Dir(out), filepath.Join(tmp, "MP3-16-44.1"); got != want {
|
|
t.Fatalf("trackOutputPath() dir=%q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRipAlbumUsesResolvedAudioProfileForFolderName(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("audio-bytes"))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
d := config.DefaultConfigData()
|
|
d.Downloads.Folder = tmp
|
|
d.Downloads.SourceSubdirectories = false
|
|
d.Downloads.Concurrency = false
|
|
d.Filepaths.RestrictCharacters = false
|
|
cfg := &config.Config{File: d, Session: d}
|
|
|
|
sqlite, err := store.NewSQLite(filepath.Join(tmp, "db.sqlite"))
|
|
if err != nil {
|
|
t.Fatalf("NewSQLite() error = %v", err)
|
|
}
|
|
defer func() { _ = sqlite.Close() }()
|
|
|
|
fake := &fakeResolvedAlbumProvider{
|
|
url: ts.URL,
|
|
downloadable: provider.Downloadable{
|
|
URL: ts.URL,
|
|
Extension: "mp3",
|
|
Source: "qobuz",
|
|
Audio: provider.AudioProfile{
|
|
Container: "MP3",
|
|
Codec: "MP3",
|
|
Quality: "HIGH",
|
|
BitDepth: 16,
|
|
SamplingRate: "44.1",
|
|
BitrateKbps: 320,
|
|
},
|
|
},
|
|
}
|
|
|
|
m := &Main{
|
|
Config: cfg,
|
|
Providers: map[string]provider.Client{
|
|
"qobuz": fake,
|
|
},
|
|
Store: sqlite,
|
|
DL: download.NewWithOptions(true, false, 0),
|
|
Tagger: noopTagger{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = m.AddByID(ctx, "qobuz", "album", "a1"); err != nil {
|
|
t.Fatalf("AddByID() error = %v", err)
|
|
}
|
|
if err = m.Resolve(ctx); err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
if err = m.Rip(ctx); err != nil {
|
|
t.Fatalf("Rip() error = %v", err)
|
|
}
|
|
|
|
folder := filepath.Join(tmp, "Fallback Artist - Fallback Album (2020) [MP3] [16B-44.1kHz]")
|
|
if _, err = os.Stat(filepath.Join(folder, "01. Fallback Artist - Fallback Song.mp3")); err != nil {
|
|
t.Fatalf("missing track in resolved-quality folder: %v", err)
|
|
}
|
|
if fake.downloadableHits != 1 {
|
|
t.Fatalf("GetDownloadable() calls=%d, want 1 (prefetched reuse)", fake.downloadableHits)
|
|
}
|
|
}
|
|
|
|
func TestBuildTagMetadataReplayGainFallbacks(t *testing.T) {
|
|
meta := map[string]any{
|
|
"replayGain": float64(-7.25),
|
|
"peak": float64(0.989),
|
|
"album": map[string]any{
|
|
"title": "Album",
|
|
"replaygain_album_gain": float64(-8.1),
|
|
"replaygain_album_peak": float64(1.001),
|
|
},
|
|
"performer": map[string]any{"name": "Artist"},
|
|
}
|
|
|
|
tags := buildTagMetadata(meta, "Song", "tidal", "t1", ripTrackOptions{})
|
|
if tags.ReplaygainTrackGain != "-7.25 dB" {
|
|
t.Fatalf("track replaygain gain=%q", tags.ReplaygainTrackGain)
|
|
}
|
|
if tags.ReplaygainAlbumGain != "-8.1 dB" {
|
|
t.Fatalf("album replaygain gain=%q", tags.ReplaygainAlbumGain)
|
|
}
|
|
if tags.ReplaygainTrackPeak != "0.989" {
|
|
t.Fatalf("track replaygain peak=%q", tags.ReplaygainTrackPeak)
|
|
}
|
|
if tags.ReplaygainAlbumPeak != "1.001" {
|
|
t.Fatalf("album replaygain peak=%q", tags.ReplaygainAlbumPeak)
|
|
}
|
|
}
|
|
|
|
func TestBuildTagMetadataReplayGainFallsBackToDeezerGain(t *testing.T) {
|
|
meta := map[string]any{
|
|
"gain": float64(-10),
|
|
"performer": map[string]any{"name": "Artist"},
|
|
"album": map[string]any{"title": "Album"},
|
|
}
|
|
|
|
tags := buildTagMetadata(meta, "Song", "deezer", "2675762392", ripTrackOptions{})
|
|
if tags.ReplaygainTrackGain != "-10 dB" {
|
|
t.Fatalf("track replaygain gain=%q", tags.ReplaygainTrackGain)
|
|
}
|
|
}
|