Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GetLibraryItemAsyncEnumerable for unlimited library books #27

Merged
merged 15 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AudibleApi/Api.Catalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public partial class Api
{
const string CATALOG_PATH = "/1.0/catalog";

#region GetCatalogProductAsyncAsync
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kinda weird find + replace must I have done to cause this??

#region GetCatalogProductAsync
public Task<Item> GetCatalogProductAsync(string asin, CatalogOptions.ResponseGroupOptions responseGroups)
=> GetCatalogProductAsync(asin, responseGroups.ToQueryString());

Expand Down Expand Up @@ -156,7 +156,7 @@ public async Task<Item> GetCatalogProductAsync(string asin, string responseGroup
}
#endregion

#region GetCatalogProductsAsyncAsync
#region GetCatalogProductsAsync
public Task<List<Item>> GetCatalogProductsAsync(IEnumerable<string> asins, CatalogOptions.ResponseGroupOptions responseGroups)
=> GetCatalogProductsAsync(new CatalogOptions { ResponseGroups = responseGroups, Asins = asins.ToList() });

Expand Down
86 changes: 83 additions & 3 deletions AudibleApi/Api.Library.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi.Common;
using Dinah.Core;
Expand Down Expand Up @@ -84,7 +85,7 @@ public static string ToQueryString(this LibraryOptions libraryOptions)
}
public class LibraryOptions
{
public const int NUMBER_OF_RESULTS_PER_PAGE_MIN = 1;
public const int NUMBER_OF_RESULTS_PER_PAGE_MIN = 0;
public const int NUMBER_OF_RESULTS_PER_PAGE_MAX = 1000;

private int? _numResults;
Expand Down Expand Up @@ -261,11 +262,17 @@ public async Task<JObject> GetLibraryAsync(string libraryOptions)
// all strings passed here are assumed to be unconditionally valid
private async Task<JObject> getLibraryAsync(string parameters)
{
var url = $"{LIBRARY_PATH}?{parameters}";
var response = await AdHocAuthenticatedGetAsync(url);
var response = await getLibraryResponseAsync(parameters);
var obj = await response.Content.ReadAsJObjectAsync();
return obj;
}

internal async Task<System.Net.Http.HttpResponseMessage> getLibraryResponseAsync(string parameters)
{
var url = $"{LIBRARY_PATH}?{parameters}";
var response = await AdHocAuthenticatedGetAsync(url);
return response;
}
#endregion

#region GetLibraryBookAsync
Expand Down Expand Up @@ -349,6 +356,79 @@ public async Task<List<Item>> GetAllLibraryItemsAsync(LibraryOptions.ResponseGro

public async Task<List<Item>> GetAllLibraryItemsAsync(LibraryOptions libraryOptions)
=> await getAllLibraryItemsAsync_gated(libraryOptions);

public async IAsyncEnumerable<Item> GetLibraryItemAsyncEnumerable(LibraryOptions libraryOptions, int numItemsPerRequest = 50, int maxConcurrentRequests = 10)
{
if (!libraryOptions.PurchasedAfter.HasValue || libraryOptions.PurchasedAfter.Value < new DateTime(1970, 1, 1))
libraryOptions.PurchasedAfter = new DateTime(1970, 1, 1);

libraryOptions.NumberOfResultPerPage = 0;
int totalCount = await GetItemsCountAsync(this, libraryOptions);
libraryOptions.NumberOfResultPerPage = numItemsPerRequest;

int numPages = totalCount / libraryOptions.NumberOfResultPerPage.Value;
if (numPages * libraryOptions.NumberOfResultPerPage.Value < totalCount)
numPages++;

List<Task<Item[]>> allDlTasks = new();
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentRequests);

for (int page = 1; page <= numPages; page++)
{
libraryOptions.PageNumber = page;
var queryString = libraryOptions.ToQueryString();

allDlTasks.Add(downloadItemPage(concurrencySemaphore, queryString, page));
}

while (allDlTasks.Count > 0)
{
var completed = await Task.WhenAny(allDlTasks);
allDlTasks.Remove(completed);
foreach (var item in completed.Result)
yield return item;
}
}

private async Task<Item[]> downloadItemPage(SemaphoreSlim concurrencySemaphore, string queryString, int pageNumber)
{
await concurrencySemaphore.WaitAsync();
try
{
var response = await getLibraryResponseAsync($"{queryString}");

var page = await response.Content.ReadAsStringAsync();
LibraryDtoV10 libResult;
try
{
// important! use this convert/deser method
libResult = LibraryDtoV10.FromJson(page);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error converting library for importing use. Full library:\r\n" + page);
throw;
}

Serilog.Log.Logger.Information($"Page {pageNumber}: {libResult.Items.Length} results");
return libResult.Items;
}
finally
{
concurrencySemaphore.Release();
}
}

private async Task<int> GetItemsCountAsync(Api api, LibraryOptions libraryOptions)
{
var response = await api.getLibraryResponseAsync(libraryOptions.ToQueryString());

int totalCount = -1;
if (response.Headers.TryGetValues("Total-Count", out var values))
totalCount = int.Parse(values.First());

return totalCount;
}

private async Task<List<Item>> getAllLibraryItemsAsync_gated(LibraryOptions libraryOptions)
{
Expand Down