using System; using System.Collections.Generic; using System.Threading.Tasks; using SpotifyAPI.Web.Http; using System.Runtime.CompilerServices; using System.Threading; namespace SpotifyAPI.Web { public class SpotifyClient : ISpotifyClient { private readonly IAPIConnector _apiConnector; public SpotifyClient(IToken token) : this(SpotifyClientConfig.CreateDefault(token?.AccessToken ?? throw new ArgumentNullException(nameof(token)), token.TokenType)) { } public SpotifyClient(string token, string tokenType = "Bearer") : this(SpotifyClientConfig.CreateDefault(token, tokenType)) { } public SpotifyClient(SpotifyClientConfig config) { Ensure.ArgumentNotNull(config, nameof(config)); if (config.Authenticator == null) { #pragma warning disable CA2208 throw new ArgumentNullException("Authenticator in config is null. Please supply it via `WithAuthenticator` or `WithToken`"); #pragma warning restore CA2208 } _apiConnector = config.BuildAPIConnector(); _apiConnector.ResponseReceived += (sender, response) => { LastResponse = response; }; DefaultPaginator = config.DefaultPaginator; UserProfile = new UserProfileClient(_apiConnector); Browse = new BrowseClient(_apiConnector); Shows = new ShowsClient(_apiConnector); Playlists = new PlaylistsClient(_apiConnector); Search = new SearchClient(_apiConnector); Follow = new FollowClient(_apiConnector); Tracks = new TracksClient(_apiConnector); Player = new PlayerClient(_apiConnector); Albums = new AlbumsClient(_apiConnector); Artists = new ArtistsClient(_apiConnector); Personalization = new PersonalizationClient(_apiConnector); Episodes = new EpisodesClient(_apiConnector); Library = new LibraryClient(_apiConnector); Markets = new MarketsClient(_apiConnector); } public IPaginator DefaultPaginator { get; } public IUserProfileClient UserProfile { get; } public IBrowseClient Browse { get; } public IShowsClient Shows { get; } public IPlaylistsClient Playlists { get; } public ISearchClient Search { get; } public IFollowClient Follow { get; } public ITracksClient Tracks { get; } public IPlayerClient Player { get; } public IAlbumsClient Albums { get; } public IArtistsClient Artists { get; } public IPersonalizationClient Personalization { get; } public IEpisodesClient Episodes { get; } public ILibraryClient Library { get; } public IMarketsClient Markets { get; } public IResponse? LastResponse { get; private set; } /// /// Fetches all pages and returns them grouped in a list. /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// The first page, will be included in the output list! /// Optional. If not supplied, DefaultPaginator will be used /// The Paging-Type /// A list containing all fetched pages public Task> PaginateAll(IPaginatable firstPage, IPaginator? paginator = null) { return (paginator ?? DefaultPaginator).PaginateAll(firstPage, _apiConnector); } /// /// Fetches all pages and returns them grouped in a list. /// Some responses (e.g search response) have the pagination nested in a JSON Property. /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// A first page, will be included in the output list! /// A function which maps response objects to the next paging object /// Optional. If not supplied, DefaultPaginator will be used /// The Paging-Type /// The Response-Type /// A list containing all fetched pages public Task> PaginateAll( IPaginatable firstPage, Func> mapper, IPaginator? paginator = null ) { return (paginator ?? DefaultPaginator).PaginateAll(firstPage, mapper, _apiConnector); } private Task FetchPage(string? nextUrl) { if (nextUrl == null) { throw new APIPagingException("The paging object has no next page"); } return _apiConnector.Get(new Uri(nextUrl, UriKind.Absolute)); } /// /// Fetches the next page of the paging object /// /// A paging object which has a next page /// /// public Task> NextPage(Paging paging) { Ensure.ArgumentNotNull(paging, nameof(paging)); return FetchPage>(paging.Next); } /// /// Fetches the next page of the cursor paging object /// /// A cursor paging object which has a next page /// /// public Task> NextPage(CursorPaging cursorPaging) { Ensure.ArgumentNotNull(cursorPaging, nameof(cursorPaging)); return FetchPage>(cursorPaging.Next); } /// /// Fetches the next page of the complex IPaginatable object. /// /// A complex IPaginatable object with a next page /// /// The type of the next page /// public Task NextPage(IPaginatable paginatable) { Ensure.ArgumentNotNull(paginatable, nameof(paginatable)); return FetchPage(paginatable.Next); } /// /// Fetches the previous page of the paging object. /// /// A paging object with a previous page /// /// public Task> PreviousPage(Paging paging) { Ensure.ArgumentNotNull(paging, nameof(paging)); return FetchPage>(paging.Previous); } /// /// Fetches the previous page of the complex paging object. /// /// A complex paging object with a previous page /// /// The type of the next page /// public Task PreviousPage(Paging paging) { Ensure.ArgumentNotNull(paging, nameof(paging)); return FetchPage(paging.Previous); } /// /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// A first page, will be included in the output list! /// Optional. If not supplied, DefaultPaginator will be used /// An optional Cancellation Token /// The Paging-Type /// An iterable IAsyncEnumerable public IAsyncEnumerable Paginate( IPaginatable firstPage, IPaginator? paginator = null, CancellationToken cancellationToken = default ) { return (paginator ?? DefaultPaginator).Paginate(firstPage, _apiConnector, cancellationToken); } /// /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 /// Some responses (e.g search response) have the pagination nested in a JSON Property. /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// A first page, will be included in the output list! /// A function which maps response objects to the next paging object /// Optional. If not supplied, DefaultPaginator will be used /// An optional Cancellation Token /// The Paging-Type /// The Response-Type /// public IAsyncEnumerable Paginate( IPaginatable firstPage, Func> mapper, IPaginator? paginator = null, CancellationToken cancellationToken = default ) { return (paginator ?? DefaultPaginator).Paginate(firstPage, mapper, _apiConnector, cancellationToken); } /// /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 /// Some responses (e.g search response) have the pagination nested in a JSON Property. /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// A Function to retrive the first page, will be included in the output list! /// A function which maps response objects to the next paging object /// Optional. If not supplied, DefaultPaginator will be used /// An optional Cancellation Token /// The Paging-Type /// The Response-Type /// public async IAsyncEnumerable Paginate( Func>> getFirstPage, Func> mapper, IPaginator? paginator = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Ensure.ArgumentNotNull(getFirstPage, nameof(getFirstPage)); var firstPage = await getFirstPage().ConfigureAwait(false); await foreach (var item in (paginator ?? DefaultPaginator) .Paginate(firstPage, mapper, _apiConnector, cancellationToken) .WithCancellation(cancellationToken) ) { yield return item; } } /// /// Paginate through pages by using IAsyncEnumerable, introduced in C# 8 /// Some responses (e.g search response) have the pagination nested in a JSON Property. /// To workaround this limitation, the mapper is required and needs to point to the correct next pagination. /// The default paginator will fetch all available resources without a delay between requests. /// This can drain your request limit quite fast, so consider using a custom paginator with delays. /// /// A Task to retrive the first page, will be included in the output list! /// A function which maps response objects to the next paging object /// Optional. If not supplied, DefaultPaginator will be used /// An optional Cancellation Token /// The Paging-Type /// The Response-Type /// public async IAsyncEnumerable Paginate( Task> firstPageTask, Func> mapper, IPaginator? paginator = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Ensure.ArgumentNotNull(firstPageTask, nameof(firstPageTask)); var firstPage = await firstPageTask.ConfigureAwait(false); await foreach (var item in (paginator ?? DefaultPaginator) .Paginate(firstPage, mapper, _apiConnector, cancellationToken) .WithCancellation(cancellationToken) ) { yield return item; } } } }