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), 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), 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), 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), 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) } }