mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 22:03:14 -04:00
520 lines
22 KiB
C#
520 lines
22 KiB
C#
using allstarr.Core.Capabilities;
|
|
using allstarr.Core.Providers.Deezer;
|
|
using allstarr.Core.Providers.Qobuz;
|
|
using allstarr.Core.Matching;
|
|
using allstarr.Core.Routing;
|
|
using allstarr.Core.Storage;
|
|
using allstarr.Models.Domain;
|
|
using allstarr.Models.Subsonic;
|
|
using allstarr.Services;
|
|
using Moq;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public sealed class DeezerMetadataCapabilityAdapterTests
|
|
{
|
|
[Fact]
|
|
public async Task SearchTracks_MapsLegacyResultsToTypedProviderIds()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.SearchSongsAsync(
|
|
"daft punk",
|
|
2,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new Song
|
|
{
|
|
Id = "ext-deezer-3135556",
|
|
ExternalId = "3135556",
|
|
ExternalProvider = "deezer",
|
|
Title = "Harder Better Faster Stronger",
|
|
Artist = "Daft Punk",
|
|
Artists = ["Daft Punk"],
|
|
ArtistIds = ["27"],
|
|
Album = "Discovery",
|
|
AlbumId = "302127",
|
|
Duration = 224,
|
|
Isrc = "GBDUW0000059",
|
|
CoverArtUrlLarge = "https://cdn.example.invalid/cover.jpg"
|
|
}
|
|
]);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.SearchTracksAsync(
|
|
Context(),
|
|
new ProviderMetadataSearchRequest(
|
|
"daft punk",
|
|
new ProviderPageRequest(limit: 2)));
|
|
|
|
Assert.True(outcome.IsSuccess);
|
|
var page = outcome.RequireValue();
|
|
Assert.Equal("deezer", page.ProviderId);
|
|
var track = Assert.Single(page.Items);
|
|
Assert.Equal("3135556", track.Id.Value);
|
|
Assert.Equal(ProviderResourceKind.Track, track.Id.ResourceKind);
|
|
Assert.Equal("27", Assert.Single(track.Artists).ArtistId!.Value);
|
|
Assert.Equal("302127", track.AlbumId!.Value);
|
|
Assert.Equal(TimeSpan.FromSeconds(224), track.Duration);
|
|
Assert.Equal("https://cdn.example.invalid/cover.jpg", track.Artwork!.PublicUri!.AbsoluteUri);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchTracks_StripsTypedCompatibilityPrefixFromAlbumId()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.SearchSongsAsync("track", 1, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new Song
|
|
{
|
|
Id = "ext-deezer-song-123",
|
|
ExternalId = "123",
|
|
ExternalProvider = "deezer",
|
|
Title = "Track",
|
|
Artist = "Artist",
|
|
Artists = ["Artist"],
|
|
ArtistIds = ["789"],
|
|
Album = "Album",
|
|
AlbumId = "ext-deezer-album-456",
|
|
Duration = 180
|
|
}
|
|
]);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.SearchTracksAsync(
|
|
Context(),
|
|
new ProviderMetadataSearchRequest("track", new ProviderPageRequest(1)));
|
|
|
|
Assert.Equal("456", Assert.Single(outcome.RequireValue().Items).AlbumId!.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CollectionMapping_SkipsInvalidProviderRecordsInsteadOfFailingThePage()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.GetArtistAlbumsAsync("deezer", "27", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([
|
|
new Album { Id = "1", ExternalId = "1", Title = "Valid", Artist = "Artist" },
|
|
new Album { Id = "2", ExternalId = "2", Title = "Invalid", Artist = new string('x', 301) }
|
|
]);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.GetArtistAlbumsAsync(
|
|
Context(),
|
|
new ProviderArtistItemsRequest(
|
|
new ProviderExternalResourceId("deezer", ProviderResourceKind.Artist, "27"),
|
|
new ProviderPageRequest(10)));
|
|
|
|
Assert.Equal("1", Assert.Single(outcome.RequireValue().Items).Id.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CursorThatLegacyProviderCannotHonor_ReturnsTypedNotSupported()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.SearchTracksAsync(
|
|
Context(),
|
|
new ProviderMetadataSearchRequest(
|
|
"query",
|
|
new ProviderPageRequest(cursor: "next-page")));
|
|
|
|
Assert.False(outcome.IsSuccess);
|
|
Assert.Equal(ProviderErrorKind.NotSupported, outcome.Error!.Kind);
|
|
legacy.VerifyNoOtherCalls();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingLookup_ReturnsTypedNotFound()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.GetSongAsync(
|
|
"deezer",
|
|
"missing",
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((Song?)null);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.GetTrackAsync(
|
|
Context(),
|
|
new ProviderTrackLookupRequest(new ProviderExternalResourceId(
|
|
"deezer",
|
|
ProviderResourceKind.Track,
|
|
"missing")));
|
|
|
|
Assert.False(outcome.IsSuccess);
|
|
Assert.Equal(ProviderErrorKind.NotFound, outcome.Error!.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProviderException_CannotEscapeThroughOutcomeText()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.FindSongByIsrcAsync(
|
|
"GBDUW0000059",
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new InvalidOperationException(
|
|
"Authorization: Bearer secret-token https://signed.example.invalid/media"));
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.LookupByIsrcAsync(
|
|
Context(),
|
|
new ProviderIsrcLookupRequest("GBDUW0000059"));
|
|
|
|
Assert.False(outcome.IsSuccess);
|
|
Assert.Equal(ProviderErrorKind.TransientFailure, outcome.Error!.Kind);
|
|
Assert.DoesNotContain("secret-token", outcome.Error.SafeMessage, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("signed.example", outcome.Error.SafeMessage, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancellationSwallowedByConcreteServiceStillReturnsCanceled()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.SearchSongsAsync("query", 1, cancellation.Token))
|
|
.ReturnsAsync(() =>
|
|
{
|
|
cancellation.Cancel();
|
|
return [];
|
|
});
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.SearchTracksAsync(
|
|
Context(cancellation.Token),
|
|
new ProviderMetadataSearchRequest("query", new ProviderPageRequest(1)));
|
|
|
|
Assert.Equal(ProviderErrorKind.Canceled, outcome.Error!.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltInRegistration_IsAtomicAndResolvesTypedImplementation()
|
|
{
|
|
var adapter = new DeezerMetadataCapabilityAdapter(
|
|
new Mock<IConcreteMetadataService>(MockBehavior.Strict).Object);
|
|
var registry = new ProviderRegistry(
|
|
[DeezerMetadataCapabilityAdapter.CreateRegistration(adapter, Playlist(), Download(), Streaming())]);
|
|
|
|
var descriptor = registry.GetRequired("deezer");
|
|
var resolved = registry.GetRequiredCapability<IProviderMetadataCapability>(
|
|
"deezer",
|
|
ProviderCapabilityKind.Metadata);
|
|
|
|
Assert.Equal(ProviderOrigin.BuiltIn, descriptor.Origin);
|
|
Assert.Same(adapter, resolved);
|
|
Assert.Single(registry.FindByCapability(ProviderCapabilityKind.Metadata));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProviderRouter_RoutesAndExecutesTheRealBuiltInAdapter()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.SearchSongsAsync(
|
|
"route me",
|
|
1,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new Song
|
|
{
|
|
Id = "ext-deezer-route-result",
|
|
ExternalId = "route-result",
|
|
ExternalProvider = "deezer",
|
|
Title = "Routed result",
|
|
Artist = "Artist",
|
|
Artists = ["Artist"],
|
|
ArtistIds = ["artist-result"],
|
|
Album = "Album",
|
|
AlbumId = "album-result",
|
|
Duration = 180,
|
|
Isrc = "USRC17607839"
|
|
}
|
|
]);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
var registry = new ProviderRegistry(
|
|
[DeezerMetadataCapabilityAdapter.CreateRegistration(adapter, Playlist(), Download(), Streaming())]);
|
|
var health = new Mock<IProviderRouteHealthSource>(MockBehavior.Strict);
|
|
health.Setup(item => item.Get("deezer", null, ProviderCapabilityKind.Metadata))
|
|
.Returns(new ProviderRouteHealthSnapshot(
|
|
ProviderRouteHealthState.Unknown,
|
|
CircuitOpen: false));
|
|
var router = new ProviderRouter(
|
|
registry,
|
|
new Mock<IProviderRouteAccountResolver>(MockBehavior.Strict).Object,
|
|
health.Object,
|
|
new Mock<IProviderRouteSidecarSource>(MockBehavior.Strict).Object,
|
|
new Mock<ITrackIdentityService>(MockBehavior.Strict).Object);
|
|
var context = Context();
|
|
var route = new ProviderRouteRequest(
|
|
ProviderCapabilityKind.Metadata,
|
|
context.Actor,
|
|
context.Policy,
|
|
"route-deezer-metadata",
|
|
"route-deezer-fixture",
|
|
DateTimeOffset.UtcNow.AddMinutes(1),
|
|
providerPriority: ["deezer"]);
|
|
|
|
var plan = await router.PlanAsync<IProviderMetadataCapability>(route);
|
|
var candidate = Assert.Single(plan.Candidates);
|
|
var outcome = await candidate.Implementation.SearchTracksAsync(
|
|
candidate.Context,
|
|
new ProviderMetadataSearchRequest("route me", new ProviderPageRequest(1)));
|
|
|
|
Assert.Same(adapter, candidate.Implementation);
|
|
Assert.Equal("deezer", plan.Decision.SelectedProviderId);
|
|
Assert.Equal("route-result", Assert.Single(outcome.RequireValue().Items).Id.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ArtistAlbumsAndTracksUseTypedPagedResults()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.GetArtistAlbumsAsync("deezer", "artist-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([
|
|
new Album { ExternalId = "album-1", Title = "First", Artist = "Artist" },
|
|
new Album { ExternalId = "album-2", Title = "Second", Artist = "Artist" },
|
|
new Album { ExternalId = "album-3", Title = "Third", Artist = "Artist" }
|
|
]);
|
|
legacy.Setup(item => item.GetArtistTracksAsync("deezer", "artist-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([
|
|
new Song { ExternalId = "track-1", Title = "First", Artist = "Artist", Artists = ["Artist"], ArtistId = "artist-1" },
|
|
new Song { ExternalId = "track-2", Title = "Second", Artist = "Artist", Artists = ["Artist"] }
|
|
]);
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
var artist = new ProviderExternalResourceId("deezer", ProviderResourceKind.Artist, "artist-1");
|
|
|
|
var albums = (await adapter.GetArtistAlbumsAsync(
|
|
Context(), new(artist, new(limit: 2)))).RequireValue();
|
|
var remaining = (await adapter.GetArtistAlbumsAsync(
|
|
Context(), new(artist, new(limit: 2, cursor: albums.NextCursor)))).RequireValue();
|
|
var tracks = (await adapter.GetArtistTracksAsync(
|
|
Context(), new(artist, new(limit: 2)))).RequireValue();
|
|
|
|
Assert.Equal(["album-1", "album-2"], albums.Items.Select(item => item.Id.Value));
|
|
Assert.Equal("2", albums.NextCursor);
|
|
Assert.True(albums.IsPartial);
|
|
Assert.Equal("album-3", Assert.Single(remaining.Items).Id.Value);
|
|
Assert.Null(remaining.NextCursor);
|
|
Assert.Equal(["track-1", "track-2"], tracks.Items.Select(item => item.Id.Value));
|
|
Assert.Equal("artist-1", tracks.Items[0].Artists[0].ArtistId!.Value);
|
|
legacy.VerifyAll();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AlbumLookup_PreservesProtocolVisibleAlbumAndTrackFacts()
|
|
{
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.GetAlbumAsync("deezer", "album-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new Album
|
|
{
|
|
ExternalId = "album-1",
|
|
Title = "Album",
|
|
Artist = "Artist",
|
|
ArtistId = "artist-1",
|
|
Year = 2024,
|
|
Genre = "Electronic",
|
|
SongCount = 1,
|
|
Songs =
|
|
[
|
|
new Song
|
|
{
|
|
ExternalId = "track-1",
|
|
Title = "Track",
|
|
Artist = "Artist",
|
|
Artists = ["Artist"],
|
|
ArtistIds = ["artist-1"],
|
|
Album = "Album",
|
|
AlbumId = "album-1",
|
|
Track = 2,
|
|
DiscNumber = 1,
|
|
TotalTracks = 10,
|
|
Year = 2024,
|
|
Genre = "Electronic",
|
|
Bpm = 120,
|
|
ReleaseDate = "2024-02-03",
|
|
AlbumArtist = "Album Artist",
|
|
Composer = "Composer",
|
|
Label = "Label",
|
|
Copyright = "Copyright",
|
|
Contributors = ["Contributor"],
|
|
ExplicitContentLyrics = 3
|
|
}
|
|
]
|
|
});
|
|
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
|
|
|
var outcome = await adapter.GetAlbumAsync(
|
|
Context(),
|
|
new ProviderAlbumLookupRequest(new("deezer", ProviderResourceKind.Album, "album-1")));
|
|
|
|
var album = outcome.RequireValue();
|
|
Assert.Equal(2024, album.Year);
|
|
Assert.Equal("Electronic", album.Genre);
|
|
var track = Assert.Single(album.Tracks);
|
|
Assert.Equal(2, track.TrackNumber);
|
|
Assert.Equal(1, track.DiscNumber);
|
|
Assert.Equal(10, track.TotalTracks);
|
|
Assert.Equal(120, track.Bpm);
|
|
Assert.Equal("2024-02-03", track.ReleaseDate);
|
|
Assert.Equal("Album Artist", track.AlbumArtist);
|
|
Assert.Equal("Composer", track.Composer);
|
|
Assert.Equal("Label", track.Label);
|
|
Assert.Equal("Copyright", track.Copyright);
|
|
Assert.Equal(["Contributor"], track.Contributors);
|
|
Assert.Equal(3, track.ExplicitContentLyrics);
|
|
legacy.VerifyAll();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("deezer")]
|
|
[InlineData("qobuz")]
|
|
public async Task PublicPlaylistAdapters_PreserveSummaryAndOrderedTrackFacts(string providerId)
|
|
{
|
|
var created = new DateTime(2023, 4, 5, 0, 0, 0, DateTimeKind.Utc);
|
|
var playlist = new ExternalPlaylist
|
|
{
|
|
ExternalId = "playlist-1",
|
|
Name = "Road",
|
|
Description = "Drive",
|
|
CuratorName = "Curator",
|
|
Provider = providerId,
|
|
TrackCount = 2,
|
|
Duration = 420,
|
|
CoverUrl = "https://images.example.test/playlist.webp",
|
|
CreatedDate = created
|
|
};
|
|
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
|
legacy.Setup(item => item.SearchPlaylistsAsync("road", 5, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([playlist]);
|
|
legacy.Setup(item => item.GetPlaylistAsync(providerId, "playlist-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(playlist);
|
|
legacy.Setup(item => item.GetPlaylistTracksAsync(providerId, "playlist-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new Song
|
|
{
|
|
ExternalId = "track-1", Title = "First", Artist = "Artist", Artists = ["Artist"],
|
|
Track = 1, DiscNumber = 1, Year = 2023
|
|
},
|
|
new Song
|
|
{
|
|
ExternalId = "track-2", Title = "Second", Artist = "Artist", Artists = ["Artist"],
|
|
Track = 2, DiscNumber = 1, Year = 2023
|
|
}
|
|
]);
|
|
var (metadata, adapter) = PublicPlaylistAdapter(providerId, legacy.Object);
|
|
var context = PlaylistContext(providerId);
|
|
|
|
var search = (await adapter.SearchPlaylistsAsync(
|
|
context, new("road", new ProviderPageRequest(5)))).RequireValue();
|
|
var summary = Assert.Single(search.Items);
|
|
var read = (await adapter.GetPlaylistTracksAsync(
|
|
context,
|
|
new(new(providerId, ProviderResourceKind.Playlist, "playlist-1"),
|
|
new ProviderPageRequest(1), summary.SourceRevision))).RequireValue();
|
|
|
|
Assert.Equal("Curator", summary.Owner.DisplayName);
|
|
Assert.Equal(420, summary.DurationSeconds);
|
|
Assert.Equal(created, summary.CreatedDate);
|
|
Assert.Equal("https://images.example.test/playlist.webp", summary.Artwork!.PublicUri!.AbsoluteUri);
|
|
Assert.Equal("1", read.Tracks.NextCursor);
|
|
var first = Assert.Single(read.Tracks.Items);
|
|
Assert.Equal(0, first.Position);
|
|
Assert.Equal("track-1", first.TrackId.Value);
|
|
Assert.Equal(1, first.Metadata!.TrackNumber);
|
|
Assert.Equal(2023, first.Metadata.Year);
|
|
legacy.VerifyAll();
|
|
}
|
|
|
|
private static ProviderExecutionContext Context(CancellationToken cancellationToken = default)
|
|
{
|
|
var actor = new ProviderActorContext(
|
|
Guid.CreateVersion7(),
|
|
ProviderActorKind.User,
|
|
Guid.CreateVersion7(),
|
|
new ProviderBackendPrincipal("jellyfin", "fixture", "fixture-user"));
|
|
return new ProviderExecutionContext(
|
|
actor,
|
|
"deezer",
|
|
account: null,
|
|
library: null,
|
|
new ProviderExecutionPolicy(
|
|
new ProviderQualityPolicy(
|
|
ProviderAudioQuality.Any,
|
|
ProviderAudioQuality.HighResolution,
|
|
allowTranscode: true),
|
|
ProviderExplicitContentPolicy.Allow,
|
|
allowFallback: true,
|
|
allowSharedAccount: false,
|
|
allowManagedDownloads: false,
|
|
allowedProviderIds: ["deezer"]),
|
|
operationId: "metadata-search",
|
|
correlationId: "metadata-search-fixture",
|
|
deadline: DateTimeOffset.UtcNow.AddMinutes(1),
|
|
cancellationToken: cancellationToken);
|
|
}
|
|
|
|
private static ProviderExecutionContext PlaylistContext(string providerId)
|
|
{
|
|
var context = Context();
|
|
return new(
|
|
context.Actor,
|
|
providerId,
|
|
new ProviderAccountContext(
|
|
Guid.CreateVersion7(),
|
|
providerId,
|
|
ProviderAccountScope.User,
|
|
1,
|
|
tenantId: context.Actor.TenantId,
|
|
ownerUserId: context.Actor.EffectiveUserId),
|
|
context.Library,
|
|
new ProviderExecutionPolicy(
|
|
context.Policy.Quality,
|
|
context.Policy.ExplicitContent,
|
|
context.Policy.AllowFallback,
|
|
context.Policy.AllowSharedAccount,
|
|
context.Policy.AllowManagedDownloads,
|
|
[providerId]),
|
|
context.OperationId,
|
|
context.CorrelationId,
|
|
context.Deadline,
|
|
context.CancellationToken);
|
|
}
|
|
|
|
private static (IProviderMetadataCapability Metadata, IProviderPlaylistCapability Playlist)
|
|
PublicPlaylistAdapter(string providerId, IConcreteMetadataService legacy) => providerId switch
|
|
{
|
|
"deezer" => CreateDeezer(legacy),
|
|
"qobuz" => CreateQobuz(legacy),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(providerId), providerId, null)
|
|
};
|
|
|
|
private static (IProviderMetadataCapability, IProviderPlaylistCapability) CreateDeezer(
|
|
IConcreteMetadataService legacy)
|
|
{
|
|
var metadata = new DeezerMetadataCapabilityAdapter(legacy);
|
|
return (metadata, new DeezerPlaylistCapabilityAdapter(legacy, metadata));
|
|
}
|
|
|
|
private static (IProviderMetadataCapability, IProviderPlaylistCapability) CreateQobuz(
|
|
IConcreteMetadataService legacy)
|
|
{
|
|
var metadata = new QobuzMetadataCapabilityAdapter(legacy);
|
|
return (metadata, new QobuzPlaylistCapabilityAdapter(legacy, metadata));
|
|
}
|
|
|
|
private static IProviderDownloadCapability Download() =>
|
|
Mock.Of<IProviderDownloadCapability>(item =>
|
|
item.ProviderId == "deezer" && item.Capability == ProviderCapabilityKind.Download);
|
|
|
|
private static IProviderPlaylistCapability Playlist() =>
|
|
Mock.Of<IProviderPlaylistCapability>(item =>
|
|
item.ProviderId == "deezer" && item.Capability == ProviderCapabilityKind.Playlist);
|
|
|
|
private static IProviderStreamingCapability Streaming() =>
|
|
Mock.Of<IProviderStreamingCapability>(item =>
|
|
item.ProviderId == "deezer" && item.Capability == ProviderCapabilityKind.Streaming);
|
|
}
|