fix(protocols): hide non-playable provider tracks

This commit is contained in:
joshpatra committed 2026-08-19 14:15:03 -04:00
1 parent b9c962e036
commit e42f38deaa
2 files changed
+99 -3

No files matched your search

@@ -160,6 +160,10 @@ public sealed class ProtocolProviderStreamingGatewayTests
request.ProviderPriority.SequenceEqual(new[] { "deezer" }))))
.ReturnsAsync((ProviderRouteRequest request) =>
EmptyPlan<IProviderMetadataCapability>(request));
router.Setup(item => item.PlanAsync<IProviderStreamingCapability>(
It.IsAny<ProviderRouteRequest>()))
.ReturnsAsync((ProviderRouteRequest request) =>
EmptyPlan<IProviderStreamingCapability>(request));
var gateway = new ProtocolProviderGateway(
router.Object,
registry,
@@ -174,6 +178,68 @@ public sealed class ProtocolProviderStreamingGatewayTests
router.VerifyAll();
}
[Fact]
public async Task MetadataSearch_DoesNotPublishSongsWithoutAPlayableRoute()
{
var metadata = new Mock<IProviderMetadataCapability>(MockBehavior.Strict);
metadata.SetupGet(item => item.ProviderId).Returns("metadata-only");
metadata.SetupGet(item => item.Capability).Returns(ProviderCapabilityKind.Metadata);
metadata.Setup(item => item.SearchTracksAsync(
It.IsAny<ProviderExecutionContext>(),
It.IsAny<ProviderMetadataSearchRequest>()))
.ReturnsAsync(ProviderOutcome<ProviderPage<ProviderTrackMetadata>>.Success(new(
"metadata-only",
[new ProviderTrackMetadata(
new("metadata-only", ProviderResourceKind.Track, "track-1"),
"Track",
[new("Artist")])])));
metadata.Setup(item => item.SearchAlbumsAsync(
It.IsAny<ProviderExecutionContext>(),
It.IsAny<ProviderMetadataSearchRequest>()))
.ReturnsAsync(ProviderOutcome<ProviderPage<ProviderAlbumMetadata>>.Success(
new("metadata-only", [])));
metadata.Setup(item => item.SearchArtistsAsync(
It.IsAny<ProviderExecutionContext>(),
It.IsAny<ProviderMetadataSearchRequest>()))
.ReturnsAsync(ProviderOutcome<ProviderPage<ProviderArtistMetadata>>.Success(
new("metadata-only", [])));
var registry = new ProviderRegistry([
new ProviderRegistration(
new ProviderDescriptor(
"metadata-only",
"Metadata only",
"Fixture provider",
ProviderOrigin.BuiltIn,
"1",
"1",
[new ProviderCapabilityDescriptor(
ProviderCapabilityKind.Metadata,
ProviderCapabilitySupportState.Supported,
ProviderAccountRequirement.None,
"1",
["searchTracks", "getTrack"])],
new ProviderPermissionDescriptor()),
[metadata.Object])
]);
var router = new Mock<IProviderRouter>(MockBehavior.Strict);
router.Setup(item => item.PlanAsync<IProviderMetadataCapability>(
It.IsAny<ProviderRouteRequest>()))
.ReturnsAsync((ProviderRouteRequest request) =>
MetadataPlan(request, registry, metadata.Object));
var gateway = new ProtocolProviderGateway(
router.Object,
registry,
Mock.Of<IProviderRouteAccountResolver>(),
Mock.Of<IMusicMetadataService>(),
new HttpClientFactory());
var result = await gateway.SearchAsync(Context(), "Track", 10, 0, 0);
Assert.Empty(result.Songs);
metadata.VerifyAll();
router.VerifyAll();
}
[Fact]
public async Task MetadataRelationships_UseOnlyUniqueExactIdsFromTheSameProvider()
{
@@ -231,7 +297,14 @@ public sealed class ProtocolProviderStreamingGatewayTests
It.Is<ProviderTrackLookupRequest>(request => request.Id.Value == "track-1")))
.ReturnsAsync(ProviderOutcome<ProviderTrackMetadata>.Success(track));
var registry = MetadataRegistry(capability.Object);
var streaming = new Mock<IProviderStreamingCapability>(MockBehavior.Strict);
streaming.SetupGet(item => item.ProviderId).Returns(providerId);
streaming.SetupGet(item => item.Capability).Returns(ProviderCapabilityKind.Streaming);
var router = new Mock<IProviderRouter>(MockBehavior.Strict);
router.Setup(item => item.PlanAsync<IProviderStreamingCapability>(
It.IsAny<ProviderRouteRequest>()))
.ReturnsAsync((ProviderRouteRequest request) =>
Plan(request, registry, streaming.Object));
router.Setup(item => item.PlanAsync<IProviderMetadataCapability>(
It.IsAny<ProviderRouteRequest>()))
.ReturnsAsync((ProviderRouteRequest request) =>
@@ -680,7 +753,8 @@ public sealed class ProtocolProviderStreamingGatewayTests
return new ProviderRouteCandidate<IProviderStreamingCapability>(
index,
provider,
provider.Capabilities.Single(),
provider.Capabilities.Single(item =>
item.Capability == ProviderCapabilityKind.Streaming),
capability,
new ProviderExecutionContext(
request.Actor,
@@ -125,7 +125,11 @@ public sealed class ProtocolProviderGateway(
query, songLimit, albumLimit, artistLimit, protocol.CancellationToken);
return new SearchResult
{
Songs = publicLegacy.Songs.Where(item => IsRequestedPublicProvider(item.ExternalProvider)).Take(songLimit).ToList(),
Songs = publicLegacy.Songs
.Where(item => IsRequestedPublicProvider(item.ExternalProvider) &&
IsPublicStreamingProvider(item.ExternalProvider))
.Take(songLimit)
.ToList(),
Albums = publicLegacy.Albums.Where(item => IsRequestedPublicProvider(item.ExternalProvider)).Take(albumLimit).ToList(),
Artists = publicLegacy.Artists.Where(item => IsRequestedPublicProvider(item.ExternalProvider)).Take(artistLimit).ToList()
};
@@ -136,6 +140,13 @@ public sealed class ProtocolProviderGateway(
NormalizeProvider(itemProvider) == requestedProviderId);
}
var actor = protocol.RequireActor();
var playableProviders = songLimit > 0
? (await ResolvePlayableProviderOrderAsync(
protocol,
actor,
ResolveProviderOrder(ProviderCapabilityKind.Streaming)))
.ToHashSet(StringComparer.Ordinal)
: [];
var fetchLimit = Math.Clamp(Math.Max(songLimit, Math.Max(albumLimit, artistLimit)), 1, 200);
var providerOrder = ResolveProviderOrder(ProviderCapabilityKind.Metadata)
.Where(item => requestedProviderId == null || item == requestedProviderId)
@@ -163,6 +174,7 @@ public sealed class ProtocolProviderGateway(
await Task.WhenAll(songsTask, albumsTask, artistsTask);
return new
{
ProviderId = NormalizeProvider(candidate.Provider.Id),
SongsResult = await songsTask,
AlbumsResult = await albumsTask,
ArtistsResult = await artistsTask
@@ -184,7 +196,7 @@ public sealed class ProtocolProviderGateway(
var artists = outcome.ArtistsResult.IsSuccess
? outcome.ArtistsResult.RequireValue().Items
: [];
if (outcome.SongsResult.IsSuccess)
if (playableProviders.Contains(outcome.ProviderId) && outcome.SongsResult.IsSuccess)
{
routed.Songs.AddRange(outcome.SongsResult.RequireValue().Items
.Select(item => EnrichRelationships(Map(item), albums, artists)));
@@ -286,6 +298,7 @@ public sealed class ProtocolProviderGateway(
ProviderActorContext actor,
IReadOnlyList<string> configuredProviderOrder)
{
if (configuredProviderOrder.Count == 0) return [];
var streaming = await router.PlanAsync<IProviderStreamingCapability>(Request(
protocol,
actor,
@@ -877,6 +890,15 @@ public sealed class ProtocolProviderGateway(
.AccountRequirement == ProviderAccountRequirement.None);
}
private bool IsPublicStreamingProvider(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId)) return false;
return registry.FindByCapability(ProviderCapabilityKind.Streaming, includeNonOperational: true)
.Any(descriptor => descriptor.Id.Equals(providerId, StringComparison.Ordinal) &&
descriptor.Capabilities.Single(item => item.Capability == ProviderCapabilityKind.Streaming)
.AccountRequirement == ProviderAccountRequirement.None);
}
private IReadOnlyList<string> ResolveProviderOrder(ProviderCapabilityKind capability)
{
var (settingKey, environmentKey, fallback) = capability switch