mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-17 07:15:33 +02:00
test: decouple app fixtures from extension identities
This commit is contained in:
@@ -63,11 +63,11 @@ function track(id) {
|
||||
isrc: "USRC17607839",
|
||||
itemType: "track",
|
||||
albumType: "album",
|
||||
tidalId: "tidal-1",
|
||||
qobuzId: "qobuz-1",
|
||||
deezerId: "deezer-1",
|
||||
spotifyId: "spotify:track:1",
|
||||
externalLinks: { tidal: "https://tidal.example/1" },
|
||||
tidalId: "alternate-id-a",
|
||||
qobuzId: "alternate-id-b",
|
||||
deezerId: "alternate-id-c",
|
||||
spotifyId: "source-track-1",
|
||||
externalLinks: { provider: "https://provider.example/1" },
|
||||
label: "Label",
|
||||
copyright: "Copyright",
|
||||
genre: "Pop",
|
||||
|
||||
@@ -7,7 +7,7 @@ func TestCrossExtensionShareUsesAlbumCollectionItems(t *testing.T) {
|
||||
Manifest: &ExtensionManifest{
|
||||
Capabilities: map[string]any{
|
||||
"shareUrlTemplates": map[string]any{
|
||||
"album": "https://music.apple.com/us/album/{id}",
|
||||
"album": "https://media.example/album/{id}",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -25,7 +25,7 @@ func TestCrossExtensionShareUsesAlbumCollectionItems(t *testing.T) {
|
||||
if best == nil {
|
||||
t.Fatal("expected album collection item to match")
|
||||
}
|
||||
if url := resolveCollectionShareURL(ext, "album", best); url != "https://music.apple.com/us/album/1440783617" {
|
||||
if url := resolveCollectionShareURL(ext, "album", best); url != "https://media.example/album/1440783617" {
|
||||
t.Fatalf("album share URL = %q", url)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
|
||||
Manifest: &ExtensionManifest{
|
||||
Capabilities: map[string]any{
|
||||
"shareUrlTemplates": map[string]any{
|
||||
"artist": "https://music.youtube.com/browse/{id}",
|
||||
"artist": "https://media.example/artist/{id}",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -52,29 +52,29 @@ func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
|
||||
if best == nil {
|
||||
t.Fatal("expected artist collection item to match")
|
||||
}
|
||||
if url := resolveCollectionShareURL(ext, "artist", best); url != "https://music.youtube.com/browse/UCrPe3hLA51968GwxHSZ1llw" {
|
||||
if url := resolveCollectionShareURL(ext, "artist", best); url != "https://media.example/artist/UCrPe3hLA51968GwxHSZ1llw" {
|
||||
t.Fatalf("artist share URL = %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossExtensionShareCacheKeyIsProviderOrderStable(t *testing.T) {
|
||||
apple := &extensionProviderWrapper{
|
||||
providerA := &extensionProviderWrapper{
|
||||
extension: &loadedExtension{
|
||||
ID: "apple",
|
||||
SourceDir: "/extensions/apple",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Apple Music"},
|
||||
ID: "provider-a",
|
||||
SourceDir: "/extensions/provider-a",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Provider A"},
|
||||
},
|
||||
}
|
||||
qobuz := &extensionProviderWrapper{
|
||||
providerB := &extensionProviderWrapper{
|
||||
extension: &loadedExtension{
|
||||
ID: "qobuz",
|
||||
SourceDir: "/extensions/qobuz",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Qobuz"},
|
||||
ID: "provider-b",
|
||||
SourceDir: "/extensions/provider-b",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Provider B"},
|
||||
},
|
||||
}
|
||||
|
||||
first := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{apple, qobuz})
|
||||
second := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{qobuz, apple})
|
||||
first := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "metadata-source", []*extensionProviderWrapper{providerA, providerB})
|
||||
second := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "metadata-source", []*extensionProviderWrapper{providerB, providerA})
|
||||
if first != second {
|
||||
t.Fatalf("cache key should not depend on provider order:\n%s\n%s", first, second)
|
||||
}
|
||||
@@ -82,17 +82,17 @@ func TestCrossExtensionShareCacheKeyIsProviderOrderStable(t *testing.T) {
|
||||
|
||||
func TestCrossExtensionShareCacheableSkipsTransientErrors(t *testing.T) {
|
||||
cacheable := []CrossExtensionShareResult{
|
||||
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
|
||||
{ExtensionID: "qobuz", Error: "album not found"},
|
||||
{ExtensionID: "tidal", Error: "no results"},
|
||||
{ExtensionID: "provider-a", Found: true, URL: "https://media.example/album/1"},
|
||||
{ExtensionID: "provider-b", Error: "album not found"},
|
||||
{ExtensionID: "provider-c", Error: "no results"},
|
||||
}
|
||||
if !crossExtensionShareResultsCacheable(cacheable) {
|
||||
t.Fatal("expected found and deterministic not-found results to be cacheable")
|
||||
}
|
||||
|
||||
transient := []CrossExtensionShareResult{
|
||||
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
|
||||
{ExtensionID: "qobuz", Error: "request failed: timeout"},
|
||||
{ExtensionID: "provider-a", Found: true, URL: "https://media.example/album/1"},
|
||||
{ExtensionID: "provider-b", Error: "request failed: timeout"},
|
||||
}
|
||||
if crossExtensionShareResultsCacheable(transient) {
|
||||
t.Fatal("expected transient extension errors to skip cache")
|
||||
|
||||
@@ -3,15 +3,15 @@ package gobackend
|
||||
import "testing"
|
||||
|
||||
func TestBuildDownloadedFileCommentKeepsSourceComment(t *testing.T) {
|
||||
const source = "https://music.apple.com/us/album/example/123"
|
||||
got := buildDownloadedFileComment(source, "https://music.amazon.com/albums/example")
|
||||
const source = "https://source.example/album/example/123"
|
||||
got := buildDownloadedFileComment(source, "https://provider.example/albums/example")
|
||||
if got != source {
|
||||
t.Fatalf("comment = %q, want %q", got, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDownloadedFileCommentUsesProviderCommentWhenSourceIsEmpty(t *testing.T) {
|
||||
const provider = "https://music.amazon.com/albums/example"
|
||||
const provider = "https://provider.example/albums/example"
|
||||
got := buildDownloadedFileComment("", provider)
|
||||
if got != provider {
|
||||
t.Fatalf("comment = %q, want %q", got, provider)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestPreparedDownloadRequestCache(t *testing.T) {
|
||||
ItemID: "item-1",
|
||||
Service: "provider-a",
|
||||
Source: "source-a",
|
||||
SpotifyID: "spotify-1",
|
||||
SpotifyID: "source-track-1",
|
||||
TrackName: "Track",
|
||||
ArtistName: "Artist",
|
||||
OutputDir: "/new/output",
|
||||
@@ -33,7 +33,7 @@ func TestPreparedDownloadRequestCache(t *testing.T) {
|
||||
prepared.ISRC = "USRC17607839"
|
||||
prepared.AlbumName = "Resolved Album"
|
||||
prepared.AlbumArtist = "Resolved Album Artist"
|
||||
prepared.DeezerID = "deezer-1"
|
||||
prepared.DeezerID = "alternate-track-1"
|
||||
prepared.Genre = "Pop"
|
||||
prepared.OutputDir = "/stale/output"
|
||||
prepared.OutputPath = "/stale/output/old.flac"
|
||||
@@ -73,7 +73,7 @@ func TestPreparedDownloadRequestCacheRejectsChangedTrackAndExpiry(t *testing.T)
|
||||
req := DownloadRequest{
|
||||
ItemID: "item-2",
|
||||
Service: "provider-a",
|
||||
SpotifyID: "spotify-2",
|
||||
SpotifyID: "source-track-2",
|
||||
TrackName: "Track",
|
||||
ArtistName: "Artist",
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestPreparedDownloadRequestCacheRejectsChangedTrackAndExpiry(t *testing.T)
|
||||
cachePreparedDownloadRequest(key, req)
|
||||
|
||||
changed := req
|
||||
changed.SpotifyID = "spotify-other"
|
||||
changed.SpotifyID = "source-track-other"
|
||||
if _, _, ok := takePreparedDownloadRequest(downloadPreparationKey(changed), changed); ok {
|
||||
t.Fatal("changed track must not reuse another track's prepared metadata")
|
||||
}
|
||||
|
||||
+11
-11
@@ -51,7 +51,7 @@ func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T)
|
||||
resp := buildDownloadSuccessResponse(
|
||||
req,
|
||||
result,
|
||||
"tidal",
|
||||
"download-provider",
|
||||
"ok",
|
||||
"/tmp/test.flac",
|
||||
false,
|
||||
@@ -186,7 +186,7 @@ func TestBuildDownloadSuccessResponseReturnsResolvedProviderFilename(t *testing.
|
||||
req := DownloadRequest{
|
||||
TrackName: "Track",
|
||||
ArtistName: "Artist",
|
||||
DownloadProvider: "soundcloud",
|
||||
DownloadProvider: "download-provider",
|
||||
ProviderTrackID: "998877",
|
||||
FilenameFormat: "{artist} - {title} [{isrc}] [{provider}-{provider_id}]",
|
||||
OutputExt: ".flac",
|
||||
@@ -199,13 +199,13 @@ func TestBuildDownloadSuccessResponseReturnsResolvedProviderFilename(t *testing.
|
||||
resp := buildDownloadSuccessResponse(
|
||||
req,
|
||||
result,
|
||||
"soundcloud",
|
||||
"download-provider",
|
||||
"ok",
|
||||
"/proc/self/fd/10",
|
||||
false,
|
||||
)
|
||||
|
||||
want := "Artist - Track [USABC1234567] [soundcloud-998877].m4a"
|
||||
want := "Artist - Track [USABC1234567] [download-provider-998877].m4a"
|
||||
if resp.ResolvedFileName != want {
|
||||
t.Fatalf("resolved filename = %q, want %q", resp.ResolvedFileName, want)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func TestBuildDownloadSuccessResponseNormalizesDecryptionDescriptor(t *testing.T
|
||||
resp := buildDownloadSuccessResponse(
|
||||
req,
|
||||
result,
|
||||
"amazon",
|
||||
"download-provider",
|
||||
"ok",
|
||||
"/tmp/test.m4a",
|
||||
false,
|
||||
@@ -426,7 +426,7 @@ func TestEnrichExtraMetadataByISRCPrefersDeezerGenre(t *testing.T) {
|
||||
|
||||
func TestApplyReEnrichTrackMetadataPreservesExistingReleaseDateWhenCandidateMissing(t *testing.T) {
|
||||
req := reEnrichRequest{
|
||||
SpotifyID: "spotify-track-id",
|
||||
SpotifyID: "source-track-id",
|
||||
AlbumName: "Original Album",
|
||||
ReleaseDate: "2024-01-01",
|
||||
ISRC: "REQ123",
|
||||
@@ -541,7 +541,7 @@ func TestSelectBestReEnrichTrackPrefersCandidateWithReleaseDate(t *testing.T) {
|
||||
AlbumName: "Album Name",
|
||||
DurationMS: 180000,
|
||||
ReleaseDate: "",
|
||||
ProviderID: "spotify",
|
||||
ProviderID: "metadata-a",
|
||||
},
|
||||
{
|
||||
ID: "second",
|
||||
@@ -550,7 +550,7 @@ func TestSelectBestReEnrichTrackPrefersCandidateWithReleaseDate(t *testing.T) {
|
||||
AlbumName: "Album Name",
|
||||
DurationMS: 180000,
|
||||
ReleaseDate: "2024-03-09",
|
||||
ProviderID: "deezer",
|
||||
ProviderID: "metadata-b",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -582,7 +582,7 @@ func TestSelectBestReEnrichTrackRejectsMismatchedSearchResults(t *testing.T) {
|
||||
TrackNumber: 4,
|
||||
DiscNumber: 1,
|
||||
ISRC: "WRONG1234567",
|
||||
ProviderID: "deezer",
|
||||
ProviderID: "metadata-b",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ func TestSelectBestReEnrichTrackAllowsExactISRCDespiteMetadataMismatch(t *testin
|
||||
Artists: "Different Artist",
|
||||
DurationMS: 180000,
|
||||
ISRC: "USRC17607839",
|
||||
ProviderID: "deezer",
|
||||
ProviderID: "metadata-b",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ func TestSelectBestReEnrichTrackPlaceholderFallsBackToAlbum(t *testing.T) {
|
||||
Artists: "Harry Styles",
|
||||
AlbumName: "Harry Styles",
|
||||
DurationMS: 180000,
|
||||
ProviderID: "deezer",
|
||||
ProviderID: "metadata-b",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ func qualityTestManifest(name string, options ...QualityOption) *ExtensionManife
|
||||
}
|
||||
|
||||
func TestExtensionQualityKeepsAudioKindAcrossProviders(t *testing.T) {
|
||||
amazon := qualityTestManifest("source", QualityOption{ID: "best", Label: "FLAC Best Available"}, QualityOption{ID: "ac4", Label: "Dolby Atmos"})
|
||||
tidal := qualityTestManifest("target",
|
||||
source := qualityTestManifest("source", QualityOption{ID: "best", Label: "FLAC Best Available"}, QualityOption{ID: "ac4", Label: "Dolby Atmos"})
|
||||
target := qualityTestManifest("target",
|
||||
QualityOption{ID: "DOLBY_ATMOS", Label: "Dolby Atmos", Description: "falls back to FLAC"},
|
||||
QualityOption{ID: "HI_RES_LOSSLESS", Label: "HiRes FLAC"},
|
||||
QualityOption{ID: "LOSSLESS", Label: "Lossless"},
|
||||
@@ -25,13 +25,13 @@ func TestExtensionQualityKeepsAudioKindAcrossProviders(t *testing.T) {
|
||||
{"ac4", "DOLBY_ATMOS"}, {"DOLBY_ATMOS", "DOLBY_ATMOS"}, {"HIGH", "HIGH"},
|
||||
} {
|
||||
t.Run(tc.requested, func(t *testing.T) {
|
||||
got, err := resolveExtensionDownloadQuality(tc.requested, amazon, tidal)
|
||||
got, err := resolveExtensionDownloadQuality(tc.requested, source, target)
|
||||
if err != nil || got != tc.want {
|
||||
t.Fatalf("quality=%q err=%v; want %q", got, err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if got, err := resolveExtensionDownloadQuality("best", amazon, amazon); err != nil || got != "best" {
|
||||
if got, err := resolveExtensionDownloadQuality("best", source, source); err != nil || got != "best" {
|
||||
t.Fatalf("same provider selection changed: %s %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ func TestOverlayExtensionReleaseMetadataFillsMissingRequestFields(t *testing.T)
|
||||
AlbumType: "album",
|
||||
Explicit: true,
|
||||
UPC: "4006381333931",
|
||||
Comment: "https://music.apple.com/jp/album/1532211596",
|
||||
Comment: "https://source.example/album/1532211596",
|
||||
}
|
||||
|
||||
overlayExtensionReleaseMetadata(&req, track)
|
||||
@@ -29,7 +29,7 @@ func TestOverlayExtensionReleaseMetadataFillsMissingRequestFields(t *testing.T)
|
||||
response := buildDownloadSuccessResponse(
|
||||
req,
|
||||
DownloadResult{},
|
||||
"amazon",
|
||||
"download-provider",
|
||||
"downloaded",
|
||||
"song.m4a",
|
||||
false,
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestVerifiedDownloadResumeTriesSelectedProviderBeforeMetadata(t *testing.T)
|
||||
ItemID: "resume-item",
|
||||
Service: downloadExt.ID,
|
||||
Source: metadataExt.ID,
|
||||
SpotifyID: "spotify:track:1",
|
||||
SpotifyID: "source-track-1",
|
||||
TrackName: "Original Song",
|
||||
ArtistName: "Artist",
|
||||
AlbumName: "Album",
|
||||
@@ -93,7 +93,7 @@ func TestVerifiedDownloadResumeReusesPreparedMetadata(t *testing.T) {
|
||||
ItemID: "prepared-item",
|
||||
Service: downloadExt.ID,
|
||||
Source: metadataExt.ID,
|
||||
SpotifyID: "spotify:track:2",
|
||||
SpotifyID: "source-track-2",
|
||||
TrackName: "Original Song",
|
||||
ArtistName: "Artist",
|
||||
AlbumName: "Album",
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) {
|
||||
if status, _ := classifyExtensionHealthBody([]byte(`not-json`), ""); status != "online" {
|
||||
t.Fatalf("invalid JSON status = %q", status)
|
||||
}
|
||||
if status, msg := classifyExtensionHealthBody([]byte(`{"services":{"tidal":{"status":401,"label":"Tidal","detail":"auth_required"}}}`), "tidal"); status != "degraded" || !strings.Contains(msg, "Tidal") {
|
||||
if status, msg := classifyExtensionHealthBody([]byte(`{"services":{"provider":{"status":401,"label":"Provider","detail":"auth_required"}}}`), "provider"); status != "degraded" || !strings.Contains(msg, "Provider") {
|
||||
t.Fatalf("service status/message = %q/%q", status, msg)
|
||||
}
|
||||
if status, msg, ok := classifyExtensionHealthService(map[string]any{"services": map[string]any{}}, "missing"); !ok || status != "unknown" || !strings.Contains(msg, "missing") {
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracks: %v", err)
|
||||
}
|
||||
if search.Total != 1 || search.Tracks[0].ProviderID != ext.ID || search.Tracks[0].ExternalLinks["tidal"] == "" {
|
||||
if search.Total != 1 || search.Tracks[0].ProviderID != ext.ID || search.Tracks[0].ExternalLinks["provider"] == "" {
|
||||
t.Fatalf("search = %#v", search)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
|
||||
t.Fatalf("enriched = %#v", enriched)
|
||||
}
|
||||
|
||||
availability, err := provider.CheckAvailabilityForItemID("ISRC", "Song", "Artist", "spotify:1", "dz", "tidal", "qobuz", 0, "")
|
||||
availability, err := provider.CheckAvailabilityForItemID("ISRC", "Song", "Artist", "source-id", "alternate-id-a", "alternate-id-b", "alternate-id-c", 0, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckAvailabilityForItemID: %v", err)
|
||||
}
|
||||
@@ -172,15 +172,15 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
|
||||
|
||||
func TestExtensionProviderAndManagerSelectionHelpers(t *testing.T) {
|
||||
manifest := &ExtensionManifest{Capabilities: map[string]any{
|
||||
"replacesBuiltInProviders": []any{" Deezer ", 7, ""},
|
||||
"replacesBuiltInProviders": []any{" Legacy-Provider ", 7, ""},
|
||||
}}
|
||||
if values := manifestCapabilityStringList(manifest, "replacesBuiltInProviders"); len(values) != 1 || values[0] != "deezer" {
|
||||
if values := manifestCapabilityStringList(manifest, "replacesBuiltInProviders"); len(values) != 1 || values[0] != "legacy-provider" {
|
||||
t.Fatalf("capability list = %#v", values)
|
||||
}
|
||||
if !extensionReplacesBuiltInProvider(&loadedExtension{Manifest: manifest}, "deezer") || extensionReplacesBuiltInProvider(nil, "deezer") {
|
||||
if !extensionReplacesBuiltInProvider(&loadedExtension{Manifest: manifest}, "legacy-provider") || extensionReplacesBuiltInProvider(nil, "legacy-provider") {
|
||||
t.Fatal("extension replacement mismatch")
|
||||
}
|
||||
if trimKnownProviderPrefix("Deezer:101", "deezer") != "101" || trimKnownProviderPrefix("101", "deezer") != "101" {
|
||||
if trimKnownProviderPrefix("Legacy-Provider:101", "legacy-provider") != "101" || trimKnownProviderPrefix("101", "legacy-provider") != "101" {
|
||||
t.Fatal("trimKnownProviderPrefix mismatch")
|
||||
}
|
||||
if metadataTrackDedupKey(ExtTrackMetadata{ISRC: "usrc"}) != "isrc:USRC" ||
|
||||
|
||||
@@ -133,10 +133,10 @@ func TestSetProviderPriorityKeepsExtensionNamedLikeRetiredDownloader(t *testing.
|
||||
|
||||
func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *testing.T) {
|
||||
manager := getExtensionManager()
|
||||
amazon := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
amazon.ID = "amazon"
|
||||
amazon.Manifest.Name = "amazon"
|
||||
amazon.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
unavailable := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
unavailable.ID = "unavailable-provider"
|
||||
unavailable.Manifest.Name = "unavailable-provider"
|
||||
unavailable.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
ID: "main",
|
||||
URL: "://bad",
|
||||
Required: true,
|
||||
@@ -146,59 +146,59 @@ func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *test
|
||||
plain.ID = "plain"
|
||||
plain.Manifest.Name = "plain"
|
||||
|
||||
deezer := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
deezer.ID = "deezer"
|
||||
deezer.Manifest.Name = "deezer"
|
||||
deezer.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
available := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
available.ID = "available-provider"
|
||||
available.Manifest.Name = "available-provider"
|
||||
available.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
ID: "main",
|
||||
URL: "https://example.test/health",
|
||||
}}
|
||||
|
||||
manager.mu.Lock()
|
||||
previousAmazon, hadAmazon := manager.extensions[amazon.ID]
|
||||
previousUnavailable, hadUnavailable := manager.extensions[unavailable.ID]
|
||||
previousPlain, hadPlain := manager.extensions[plain.ID]
|
||||
previousDeezer, hadDeezer := manager.extensions[deezer.ID]
|
||||
manager.extensions[amazon.ID] = amazon
|
||||
previousAvailable, hadAvailable := manager.extensions[available.ID]
|
||||
manager.extensions[unavailable.ID] = unavailable
|
||||
manager.extensions[plain.ID] = plain
|
||||
manager.extensions[deezer.ID] = deezer
|
||||
manager.extensions[available.ID] = available
|
||||
manager.mu.Unlock()
|
||||
defer func() {
|
||||
manager.mu.Lock()
|
||||
if hadAmazon {
|
||||
manager.extensions[amazon.ID] = previousAmazon
|
||||
if hadUnavailable {
|
||||
manager.extensions[unavailable.ID] = previousUnavailable
|
||||
} else {
|
||||
delete(manager.extensions, amazon.ID)
|
||||
delete(manager.extensions, unavailable.ID)
|
||||
}
|
||||
if hadPlain {
|
||||
manager.extensions[plain.ID] = previousPlain
|
||||
} else {
|
||||
delete(manager.extensions, plain.ID)
|
||||
}
|
||||
if hadDeezer {
|
||||
manager.extensions[deezer.ID] = previousDeezer
|
||||
if hadAvailable {
|
||||
manager.extensions[available.ID] = previousAvailable
|
||||
} else {
|
||||
delete(manager.extensions, deezer.ID)
|
||||
delete(manager.extensions, available.ID)
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
|
||||
extensionHealthCacheMu.Lock()
|
||||
delete(extensionHealthCache, amazon.ID)
|
||||
delete(extensionHealthCache, deezer.ID)
|
||||
delete(extensionHealthCache, unavailable.ID)
|
||||
delete(extensionHealthCache, available.ID)
|
||||
extensionHealthCacheMu.Unlock()
|
||||
}()
|
||||
|
||||
extensionHealthCacheMu.Lock()
|
||||
extensionHealthCache[amazon.ID] = cachedExtensionHealthResult{
|
||||
extensionHealthCache[unavailable.ID] = cachedExtensionHealthResult{
|
||||
result: ExtensionHealthResult{
|
||||
ExtensionID: amazon.ID,
|
||||
ExtensionID: unavailable.ID,
|
||||
Status: "offline",
|
||||
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
expiresAt: time.Now().Add(time.Minute),
|
||||
}
|
||||
extensionHealthCache[deezer.ID] = cachedExtensionHealthResult{
|
||||
extensionHealthCache[available.ID] = cachedExtensionHealthResult{
|
||||
result: ExtensionHealthResult{
|
||||
ExtensionID: deezer.ID,
|
||||
ExtensionID: available.ID,
|
||||
Status: "online",
|
||||
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
@@ -207,11 +207,11 @@ func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *test
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
got := prioritizeFallbackProvidersByHealth(
|
||||
[]string{"amazon", "plain", "deezer"},
|
||||
[]string{"unavailable-provider", "plain", "available-provider"},
|
||||
manager,
|
||||
"",
|
||||
)
|
||||
want := []string{"deezer", "plain"}
|
||||
want := []string{"available-provider", "plain"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected provider order length: got %v want %v", got, want)
|
||||
}
|
||||
@@ -468,14 +468,14 @@ func TestShouldStopProviderFallback(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMoveProviderToFrontPreservesExplicitSelection(t *testing.T) {
|
||||
priority := []string{"qobuz-web", "amazon-web", "tidal-web"}
|
||||
got := moveProviderToFront(priority, "AMAZON-WEB")
|
||||
want := []string{"amazon-web", "qobuz-web", "tidal-web"}
|
||||
priority := []string{"provider-a", "provider-b", "provider-c"}
|
||||
got := moveProviderToFront(priority, "PROVIDER-B")
|
||||
want := []string{"provider-b", "provider-a", "provider-c"}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("moveProviderToFront() = %#v, want %#v", got, want)
|
||||
}
|
||||
if !reflect.DeepEqual(priority, []string{"qobuz-web", "amazon-web", "tidal-web"}) {
|
||||
if !reflect.DeepEqual(priority, []string{"provider-a", "provider-b", "provider-c"}) {
|
||||
t.Fatalf("moveProviderToFront mutated input: %#v", priority)
|
||||
}
|
||||
}
|
||||
@@ -520,15 +520,15 @@ func TestDiscardRejectedExtensionOutputPreservesExistingLibraryHit(t *testing.T)
|
||||
}
|
||||
|
||||
func TestBuildExtensionFallbackStoppedResponsePrefersAvailabilityReason(t *testing.T) {
|
||||
resp := buildExtensionFallbackStoppedResponse("soundcloud", &ExtAvailabilityResult{
|
||||
Reason: "direct SoundCloud track ID",
|
||||
resp := buildExtensionFallbackStoppedResponse("direct-provider", &ExtAvailabilityResult{
|
||||
Reason: "direct provider track ID",
|
||||
SkipFallback: true,
|
||||
}, errors.New("ignored"))
|
||||
|
||||
if resp.Service != "soundcloud" {
|
||||
if resp.Service != "direct-provider" {
|
||||
t.Fatalf("service = %q", resp.Service)
|
||||
}
|
||||
if resp.Error != "Fallback stopped by soundcloud: direct SoundCloud track ID" {
|
||||
if resp.Error != "Fallback stopped by direct-provider: direct provider track ID" {
|
||||
t.Fatalf("unexpected error message: %q", resp.Error)
|
||||
}
|
||||
if resp.ErrorType != "extension_error" {
|
||||
@@ -537,11 +537,11 @@ func TestBuildExtensionFallbackStoppedResponsePrefersAvailabilityReason(t *testi
|
||||
}
|
||||
|
||||
func TestBuildExtensionFallbackStoppedResponseFallsBackToError(t *testing.T) {
|
||||
resp := buildExtensionFallbackStoppedResponse("soundcloud", &ExtAvailabilityResult{
|
||||
resp := buildExtensionFallbackStoppedResponse("direct-provider", &ExtAvailabilityResult{
|
||||
SkipFallback: true,
|
||||
}, errors.New("lookup failed"))
|
||||
|
||||
if resp.Error != "Fallback stopped by soundcloud: lookup failed" {
|
||||
if resp.Error != "Fallback stopped by direct-provider: lookup failed" {
|
||||
t.Fatalf("unexpected error message: %q", resp.Error)
|
||||
}
|
||||
}
|
||||
@@ -619,7 +619,7 @@ func TestParseExtensionSearchResultAcceptsObjectAndArrayShapes(t *testing.T) {
|
||||
album_name: "Album",
|
||||
duration_ms: 123000,
|
||||
cover_url: "https://img.test/cover.jpg",
|
||||
external_links: { spotify: "spotify:track:1" },
|
||||
external_links: { provider: "https://provider.example/track/1" },
|
||||
audio_quality: "LOSSLESS"
|
||||
}],
|
||||
total: 9
|
||||
@@ -640,7 +640,7 @@ func TestParseExtensionSearchResultAcceptsObjectAndArrayShapes(t *testing.T) {
|
||||
track.AlbumName != "Album" ||
|
||||
track.DurationMS != 123000 ||
|
||||
track.CoverURL != "https://img.test/cover.jpg" ||
|
||||
track.ExternalLinks["spotify"] != "spotify:track:1" ||
|
||||
track.ExternalLinks["provider"] != "https://provider.example/track/1" ||
|
||||
track.AudioQuality != "LOSSLESS" {
|
||||
t.Fatalf("unexpected parsed track: %+v", track)
|
||||
}
|
||||
@@ -846,16 +846,16 @@ func TestMatchesURLHostAnchored(t *testing.T) {
|
||||
manifest := &ExtensionManifest{
|
||||
URLHandler: &URLHandlerConfig{
|
||||
Enabled: true,
|
||||
Patterns: []string{"spotify.com", "deezer.page.link", "spotify:"},
|
||||
Patterns: []string{"catalog.example", "short.example", "catalog:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, urlStr := range []string{
|
||||
"https://open.spotify.com/track/abc",
|
||||
"https://spotify.com/track/abc",
|
||||
"HTTPS://OPEN.SPOTIFY.COM/track/ABC",
|
||||
"https://deezer.page.link/xyz",
|
||||
"spotify:track:abc123",
|
||||
"https://open.catalog.example/track/abc",
|
||||
"https://catalog.example/track/abc",
|
||||
"HTTPS://OPEN.CATALOG.EXAMPLE/track/ABC",
|
||||
"https://short.example/xyz",
|
||||
"catalog:track:abc123",
|
||||
} {
|
||||
if !manifest.MatchesURL(urlStr) {
|
||||
t.Fatalf("expected match for %q", urlStr)
|
||||
@@ -864,10 +864,10 @@ func TestMatchesURLHostAnchored(t *testing.T) {
|
||||
|
||||
for _, urlStr := range []string{
|
||||
// The old substring matching accepted all of these.
|
||||
"https://evil.example/?next=https://spotify.com/track/abc",
|
||||
"https://notspotify.com/track/abc",
|
||||
"https://spotify.com.evil.example/track/abc",
|
||||
"https://example.com/spotify.com",
|
||||
"https://evil.example/?next=https://catalog.example/track/abc",
|
||||
"https://notcatalog.example/track/abc",
|
||||
"https://catalog.example.evil.example/track/abc",
|
||||
"https://example.com/catalog.example",
|
||||
"not a url at all",
|
||||
} {
|
||||
if manifest.MatchesURL(urlStr) {
|
||||
@@ -878,13 +878,13 @@ func TestMatchesURLHostAnchored(t *testing.T) {
|
||||
withPath := &ExtensionManifest{
|
||||
URLHandler: &URLHandlerConfig{
|
||||
Enabled: true,
|
||||
Patterns: []string{"youtube.com/watch"},
|
||||
Patterns: []string{"video.example/watch"},
|
||||
},
|
||||
}
|
||||
if !withPath.MatchesURL("https://www.youtube.com/watch?v=abc") {
|
||||
if !withPath.MatchesURL("https://www.video.example/watch?v=abc") {
|
||||
t.Fatal("expected host+path prefix to match")
|
||||
}
|
||||
if withPath.MatchesURL("https://www.youtube.com/playlist?list=abc") {
|
||||
if withPath.MatchesURL("https://www.video.example/playlist?list=abc") {
|
||||
t.Fatal("expected different path to not match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,7 +1170,7 @@ func TestExtensionRuntimeUtilityAPIs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClassifySignedSessionExpiredAsVerification(t *testing.T) {
|
||||
got := classifyDownloadErrorType("Failed to resolve Deezer download: signed session expired")
|
||||
got := classifyDownloadErrorType("Failed to resolve provider download: signed session expired")
|
||||
if got != "verification_required" {
|
||||
t.Fatalf("expected verification_required, got %q", got)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestSignedSessionConfigWithDefaults(t *testing.T) {
|
||||
|
||||
t.Run("preserves values the manifest already set", func(t *testing.T) {
|
||||
custom := &SignedSessionConfig{
|
||||
Namespace: "tidal",
|
||||
Namespace: "provider",
|
||||
BaseURL: "https://auth.example.com",
|
||||
AppVersion: "5.0",
|
||||
Platform: "mobile",
|
||||
@@ -89,7 +89,7 @@ func TestSignedSessionConfigWithDefaults(t *testing.T) {
|
||||
Endpoints: SignedSessionEndpoints{Exchange: "/custom/exchange"},
|
||||
}
|
||||
got := signedSessionConfigWithDefaults(custom)
|
||||
if got.Namespace != "tidal" || got.BaseURL != "https://auth.example.com" {
|
||||
if got.Namespace != "provider" || got.BaseURL != "https://auth.example.com" {
|
||||
t.Errorf("namespace/baseUrl were overwritten: %+v", got)
|
||||
}
|
||||
if got.AppVersion != "5.0" || got.Platform != "mobile" || got.TimeWindowSeconds != 60 {
|
||||
@@ -360,7 +360,7 @@ func TestDownloadWithExtensionsPreflightsBeforeMetadataEnrichment(t *testing.T)
|
||||
|
||||
// Metadata may originate from another extension, but the explicitly chosen
|
||||
// download provider owns the signed-session namespace and verification.
|
||||
requestJSON := `{"source":"spotify-metadata","service":"preflight-download","item_id":"preflight-item","isrc":"USRC17607839"}`
|
||||
requestJSON := `{"source":"source-metadata","service":"preflight-download","item_id":"preflight-item","isrc":"USRC17607839"}`
|
||||
responseJSON, err := DownloadWithExtensionsJSON(requestJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadWithExtensionsJSON: %v", err)
|
||||
@@ -718,12 +718,12 @@ func TestSignedSessionProviderRetryDuration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeSignedSessionRecordScope(t *testing.T) {
|
||||
config := SignedSessionConfig{Namespace: "Tidal", BaseURL: "https://a.example.com", AppVersion: "1.0", Platform: "mobile"}
|
||||
config := SignedSessionConfig{Namespace: "Provider", BaseURL: "https://a.example.com", AppVersion: "1.0", Platform: "mobile"}
|
||||
|
||||
t.Run("first save just stamps the scope", func(t *testing.T) {
|
||||
record := &signedSessionRecord{SessionID: "s1", SessionSecret: "secret"}
|
||||
normalizeSignedSessionRecordScope(config, record)
|
||||
if record.Namespace != "tidal" || record.BaseURL != config.BaseURL {
|
||||
if record.Namespace != "provider" || record.BaseURL != config.BaseURL {
|
||||
t.Errorf("scope not stamped: %+v", record)
|
||||
}
|
||||
if record.SessionID != "s1" || record.SessionSecret != "secret" {
|
||||
@@ -733,7 +733,7 @@ func TestNormalizeSignedSessionRecordScope(t *testing.T) {
|
||||
|
||||
t.Run("same scope preserves the session", func(t *testing.T) {
|
||||
record := &signedSessionRecord{
|
||||
Namespace: "tidal", BaseURL: config.BaseURL, AppVersion: config.AppVersion, Platform: config.Platform,
|
||||
Namespace: "provider", BaseURL: config.BaseURL, AppVersion: config.AppVersion, Platform: config.Platform,
|
||||
SessionID: "s1", SessionSecret: "secret", ExpiresAt: "later",
|
||||
}
|
||||
normalizeSignedSessionRecordScope(config, record)
|
||||
@@ -744,7 +744,7 @@ func TestNormalizeSignedSessionRecordScope(t *testing.T) {
|
||||
|
||||
t.Run("changed scope wipes the session secret", func(t *testing.T) {
|
||||
record := &signedSessionRecord{
|
||||
Namespace: "tidal", BaseURL: "https://old.example.com", AppVersion: config.AppVersion, Platform: config.Platform,
|
||||
Namespace: "provider", BaseURL: "https://old.example.com", AppVersion: config.AppVersion, Platform: config.Platform,
|
||||
SessionID: "s1", SessionSecret: "secret", ExpiresAt: "later",
|
||||
}
|
||||
normalizeSignedSessionRecordScope(config, record)
|
||||
@@ -791,10 +791,10 @@ func saveUsableSignedSession(
|
||||
}
|
||||
|
||||
func TestSignedSessionFilePathDeterminism(t *testing.T) {
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
|
||||
|
||||
configA := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://a.example.com"}
|
||||
configB := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://b.example.com"}
|
||||
configA := SignedSessionConfig{Namespace: "provider", BaseURL: "https://a.example.com"}
|
||||
configB := SignedSessionConfig{Namespace: "provider", BaseURL: "https://b.example.com"}
|
||||
|
||||
pathA1, err := runtime.signedSessionFilePath(configA)
|
||||
if err != nil {
|
||||
@@ -822,8 +822,8 @@ func TestSignedSessionFilePathDeterminism(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadAndSaveSignedSessionRoundTrip(t *testing.T) {
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
|
||||
config := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
|
||||
config := SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
record, err := runtime.loadSignedSession(config)
|
||||
if err != nil {
|
||||
@@ -868,8 +868,8 @@ func TestLoadAndSaveSignedSessionRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSignedSessionStatusAndClear(t *testing.T) {
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
readStatus := func() map[string]any {
|
||||
v := runtime.signedSessionStatus(goja.FunctionCall{})
|
||||
@@ -929,7 +929,7 @@ func TestDoSignedSessionRequestSignature(t *testing.T) {
|
||||
|
||||
var capturedErr string
|
||||
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"})
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"})
|
||||
prefix := config.HeaderPrefix
|
||||
|
||||
ts := req.Header.Get(prefix + "Timestamp")
|
||||
@@ -982,8 +982,8 @@ func TestDoSignedSessionRequestSignature(t *testing.T) {
|
||||
}, nil
|
||||
})
|
||||
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"})
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"})
|
||||
record := &signedSessionRecord{InstallID: "install-1", SessionID: sessionID, SessionSecret: sessionSecret}
|
||||
|
||||
resp, body, _, err := runtime.doSignedSessionRequest(config, record, http.MethodPost, "/tracks/search", []byte(`{"q":"test"}`), nil)
|
||||
@@ -1017,8 +1017,8 @@ func TestSignedSessionFetchUnauthenticatedTriggersVerification(t *testing.T) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
call := goja.FunctionCall{Arguments: []goja.Value{runtime.vm.ToValue("GET"), runtime.vm.ToValue("/tracks/search")}}
|
||||
result := runtime.signedSessionFetch(call).Export().(map[string]any)
|
||||
@@ -1056,8 +1056,8 @@ func TestSignedSessionFetchRevokesSessionOnCanonicalSessionInvalid(t *testing.T)
|
||||
}
|
||||
})
|
||||
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
config := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
config := SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
runtime.manifest.SignedSession = &config
|
||||
|
||||
resolved := signedSessionConfigWithDefaults(&config)
|
||||
@@ -1849,8 +1849,8 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
|
||||
body, _ := json.Marshal(payload)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
if err := runtime.exchangeSignedSessionGrant("grant-token"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -1876,8 +1876,8 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
|
||||
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: 400, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
if err := runtime.exchangeSignedSessionGrant("bad-grant"); err == nil {
|
||||
t.Fatal("expected an error for a non-2xx exchange response")
|
||||
@@ -1919,9 +1919,9 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
|
||||
Request: req,
|
||||
}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-rate-limit", transport)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-rate-limit", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{
|
||||
Namespace: "tidal-rate-limit",
|
||||
Namespace: "provider-rate-limit",
|
||||
BaseURL: "https://auth.example.com",
|
||||
}
|
||||
setPendingSignedSessionGrant(runtime.extensionID, "grant-preserved")
|
||||
@@ -1964,9 +1964,9 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
|
||||
Request: req,
|
||||
}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-rate-limit-exhausted", transport)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-rate-limit-exhausted", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{
|
||||
Namespace: "tidal-rate-limit-exhausted",
|
||||
Namespace: "provider-rate-limit-exhausted",
|
||||
BaseURL: "https://auth.example.com",
|
||||
}
|
||||
setPendingSignedSessionGrant(runtime.extensionID, "grant-retry-later")
|
||||
@@ -2025,9 +2025,9 @@ func TestRefreshSignedSession(t *testing.T) {
|
||||
body, _ := json.Marshal(payload)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
|
||||
Namespace: "tidal", BaseURL: "https://auth.example.com",
|
||||
Namespace: "provider", BaseURL: "https://auth.example.com",
|
||||
Endpoints: SignedSessionEndpoints{Refresh: "/session/refresh"},
|
||||
})
|
||||
record := &signedSessionRecord{InstallID: "install-1", SessionID: "sess-1", SessionSecret: "old-secret", ExpiresAt: "2030-01-01T00:00:00Z"}
|
||||
@@ -2055,9 +2055,9 @@ func TestRefreshSignedSession(t *testing.T) {
|
||||
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: 500, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
|
||||
Namespace: "tidal", BaseURL: "https://auth.example.com",
|
||||
Namespace: "provider", BaseURL: "https://auth.example.com",
|
||||
Endpoints: SignedSessionEndpoints{Refresh: "/session/refresh"},
|
||||
})
|
||||
record := &signedSessionRecord{InstallID: "install-1", SessionID: "sess-1", SessionSecret: "old-secret"}
|
||||
@@ -2229,8 +2229,8 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
|
||||
body, _ := json.Marshal(payload)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
|
||||
call := goja.FunctionCall{Arguments: []goja.Value{runtime.vm.ToValue("grant-from-arg")}}
|
||||
result := runtime.signedSessionCompleteGrant(call).Export().(map[string]any)
|
||||
@@ -2245,8 +2245,8 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
|
||||
body, _ := json.Marshal(payload)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext-pending", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext-pending", transport)
|
||||
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
|
||||
setPendingSignedSessionGrant(runtime.extensionID, "pending-grant")
|
||||
|
||||
result := runtime.signedSessionCompleteGrant(goja.FunctionCall{}).Export().(map[string]any)
|
||||
@@ -2263,7 +2263,7 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("no grant available reports failure", func(t *testing.T) {
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext-none", nil)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext-none", nil)
|
||||
result := runtime.signedSessionCompleteGrant(goja.FunctionCall{}).Export().(map[string]any)
|
||||
if result["success"] != false {
|
||||
t.Fatalf("expected failure without a grant, got %+v", result)
|
||||
@@ -2273,11 +2273,11 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
|
||||
|
||||
func TestBuildSignedSessionChallengeURL(t *testing.T) {
|
||||
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
|
||||
Namespace: "tidal",
|
||||
Namespace: "provider",
|
||||
BaseURL: "https://auth.example.com",
|
||||
CallbackURL: "spotiflac://session-grant",
|
||||
})
|
||||
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
|
||||
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
|
||||
|
||||
got := runtime.buildSignedSessionChallengeURL(config, "chal-123", "state-123")
|
||||
|
||||
|
||||
@@ -123,13 +123,13 @@ func TestBuildDownloadFilename_ProvidesTraceabilityPlaceholders(t *testing.T) {
|
||||
TrackName: "Song Name",
|
||||
ArtistName: "Artist Name",
|
||||
ISRC: "USABC1234567",
|
||||
DownloadProvider: "tidal-web",
|
||||
DownloadProvider: "download-provider",
|
||||
ProviderTrackID: "123456789",
|
||||
FilenameFormat: "{artist} - {title} [{isrc}] [{provider}-{provider_id}]",
|
||||
OutputExt: ".flac",
|
||||
})
|
||||
|
||||
expected := "Artist Name - Song Name [USABC1234567] [tidal-web-123456789].flac"
|
||||
expected := "Artist Name - Song Name [USABC1234567] [download-provider-123456789].flac"
|
||||
if filename != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, filename)
|
||||
}
|
||||
@@ -137,12 +137,12 @@ func TestBuildDownloadFilename_ProvidesTraceabilityPlaceholders(t *testing.T) {
|
||||
|
||||
func TestBuildFilenameFromTemplate_TraceabilityAliases(t *testing.T) {
|
||||
metadata := map[string]any{
|
||||
"provider": "soundcloud",
|
||||
"provider": "download-provider",
|
||||
"provider_id": "998877",
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate("{platform}-{id}", metadata)
|
||||
if formatted != "soundcloud-998877" {
|
||||
if formatted != "download-provider-998877" {
|
||||
t.Fatalf("unexpected alias filename: %q", formatted)
|
||||
}
|
||||
}
|
||||
@@ -150,14 +150,14 @@ func TestBuildFilenameFromTemplate_TraceabilityAliases(t *testing.T) {
|
||||
func TestBuildFilenameFromTemplate_CleansEmptyTraceabilityDecorations(t *testing.T) {
|
||||
metadata := map[string]any{
|
||||
"title": "Song Name",
|
||||
"provider": "tidal-web",
|
||||
"provider": "download-provider",
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate(
|
||||
"{title} [{isrc}] [{provider}-{provider_id}]",
|
||||
metadata,
|
||||
)
|
||||
if formatted != "Song Name [tidal-web]" {
|
||||
if formatted != "Song Name [download-provider]" {
|
||||
t.Fatalf("unexpected empty placeholder cleanup: %q", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) {
|
||||
if ua := appUserAgent(); !strings.Contains(ua, "4.5.0") {
|
||||
t.Fatalf("user agent = %q", ua)
|
||||
}
|
||||
SetLyricsProviderOrder([]string{"LRCLIB", "bad", "extension:Apple-Music", "netease", "extension:apple-music"})
|
||||
if providers := GetLyricsProviderOrder(); len(providers) != 3 || providers[0] != LyricsProviderLRCLIB || providers[1] != "extension:apple-music" {
|
||||
SetLyricsProviderOrder([]string{"LRCLIB", "bad", "extension:Lyrics-Fixture", "netease", "extension:lyrics-fixture"})
|
||||
if providers := GetLyricsProviderOrder(); len(providers) != 3 || providers[0] != LyricsProviderLRCLIB || providers[1] != "extension:lyrics-fixture" {
|
||||
t.Fatalf("providers = %#v", providers)
|
||||
}
|
||||
SetLyricsProviderOrder(nil)
|
||||
@@ -332,7 +332,7 @@ func TestConcurrentLyricsProvidersReturnFastFallback(t *testing.T) {
|
||||
|
||||
func TestResolveLyricsProviderOrderOnlyIncludesSelectedAvailableExtensions(t *testing.T) {
|
||||
availableExtensions := map[string]*extensionProviderWrapper{
|
||||
"extension:apple-music": nil,
|
||||
"extension:lyrics-fixture": nil,
|
||||
"extension:future-provider": nil,
|
||||
}
|
||||
providers := resolveLyricsProviderOrder(
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestTrackMatchesRequestRejectsDurationMismatchAcrossReleases(t *testing.T)
|
||||
|
||||
func TestTitlesMatch_SeparatorVariants(t *testing.T) {
|
||||
if !titlesMatch("Doctor / Cops", "Doctor _ Cops") {
|
||||
t.Fatal("expected tidal titlesMatch to accept / vs _ variant")
|
||||
t.Fatal("expected titlesMatch to accept / vs _ variant")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,14 +47,14 @@ class RunnerTests: XCTestCase {
|
||||
|
||||
func testParsesOAuthCallback() {
|
||||
let route = ExtensionCallbackParser.parse(
|
||||
URL(string: "spotiflac://callback?code=auth-code&state=spotify-web")!
|
||||
URL(string: "spotiflac://callback?code=auth-code&state=metadata-provider")!
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
route,
|
||||
ExtensionCallbackRoute(
|
||||
code: "auth-code",
|
||||
state: "spotify-web",
|
||||
state: "metadata-provider",
|
||||
isSessionGrant: false
|
||||
)
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ DownloadHistoryItem _historyItem({
|
||||
filePath: filePath,
|
||||
service: 'test',
|
||||
downloadedAt: downloadedAt,
|
||||
spotifyId: 'spotify:track:same',
|
||||
spotifyId: 'source-track-same',
|
||||
isrc: 'SAMEISRC',
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ void main() {
|
||||
totalCount: 2,
|
||||
);
|
||||
|
||||
expect(state.getBySpotifyId('spotify:track:same')?.id, 'newest');
|
||||
expect(state.getBySpotifyId('source-track-same')?.id, 'newest');
|
||||
expect(state.getByIsrc('SAMEISRC')?.id, 'newest');
|
||||
expect(
|
||||
state.findByTrackAndArtist('Same Song', 'Same Artist')?.id,
|
||||
|
||||
@@ -8,43 +8,43 @@ void main() {
|
||||
test('failed challenge attempts do not consume the granted retry', () {
|
||||
final guard = DownloadVerificationRetryGuard();
|
||||
|
||||
guard.recordVerificationResult('item-1', 'tidal-web', granted: false);
|
||||
guard.recordVerificationResult('item-1', 'provider-a', granted: false);
|
||||
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isFalse);
|
||||
});
|
||||
|
||||
test('completed grants allow only one automatic retry per service', () {
|
||||
final guard = DownloadVerificationRetryGuard();
|
||||
|
||||
guard.recordVerificationResult('item-1', ' TIDAL-WEB ', granted: true);
|
||||
guard.recordVerificationResult('item-1', ' PROVIDER-A ', granted: true);
|
||||
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isTrue);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'qobuz-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-2', 'tidal-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isTrue);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'provider-b'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-2', 'provider-a'), isFalse);
|
||||
});
|
||||
|
||||
test('manual retry clears every service marker for an item', () {
|
||||
final guard = DownloadVerificationRetryGuard()
|
||||
..recordVerificationResult('item-1', 'tidal-web', granted: true)
|
||||
..recordVerificationResult('item-1', 'qobuz-web', granted: true)
|
||||
..recordVerificationResult('item-2', 'tidal-web', granted: true);
|
||||
..recordVerificationResult('item-1', 'provider-a', granted: true)
|
||||
..recordVerificationResult('item-1', 'provider-b', granted: true)
|
||||
..recordVerificationResult('item-2', 'provider-a', granted: true);
|
||||
|
||||
guard.clearItem('item-1');
|
||||
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'qobuz-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-2', 'tidal-web'), isTrue);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-1', 'provider-b'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('item-2', 'provider-a'), isTrue);
|
||||
});
|
||||
|
||||
test('queue cleanup retains markers only for remaining items', () {
|
||||
final guard = DownloadVerificationRetryGuard()
|
||||
..recordVerificationResult('removed', 'tidal-web', granted: true)
|
||||
..recordVerificationResult('remaining', 'tidal-web', granted: true);
|
||||
..recordVerificationResult('removed', 'provider-a', granted: true)
|
||||
..recordVerificationResult('remaining', 'provider-a', granted: true);
|
||||
|
||||
guard.retainItems({'remaining'});
|
||||
|
||||
expect(guard.hasRetriedAfterGrant('removed', 'tidal-web'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('remaining', 'tidal-web'), isTrue);
|
||||
expect(guard.hasRetriedAfterGrant('removed', 'provider-a'), isFalse);
|
||||
expect(guard.hasRetriedAfterGrant('remaining', 'provider-a'), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ void main() {
|
||||
|
||||
final result = coordinator.waitForGrant(
|
||||
itemId: 'item-1',
|
||||
service: 'tidal-web',
|
||||
service: 'provider-a',
|
||||
startFlow: (cancellationSignal) async {
|
||||
flowStarted.complete();
|
||||
await cancellationSignal;
|
||||
@@ -81,7 +81,7 @@ void main() {
|
||||
|
||||
final first = coordinator.waitForGrant(
|
||||
itemId: 'item-1',
|
||||
service: 'tidal-web',
|
||||
service: 'provider-a',
|
||||
startFlow: (cancellationSignal) async {
|
||||
starts++;
|
||||
await cancellationSignal;
|
||||
@@ -93,7 +93,7 @@ void main() {
|
||||
|
||||
final second = coordinator.waitForGrant(
|
||||
itemId: 'item-2',
|
||||
service: 'tidal-web',
|
||||
service: 'provider-a',
|
||||
startFlow: (_) async {
|
||||
starts++;
|
||||
return true;
|
||||
@@ -122,12 +122,12 @@ void main() {
|
||||
|
||||
final first = coordinator.waitForGrant(
|
||||
itemId: 'item-1',
|
||||
service: ' TIDAL-WEB ',
|
||||
service: ' PROVIDER-A ',
|
||||
startFlow: startFlow,
|
||||
);
|
||||
final second = coordinator.waitForGrant(
|
||||
itemId: 'item-2',
|
||||
service: 'tidal-web',
|
||||
service: 'provider-a',
|
||||
startFlow: startFlow,
|
||||
);
|
||||
|
||||
|
||||
@@ -680,7 +680,7 @@ void main() {
|
||||
|
||||
test('round-trips json with service availability', () {
|
||||
final track = Track.fromJson({
|
||||
'id': 'spotify:track:1',
|
||||
'id': 'source-track-1',
|
||||
'name': 'Song',
|
||||
'artistName': 'Artist',
|
||||
'albumName': 'Album',
|
||||
@@ -691,7 +691,7 @@ void main() {
|
||||
expect(track.availability?.tidal, isTrue);
|
||||
expect(track.availability?.qobuz, isFalse);
|
||||
expect(track.availability?.deezerId, '31337');
|
||||
expect(track.toJson()['id'], 'spotify:track:1');
|
||||
expect(track.toJson()['id'], 'source-track-1');
|
||||
expect(track.availability!.toJson()['deezer'], isTrue);
|
||||
});
|
||||
});
|
||||
@@ -710,7 +710,7 @@ void main() {
|
||||
final item = DownloadItem(
|
||||
id: 'download-1',
|
||||
track: sampleTrack(),
|
||||
service: 'tidal',
|
||||
service: 'provider-a',
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
@@ -745,7 +745,7 @@ void main() {
|
||||
final base = DownloadItem(
|
||||
id: 'download-1',
|
||||
track: sampleTrack(),
|
||||
service: 'qobuz',
|
||||
service: 'provider-b',
|
||||
createdAt: DateTime.utc(2026),
|
||||
filePath: '/music/stale.flac',
|
||||
error: 'raw backend failure',
|
||||
@@ -790,7 +790,7 @@ void main() {
|
||||
'albumName': 'Album',
|
||||
'duration': 1000,
|
||||
},
|
||||
'service': 'deezer',
|
||||
'service': 'provider-a',
|
||||
'status': 'failed',
|
||||
'errorType': 'network',
|
||||
'createdAt': '2026-05-04T10:00:00.000Z',
|
||||
@@ -811,7 +811,7 @@ void main() {
|
||||
final item = DownloadItem(
|
||||
id: 'download-active',
|
||||
track: sampleTrack(),
|
||||
service: 'tidal',
|
||||
service: 'provider-a',
|
||||
createdAt: DateTime.utc(2026),
|
||||
status: DownloadStatus.finalizing,
|
||||
progress: 0.97,
|
||||
@@ -937,7 +937,7 @@ void main() {
|
||||
);
|
||||
|
||||
final updated = settings.copyWith(
|
||||
defaultService: 'tidal',
|
||||
defaultService: 'provider-a',
|
||||
embedReplayGain: true,
|
||||
embeddedCoverMaxDimension: 1000,
|
||||
lyricsProviders: ['apple_music'],
|
||||
@@ -950,7 +950,7 @@ void main() {
|
||||
clearHomeFeedProvider: true,
|
||||
);
|
||||
|
||||
expect(updated.defaultService, 'tidal');
|
||||
expect(updated.defaultService, 'provider-a');
|
||||
expect(updated.embedReplayGain, isTrue);
|
||||
expect(updated.embeddedCoverMaxDimension, 1000);
|
||||
expect(updated.lyricsProviders, ['apple_music']);
|
||||
@@ -969,7 +969,7 @@ void main() {
|
||||
|
||||
test('round-trips json including recently added settings', () {
|
||||
const settings = AppSettings(
|
||||
defaultService: 'qobuz',
|
||||
defaultService: 'provider-b',
|
||||
storageMode: 'saf',
|
||||
downloadTreeUri: 'content://tree/music',
|
||||
downloadFallbackExtensionIds: ['ext.a', 'ext.b'],
|
||||
@@ -996,7 +996,7 @@ void main() {
|
||||
|
||||
final decoded = AppSettings.fromJson(settings.toJson());
|
||||
|
||||
expect(decoded.defaultService, 'qobuz');
|
||||
expect(decoded.defaultService, 'provider-b');
|
||||
expect(decoded.storageMode, 'saf');
|
||||
expect(decoded.downloadTreeUri, 'content://tree/music');
|
||||
expect(decoded.downloadFallbackExtensionIds, ['ext.a', 'ext.b']);
|
||||
@@ -1053,10 +1053,10 @@ void main() {
|
||||
test('serializes all backend field names', () {
|
||||
const payload = DownloadRequestPayload(
|
||||
isrc: 'ISRC123',
|
||||
service: 'tidal',
|
||||
downloadProvider: 'tidal-web',
|
||||
service: 'source-provider',
|
||||
downloadProvider: 'download-provider',
|
||||
providerTrackId: '123456789',
|
||||
spotifyId: 'spotify:track:1',
|
||||
spotifyId: 'source-track-1',
|
||||
trackName: 'Song',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
@@ -1092,9 +1092,9 @@ void main() {
|
||||
explicit: true,
|
||||
albumType: 'compilation',
|
||||
upc: '0012345678901',
|
||||
tidalId: 'tidal-1',
|
||||
qobuzId: 'qobuz-1',
|
||||
deezerId: 'deezer-1',
|
||||
tidalId: 'alternate-id-a',
|
||||
qobuzId: 'alternate-id-b',
|
||||
deezerId: 'alternate-id-c',
|
||||
lyricsMode: 'sidecar',
|
||||
useExtensions: true,
|
||||
useFallback: true,
|
||||
@@ -1114,10 +1114,10 @@ void main() {
|
||||
expect(payload.toJson(), {
|
||||
'contract_version': DownloadRequestPayload.nativeWorkerContractVersion,
|
||||
'isrc': 'ISRC123',
|
||||
'service': 'tidal',
|
||||
'download_provider': 'tidal-web',
|
||||
'service': 'source-provider',
|
||||
'download_provider': 'download-provider',
|
||||
'provider_track_id': '123456789',
|
||||
'spotify_id': 'spotify:track:1',
|
||||
'spotify_id': 'source-track-1',
|
||||
'track_name': 'Song',
|
||||
'artist_name': 'Artist',
|
||||
'album_name': 'Album',
|
||||
@@ -1153,9 +1153,9 @@ void main() {
|
||||
'explicit': true,
|
||||
'album_type': 'compilation',
|
||||
'upc': '0012345678901',
|
||||
'tidal_id': 'tidal-1',
|
||||
'qobuz_id': 'qobuz-1',
|
||||
'deezer_id': 'deezer-1',
|
||||
'tidal_id': 'alternate-id-a',
|
||||
'qobuz_id': 'alternate-id-b',
|
||||
'deezer_id': 'alternate-id-c',
|
||||
'lyrics_mode': 'sidecar',
|
||||
'use_extensions': true,
|
||||
'use_fallback': true,
|
||||
@@ -1283,7 +1283,7 @@ void main() {
|
||||
'albumType': 'album',
|
||||
'explicit': true,
|
||||
'upc': '4006381333931',
|
||||
'comment': 'https://listen.tidal.com/album/1',
|
||||
'comment': 'https://media.example/album/1',
|
||||
'format': 'flac',
|
||||
});
|
||||
|
||||
@@ -1302,7 +1302,7 @@ void main() {
|
||||
expect(normalized['album_type'], 'album');
|
||||
expect(normalized['explicit'], isTrue);
|
||||
expect(normalized['upc'], '4006381333931');
|
||||
expect(normalized['comment'], 'https://listen.tidal.com/album/1');
|
||||
expect(normalized['comment'], 'https://media.example/album/1');
|
||||
expect(normalized['audio_codec'], 'flac');
|
||||
});
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ void main() {
|
||||
addTearDown(subscription.cancel);
|
||||
|
||||
final first = PlatformBridge.completeExtensionSessionGrant(
|
||||
' qobuz-web ',
|
||||
' provider-a ',
|
||||
'grant-value',
|
||||
);
|
||||
final second = PlatformBridge.completeExtensionSessionGrant(
|
||||
'QOBUZ-WEB',
|
||||
'PROVIDER-A',
|
||||
'grant-value',
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
|
||||
expect(callCount, 1);
|
||||
expect(events, hasLength(1));
|
||||
expect(events.single.extensionId, 'qobuz-web');
|
||||
expect(events.single.extensionId, 'provider-a');
|
||||
expect(events.single.success, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ void main() {
|
||||
test('prefers a known provider id over guessing from the resource id', () {
|
||||
expect(
|
||||
resolvePreferredMetadataProviderId(
|
||||
'apple-music-ext',
|
||||
'sample-provider',
|
||||
'37i9dQZF1DXcBWIGoYBM5M',
|
||||
),
|
||||
'apple-music-ext',
|
||||
'sample-provider',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ void main() {
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: r'Z:\missing\track.flac',
|
||||
service: 'tidal-web',
|
||||
service: 'provider-a',
|
||||
downloadedAt: DateTime(2026),
|
||||
duration: 250,
|
||||
bitDepth: 16,
|
||||
@@ -45,7 +45,7 @@ void main() {
|
||||
expect(headerMeta, findsOneWidget);
|
||||
expect(find.byType(ExplicitBadge), findsNWidgets(2));
|
||||
expect(find.text('Explicit'), findsNothing);
|
||||
for (final label in const ['16-bit/44.1kHz', '4:10', 'Tidal-web']) {
|
||||
for (final label in const ['16-bit/44.1kHz', '4:10', 'Provider-a']) {
|
||||
final text = tester.widget<Text>(
|
||||
find.descendant(of: headerMeta, matching: find.text(label)),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user