From 591d84e7199ff832226dc98b8f44b090386daaeb Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Mon, 4 Nov 2019 14:16:57 -0500 Subject: [PATCH] scraping => api transition almost complete --- .../AudibleApiDomainService.csproj | 13 - .../AudibleApiLibationClient.cs | 151 -- .../IAudibleApiResponder.cs | 9 - AudibleApiDomainService/Settings.cs | 17 - CookieMonster/CookieMonster.csproj | 2 +- DTOs/DTOs.csproj | 11 - DTOs/LibraryApiV10.cs | 1829 ----------------- DTOs/LibraryApiV10.custom.cs | 148 -- DTOs/LibraryApiV10Extensions.cs | 31 - DataLayer/UNTESTED/EfClasses/Book.cs | 6 +- DataLayer/UNTESTED/EfClasses/LibraryBook.cs | 10 +- DataLayer/UNTESTED/EfClasses/Rating.cs | 4 +- .../UNTESTED/QueryObjects/LibraryQueries.cs | 6 +- DtoImporterService/BookImporter.cs | 136 ++ DtoImporterService/CategoryImporter.cs | 83 + DtoImporterService/ContributorImporter.cs | 106 + DtoImporterService/DtoImporterService.csproj | 12 + DtoImporterService/ImporterBase.cs | 44 + DtoImporterService/LibraryImporter.cs | 50 + DtoImporterService/SeriesImporter.cs | 68 + InternalUtilities/InternalUtilities.csproj | 1 + .../UNTESTED/AudibleApiExtensions.cs | 42 + Libation.sln | 45 +- LibationWinForm/LibationWinForm.csproj | 2 +- .../ProcessorAutomationController.cs | 4 +- .../Dialogs/Login/WinformResponder.cs | 2 +- LibationWinForm/UNTESTED/Form1.cs | 56 +- LibationWinForm/UNTESTED/GridEntry.cs | 173 +- LibationWinForm/UNTESTED/ProductsGrid.cs | 26 +- Scraping/Scraping.csproj | 1 - .../UNTESTED/DTOs}/BookDetailDTO.cs | 0 .../UNTESTED/DTOs}/LibraryDTO.cs | 0 .../UNTESTED/DownloadBook.cs | 108 +- .../UNTESTED/DownloadPdf.cs | 2 +- .../UNTESTED/DownloadableBase.cs | 48 +- .../UNTESTED/DtoImporter.cs | 13 +- .../UNTESTED/IDownloadable.cs | 3 +- .../UNTESTED/IProcessableExt.cs | 9 +- ScrapingDomainServices/UNTESTED/Indexer.cs | 28 +- .../UNTESTED/ScrapeBookDetails.cs | 2 +- __TODO.txt | 188 +- 41 files changed, 995 insertions(+), 2494 deletions(-) delete mode 100644 AudibleApiDomainService/AudibleApiDomainService.csproj delete mode 100644 AudibleApiDomainService/AudibleApiLibationClient.cs delete mode 100644 AudibleApiDomainService/IAudibleApiResponder.cs delete mode 100644 AudibleApiDomainService/Settings.cs delete mode 100644 DTOs/DTOs.csproj delete mode 100644 DTOs/LibraryApiV10.cs delete mode 100644 DTOs/LibraryApiV10.custom.cs delete mode 100644 DTOs/LibraryApiV10Extensions.cs create mode 100644 DtoImporterService/BookImporter.cs create mode 100644 DtoImporterService/CategoryImporter.cs create mode 100644 DtoImporterService/ContributorImporter.cs create mode 100644 DtoImporterService/DtoImporterService.csproj create mode 100644 DtoImporterService/ImporterBase.cs create mode 100644 DtoImporterService/LibraryImporter.cs create mode 100644 DtoImporterService/SeriesImporter.cs create mode 100644 InternalUtilities/UNTESTED/AudibleApiExtensions.cs rename {DTOs => Scraping/UNTESTED/DTOs}/BookDetailDTO.cs (100%) rename {DTOs => Scraping/UNTESTED/DTOs}/LibraryDTO.cs (100%) diff --git a/AudibleApiDomainService/AudibleApiDomainService.csproj b/AudibleApiDomainService/AudibleApiDomainService.csproj deleted file mode 100644 index 8c0bac50..00000000 --- a/AudibleApiDomainService/AudibleApiDomainService.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.1 - - - - - - - - - diff --git a/AudibleApiDomainService/AudibleApiLibationClient.cs b/AudibleApiDomainService/AudibleApiLibationClient.cs deleted file mode 100644 index 93128734..00000000 --- a/AudibleApiDomainService/AudibleApiLibationClient.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using AudibleApi; -using AudibleApi.Authentication; -using AudibleApi.Authorization; -using DTOs; - -namespace AudibleApiDomainService -{ - public class AudibleApiLibationClient - { - private Api _api; - - #region initialize api - private AudibleApiLibationClient() { } - public async static Task CreateClientAsync(Settings settings, IAudibleApiResponder responder) - { - Localization.SetLocale(settings.LocaleCountryCode); - - Api api; - try - { - api = await EzApiCreator.GetApiAsync(settings.IdentityFilePath); - } - catch - { - var inMemoryIdentity = await loginAsync(responder); - api = await EzApiCreator.GetApiAsync(settings.IdentityFilePath, inMemoryIdentity); - } - - return new AudibleApiLibationClient { _api = api }; - } - - // LOGIN PATTERN - // - Start with Authenticate. Submit email + pw - // - Each step in the login process will return a LoginResult - // - Each result which has required user input has a SubmitAsync method - // - The final LoginComplete result returns "Identity" -- in-memory authorization items - private static async Task loginAsync(IAudibleApiResponder responder) - { - var login = new Authenticate(); - - var (email, password) = responder.GetLogin(); - - var loginResult = await login.SubmitCredentialsAsync(email, password); - - while (true) - { - switch (loginResult) - { - case CredentialsPage credentialsPage: - var (emailInput, pwInput) = responder.GetLogin(); - loginResult = await credentialsPage.SubmitAsync(emailInput, pwInput); - break; - - case CaptchaPage captchaResult: - var imageBytes = await downloadImageAsync(captchaResult.CaptchaImage); - var guess = responder.GetCaptchaAnswer(imageBytes); - loginResult = await captchaResult.SubmitAsync(guess); - break; - - case TwoFactorAuthenticationPage _2fa: - var _2faCode = responder.Get2faCode(); - loginResult = await _2fa.SubmitAsync(_2faCode); - break; - - case LoginComplete final: - return final.Identity; - - default: - throw new Exception("Unknown LoginResult"); - } - } - } - - private static async Task downloadImageAsync(Uri imageUri) - { - using var client = new HttpClient(); - using var contentStream = await client.GetStreamAsync(imageUri); - using var localStream = new MemoryStream(); - await contentStream.CopyToAsync(localStream); - return localStream.ToArray(); - } - #endregion - - public async Task ImportLibraryAsync() - { - try - { - var items = await GetLibraryItemsAsync(); -//var (total, newEntries) = await ScrapingDomainServices.Indexer.IndexLibraryAsync(items); - } - catch (Exception ex) - { - // catch here for easier debugging - throw; - } - } - - private async Task> GetLibraryItemsAsync() - { - var allItems = new List(); - - for (var i = 1; ; i++) - { - var page = await _api.GetLibraryAsync(new LibraryOptions - { - NumberOfResultPerPage = 1000, - PageNumber = i, - PurchasedAfter = new DateTime(2000, 1, 1), - ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS - }); - - // important! use this convert method - var libResult = LibraryApiV10.FromJson(page.ToString()); - - if (!libResult.Items.Any()) - break; - - allItems.AddRange(libResult.Items); - } - - return allItems; - } - - //public async Task DownloadBookAsync(string asinToDownload) - //{ - // // console example - // using var progressBar = new Dinah.Core.ConsoleLib.ProgressBar(); - // var progress = new Progress(); - // progress.ProgressChanged += (_, e) => progressBar.Report(Math.Round((double)(100 * e.BytesReceived) / e.TotalFileSize.Value) / 100); - - // logger.WriteLine("Download book"); - // var finalFile = await _api.DownloadAaxWorkaroundAsync(asinToDownload, "downloadExample.xyz", progress); - - // logger.WriteLine(" Done!"); - // logger.WriteLine("final file: " + Path.GetFullPath(finalFile)); - - // // benefit of this small delay: - // // - if you try to delete a file too soon after it's created, the OS isn't done with the creation and you can get an unexpected error - // // - give progressBar's internal timer time to finish. if timer is disposed before the final message is processed, "100%" will never get a chance to be displayed - // await Task.Delay(100); - - // File.Delete(finalFile); - //} - } -} diff --git a/AudibleApiDomainService/IAudibleApiResponder.cs b/AudibleApiDomainService/IAudibleApiResponder.cs deleted file mode 100644 index 6b9da2c2..00000000 --- a/AudibleApiDomainService/IAudibleApiResponder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace AudibleApiDomainService -{ - public interface IAudibleApiResponder - { - (string email, string password) GetLogin(); - string GetCaptchaAnswer(byte[] captchaImage); - string Get2faCode(); - } -} \ No newline at end of file diff --git a/AudibleApiDomainService/Settings.cs b/AudibleApiDomainService/Settings.cs deleted file mode 100644 index 681adc8e..00000000 --- a/AudibleApiDomainService/Settings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.IO; -using FileManager; - -namespace AudibleApiDomainService -{ - public class Settings - { - public string IdentityFilePath { get; } - public string LocaleCountryCode { get; } - - public Settings(Configuration config) - { - IdentityFilePath = Path.Combine(config.LibationFiles, "IdentityTokens.json"); - LocaleCountryCode = config.LocaleCountryCode; - } - } -} \ No newline at end of file diff --git a/CookieMonster/CookieMonster.csproj b/CookieMonster/CookieMonster.csproj index 84fe44ec..62e79934 100644 --- a/CookieMonster/CookieMonster.csproj +++ b/CookieMonster/CookieMonster.csproj @@ -5,7 +5,7 @@ - + diff --git a/DTOs/DTOs.csproj b/DTOs/DTOs.csproj deleted file mode 100644 index af0258e8..00000000 --- a/DTOs/DTOs.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - netstandard2.1 - - - - - - - diff --git a/DTOs/LibraryApiV10.cs b/DTOs/LibraryApiV10.cs deleted file mode 100644 index 1b450dc1..00000000 --- a/DTOs/LibraryApiV10.cs +++ /dev/null @@ -1,1829 +0,0 @@ -// INSTRUCTIONS FOR GENERATING -// =========================== -// library api with all response_groups -// https://app.quicktype.io/ -// left pane: -// name=LibraryApiV10 -// paste in full output of library api with all response_groups -// right pane: -// namespace: DTOs -// array -// complete -// make all properties optional. this adds unneeded stuff (see next step) but also needed stuff -// remove all instances of ", NullValueHandling = NullValueHandling.Ignore" -// PurchaseDate is not optional. remove "?" from type -// Status is not optional. remove "?" from type -// RuntimeLengthMin type: int? -// add comments to class -// Audible API. GET /1.0/library , GET /1.0/library/{asin} -// rename Author class to Person - -// -// -// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: -// -// using DTOs; -// -// var libraryApiV10 = LibraryApiV10.FromJson(jsonString); - -namespace DTOs -{ - using System; - using System.Globalization; - using Newtonsoft.Json; - using Newtonsoft.Json.Converters; - - /// - /// Audible API. GET /1.0/library , GET /1.0/library/{asin} - /// - public partial class LibraryApiV10 - { - [JsonProperty("items")] - public Item[] Items { get; set; } - - [JsonProperty("response_groups")] - public string[] ResponseGroups { get; set; } - } - - public partial class Item - { - [JsonProperty("asin")] - public string Asin { get; set; } - - [JsonProperty("audible_editors_summary")] - public string AudibleEditorsSummary { get; set; } - - [JsonProperty("authors")] - public Person[] Authors { get; set; } - - [JsonProperty("availability")] - public object Availability { get; set; } - - [JsonProperty("available_codecs")] - public AvailableCodec[] AvailableCodecs { get; set; } - - [JsonProperty("badge_types")] - public object BadgeTypes { get; set; } - - [JsonProperty("buying_options")] - public object BuyingOptions { get; set; } - - [JsonProperty("category_ladders")] - public CategoryLadder[] CategoryLadders { get; set; } - - [JsonProperty("claim_code_url")] - public object ClaimCodeUrl { get; set; } - - [JsonProperty("content_delivery_type")] - public ContentDeliveryType? ContentDeliveryType { get; set; } - - [JsonProperty("content_level")] - public object ContentLevel { get; set; } - - [JsonProperty("content_rating")] - public ContentRating ContentRating { get; set; } - - [JsonProperty("content_type")] - public ContentType? ContentType { get; set; } - - [JsonProperty("copyright")] - public object Copyright { get; set; } - - [JsonProperty("credits_required")] - public object CreditsRequired { get; set; } - - [JsonProperty("customer_reviews")] - public Review[] CustomerReviews { get; set; } - - [JsonProperty("date_first_available")] - public object DateFirstAvailable { get; set; } - - [JsonProperty("distribution_rights_region")] - public object DistributionRightsRegion { get; set; } - - [JsonProperty("editorial_reviews")] - public string[] EditorialReviews { get; set; } - - [JsonProperty("extended_product_description")] - public object ExtendedProductDescription { get; set; } - - [JsonProperty("format_type")] - public FormatType? FormatType { get; set; } - - [JsonProperty("generic_keyword")] - public object GenericKeyword { get; set; } - - [JsonProperty("has_children")] - public bool? HasChildren { get; set; } - - [JsonProperty("image_url")] - public object ImageUrl { get; set; } - - [JsonProperty("invites_remaining")] - public object InvitesRemaining { get; set; } - - [JsonProperty("is_adult_product")] - public bool? IsAdultProduct { get; set; } - - [JsonProperty("is_buyable")] - public object IsBuyable { get; set; } - - [JsonProperty("is_downloaded")] - public bool? IsDownloaded { get; set; } - - [JsonProperty("is_finished")] - public object IsFinished { get; set; } - - [JsonProperty("is_in_wishlist")] - public object IsInWishlist { get; set; } - - [JsonProperty("is_listenable")] - public bool? IsListenable { get; set; } - - [JsonProperty("is_preorderable")] - public object IsPreorderable { get; set; } - - [JsonProperty("is_returnable")] - public bool? IsReturnable { get; set; } - - [JsonProperty("is_searchable")] - public object IsSearchable { get; set; } - - [JsonProperty("is_ws4v_companion_asin_owned")] - public object IsWs4VCompanionAsinOwned { get; set; } - - [JsonProperty("is_ws4v_enabled")] - public object IsWs4VEnabled { get; set; } - - [JsonProperty("isbn")] - public object Isbn { get; set; } - - [JsonProperty("issue_date")] - public DateTimeOffset? IssueDate { get; set; } - - [JsonProperty("language")] - public Language? Language { get; set; } - - [JsonProperty("member_giving_status")] - public object MemberGivingStatus { get; set; } - - [JsonProperty("merchandising_description")] - public object MerchandisingDescription { get; set; } - - [JsonProperty("merchandising_summary")] - public string MerchandisingSummary { get; set; } - - [JsonProperty("narration_accent")] - public object NarrationAccent { get; set; } - - [JsonProperty("narrators")] - public Person[] Narrators { get; set; } - - [JsonProperty("order_id")] - public object OrderId { get; set; } - - [JsonProperty("order_item_id")] - public object OrderItemId { get; set; } - - [JsonProperty("origin_asin")] - public string OriginAsin { get; set; } - - [JsonProperty("origin_id")] - public string OriginId { get; set; } - - [JsonProperty("origin_marketplace")] - public OriginMarketplace? OriginMarketplace { get; set; } - - [JsonProperty("origin_type")] - public OriginType? OriginType { get; set; } - - [JsonProperty("part_number")] - public object PartNumber { get; set; } - - [JsonProperty("pdf_link")] - public Uri PdfLink { get; set; } - - [JsonProperty("pdf_url")] - public Uri PdfUrl { get; set; } - - [JsonProperty("percent_complete")] - public double? PercentComplete { get; set; } - - [JsonProperty("periodical_info")] - public object PeriodicalInfo { get; set; } - - [JsonProperty("plans")] - public Plan[] Plans { get; set; } - - [JsonProperty("platinum_keywords")] - public object PlatinumKeywords { get; set; } - - [JsonProperty("price")] - public Price Price { get; set; } - - [JsonProperty("product_images")] - public ProductImages ProductImages { get; set; } - - [JsonProperty("product_page_url")] - public object ProductPageUrl { get; set; } - - [JsonProperty("product_site_launch_date")] - public object ProductSiteLaunchDate { get; set; } - - [JsonProperty("provided_review")] - public Review ProvidedReview { get; set; } - - [JsonProperty("publication_name")] - public string PublicationName { get; set; } - - [JsonProperty("publisher_name")] - public string PublisherName { get; set; } - - [JsonProperty("publisher_summary")] - public string PublisherSummary { get; set; } - - [JsonProperty("purchase_date")] - public DateTimeOffset PurchaseDate { get; set; } - - [JsonProperty("rating")] - public Rating Rating { get; set; } - - [JsonProperty("read_along_support")] - public object ReadAlongSupport { get; set; } - - [JsonProperty("relationships")] - public Relationship[] Relationships { get; set; } - - [JsonProperty("release_date")] - public DateTimeOffset? ReleaseDate { get; set; } - - [JsonProperty("runtime_length_min")] - public int? RuntimeLengthMin { get; set; } - - [JsonProperty("sample_url")] - public Uri SampleUrl { get; set; } - - [JsonProperty("series")] - public Series[] Series { get; set; } - - [JsonProperty("sku")] - public string Sku { get; set; } - - [JsonProperty("sku_lite")] - public string SkuLite { get; set; } - - [JsonProperty("status")] - public Status Status { get; set; } - - [JsonProperty("subscription_asins")] - public object SubscriptionAsins { get; set; } - - [JsonProperty("subtitle")] - public string Subtitle { get; set; } - - [JsonProperty("thesaurus_subject_keywords")] - public string[] ThesaurusSubjectKeywords { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("ws4v_companion_asin")] - public object Ws4VCompanionAsin { get; set; } - } - - public partial class Person - { - [JsonProperty("asin")] - public string Asin { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - } - - public partial class AvailableCodec - { - [JsonProperty("enhanced_codec")] - public EnhancedCodec? EnhancedCodec { get; set; } - - [JsonProperty("format")] - public AvailableCodecFormat? Format { get; set; } - - [JsonProperty("is_kindle_enhanced")] - public bool? IsKindleEnhanced { get; set; } - - [JsonProperty("name")] - public Name? Name { get; set; } - } - - public partial class CategoryLadder - { - [JsonProperty("date_product_available_in_category")] - public object DateProductAvailableInCategory { get; set; } - - [JsonProperty("ladder")] - public Ladder[] Ladder { get; set; } - - [JsonProperty("root")] - public Root? Root { get; set; } - } - - public partial class Ladder - { - [JsonProperty("category_presentation")] - public object CategoryPresentation { get; set; } - - [JsonProperty("children")] - public object Children { get; set; } - - [JsonProperty("default_offline_storage_days_count")] - public object DefaultOfflineStorageDaysCount { get; set; } - - [JsonProperty("default_offline_storage_item_count")] - public object DefaultOfflineStorageItemCount { get; set; } - - [JsonProperty("description")] - public object Description { get; set; } - - [JsonProperty("header")] - public object Header { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("images")] - public object Images { get; set; } - - [JsonProperty("initial_download_days_count")] - public object InitialDownloadDaysCount { get; set; } - - [JsonProperty("initial_download_item_count")] - public object InitialDownloadItemCount { get; set; } - - [JsonProperty("is_new")] - public object IsNew { get; set; } - - [JsonProperty("localized_name")] - public object LocalizedName { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("products")] - public object Products { get; set; } - - [JsonProperty("promote_upsell")] - public object PromoteUpsell { get; set; } - - [JsonProperty("suppress_download_option")] - public object SuppressDownloadOption { get; set; } - - [JsonProperty("suppress_release_date")] - public object SuppressReleaseDate { get; set; } - } - - public partial class ContentRating - { - [JsonProperty("steaminess")] - [JsonConverter(typeof(ParseStringConverter))] - public long? Steaminess { get; set; } - } - - public partial class Review - { - [JsonProperty("asin")] - public string Asin { get; set; } - - [JsonProperty("author_id")] - public string AuthorId { get; set; } - - [JsonProperty("author_name")] - public string AuthorName { get; set; } - - [JsonProperty("body")] - public string Body { get; set; } - - [JsonProperty("customer_vote")] - public object CustomerVote { get; set; } - - [JsonProperty("format")] - public CustomerReviewFormat? Format { get; set; } - - [JsonProperty("guided_responses")] - public GuidedResponse[] GuidedResponses { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("location")] - public string Location { get; set; } - - [JsonProperty("ratings")] - public Ratings Ratings { get; set; } - - [JsonProperty("review_content_scores")] - public ReviewContentScores ReviewContentScores { get; set; } - - [JsonProperty("submission_date")] - public DateTimeOffset? SubmissionDate { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - } - - public partial class GuidedResponse - { - [JsonProperty("answer")] - public string Answer { get; set; } - - [JsonProperty("id")] - [JsonConverter(typeof(ParseStringConverter))] - public long? Id { get; set; } - - [JsonProperty("question")] - public string Question { get; set; } - - [JsonProperty("question_type")] - public QuestionType? QuestionType { get; set; } - } - - public partial class Ratings - { - [JsonProperty("overall_rating")] - public long? OverallRating { get; set; } - - [JsonProperty("performance_rating")] - public long? PerformanceRating { get; set; } - - [JsonProperty("story_rating")] - public long? StoryRating { get; set; } - } - - public partial class ReviewContentScores - { - [JsonProperty("content_quality")] - public long? ContentQuality { get; set; } - - [JsonProperty("num_helpful_votes")] - public long? NumHelpfulVotes { get; set; } - - [JsonProperty("num_unhelpful_votes")] - public long? NumUnhelpfulVotes { get; set; } - } - - public partial class Plan - { - [JsonProperty("detail_plan_names")] - public object DetailPlanNames { get; set; } - - [JsonProperty("end_date")] - public DateTimeOffset? EndDate { get; set; } - - [JsonProperty("plan_name")] - public PlanName? PlanName { get; set; } - - [JsonProperty("start_date")] - public DateTimeOffset? StartDate { get; set; } - } - - public partial class Price - { - [JsonProperty("credit_price")] - public long? CreditPrice { get; set; } - - [JsonProperty("is_buy_for_free_eligible")] - public object IsBuyForFreeEligible { get; set; } - - [JsonProperty("is_credit_price_eligible")] - public object IsCreditPriceEligible { get; set; } - - [JsonProperty("is_free_eligible")] - public object IsFreeEligible { get; set; } - - [JsonProperty("is_ws4v_upsell_eligible")] - public object IsWs4VUpsellEligible { get; set; } - - [JsonProperty("list_price")] - public ListPriceClass ListPrice { get; set; } - - [JsonProperty("lowest_price")] - public ListPriceClass LowestPrice { get; set; } - - [JsonProperty("ws4v_upsell_price")] - public ListPriceClass Ws4VUpsellPrice { get; set; } - } - - public partial class ListPriceClass - { - [JsonProperty("base")] - public double? Base { get; set; } - - [JsonProperty("currency_code")] - public CurrencyCode? CurrencyCode { get; set; } - - [JsonProperty("merchant_id")] - public MerchantId? MerchantId { get; set; } - - [JsonProperty("type")] - public TypeEnum? Type { get; set; } - } - - public partial class ProductImages - { - [JsonProperty("500")] - public Uri The500 { get; set; } - } - - public partial class Rating - { - [JsonProperty("num_reviews")] - public long? NumReviews { get; set; } - - [JsonProperty("overall_distribution")] - public Distribution OverallDistribution { get; set; } - - [JsonProperty("performance_distribution")] - public Distribution PerformanceDistribution { get; set; } - - [JsonProperty("story_distribution")] - public Distribution StoryDistribution { get; set; } - } - - public partial class Distribution - { - [JsonProperty("average_rating")] - public double? AverageRating { get; set; } - - [JsonProperty("display_average_rating")] - public string DisplayAverageRating { get; set; } - - [JsonProperty("display_stars")] - public double? DisplayStars { get; set; } - - [JsonProperty("num_five_star_ratings")] - public long? NumFiveStarRatings { get; set; } - - [JsonProperty("num_four_star_ratings")] - public long? NumFourStarRatings { get; set; } - - [JsonProperty("num_one_star_ratings")] - public long? NumOneStarRatings { get; set; } - - [JsonProperty("num_ratings")] - public long? NumRatings { get; set; } - - [JsonProperty("num_three_star_ratings")] - public long? NumThreeStarRatings { get; set; } - - [JsonProperty("num_two_star_ratings")] - public long? NumTwoStarRatings { get; set; } - } - - public partial class Relationship - { - [JsonProperty("asin")] - public string Asin { get; set; } - - [JsonProperty("relationship_to_product")] - public RelationshipToProduct? RelationshipToProduct { get; set; } - - [JsonProperty("relationship_type")] - public RelationshipType? RelationshipType { get; set; } - - [JsonProperty("sequence")] - public string Sequence { get; set; } - - [JsonProperty("sku")] - public string Sku { get; set; } - - [JsonProperty("sku_lite")] - public string SkuLite { get; set; } - - [JsonProperty("sort")] - [JsonConverter(typeof(ParseStringConverter))] - public long? Sort { get; set; } - } - - public partial class Series - { - [JsonProperty("asin")] - public string Asin { get; set; } - - [JsonProperty("sequence")] - public string Sequence { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("url")] - public string Url { get; set; } - } - - public enum EnhancedCodec { Aax, Format4, Lc128_44100_Stereo, Lc32_22050_Stereo, Lc64_22050_Stereo, Lc64_44100_Stereo, Mp42232, Mp42264, Mp444128, Mp44464, Piff2232, Piff2264, Piff44128, Piff4464 }; - - public enum AvailableCodecFormat { Enhanced, Format4 }; - - public enum Name { Aax, Aax22_32, Aax22_64, Aax44_128, Aax44_64, Format4, Mp422_32, Mp422_64, Mp444_128, Mp444_64, Piff22_32, Piff22_64, Piff44_128, Piff44_64 }; - - public enum Root { EditorsPicks, ExploreBy, Genres, InstitutionsHpMarketing, RodizioBuckets, RodizioGenres, ShortsPrime }; - - public enum ContentDeliveryType { MultiPartBook, Periodical, SinglePartBook }; - - public enum ContentType { Episode, Lecture, Meditation, Misc, Performance, Product, RadioTvProgram, Show, Speech }; - - public enum CustomerReviewFormat { Freeform, Guided }; - - public enum QuestionType { Genre, Misc, Overall, Performance, Story }; - - public enum FormatType { Abridged, OriginalRecording, Unabridged }; - - public enum Language { English }; - - public enum OriginMarketplace { Af2M0Kc94Rcea }; - - public enum OriginType { AudibleChannels, AudibleComplimentaryOriginal, Purchase }; - - public enum PlanName { AyceRomance, ComplimentaryOriginalMemberBenefit, Radio, Rodizio, SpecialBenefit }; - - public enum CurrencyCode { Usd }; - - public enum MerchantId { A2Zo8Jx97D5Mn9 }; - - public enum TypeEnum { List, Member, Sale, Ws4VUpsell }; - - public enum RelationshipToProduct { Child, Parent }; - - public enum RelationshipType { Component, Episode, MerchantTitleAuthority, Season, Series }; - - public enum Status { Active }; - - public enum ThesaurusSubjectKeyword { AdventurersExplorers, AlternateHistory, Comedians, Contemporary, Dramatizations, EasternReligions, LaConfidential, LiteratureAndFiction, Medicine, Spirituality, StandupComedy, Storytelling, SwordSorcery, Workouts }; - - public partial class LibraryApiV10 - { - public static LibraryApiV10 FromJson(string json) => JsonConvert.DeserializeObject(json, DTOs.Converter.Settings); - } - - public static class Serialize - { - public static string ToJson(this LibraryApiV10 self) => JsonConvert.SerializeObject(self, DTOs.Converter.Settings); - } - - internal static class Converter - { - public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings - { - MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - DateParseHandling = DateParseHandling.None, - Converters = - { - EnhancedCodecConverter.Singleton, - AvailableCodecFormatConverter.Singleton, - NameConverter.Singleton, - RootConverter.Singleton, - ContentDeliveryTypeConverter.Singleton, - ContentTypeConverter.Singleton, - CustomerReviewFormatConverter.Singleton, - QuestionTypeConverter.Singleton, - FormatTypeConverter.Singleton, - LanguageConverter.Singleton, - OriginMarketplaceConverter.Singleton, - OriginTypeConverter.Singleton, - PlanNameConverter.Singleton, - CurrencyCodeConverter.Singleton, - MerchantIdConverter.Singleton, - TypeEnumConverter.Singleton, - RelationshipToProductConverter.Singleton, - RelationshipTypeConverter.Singleton, - StatusConverter.Singleton, - ThesaurusSubjectKeywordConverter.Singleton, - new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } - }, - }; - } - - internal class EnhancedCodecConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(EnhancedCodec) || t == typeof(EnhancedCodec?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "LC_128_44100_stereo": - return EnhancedCodec.Lc128_44100_Stereo; - case "LC_32_22050_stereo": - return EnhancedCodec.Lc32_22050_Stereo; - case "LC_64_22050_stereo": - return EnhancedCodec.Lc64_22050_Stereo; - case "LC_64_44100_stereo": - return EnhancedCodec.Lc64_44100_Stereo; - case "aax": - return EnhancedCodec.Aax; - case "format4": - return EnhancedCodec.Format4; - case "mp42232": - return EnhancedCodec.Mp42232; - case "mp42264": - return EnhancedCodec.Mp42264; - case "mp444128": - return EnhancedCodec.Mp444128; - case "mp44464": - return EnhancedCodec.Mp44464; - case "piff2232": - return EnhancedCodec.Piff2232; - case "piff2264": - return EnhancedCodec.Piff2264; - case "piff44128": - return EnhancedCodec.Piff44128; - case "piff4464": - return EnhancedCodec.Piff4464; - } - throw new Exception("Cannot unmarshal type EnhancedCodec"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (EnhancedCodec)untypedValue; - switch (value) - { - case EnhancedCodec.Lc128_44100_Stereo: - serializer.Serialize(writer, "LC_128_44100_stereo"); - return; - case EnhancedCodec.Lc32_22050_Stereo: - serializer.Serialize(writer, "LC_32_22050_stereo"); - return; - case EnhancedCodec.Lc64_22050_Stereo: - serializer.Serialize(writer, "LC_64_22050_stereo"); - return; - case EnhancedCodec.Lc64_44100_Stereo: - serializer.Serialize(writer, "LC_64_44100_stereo"); - return; - case EnhancedCodec.Aax: - serializer.Serialize(writer, "aax"); - return; - case EnhancedCodec.Format4: - serializer.Serialize(writer, "format4"); - return; - case EnhancedCodec.Mp42232: - serializer.Serialize(writer, "mp42232"); - return; - case EnhancedCodec.Mp42264: - serializer.Serialize(writer, "mp42264"); - return; - case EnhancedCodec.Mp444128: - serializer.Serialize(writer, "mp444128"); - return; - case EnhancedCodec.Mp44464: - serializer.Serialize(writer, "mp44464"); - return; - case EnhancedCodec.Piff2232: - serializer.Serialize(writer, "piff2232"); - return; - case EnhancedCodec.Piff2264: - serializer.Serialize(writer, "piff2264"); - return; - case EnhancedCodec.Piff44128: - serializer.Serialize(writer, "piff44128"); - return; - case EnhancedCodec.Piff4464: - serializer.Serialize(writer, "piff4464"); - return; - } - throw new Exception("Cannot marshal type EnhancedCodec"); - } - - public static readonly EnhancedCodecConverter Singleton = new EnhancedCodecConverter(); - } - - internal class AvailableCodecFormatConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(AvailableCodecFormat) || t == typeof(AvailableCodecFormat?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "Enhanced": - return AvailableCodecFormat.Enhanced; - case "Format4": - return AvailableCodecFormat.Format4; - } - throw new Exception("Cannot unmarshal type AvailableCodecFormat"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (AvailableCodecFormat)untypedValue; - switch (value) - { - case AvailableCodecFormat.Enhanced: - serializer.Serialize(writer, "Enhanced"); - return; - case AvailableCodecFormat.Format4: - serializer.Serialize(writer, "Format4"); - return; - } - throw new Exception("Cannot marshal type AvailableCodecFormat"); - } - - public static readonly AvailableCodecFormatConverter Singleton = new AvailableCodecFormatConverter(); - } - - internal class NameConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(Name) || t == typeof(Name?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "aax": - return Name.Aax; - case "aax_22_32": - return Name.Aax22_32; - case "aax_22_64": - return Name.Aax22_64; - case "aax_44_128": - return Name.Aax44_128; - case "aax_44_64": - return Name.Aax44_64; - case "format4": - return Name.Format4; - case "mp4_22_32": - return Name.Mp422_32; - case "mp4_22_64": - return Name.Mp422_64; - case "mp4_44_128": - return Name.Mp444_128; - case "mp4_44_64": - return Name.Mp444_64; - case "piff_22_32": - return Name.Piff22_32; - case "piff_22_64": - return Name.Piff22_64; - case "piff_44_128": - return Name.Piff44_128; - case "piff_44_64": - return Name.Piff44_64; - } - throw new Exception("Cannot unmarshal type Name"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (Name)untypedValue; - switch (value) - { - case Name.Aax: - serializer.Serialize(writer, "aax"); - return; - case Name.Aax22_32: - serializer.Serialize(writer, "aax_22_32"); - return; - case Name.Aax22_64: - serializer.Serialize(writer, "aax_22_64"); - return; - case Name.Aax44_128: - serializer.Serialize(writer, "aax_44_128"); - return; - case Name.Aax44_64: - serializer.Serialize(writer, "aax_44_64"); - return; - case Name.Format4: - serializer.Serialize(writer, "format4"); - return; - case Name.Mp422_32: - serializer.Serialize(writer, "mp4_22_32"); - return; - case Name.Mp422_64: - serializer.Serialize(writer, "mp4_22_64"); - return; - case Name.Mp444_128: - serializer.Serialize(writer, "mp4_44_128"); - return; - case Name.Mp444_64: - serializer.Serialize(writer, "mp4_44_64"); - return; - case Name.Piff22_32: - serializer.Serialize(writer, "piff_22_32"); - return; - case Name.Piff22_64: - serializer.Serialize(writer, "piff_22_64"); - return; - case Name.Piff44_128: - serializer.Serialize(writer, "piff_44_128"); - return; - case Name.Piff44_64: - serializer.Serialize(writer, "piff_44_64"); - return; - } - throw new Exception("Cannot marshal type Name"); - } - - public static readonly NameConverter Singleton = new NameConverter(); - } - - internal class RootConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(Root) || t == typeof(Root?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "EditorsPicks": - return Root.EditorsPicks; - case "ExploreBy": - return Root.ExploreBy; - case "Genres": - return Root.Genres; - case "InstitutionsHpMarketing": - return Root.InstitutionsHpMarketing; - case "RodizioBuckets": - return Root.RodizioBuckets; - case "RodizioGenres": - return Root.RodizioGenres; - case "ShortsPrime": - return Root.ShortsPrime; - } - throw new Exception("Cannot unmarshal type Root"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (Root)untypedValue; - switch (value) - { - case Root.EditorsPicks: - serializer.Serialize(writer, "EditorsPicks"); - return; - case Root.ExploreBy: - serializer.Serialize(writer, "ExploreBy"); - return; - case Root.Genres: - serializer.Serialize(writer, "Genres"); - return; - case Root.InstitutionsHpMarketing: - serializer.Serialize(writer, "InstitutionsHpMarketing"); - return; - case Root.RodizioBuckets: - serializer.Serialize(writer, "RodizioBuckets"); - return; - case Root.RodizioGenres: - serializer.Serialize(writer, "RodizioGenres"); - return; - case Root.ShortsPrime: - serializer.Serialize(writer, "ShortsPrime"); - return; - } - throw new Exception("Cannot marshal type Root"); - } - - public static readonly RootConverter Singleton = new RootConverter(); - } - - internal class ContentDeliveryTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(ContentDeliveryType) || t == typeof(ContentDeliveryType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "MultiPartBook": - return ContentDeliveryType.MultiPartBook; - case "Periodical": - return ContentDeliveryType.Periodical; - case "SinglePartBook": - return ContentDeliveryType.SinglePartBook; - } - throw new Exception("Cannot unmarshal type ContentDeliveryType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (ContentDeliveryType)untypedValue; - switch (value) - { - case ContentDeliveryType.MultiPartBook: - serializer.Serialize(writer, "MultiPartBook"); - return; - case ContentDeliveryType.Periodical: - serializer.Serialize(writer, "Periodical"); - return; - case ContentDeliveryType.SinglePartBook: - serializer.Serialize(writer, "SinglePartBook"); - return; - } - throw new Exception("Cannot marshal type ContentDeliveryType"); - } - - public static readonly ContentDeliveryTypeConverter Singleton = new ContentDeliveryTypeConverter(); - } - - internal class ParseStringConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - long l; - if (Int64.TryParse(value, out l)) - { - return l; - } - throw new Exception("Cannot unmarshal type long"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (long)untypedValue; - serializer.Serialize(writer, value.ToString()); - return; - } - - public static readonly ParseStringConverter Singleton = new ParseStringConverter(); - } - - internal class ContentTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(ContentType) || t == typeof(ContentType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "Episode": - return ContentType.Episode; - case "Lecture": - return ContentType.Lecture; - case "Meditation": - return ContentType.Meditation; - case "Misc": - return ContentType.Misc; - case "Performance": - return ContentType.Performance; - case "Product": - return ContentType.Product; - case "Radio/TV Program": - return ContentType.RadioTvProgram; - case "Show": - return ContentType.Show; - case "Speech": - return ContentType.Speech; - } - throw new Exception("Cannot unmarshal type ContentType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (ContentType)untypedValue; - switch (value) - { - case ContentType.Episode: - serializer.Serialize(writer, "Episode"); - return; - case ContentType.Lecture: - serializer.Serialize(writer, "Lecture"); - return; - case ContentType.Meditation: - serializer.Serialize(writer, "Meditation"); - return; - case ContentType.Misc: - serializer.Serialize(writer, "Misc"); - return; - case ContentType.Performance: - serializer.Serialize(writer, "Performance"); - return; - case ContentType.Product: - serializer.Serialize(writer, "Product"); - return; - case ContentType.RadioTvProgram: - serializer.Serialize(writer, "Radio/TV Program"); - return; - case ContentType.Show: - serializer.Serialize(writer, "Show"); - return; - case ContentType.Speech: - serializer.Serialize(writer, "Speech"); - return; - } - throw new Exception("Cannot marshal type ContentType"); - } - - public static readonly ContentTypeConverter Singleton = new ContentTypeConverter(); - } - - internal class CustomerReviewFormatConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(CustomerReviewFormat) || t == typeof(CustomerReviewFormat?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "Freeform": - return CustomerReviewFormat.Freeform; - case "Guided": - return CustomerReviewFormat.Guided; - } - throw new Exception("Cannot unmarshal type CustomerReviewFormat"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (CustomerReviewFormat)untypedValue; - switch (value) - { - case CustomerReviewFormat.Freeform: - serializer.Serialize(writer, "Freeform"); - return; - case CustomerReviewFormat.Guided: - serializer.Serialize(writer, "Guided"); - return; - } - throw new Exception("Cannot marshal type CustomerReviewFormat"); - } - - public static readonly CustomerReviewFormatConverter Singleton = new CustomerReviewFormatConverter(); - } - - internal class QuestionTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(QuestionType) || t == typeof(QuestionType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "Genre": - return QuestionType.Genre; - case "Misc": - return QuestionType.Misc; - case "Overall": - return QuestionType.Overall; - case "Performance": - return QuestionType.Performance; - case "Story": - return QuestionType.Story; - } - throw new Exception("Cannot unmarshal type QuestionType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (QuestionType)untypedValue; - switch (value) - { - case QuestionType.Genre: - serializer.Serialize(writer, "Genre"); - return; - case QuestionType.Misc: - serializer.Serialize(writer, "Misc"); - return; - case QuestionType.Overall: - serializer.Serialize(writer, "Overall"); - return; - case QuestionType.Performance: - serializer.Serialize(writer, "Performance"); - return; - case QuestionType.Story: - serializer.Serialize(writer, "Story"); - return; - } - throw new Exception("Cannot marshal type QuestionType"); - } - - public static readonly QuestionTypeConverter Singleton = new QuestionTypeConverter(); - } - - internal class FormatTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(FormatType) || t == typeof(FormatType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "abridged": - return FormatType.Abridged; - case "original_recording": - return FormatType.OriginalRecording; - case "unabridged": - return FormatType.Unabridged; - } - throw new Exception("Cannot unmarshal type FormatType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (FormatType)untypedValue; - switch (value) - { - case FormatType.Abridged: - serializer.Serialize(writer, "abridged"); - return; - case FormatType.OriginalRecording: - serializer.Serialize(writer, "original_recording"); - return; - case FormatType.Unabridged: - serializer.Serialize(writer, "unabridged"); - return; - } - throw new Exception("Cannot marshal type FormatType"); - } - - public static readonly FormatTypeConverter Singleton = new FormatTypeConverter(); - } - - internal class LanguageConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(Language) || t == typeof(Language?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - if (value == "english") - { - return Language.English; - } - throw new Exception("Cannot unmarshal type Language"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (Language)untypedValue; - if (value == Language.English) - { - serializer.Serialize(writer, "english"); - return; - } - throw new Exception("Cannot marshal type Language"); - } - - public static readonly LanguageConverter Singleton = new LanguageConverter(); - } - - internal class OriginMarketplaceConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(OriginMarketplace) || t == typeof(OriginMarketplace?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - if (value == "AF2M0KC94RCEA") - { - return OriginMarketplace.Af2M0Kc94Rcea; - } - throw new Exception("Cannot unmarshal type OriginMarketplace"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (OriginMarketplace)untypedValue; - if (value == OriginMarketplace.Af2M0Kc94Rcea) - { - serializer.Serialize(writer, "AF2M0KC94RCEA"); - return; - } - throw new Exception("Cannot marshal type OriginMarketplace"); - } - - public static readonly OriginMarketplaceConverter Singleton = new OriginMarketplaceConverter(); - } - - internal class OriginTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(OriginType) || t == typeof(OriginType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "AudibleChannels": - return OriginType.AudibleChannels; - case "AudibleComplimentaryOriginal": - return OriginType.AudibleComplimentaryOriginal; - case "Purchase": - return OriginType.Purchase; - } - throw new Exception("Cannot unmarshal type OriginType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (OriginType)untypedValue; - switch (value) - { - case OriginType.AudibleChannels: - serializer.Serialize(writer, "AudibleChannels"); - return; - case OriginType.AudibleComplimentaryOriginal: - serializer.Serialize(writer, "AudibleComplimentaryOriginal"); - return; - case OriginType.Purchase: - serializer.Serialize(writer, "Purchase"); - return; - } - throw new Exception("Cannot marshal type OriginType"); - } - - public static readonly OriginTypeConverter Singleton = new OriginTypeConverter(); - } - - internal class PlanNameConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(PlanName) || t == typeof(PlanName?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "AyceRomance": - return PlanName.AyceRomance; - case "ComplimentaryOriginalMemberBenefit": - return PlanName.ComplimentaryOriginalMemberBenefit; - case "Radio": - return PlanName.Radio; - case "Rodizio": - return PlanName.Rodizio; - case "SpecialBenefit": - return PlanName.SpecialBenefit; - } - throw new Exception("Cannot unmarshal type PlanName"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (PlanName)untypedValue; - switch (value) - { - case PlanName.AyceRomance: - serializer.Serialize(writer, "AyceRomance"); - return; - case PlanName.ComplimentaryOriginalMemberBenefit: - serializer.Serialize(writer, "ComplimentaryOriginalMemberBenefit"); - return; - case PlanName.Radio: - serializer.Serialize(writer, "Radio"); - return; - case PlanName.Rodizio: - serializer.Serialize(writer, "Rodizio"); - return; - case PlanName.SpecialBenefit: - serializer.Serialize(writer, "SpecialBenefit"); - return; - } - throw new Exception("Cannot marshal type PlanName"); - } - - public static readonly PlanNameConverter Singleton = new PlanNameConverter(); - } - - internal class CurrencyCodeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(CurrencyCode) || t == typeof(CurrencyCode?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - if (value == "USD") - { - return CurrencyCode.Usd; - } - throw new Exception("Cannot unmarshal type CurrencyCode"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (CurrencyCode)untypedValue; - if (value == CurrencyCode.Usd) - { - serializer.Serialize(writer, "USD"); - return; - } - throw new Exception("Cannot marshal type CurrencyCode"); - } - - public static readonly CurrencyCodeConverter Singleton = new CurrencyCodeConverter(); - } - - internal class MerchantIdConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(MerchantId) || t == typeof(MerchantId?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - if (value == "A2ZO8JX97D5MN9") - { - return MerchantId.A2Zo8Jx97D5Mn9; - } - throw new Exception("Cannot unmarshal type MerchantId"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (MerchantId)untypedValue; - if (value == MerchantId.A2Zo8Jx97D5Mn9) - { - serializer.Serialize(writer, "A2ZO8JX97D5MN9"); - return; - } - throw new Exception("Cannot marshal type MerchantId"); - } - - public static readonly MerchantIdConverter Singleton = new MerchantIdConverter(); - } - - internal class TypeEnumConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(TypeEnum) || t == typeof(TypeEnum?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "list": - return TypeEnum.List; - case "member": - return TypeEnum.Member; - case "sale": - return TypeEnum.Sale; - case "ws4v_upsell": - return TypeEnum.Ws4VUpsell; - } - throw new Exception("Cannot unmarshal type TypeEnum"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (TypeEnum)untypedValue; - switch (value) - { - case TypeEnum.List: - serializer.Serialize(writer, "list"); - return; - case TypeEnum.Member: - serializer.Serialize(writer, "member"); - return; - case TypeEnum.Sale: - serializer.Serialize(writer, "sale"); - return; - case TypeEnum.Ws4VUpsell: - serializer.Serialize(writer, "ws4v_upsell"); - return; - } - throw new Exception("Cannot marshal type TypeEnum"); - } - - public static readonly TypeEnumConverter Singleton = new TypeEnumConverter(); - } - - internal class RelationshipToProductConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(RelationshipToProduct) || t == typeof(RelationshipToProduct?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "child": - return RelationshipToProduct.Child; - case "parent": - return RelationshipToProduct.Parent; - } - throw new Exception("Cannot unmarshal type RelationshipToProduct"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (RelationshipToProduct)untypedValue; - switch (value) - { - case RelationshipToProduct.Child: - serializer.Serialize(writer, "child"); - return; - case RelationshipToProduct.Parent: - serializer.Serialize(writer, "parent"); - return; - } - throw new Exception("Cannot marshal type RelationshipToProduct"); - } - - public static readonly RelationshipToProductConverter Singleton = new RelationshipToProductConverter(); - } - - internal class RelationshipTypeConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(RelationshipType) || t == typeof(RelationshipType?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "component": - return RelationshipType.Component; - case "episode": - return RelationshipType.Episode; - case "merchant_title_authority": - return RelationshipType.MerchantTitleAuthority; - case "season": - return RelationshipType.Season; - case "series": - return RelationshipType.Series; - } - throw new Exception("Cannot unmarshal type RelationshipType"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (RelationshipType)untypedValue; - switch (value) - { - case RelationshipType.Component: - serializer.Serialize(writer, "component"); - return; - case RelationshipType.Episode: - serializer.Serialize(writer, "episode"); - return; - case RelationshipType.MerchantTitleAuthority: - serializer.Serialize(writer, "merchant_title_authority"); - return; - case RelationshipType.Season: - serializer.Serialize(writer, "season"); - return; - case RelationshipType.Series: - serializer.Serialize(writer, "series"); - return; - } - throw new Exception("Cannot marshal type RelationshipType"); - } - - public static readonly RelationshipTypeConverter Singleton = new RelationshipTypeConverter(); - } - - internal class StatusConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(Status); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - if (value == "Active") - { - return Status.Active; - } - throw new Exception("Cannot unmarshal type Status"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (Status)untypedValue; - if (value == Status.Active) - { - serializer.Serialize(writer, "Active"); - return; - } - throw new Exception("Cannot marshal type Status"); - } - - public static readonly StatusConverter Singleton = new StatusConverter(); - } - - internal class ThesaurusSubjectKeywordConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(ThesaurusSubjectKeyword) || t == typeof(ThesaurusSubjectKeyword?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) return null; - var value = serializer.Deserialize(reader); - switch (value) - { - case "adventurers_&_explorers": - return ThesaurusSubjectKeyword.AdventurersExplorers; - case "alternate_history": - return ThesaurusSubjectKeyword.AlternateHistory; - case "comedians": - return ThesaurusSubjectKeyword.Comedians; - case "contemporary": - return ThesaurusSubjectKeyword.Contemporary; - case "dramatizations": - return ThesaurusSubjectKeyword.Dramatizations; - case "eastern_religions": - return ThesaurusSubjectKeyword.EasternReligions; - case "la_confidential": - return ThesaurusSubjectKeyword.LaConfidential; - case "literature-and-fiction": - return ThesaurusSubjectKeyword.LiteratureAndFiction; - case "medicine": - return ThesaurusSubjectKeyword.Medicine; - case "spirituality": - return ThesaurusSubjectKeyword.Spirituality; - case "standup_comedy": - return ThesaurusSubjectKeyword.StandupComedy; - case "storytelling": - return ThesaurusSubjectKeyword.Storytelling; - case "sword_&_sorcery": - return ThesaurusSubjectKeyword.SwordSorcery; - case "workouts": - return ThesaurusSubjectKeyword.Workouts; - } - throw new Exception("Cannot unmarshal type ThesaurusSubjectKeyword"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - var value = (ThesaurusSubjectKeyword)untypedValue; - switch (value) - { - case ThesaurusSubjectKeyword.AdventurersExplorers: - serializer.Serialize(writer, "adventurers_&_explorers"); - return; - case ThesaurusSubjectKeyword.AlternateHistory: - serializer.Serialize(writer, "alternate_history"); - return; - case ThesaurusSubjectKeyword.Comedians: - serializer.Serialize(writer, "comedians"); - return; - case ThesaurusSubjectKeyword.Contemporary: - serializer.Serialize(writer, "contemporary"); - return; - case ThesaurusSubjectKeyword.Dramatizations: - serializer.Serialize(writer, "dramatizations"); - return; - case ThesaurusSubjectKeyword.EasternReligions: - serializer.Serialize(writer, "eastern_religions"); - return; - case ThesaurusSubjectKeyword.LaConfidential: - serializer.Serialize(writer, "la_confidential"); - return; - case ThesaurusSubjectKeyword.LiteratureAndFiction: - serializer.Serialize(writer, "literature-and-fiction"); - return; - case ThesaurusSubjectKeyword.Medicine: - serializer.Serialize(writer, "medicine"); - return; - case ThesaurusSubjectKeyword.Spirituality: - serializer.Serialize(writer, "spirituality"); - return; - case ThesaurusSubjectKeyword.StandupComedy: - serializer.Serialize(writer, "standup_comedy"); - return; - case ThesaurusSubjectKeyword.Storytelling: - serializer.Serialize(writer, "storytelling"); - return; - case ThesaurusSubjectKeyword.SwordSorcery: - serializer.Serialize(writer, "sword_&_sorcery"); - return; - case ThesaurusSubjectKeyword.Workouts: - serializer.Serialize(writer, "workouts"); - return; - } - throw new Exception("Cannot marshal type ThesaurusSubjectKeyword"); - } - - public static readonly ThesaurusSubjectKeywordConverter Singleton = new ThesaurusSubjectKeywordConverter(); - } -} diff --git a/DTOs/LibraryApiV10.custom.cs b/DTOs/LibraryApiV10.custom.cs deleted file mode 100644 index cde356a8..00000000 --- a/DTOs/LibraryApiV10.custom.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dinah.Core.Collections.Generic; - -namespace DTOs -{ - public partial class LibraryApiV10 - { - public IEnumerable AuthorsDistinct => Items.GetAuthorsDistinct(); - public IEnumerable NarratorsDistinct => Items.GetNarratorsDistinct(); - public IEnumerable PublishersDistinct => Items.GetPublishersDistinct(); - public IEnumerable SeriesDistinct => Items.GetSeriesDistinct(); - public IEnumerable ParentCategoriesDistinct => Items.GetParentCategoriesDistinct(); - public IEnumerable ChildCategoriesDistinct => Items.GetChildCategoriesDistinct(); - - public override string ToString() => $"{Items.Length} {nameof(Items)}, {ResponseGroups.Length} {nameof(ResponseGroups)}"; - } - public partial class Item - { - public string ProductId => Asin; - public int LengthInMinutes => RuntimeLengthMin ?? 0; - public string Description => PublisherSummary; - public bool Episodes - => Relationships - ?.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode) - .Any() - ?? false; - public string PictureId => ProductImages?.PictureId; - public string SupplementUrls => PdfUrl.AbsoluteUri; // item.PdfUrl == item.PdfLink - public DateTime DateAdded => PurchaseDate.UtcDateTime; - - public float Product_OverallStars => Convert.ToSingle(Rating?.OverallDistribution.DisplayStars ?? 0); - public float Product_PerformanceStars => Convert.ToSingle(Rating?.PerformanceDistribution.DisplayStars ?? 0); - public float Product_StoryStars => Convert.ToSingle(Rating?.StoryDistribution.DisplayStars ?? 0); - - public int MyUserRating_Overall => Convert.ToInt32(ProvidedReview?.Ratings.OverallRating ?? 0L); - public int MyUserRating_Performance => Convert.ToInt32(ProvidedReview?.Ratings.PerformanceRating ?? 0L); - public int MyUserRating_Story => Convert.ToInt32(ProvidedReview?.Ratings.StoryRating ?? 0L); - - public bool IsAbridged - => FormatType.HasValue - ? FormatType == DTOs.FormatType.Abridged - : false; - public DateTime? DatePublished => IssueDate?.UtcDateTime; // item.IssueDate == item.ReleaseDate - public string Publisher => PublisherName; - - // these category properties assume: - // - we're only exposing 1 category, irrespective of how many the Item actually has - // - each ladder will have either 1 or 2 levels: parent and optional child - public Ladder[] Categories => CategoryLadders?.FirstOrDefault()?.Ladder ?? new Ladder[0]; - public Ladder ParentCategory => Categories[0]; - public Ladder ChildCategory => Categories.Length > 1 ? Categories[1] : null; - - // LibraryDTO.DownloadBookLink will be handled differently. see api.DownloadAaxWorkaroundAsync(asin) - - public IEnumerable AuthorsDistinct => Authors.DistinctBy(a => new { a.Name, a.Asin }); - public IEnumerable NarratorsDistinct => Narrators.DistinctBy(a => new { a.Name, a.Asin }); - - public override string ToString() => $"[{ProductId}] {Title}"; - } - public partial class Person - { - public override string ToString() => $"{Name}"; - } - public partial class AvailableCodec - { - public override string ToString() => $"{Name} {Format} {EnhancedCodec}"; - } - public partial class CategoryLadder - { - public override string ToString() => Ladder.Select(l => l.CategoryName).Aggregate((a, b) => $"{a} | {b}"); - } - public partial class Ladder - { - public string CategoryId => Id; - public string CategoryName => Name; - - public override string ToString() => $"[{CategoryId}] {CategoryName}"; - } - public partial class ContentRating - { - public override string ToString() => $"{Steaminess}"; - } - public partial class Review - { - public override string ToString() => $"{this.Title}"; - } - public partial class GuidedResponse - { - //public override string ToString() => - } - public partial class Ratings - { - public override string ToString() => $"{OverallRating:0.0}|{PerformanceRating:0.0}|{StoryRating:0.0}"; - } - public partial class ReviewContentScores - { - public override string ToString() => $"Helpful={NumHelpfulVotes}, Unhelpful={NumUnhelpfulVotes}"; - } - public partial class Plan - { - public override string ToString() => $"{PlanName}"; - } - public partial class Price - { - public override string ToString() => $"List={ListPrice}, Lowest={LowestPrice}"; - } - public partial class ListPriceClass - { - public override string ToString() => $"{Base}"; - } - public partial class ProductImages - { - public string PictureId - => The500 - .AbsoluteUri // https://m.media-amazon.com/images/I/51T1NWIkR4L._SL500_.jpg?foo=bar - ?.Split('/').Last() // 51T1NWIkR4L._SL500_.jpg?foo=bar - ?.Split('.').First() // 51T1NWIkR4L - ; - - public override string ToString() => $"{The500}"; - } - public partial class Rating - { - public override string ToString() => $"{OverallDistribution}|{PerformanceDistribution}|{StoryDistribution}"; - } - public partial class Distribution - { - public override string ToString() => $"{DisplayStars:0.0}"; - } - public partial class Relationship - { - public override string ToString() => $"{RelationshipToProduct} {RelationshipType}"; - } - public partial class Series - { - public string SeriesName => Title; - public string SeriesId => Asin; - public float Index - => string.IsNullOrEmpty(Sequence) - ? 0 - // eg: a book containing volumes 5,6,7,8 has sequence "5-8" - : float.Parse(Sequence.Split('-').First()); - - public override string ToString() => $"[{SeriesId}] {SeriesName}"; - } -} diff --git a/DTOs/LibraryApiV10Extensions.cs b/DTOs/LibraryApiV10Extensions.cs deleted file mode 100644 index 731e58e6..00000000 --- a/DTOs/LibraryApiV10Extensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dinah.Core.Collections.Generic; - -namespace DTOs -{ - public static class LibraryApiV10Extensions - { - public static IEnumerable GetAuthorsDistinct(this IEnumerable items) - => items.SelectMany(i => i.Authors).DistinctBy(a => new { a.Name, a.Asin }); - - public static IEnumerable GetNarratorsDistinct(this IEnumerable items) - => items.SelectMany(i => i.Narrators, (i, n) => n.Name).Distinct(); - - public static IEnumerable GetPublishersDistinct(this IEnumerable items) - => items.Select(i => i.Publisher).Distinct(); - - public static IEnumerable GetSeriesDistinct(this IEnumerable items) - => items.SelectMany(i => i.Series).DistinctBy(s => new { s.SeriesName, s.SeriesId }); - - public static IEnumerable GetParentCategoriesDistinct(this IEnumerable items) - => items.Select(l => l.ParentCategory).DistinctBy(l => new { l.CategoryName, l.CategoryId }); - - public static IEnumerable GetChildCategoriesDistinct(this IEnumerable items) - => items - .Select(l => l.ChildCategory) - .Where(l => l != null) - .DistinctBy(l => new { l.CategoryName, l.CategoryId }); - } -} diff --git a/DataLayer/UNTESTED/EfClasses/Book.cs b/DataLayer/UNTESTED/EfClasses/Book.cs index adc2a8ee..dc7e40c4 100644 --- a/DataLayer/UNTESTED/EfClasses/Book.cs +++ b/DataLayer/UNTESTED/EfClasses/Book.cs @@ -62,8 +62,7 @@ namespace DataLayer string title, string description, int lengthInMinutes, - IEnumerable authors, - IEnumerable narrators) + IEnumerable authors) { // validate ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId)); @@ -88,7 +87,7 @@ namespace DataLayer // assigns with biz logic ReplaceAuthors(authors); - ReplaceNarrators(narrators); + //ReplaceNarrators(narrators); // import previously saved tags // do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost @@ -218,7 +217,6 @@ namespace DataLayer // Are automatically loaded, and can only be tracked by a DbContext alongside their owner." ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url)); - url = FileManager.FileUtility.RestoreDeclawed(url); if (!_supplements.Any(s => url.EqualsInsensitive(url))) _supplements.Add(new Supplement(this, url)); diff --git a/DataLayer/UNTESTED/EfClasses/LibraryBook.cs b/DataLayer/UNTESTED/EfClasses/LibraryBook.cs index 3146e921..e281edb9 100644 --- a/DataLayer/UNTESTED/EfClasses/LibraryBook.cs +++ b/DataLayer/UNTESTED/EfClasses/LibraryBook.cs @@ -10,16 +10,18 @@ namespace DataLayer public DateTime DateAdded { get; private set; } - /// For downloading AAX file - public string DownloadBookLink { get; private set; } +/// For downloading AAX file +public string DownloadBookLink { get; private set; } private LibraryBook() { } - public LibraryBook(Book book, DateTime dateAdded, string downloadBookLink) + public LibraryBook(Book book, DateTime dateAdded +, string downloadBookLink = null +) { ArgumentValidator.EnsureNotNull(book, nameof(book)); Book = book; DateAdded = dateAdded; - DownloadBookLink = downloadBookLink; +DownloadBookLink = downloadBookLink; } } } diff --git a/DataLayer/UNTESTED/EfClasses/Rating.cs b/DataLayer/UNTESTED/EfClasses/Rating.cs index 22adb22b..40421eba 100644 --- a/DataLayer/UNTESTED/EfClasses/Rating.cs +++ b/DataLayer/UNTESTED/EfClasses/Rating.cs @@ -64,11 +64,11 @@ namespace DataLayer var items = new List(); if (OverallRating > 0) - items.Add($"Overall: {getStars(OverallRating)}"); + items.Add($"Overall: {getStars(OverallRating)}"); if (PerformanceRating > 0) items.Add($"Perform: {getStars(PerformanceRating)}"); if (StoryRating > 0) - items.Add($"Story: {getStars(StoryRating)}"); + items.Add($"Story: {getStars(StoryRating)}"); return string.Join("\r\n", items); } diff --git a/DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs b/DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs index a3e047c2..cef556e2 100644 --- a/DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs +++ b/DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs @@ -11,7 +11,7 @@ namespace DataLayer using var context = LibationContext.Create(); return context .Library - .AsNoTracking() +//.AsNoTracking() .GetLibrary() .ToList(); } @@ -21,8 +21,8 @@ namespace DataLayer using var context = LibationContext.Create(); return context .Library - .AsNoTracking() - .GetLibraryBook(productId); +//.AsNoTracking() + .GetLibraryBook(productId); } /// This is still IQueryable. YOU MUST CALL ToList() YOURSELF diff --git a/DtoImporterService/BookImporter.cs b/DtoImporterService/BookImporter.cs new file mode 100644 index 00000000..a0b67140 --- /dev/null +++ b/DtoImporterService/BookImporter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public class BookImporter : ItemsImporterBase + { + public override IEnumerable Validate(IEnumerable items) + { + var exceptions = new List(); + + if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId))) + exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items))); + if (items.Any(i => string.IsNullOrWhiteSpace(i.Title))) + exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.Title)}", nameof(items))); + if (items.Any(i => i.Authors is null)) + exceptions.Add(new ArgumentException($"Collection contains item(s) with null {nameof(Item.Authors)}", nameof(items))); + + return exceptions; + } + + protected override int DoImport(IEnumerable items, LibationContext context) + { + // pre-req.s + new ContributorImporter().Import(items, context); + new SeriesImporter().Import(items, context); + new CategoryImporter().Import(items, context); + + // get distinct + var productIds = items.Select(i => i.ProductId).ToList(); + + // load db existing => .Local + loadLocal_books(productIds, context); + + // upsert + var qtyNew = upsertBooks(items, context); + return qtyNew; + } + + private void loadLocal_books(List productIds, LibationContext context) + { + var localProductIds = context.Books.Local.Select(b => b.AudibleProductId); + var remainingProductIds = productIds + .Distinct() + .Except(localProductIds) + .ToList(); + + // GetBooks() eager loads Series, category, et al + if (remainingProductIds.Any()) + context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList(); + } + + private int upsertBooks(IEnumerable items, LibationContext context) + { + var qtyNew = 0; + + foreach (var item in items) + { + var book = context.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.ProductId); + if (book is null) + { + // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db + var authors = item + .Authors + .Select(a => context.Contributors.Local.Single(c => a.Name == c.Name)) + .ToList(); + + book = context.Books.Add(new Book( + new AudibleProductId(item.ProductId), item.Title, item.Description, item.LengthInMinutes, authors)) + .Entity; + + qtyNew++; + } + + // if no narrators listed, author is the narrator + if (item.Narrators is null || !item.Narrators.Any()) + item.Narrators = item.Authors; + // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db + var narrators = item + .Narrators + .Select(n => context.Contributors.Local.Single(c => n.Name == c.Name)) + .ToList(); + // not all books have narrators. these will already be using author as narrator. don't undo this + if (narrators.Any()) + book.ReplaceNarrators(narrators); + + // set/update book-specific info which may have changed + book.PictureId = item.PictureId; + book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars); + if (!string.IsNullOrWhiteSpace(item.SupplementUrl)) + book.AddSupplementDownloadUrl(item.SupplementUrl); + + var publisherName = item.Publisher; + if (!string.IsNullOrWhiteSpace(publisherName)) + { + var publisher = context.Contributors.Local.Single(c => publisherName == c.Name); + book.ReplacePublisher(publisher); + } + + // important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import + book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story); + + // + // this was round 1 when it was a 2 step process + // + //// update series even for existing books. these are occasionally updated + //var seriesIds = item.Series.Select(kvp => kvp.SeriesId).ToList(); + //var allSeries = context.Series.Local.Where(c => seriesIds.Contains(c.AudibleSeriesId)).ToList(); + //foreach (var series in allSeries) + // book.UpsertSeries(series); + + // these will upsert over library-scraped series, but will not leave orphans + if (item.Series != null) + { + foreach (var seriesEntry in item.Series) + { + var series = context.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId); + book.UpsertSeries(series, seriesEntry.Index); + } + } + + // categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd + var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == item.Categories.LastOrDefault().CategoryId); + if (category != null) + book.UpdateCategory(category, context); + + book.UpdateBookDetails(item.IsAbridged, item.DatePublished); + } + + return qtyNew; + } + } +} diff --git a/DtoImporterService/CategoryImporter.cs b/DtoImporterService/CategoryImporter.cs new file mode 100644 index 00000000..707f8c96 --- /dev/null +++ b/DtoImporterService/CategoryImporter.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public class CategoryImporter : ItemsImporterBase + { + public override IEnumerable Validate(IEnumerable items) + { + var exceptions = new List(); + + var distinct = items.GetCategoriesDistinct(); + if (distinct.Any(s => s.CategoryId is null)) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryId)}", nameof(items))); + if (distinct.Any(s => s.CategoryName is null)) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryName)}", nameof(items))); + + if (items.GetCategoryPairsDistinct().Any(p => p.Length > 2)) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with wrong number of categories. Expecting 0, 1, or 2 categories per title", nameof(items))); + + return exceptions; + } + + protected override int DoImport(IEnumerable items, LibationContext context) + { + // get distinct + var categoryIds = items.GetCategoriesDistinct().Select(c => c.CategoryId).ToList(); + + // load db existing => .Local + loadLocal_categories(categoryIds, context); + + // upsert + var categoryPairs = items.GetCategoryPairsDistinct().ToList(); + var qtyNew = upsertCategories(categoryPairs, context); + return qtyNew; + } + + private void loadLocal_categories(List categoryIds, LibationContext context) + { + var localIds = context.Categories.Local.Select(c => c.AudibleCategoryId); + var remainingCategoryIds = categoryIds + .Distinct() + .Except(localIds) + .ToList(); + + if (remainingCategoryIds.Any()) + context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId)).ToList(); + } + + // only use after loading contributors => local + private int upsertCategories(List categoryPairs, LibationContext context) + { + var qtyNew = 0; + + foreach (var pair in categoryPairs) + { + for (var i = 0; i < pair.Length; i++) + { + var id = pair[i].CategoryId; + var name = pair[i].CategoryName; + + Category parentCategory = null; + if (i == 1) + parentCategory = context.Categories.Local.Single(c => c.AudibleCategoryId == pair[0].CategoryId); + + var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == id); + if (category is null) + { + category = context.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity; + qtyNew++; + } + + category.UpdateParentCategory(parentCategory); + } + } + + return qtyNew; + } + } +} diff --git a/DtoImporterService/ContributorImporter.cs b/DtoImporterService/ContributorImporter.cs new file mode 100644 index 00000000..f76830b4 --- /dev/null +++ b/DtoImporterService/ContributorImporter.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public class ContributorImporter : ItemsImporterBase + { + public override IEnumerable Validate(IEnumerable items) + { + var exceptions = new List(); + + if (items.GetAuthorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name))) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Authors)} with null {nameof(Person.Name)}", nameof(items))); + if (items.GetNarratorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name))) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Narrators)} with null {nameof(Person.Name)}", nameof(items))); + + return exceptions; + } + + protected override int DoImport(IEnumerable items, LibationContext context) + { + // get distinct + var authors = items.GetAuthorsDistinct().ToList(); + var narrators = items.GetNarratorsDistinct().ToList(); + var publishers = items.GetPublishersDistinct().ToList(); + + // load db existing => .Local + var allNames = authors + .Select(a => a.Name) + .Union(narrators.Select(n => n.Name)) + .Union(publishers) + .ToList(); + loadLocal_contributors(allNames, context); + + // upsert + var qtyNew = 0; + qtyNew += upsertPeople(authors, context); + qtyNew += upsertPeople(narrators, context); + qtyNew += upsertPublishers(publishers, context); + return qtyNew; + } + + private void loadLocal_contributors(List contributorNames, LibationContext context) + { + contributorNames.Remove(null); + contributorNames.Remove(""); + + //// BAD: very inefficient + // var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name)); + + // GOOD: Except() is efficient. Due to hashing, it's close to O(n) + var localNames = context.Contributors.Local.Select(c => c.Name); + var remainingContribNames = contributorNames + .Distinct() + .Except(localNames) + .ToList(); + + // load existing => local + if (remainingContribNames.Any()) + context.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList(); + // _________________________________^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // i tried to extract this pattern, but this part prohibits doing so + // wouldn't work anyway for Books.GetBooks() + } + + // only use after loading contributors => local + private int upsertPeople(List people, LibationContext context) + { + var qtyNew = 0; + + foreach (var p in people) + { + var person = context.Contributors.Local.SingleOrDefault(c => c.Name == p.Name); + if (person == null) + { + person = context.Contributors.Add(new Contributor(p.Name)).Entity; + qtyNew++; + } + + person.UpdateAudibleAuthorId(p.Asin); + } + + return qtyNew; + } + + // only use after loading contributors => local + private int upsertPublishers(List publishers, LibationContext context) + { + var qtyNew = 0; + + foreach (var publisherName in publishers) + { + if (context.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null) + { + context.Contributors.Add(new Contributor(publisherName)); + qtyNew++; + } + } + + return qtyNew; + } + } +} diff --git a/DtoImporterService/DtoImporterService.csproj b/DtoImporterService/DtoImporterService.csproj new file mode 100644 index 00000000..87abb7e9 --- /dev/null +++ b/DtoImporterService/DtoImporterService.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + + + + + + + + diff --git a/DtoImporterService/ImporterBase.cs b/DtoImporterService/ImporterBase.cs new file mode 100644 index 00000000..a7dfbb5d --- /dev/null +++ b/DtoImporterService/ImporterBase.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public interface IContextRunner + { + public TResult Run(Func func, T param, LibationContext context = null) + { + if (context is null) + { + using (context = LibationContext.Create()) + { + var r = Run(func, param, context); + context.SaveChanges(); + return r; + } + } + + var exceptions = Validate(param); + if (exceptions != null && exceptions.Any()) + throw new AggregateException($"Device Jobs Service configuration validation failed", exceptions); + + var result = func(param, context); + return result; + } + IEnumerable Validate(T param); + } + + public abstract class ImporterBase : IContextRunner + { + /// LONG RUNNING. call with await Task.Run + public int Import(T param, LibationContext context = null) + => ((IContextRunner)this).Run(DoImport, param, context); + + protected abstract int DoImport(T elements, LibationContext context); + public abstract IEnumerable Validate(T param); + } + + public abstract class ItemsImporterBase : ImporterBase> { } +} diff --git a/DtoImporterService/LibraryImporter.cs b/DtoImporterService/LibraryImporter.cs new file mode 100644 index 00000000..0de82371 --- /dev/null +++ b/DtoImporterService/LibraryImporter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public class LibraryImporter : ItemsImporterBase + { + public override IEnumerable Validate(IEnumerable items) + { + var exceptions = new List(); + + if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId))) + exceptions.Add(new ArgumentException($"Collection contains item(s) with null or blank {nameof(Item.ProductId)}", nameof(items))); + if (items.Any(i => i.DateAdded < new DateTime(1980, 1, 1))) + exceptions.Add(new ArgumentException($"Collection contains item(s) with invalid {nameof(Item.DateAdded)}", nameof(items))); + + return exceptions; + } + + protected override int DoImport(IEnumerable items, LibationContext context) + { + new BookImporter().Import(items, context); + + var qtyNew = upsertLibraryBooks(items, context); + return qtyNew; + } + + private int upsertLibraryBooks(IEnumerable items, LibationContext context) + { + var currentLibraryProductIds = context.Library.Select(l => l.Book.AudibleProductId).ToList(); + var newItems = items.Where(dto => !currentLibraryProductIds.Contains(dto.ProductId)).ToList(); + + foreach (var newItem in newItems) + { + var libraryBook = new LibraryBook( + context.Books.Local.Single(b => b.AudibleProductId == newItem.ProductId), + newItem.DateAdded +//,FileManager.FileUtility.RestoreDeclawed(newLibraryDTO.DownloadBookLink) + ); + context.Library.Add(libraryBook); + } + + var qtyNew = newItems.Count; + return qtyNew; + } + } +} diff --git a/DtoImporterService/SeriesImporter.cs b/DtoImporterService/SeriesImporter.cs new file mode 100644 index 00000000..8e74aff4 --- /dev/null +++ b/DtoImporterService/SeriesImporter.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApiDTOs; +using DataLayer; + +namespace DtoImporterService +{ + public class SeriesImporter : ItemsImporterBase + { + public override IEnumerable Validate(IEnumerable items) + { + var exceptions = new List(); + + var distinct = items .GetSeriesDistinct(); + if (distinct.Any(s => s.SeriesId is null)) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesId)}", nameof(items))); + if (distinct.Any(s => s.SeriesName is null)) + exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesName)}", nameof(items))); + + return exceptions; + } + + protected override int DoImport(IEnumerable items, LibationContext context) + { + // get distinct + var series = items.GetSeriesDistinct().ToList(); + + // load db existing => .Local + var seriesIds = series.Select(s => s.SeriesId).ToList(); + loadLocal_series(seriesIds, context); + + // upsert + var qtyNew = upsertSeries(series, context); + return qtyNew; + } + + private void loadLocal_series(List seriesIds, LibationContext context) + { + var localIds = context.Series.Local.Select(s => s.AudibleSeriesId); + var remainingSeriesIds = seriesIds + .Distinct() + .Except(localIds) + .ToList(); + + if (remainingSeriesIds.Any()) + context.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList(); + } + + private int upsertSeries(List requestedSeries, LibationContext context) + { + var qtyNew = 0; + + foreach (var s in requestedSeries) + { + var series = context.Series.Local.SingleOrDefault(c => c.AudibleSeriesId == s.SeriesId); + if (series is null) + { + series = context.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity; + qtyNew++; + } + series.UpdateName(s.SeriesName); + } + + return qtyNew; + } + } +} diff --git a/InternalUtilities/InternalUtilities.csproj b/InternalUtilities/InternalUtilities.csproj index 6493853d..367bd677 100644 --- a/InternalUtilities/InternalUtilities.csproj +++ b/InternalUtilities/InternalUtilities.csproj @@ -5,6 +5,7 @@ + diff --git a/InternalUtilities/UNTESTED/AudibleApiExtensions.cs b/InternalUtilities/UNTESTED/AudibleApiExtensions.cs new file mode 100644 index 00000000..0cb61210 --- /dev/null +++ b/InternalUtilities/UNTESTED/AudibleApiExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AudibleApi; +using AudibleApiDTOs; + +// +// probably not the best place for this +// but good enough for now +// +namespace InternalUtilities +{ + public static class AudibleApiExtensions + { + public static async Task> GetAllLibraryItemsAsync(this Api api) + { + var allItems = new List(); + + for (var i = 1; ; i++) + { + var page = await api.GetLibraryAsync(new LibraryOptions + { + NumberOfResultPerPage = 1000, + PageNumber = i, + PurchasedAfter = new DateTime(2000, 1, 1), + ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS + }); + + // important! use this convert method + var libResult = LibraryApiV10.FromJson(page.ToString()); + + if (!libResult.Items.Any()) + break; + + allItems.AddRange(libResult.Items); + } + + return allItems; + } + } +} diff --git a/Libation.sln b/Libation.sln index 1c6c06c0..3f86fcf8 100644 --- a/Libation.sln +++ b/Libation.sln @@ -57,8 +57,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitAllRepos", "..\GitAllRep EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\AudibleApiClientExample\AudibleApiClientExample.csproj", "{959D01B4-5EF8-4D4E-BE06-AB1A580B0B52}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Tests", "..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj", "{111420E2-D4F0-4068-B46A-C4B6DCC823DC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForm", "LibationWinForm\LibationWinForm.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}" @@ -81,9 +79,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\Lucene EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2.Tests", "..\LuceneNet303r2\LuceneNet303r2.Tests\LuceneNet303r2.Tests.csproj", "{5A7681A5-60D9-480B-9AC7-63E0812A2548}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DTOs", "DTOs\DTOs.csproj", "{5FDA62B1-55FD-407A-BECA-38A969235541}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDomainService", "AudibleApiDomainService\AudibleApiDomainService.csproj", "{A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs", "..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj", "{C03C5D65-3B7F-453B-972F-23950B7E0604}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs.Tests", "..\audible api\AudibleApi\_Tests\AudibleApiDTOs.Tests\AudibleApiDTOs.Tests.csproj", "{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\_Demos\AudibleApiClientExample\AudibleApiClientExample.csproj", "{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -151,10 +153,6 @@ Global {7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.Build.0 = Release|Any CPU - {959D01B4-5EF8-4D4E-BE06-AB1A580B0B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {959D01B4-5EF8-4D4E-BE06-AB1A580B0B52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {959D01B4-5EF8-4D4E-BE06-AB1A580B0B52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {959D01B4-5EF8-4D4E-BE06-AB1A580B0B52}.Release|Any CPU.Build.0 = Release|Any CPU {111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -199,14 +197,22 @@ Global {5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.Build.0 = Release|Any CPU - {5FDA62B1-55FD-407A-BECA-38A969235541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FDA62B1-55FD-407A-BECA-38A969235541}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FDA62B1-55FD-407A-BECA-38A969235541}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FDA62B1-55FD-407A-BECA-38A969235541}.Release|Any CPU.Build.0 = Release|Any CPU - {A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050}.Release|Any CPU.Build.0 = Release|Any CPU + {401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU + {C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.Build.0 = Release|Any CPU + {6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.Build.0 = Release|Any CPU + {282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -227,7 +233,6 @@ Global {FF12ADA0-8975-4E67-B6EA-4AC82E0C8994} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD} {AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD} {7EA01F9C-E579-4B01-A3B9-733B49DD0B60} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} - {959D01B4-5EF8-4D4E-BE06-AB1A580B0B52} = {F61184E7-2426-4A13-ACEF-5689928E2CE2} {111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD} {635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14} @@ -239,8 +244,10 @@ Global {1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1} {35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD} - {5FDA62B1-55FD-407A-BECA-38A969235541} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} - {A1AB4B4B-6855-4BD0-BC54-C2FFDB20E050} = {41CDCC73-9B81-49DD-9570-C54406E852AF} + {401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF} + {C03C5D65-3B7F-453B-972F-23950B7E0604} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} + {6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD} + {282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9} diff --git a/LibationWinForm/LibationWinForm.csproj b/LibationWinForm/LibationWinForm.csproj index f08a9949..abb394e5 100644 --- a/LibationWinForm/LibationWinForm.csproj +++ b/LibationWinForm/LibationWinForm.csproj @@ -11,7 +11,7 @@ - + diff --git a/LibationWinForm/UNTESTED/BookLiberation/ProcessorAutomationController.cs b/LibationWinForm/UNTESTED/BookLiberation/ProcessorAutomationController.cs index 21ff2729..4f31c189 100644 --- a/LibationWinForm/UNTESTED/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForm/UNTESTED/BookLiberation/ProcessorAutomationController.cs @@ -83,8 +83,8 @@ namespace LibationWinForm.BookLiberation // close form on DOWNLOAD completed, not final Completed. Else for BackupBook this form won't close until DECRYPT is also complete void fileDownloadCompleted(object _, string __) => downloadDialog.Close(); - void downloadProgressChanged(object _, System.Net.DownloadProgressChangedEventArgs arg) - => downloadDialog.DownloadProgressChanged(arg.BytesReceived, arg.TotalBytesToReceive); + void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress) + => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value); void unsubscribe(object _ = null, EventArgs __ = null) { diff --git a/LibationWinForm/UNTESTED/Dialogs/Login/WinformResponder.cs b/LibationWinForm/UNTESTED/Dialogs/Login/WinformResponder.cs index 53511a86..d9462ceb 100644 --- a/LibationWinForm/UNTESTED/Dialogs/Login/WinformResponder.cs +++ b/LibationWinForm/UNTESTED/Dialogs/Login/WinformResponder.cs @@ -3,7 +3,7 @@ using LibationWinForm.Dialogs.Login; namespace LibationWinForm.Login { - public class WinformResponder : AudibleApiDomainService.IAudibleApiResponder + public class WinformResponder : AudibleApi.ILoginCallback { public string Get2faCode() { diff --git a/LibationWinForm/UNTESTED/Form1.cs b/LibationWinForm/UNTESTED/Form1.cs index 5c891c9a..66210980 100644 --- a/LibationWinForm/UNTESTED/Form1.cs +++ b/LibationWinForm/UNTESTED/Form1.cs @@ -357,18 +357,48 @@ namespace LibationWinForm private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e) { - // audible api - var settings = new AudibleApiDomainService.Settings(config); - var responder = new Login.WinformResponder(); - var client = await AudibleApiDomainService.AudibleApiLibationClient.CreateClientAsync(settings, responder); - -await client.ImportLibraryAsync(); - - // scrape - await indexDialog(new ScanLibraryDialog()); +// legacy/scraping method + //await indexDialog(new ScanLibraryDialog()); +// new/api method +await audibleApi(); } - private async void reimportMostRecentLibraryScanToolStripMenuItem_Click(object sender, EventArgs e) + private async Task audibleApi() + { + var identityFilePath = System.IO.Path.Combine(config.LibationFiles, "IdentityTokens.json"); + var callback = new Login.WinformResponder(); + var api = await AudibleApi.EzApiCreator.GetApiAsync(identityFilePath, callback, config.LocaleCountryCode); + + int totalCount; + int newCount; + + // seems to be very common the 1st time after long absence. either figure out why, or run 2x before declaring error + try + { + var items = await InternalUtilities.AudibleApiExtensions.GetAllLibraryItemsAsync(api); + totalCount = items.Count; + newCount = await Task.Run(() => new DtoImporterService.LibraryImporter().Import(items)); + } + catch + { + try + { + var items = await InternalUtilities.AudibleApiExtensions.GetAllLibraryItemsAsync(api); + totalCount = items.Count; + newCount = await Task.Run(() => new DtoImporterService.LibraryImporter().Import(items)); + } + catch (Exception ex) + { + MessageBox.Show("Error importing library.\r\n" + ex.Message, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + + await InternalUtilities.SearchEngineActions.FullReIndexAsync(); + await indexComplete(totalCount, newCount); + } + + private async void reimportMostRecentLibraryScanToolStripMenuItem_Click(object sender, EventArgs e) { // DO NOT ConfigureAwait(false) // this would result in index() => reloadGrid() => setGrid() => "gridPanel.Controls.Remove(currProductsGrid);" @@ -377,15 +407,15 @@ await client.ImportLibraryAsync(); MessageBox.Show($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}"); - await index(NewBooksAdded, TotalBooksProcessed); + await indexComplete(TotalBooksProcessed, NewBooksAdded); } private async Task indexDialog(IIndexLibraryDialog dialog) { if (!dialog.RunDialog().In(DialogResult.Abort, DialogResult.Cancel, DialogResult.None)) - await index(dialog.NewBooksAdded, dialog.TotalBooksProcessed); + await indexComplete(dialog.TotalBooksProcessed, dialog.NewBooksAdded); } - private async Task index(int newBooksAdded, int totalBooksProcessed) + private async Task indexComplete(int totalBooksProcessed, int newBooksAdded) { // update backup counts if we have new library items if (newBooksAdded > 0) diff --git a/LibationWinForm/UNTESTED/GridEntry.cs b/LibationWinForm/UNTESTED/GridEntry.cs index 3cedfccf..0cdf4de0 100644 --- a/LibationWinForm/UNTESTED/GridEntry.cs +++ b/LibationWinForm/UNTESTED/GridEntry.cs @@ -7,103 +7,132 @@ using DataLayer; namespace LibationWinForm { - internal class GridEntry - { - private LibraryBook libraryBook; - private Book book => libraryBook.Book; + internal class GridEntry + { + private LibraryBook libraryBook; + private Book book => libraryBook.Book; - public Book GetBook() => book; + public Book GetBook() => book; - // this special case is obvious and ugly - public void REPLACE_Library_Book(LibraryBook libraryBook) => this.libraryBook = libraryBook; + // this special case is obvious and ugly + public void REPLACE_Library_Book(LibraryBook libraryBook) => this.libraryBook = libraryBook; - public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook; + public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook; - // hide from public fields from Data Source GUI with [Browsable(false)] + // hide from public fields from Data Source GUI with [Browsable(false)] - [Browsable(false)] - public string Tags => book.UserDefinedItem.Tags; - [Browsable(false)] - public IEnumerable TagsEnumerated => book.UserDefinedItem.TagsEnumerated; + [Browsable(false)] + public string Tags => book.UserDefinedItem.Tags; + [Browsable(false)] + public IEnumerable TagsEnumerated => book.UserDefinedItem.TagsEnumerated; - private Dictionary formatReplacements { get; } = new Dictionary(); - public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value); + // formatReplacements is what gets displayed + // the value that gets returned from the property is the cell's value + // this allows for the value to be sorted one way and displayed another + // eg: + // orig title: The Computer + // formatReplacement: The Computer + // value for sorting: Computer + private Dictionary formatReplacements { get; } = new Dictionary(); + public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value); - public Image Cover => - Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes( - FileManager.PictureStorage.GetImage(book.PictureId, FileManager.PictureStorage.PictureSize._80x80) - ); + public Image Cover => + Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes( + FileManager.PictureStorage.GetImage(book.PictureId, FileManager.PictureStorage.PictureSize._80x80) + ); - public string Title - { - get - { - formatReplacements[nameof(Title)] = book.Title; + public string Title + { + get + { + formatReplacements[nameof(Title)] = book.Title; - var sortName = book.Title - .Replace("|", "") - .Replace(":", "") - .ToLowerInvariant(); - if (sortName.StartsWith("the ") || sortName.StartsWith("a ") || sortName.StartsWith("an ")) - sortName = sortName.Substring(sortName.IndexOf(" ") + 1); + var sortName = book.Title + .Replace("|", "") + .Replace(":", "") + .ToLowerInvariant(); + if (sortName.StartsWith("the ") || sortName.StartsWith("a ") || sortName.StartsWith("an ")) + sortName = sortName.Substring(sortName.IndexOf(" ") + 1); - return sortName; - } - } + return sortName; + } + } - public string Authors => book.AuthorNames; - public string Narrators => book.NarratorNames; + public string Authors => book.AuthorNames; + public string Narrators => book.NarratorNames; - public int Length - { - get - { - formatReplacements[nameof(Length)] - = book.LengthInMinutes == 0 - ? "[pre-release]" - : $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min"; + public int Length + { + get + { + formatReplacements[nameof(Length)] + = book.LengthInMinutes == 0 + ? "" + : $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min"; - return book.LengthInMinutes; - } - } + return book.LengthInMinutes; + } + } - public string Series => book.SeriesNames; + public string Series => book.SeriesNames; - public string Description - => book.Description == null ? "" - : book.Description.Length < 63 ? book.Description - : book.Description.Substring(0, 60) + "..."; + private string descriptionCache = null; + public string Description + { + get + { + // HtmlAgilityPack is expensive. cache results + if (descriptionCache is null) + { + if (book.Description is null) + descriptionCache = ""; + else + { + var doc = new HtmlAgilityPack.HtmlDocument(); + doc.LoadHtml(book.Description); + var noHtml = doc.DocumentNode.InnerText; + descriptionCache + = noHtml.Length < 63 + ? noHtml + : noHtml.Substring(0, 60) + "..."; + } + } - public string Category => string.Join(" > ", book.CategoriesNames); + return descriptionCache; + } + } - // star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly: - // - star - // - star star - // - star 1/2 + public string Category => string.Join(" > ", book.CategoriesNames); - public string Product_Rating - { - get - { - Rating rating = book.Rating; + // star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly: + // - star + // - star star + // - star 1/2 - formatReplacements[nameof(Product_Rating)] = starString(rating); + public string Product_Rating + { + get + { + formatReplacements[nameof(Product_Rating)] = starString(book.Rating); + return firstScore(book.Rating); + } + } - return firstScore(rating); - } - } - - public DateTime? Purchase_Date => libraryBook.DateAdded; + public string Purchase_Date + { + get + { + formatReplacements[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d"); + return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss"); + } + } public string My_Rating { get { - Rating rating = book.UserDefinedItem.Rating; - - formatReplacements[nameof(My_Rating)] = starString(rating); - - return firstScore(rating); + formatReplacements[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating); + return firstScore(book.UserDefinedItem.Rating); } } diff --git a/LibationWinForm/UNTESTED/ProductsGrid.cs b/LibationWinForm/UNTESTED/ProductsGrid.cs index b6ceeb59..634dea2d 100644 --- a/LibationWinForm/UNTESTED/ProductsGrid.cs +++ b/LibationWinForm/UNTESTED/ProductsGrid.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows.Forms; using Dinah.Core.DataBinding; using DataLayer; +using Dinah.Core.Collections.Generic; namespace LibationWinForm { @@ -76,11 +77,14 @@ namespace LibationWinForm col.HeaderText = col.HeaderText.Replace("_", " "); - if (col.Name == nameof(GridEntry.Title)) - col.Width *= 2; - - if (col.Name == nameof(GridEntry.Misc)) - col.Width = (int)(col.Width * 1.35); + col.Width = col.Name switch + { + nameof(GridEntry.Cover) => 80, + nameof(GridEntry.Title) => col.Width * 2, + nameof(GridEntry.Misc) => (int)(col.Width * 1.35), + var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8, + _ => col.Width + }; } @@ -88,12 +92,14 @@ namespace LibationWinForm // transform into sorted GridEntry.s BEFORE binding // var lib = LibraryQueries.GetLibrary_Flat_NoTracking(); - var orderedGridEntries = lib + var orderedGridEntries = lib .Select(lb => new GridEntry(lb)).ToList() - // default load order: sort by author, then series, then title - .OrderBy(ge => ge.Authors) - .ThenBy(ge => ge.Series) - .ThenBy(ge => ge.Title) + // default load order + .OrderByDescending(ge => ge.Purchase_Date) + //// more advanced example: sort by author, then series, then title + //.OrderBy(ge => ge.Authors) + // .ThenBy(ge => ge.Series) + // .ThenBy(ge => ge.Title) .ToList(); // diff --git a/Scraping/Scraping.csproj b/Scraping/Scraping.csproj index 8d208b90..49e55777 100644 --- a/Scraping/Scraping.csproj +++ b/Scraping/Scraping.csproj @@ -6,7 +6,6 @@ - diff --git a/DTOs/BookDetailDTO.cs b/Scraping/UNTESTED/DTOs/BookDetailDTO.cs similarity index 100% rename from DTOs/BookDetailDTO.cs rename to Scraping/UNTESTED/DTOs/BookDetailDTO.cs diff --git a/DTOs/LibraryDTO.cs b/Scraping/UNTESTED/DTOs/LibraryDTO.cs similarity index 100% rename from DTOs/LibraryDTO.cs rename to Scraping/UNTESTED/DTOs/LibraryDTO.cs diff --git a/ScrapingDomainServices/UNTESTED/DownloadBook.cs b/ScrapingDomainServices/UNTESTED/DownloadBook.cs index 6fdedad6..a12e5137 100644 --- a/ScrapingDomainServices/UNTESTED/DownloadBook.cs +++ b/ScrapingDomainServices/UNTESTED/DownloadBook.cs @@ -22,48 +22,78 @@ namespace ScrapingDomainServices && !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId); public override async Task ProcessItemAsync(LibraryBook libraryBook) - { - var tempAaxFilename = FileUtility.GetValidFilename( - AudibleFileStorage.DownloadsInProgress, - libraryBook.Book.Title, - "aax", - libraryBook.Book.AudibleProductId); + { + var tempAaxFilename = FileUtility.GetValidFilename( + AudibleFileStorage.DownloadsInProgress, + libraryBook.Book.Title, + "aax", + libraryBook.Book.AudibleProductId); - // if getting from full title: - // '?' is allowed - // colons are inconsistent but not problematic to just leave them - // - 1 colon: sometimes full title is used. sometimes only the part before the colon is used - // - multple colons: only the part before the final colon is used - // e.g. Alien: Out of the Shadows: An Audible Original Drama => Alien: Out of the Shadows - // in cases where title includes '&', just use everything before the '&' and ignore the rest - //// var adhTitle = product.Title.Split('&')[0] + // if getting from full title: + // '?' is allowed + // colons are inconsistent but not problematic to just leave them + // - 1 colon: sometimes full title is used. sometimes only the part before the colon is used + // - multple colons: only the part before the final colon is used + // e.g. Alien: Out of the Shadows: An Audible Original Drama => Alien: Out of the Shadows + // in cases where title includes '&', just use everything before the '&' and ignore the rest + //// var adhTitle = product.Title.Split('&')[0] - var aaxDownloadLink = libraryBook.DownloadBookLink - .Replace("/admhelper", "") - .Replace("&DownloadType=Now", "") - + "&asin=&source=audible_adm&size=&browser_type=&assemble_url=http://cds.audible.com/download"; - var uri = new Uri(aaxDownloadLink); +// legacy/scraping method + //await performDownloadAsync(libraryBook, tempAaxFilename); +// new/api method +tempAaxFilename = await performApiDownloadAsync(libraryBook, tempAaxFilename); - using var webClient = await GetWebClient(tempAaxFilename); - // for book downloads only: pretend to be the audible download manager. from inAudible: - webClient.Headers["User-Agent"] = "Audible ADM 6.6.0.15;Windows Vista Service Pack 1 Build 7601"; - await webClient.DownloadFileTaskAsync(uri, tempAaxFilename); + // move + var aaxFilename = FileUtility.GetValidFilename( + AudibleFileStorage.DownloadsFinal, + libraryBook.Book.Title, + "aax", + libraryBook.Book.AudibleProductId); + File.Move(tempAaxFilename, aaxFilename); - // move - var aaxFilename = FileUtility.GetValidFilename( - AudibleFileStorage.DownloadsFinal, - libraryBook.Book.Title, - "aax", - libraryBook.Book.AudibleProductId); - File.Move(tempAaxFilename, aaxFilename); + var statusHandler = new StatusHandler(); + var isDownloaded = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId); + if (isDownloaded) + DoStatusUpdate($"Downloaded: {aaxFilename}"); + else + statusHandler.AddError("Downloaded AAX file cannot be found"); + return statusHandler; + } - var statusHandler = new StatusHandler(); - var isDownloaded = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId); - if (isDownloaded) - DoStatusUpdate($"Downloaded: {aaxFilename}"); - else - statusHandler.AddError("Downloaded AAX file cannot be found"); - return statusHandler; - } - } + // GetWebClientAsync: + // wires up webClient events + // [DownloadProgressChanged, DownloadFileCompleted, DownloadDataCompleted, DownloadStringCompleted] + // to DownloadableBase events + // DownloadProgressChanged, DownloadCompleted + // fires DownloadBegin event + // method begins async file download + private async Task performDownloadAsync(LibraryBook libraryBook, string tempAaxFilename) + { + var aaxDownloadLink = libraryBook.DownloadBookLink + .Replace("/admhelper", "") + .Replace("&DownloadType=Now", "") + + "&asin=&source=audible_adm&size=&browser_type=&assemble_url=http://cds.audible.com/download"; + var uri = new Uri(aaxDownloadLink); + + using var webClient = await GetWebClientAsync(tempAaxFilename); + // for book downloads only: pretend to be the audible download manager. from inAudible: + webClient.Headers["User-Agent"] = "Audible ADM 6.6.0.15;Windows Vista Service Pack 1 Build 7601"; + await webClient.DownloadFileTaskAsync(uri, tempAaxFilename); + } + + private async Task performApiDownloadAsync(LibraryBook libraryBook, string tempAaxFilename) + { + var identityFilePath = Path.Combine(Configuration.Instance.LibationFiles, "IdentityTokens.json"); + var api = await AudibleApi.EzApiCreator.GetApiAsync(identityFilePath); + + var progress = new Progress(); + progress.ProgressChanged += (_, e) => Invoke_DownloadProgressChanged(this, e); + + Invoke_DownloadBegin(tempAaxFilename); + var actualFilePath = await api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, progress); + Invoke_DownloadCompleted(this, $"Completed: {actualFilePath}"); + + return actualFilePath; + } + } } diff --git a/ScrapingDomainServices/UNTESTED/DownloadPdf.cs b/ScrapingDomainServices/UNTESTED/DownloadPdf.cs index 667fd273..8fb484ab 100644 --- a/ScrapingDomainServices/UNTESTED/DownloadPdf.cs +++ b/ScrapingDomainServices/UNTESTED/DownloadPdf.cs @@ -43,7 +43,7 @@ namespace ScrapingDomainServices var destinationFilename = Path.Combine(destinationDir, Path.GetFileName(url)); - using var webClient = await GetWebClient(destinationFilename); + using var webClient = await GetWebClientAsync(destinationFilename); await webClient.DownloadFileTaskAsync(url, destinationFilename); var statusHandler = new StatusHandler(); diff --git a/ScrapingDomainServices/UNTESTED/DownloadableBase.cs b/ScrapingDomainServices/UNTESTED/DownloadableBase.cs index a4027ebe..ca8cfd1e 100644 --- a/ScrapingDomainServices/UNTESTED/DownloadableBase.cs +++ b/ScrapingDomainServices/UNTESTED/DownloadableBase.cs @@ -7,18 +7,23 @@ using Dinah.Core.Humanizer; namespace ScrapingDomainServices { - public abstract class DownloadableBase : IDownloadable - { - public event EventHandler Begin; + public abstract class DownloadableBase : IDownloadable + { + public event EventHandler Begin; - public event EventHandler StatusUpdate; - protected void DoStatusUpdate(string message) => StatusUpdate?.Invoke(this, message); + public event EventHandler StatusUpdate; + protected void DoStatusUpdate(string message) => StatusUpdate?.Invoke(this, message); - public event EventHandler DownloadBegin; - public event DownloadProgressChangedEventHandler DownloadProgressChanged; - public event EventHandler DownloadCompleted; + public event EventHandler DownloadBegin; + public event EventHandler DownloadProgressChanged; + public event EventHandler DownloadCompleted; - public event EventHandler Completed; + protected void Invoke_DownloadBegin(string downloadMessage) => DownloadBegin?.Invoke(this, downloadMessage); + protected void Invoke_DownloadProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress progress) => DownloadProgressChanged?.Invoke(sender, progress); + protected void Invoke_DownloadCompleted(object sender, string str) => DownloadCompleted?.Invoke(sender, str); + + + public event EventHandler Completed; static DownloadableBase() { @@ -54,13 +59,14 @@ namespace ScrapingDomainServices "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36", - }; + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36", + }; // we need a minimum delay between tries when hitting audible.com // in every case except decrypt (which is already long running), we hit audible.com static Humanizer humanizer { get; } = new Humanizer { Minimum = 5, Maximum = 20 }; - static Random rnd = new Random(); - protected async Task GetWebClient(string downloadMessage) + static Random rnd { get; } = new Random(); + protected async Task GetWebClientAsync(string downloadMessage) { await humanizer.Wait(); @@ -75,16 +81,16 @@ namespace ScrapingDomainServices webClient.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"; webClient.Headers["Accept-Language"] = "en-US,en;q=0.9"; - // this breaks pdf download which uses: http://download.audible.com - // weirdly, it works for book download even though it uses https://cds.audible.com - //webClient.Headers["Host"] = "www.audible.com"; + // this breaks pdf download which uses: http://download.audible.com + // weirdly, it works for book download even though it uses https://cds.audible.com + //webClient.Headers["Host"] = "www.audible.com"; - webClient.DownloadProgressChanged += (s, e) => DownloadProgressChanged?.Invoke(s, e); - webClient.DownloadFileCompleted += (s, e) => DownloadCompleted?.Invoke(s, $"Completed: {downloadMessage}"); - webClient.DownloadDataCompleted += (s, e) => DownloadCompleted?.Invoke(s, $"Completed: {downloadMessage}"); - webClient.DownloadStringCompleted += (s, e) => DownloadCompleted?.Invoke(s, $"Completed: {downloadMessage}"); + webClient.DownloadProgressChanged += (s, e) => Invoke_DownloadProgressChanged(s, new Dinah.Core.Net.Http.DownloadProgress { BytesReceived = e.BytesReceived, ProgressPercentage = e.ProgressPercentage, TotalBytesToReceive = e.TotalBytesToReceive }); + webClient.DownloadFileCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}"); + webClient.DownloadDataCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}"); + webClient.DownloadStringCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}"); - DownloadBegin?.Invoke(this, downloadMessage); + Invoke_DownloadBegin(downloadMessage); return webClient; } diff --git a/ScrapingDomainServices/UNTESTED/DtoImporter.cs b/ScrapingDomainServices/UNTESTED/DtoImporter.cs index 0b101eba..c739d614 100644 --- a/ScrapingDomainServices/UNTESTED/DtoImporter.cs +++ b/ScrapingDomainServices/UNTESTED/DtoImporter.cs @@ -93,7 +93,7 @@ namespace ScrapingDomainServices // if no narrators listed, author is the narrator if (!libraryDTO.Narrators.Any()) - libraryDTO.Narrators = libraryDTO.Narrators = authors.Select(a => a.Name).ToArray(); + libraryDTO.Narrators = authors.Select(a => a.Name).ToArray(); // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db var narrators = libraryDTO @@ -102,18 +102,19 @@ namespace ScrapingDomainServices .ToList(); book = context.Books.Add(new Book( - new AudibleProductId(libraryDTO.ProductId), libraryDTO.Title, libraryDTO.Description, libraryDTO.LengthInMinutes, authors, narrators)) + new AudibleProductId(libraryDTO.ProductId), libraryDTO.Title, libraryDTO.Description, libraryDTO.LengthInMinutes, authors)) .Entity; + book.ReplaceNarrators(narrators); } // set/update book-specific info which may have changed book.PictureId = libraryDTO.PictureId; book.UpdateProductRating(libraryDTO.Product_OverallStars, libraryDTO.Product_PerformanceStars, libraryDTO.Product_StoryStars); - foreach (var url in libraryDTO.SupplementUrls) - book.AddSupplementDownloadUrl(url); + foreach (var url in libraryDTO.SupplementUrls) + book.AddSupplementDownloadUrl(FileManager.FileUtility.RestoreDeclawed(url)); - // important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import - book.UserDefinedItem.UpdateRating(libraryDTO.MyUserRating_Overall, libraryDTO.MyUserRating_Performance, libraryDTO.MyUserRating_Story); + // important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import + book.UserDefinedItem.UpdateRating(libraryDTO.MyUserRating_Overall, libraryDTO.MyUserRating_Performance, libraryDTO.MyUserRating_Story); // update series even for existing books. these are occasionally updated var seriesIds = libraryDTO.Series.Select(kvp => kvp.Key).ToList(); diff --git a/ScrapingDomainServices/UNTESTED/IDownloadable.cs b/ScrapingDomainServices/UNTESTED/IDownloadable.cs index baa3e856..7a41bc12 100644 --- a/ScrapingDomainServices/UNTESTED/IDownloadable.cs +++ b/ScrapingDomainServices/UNTESTED/IDownloadable.cs @@ -1,12 +1,11 @@ using System; -using System.Net; namespace ScrapingDomainServices { public interface IDownloadable : IProcessable { event EventHandler DownloadBegin; - event DownloadProgressChangedEventHandler DownloadProgressChanged; + event EventHandler DownloadProgressChanged; event EventHandler DownloadCompleted; } } diff --git a/ScrapingDomainServices/UNTESTED/IProcessableExt.cs b/ScrapingDomainServices/UNTESTED/IProcessableExt.cs index cd0ddbdc..0346b14b 100644 --- a/ScrapingDomainServices/UNTESTED/IProcessableExt.cs +++ b/ScrapingDomainServices/UNTESTED/IProcessableExt.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using DataLayer; +using Dinah.Core.Collections.Generic; using Dinah.Core.ErrorHandling; namespace ScrapingDomainServices @@ -37,8 +38,12 @@ namespace ScrapingDomainServices var libraryBooks = LibraryQueries.GetLibrary_Flat_NoTracking(); foreach (var libraryBook in libraryBooks) - if (await processable.ValidateAsync(libraryBook)) - return libraryBook; + if ( +// hardcoded blacklist +//episodes +!libraryBook.Book.AudibleProductId.In("B079ZTTL4J", "B0779LK1TX", "B0779H7B38", "B0779M3KGC", "B076PQ6G9Z", "B07D4M18YC") && + await processable.ValidateAsync(libraryBook)) + return libraryBook; return null; } diff --git a/ScrapingDomainServices/UNTESTED/Indexer.cs b/ScrapingDomainServices/UNTESTED/Indexer.cs index 6db1bd87..d63d433b 100644 --- a/ScrapingDomainServices/UNTESTED/Indexer.cs +++ b/ScrapingDomainServices/UNTESTED/Indexer.cs @@ -26,7 +26,8 @@ namespace ScrapingDomainServices public static async Task<(int total, int newEntries)> IndexLibraryAsync(List jsonFileInfos) { var productItems = jsonFileInfos.SelectMany(fi => json2libraryDtos(fi)).ToList(); - return await IndexLibraryAsync(productItems); + var newEntries = await IndexLibraryAsync(productItems); + return (productItems.Count, newEntries); } private static Regex jsonIsCollectionRegex = new Regex(@"^\s*\[\s*\{", RegexOptions.Compiled); private static IEnumerable json2libraryDtos(FileInfo jsonFileInfo) @@ -44,13 +45,11 @@ namespace ScrapingDomainServices } // new full index or library-file import: re-create search index - public static async Task<(int total, int newEntries)> IndexLibraryAsync(List productItems) - => await IndexLibraryAsync(productItems, SearchEngineActions.FullReIndexAsync); - - private static async Task<(int total, int newEntries)> IndexLibraryAsync(List productItems, Func postIndexActionAsync) + /// qty new entries + public static async Task IndexLibraryAsync(List productItems) { if (productItems == null || !productItems.Any()) - return (0, 0); + return 0; productItems = filterAndValidate(productItems); @@ -74,9 +73,9 @@ namespace ScrapingDomainServices await Task.Run(() => dtoImporter.ReloadBookDetails(productItems)); await context.SaveChangesAsync(); - await postIndexActionAsync?.Invoke(); + await SearchEngineActions.FullReIndexAsync(); - return (productItems.Count, newEntries); + return newEntries; } private static List filterAndValidate(List collection) { @@ -121,19 +120,6 @@ namespace ScrapingDomainServices #endregion #region book details - public static async Task IndexBookDetailsAsync(FileInfo jsonFileInfo) - { - var bookDetailDTO = json2bookDetailDto(jsonFileInfo); - await IndexBookDetailsAsync(bookDetailDTO); - } - private static BookDetailDTO json2bookDetailDto(FileInfo jsonFileInfo) - { - validateJsonFile(jsonFileInfo); - - var serialized = File.ReadAllText(jsonFileInfo.FullName); - return JsonConvert.DeserializeObject(serialized); - } - public static async Task IndexBookDetailsAsync(BookDetailDTO bookDetailDTO) => await indexBookDetailsAsync(bookDetailDTO, () => SearchEngineActions.ProductReIndexAsync(bookDetailDTO.ProductId)); diff --git a/ScrapingDomainServices/UNTESTED/ScrapeBookDetails.cs b/ScrapingDomainServices/UNTESTED/ScrapeBookDetails.cs index 4f10df9a..d934f7e3 100644 --- a/ScrapingDomainServices/UNTESTED/ScrapeBookDetails.cs +++ b/ScrapingDomainServices/UNTESTED/ScrapeBookDetails.cs @@ -72,7 +72,7 @@ namespace ScrapingDomainServices // download htm string source; var url = AudiblePage.Product.GetUrl(productId); - using var webClient = await GetWebClient($"Getting Book Details for {libraryBook.Book.Title}"); + using var webClient = await GetWebClientAsync($"Getting Book Details for {libraryBook.Book.Title}"); try { source = await webClient.DownloadStringTaskAsync(url); diff --git a/__TODO.txt b/__TODO.txt index 1ca2ab30..49d2b7e8 100644 --- a/__TODO.txt +++ b/__TODO.txt @@ -1,4 +1,121 @@ --- begin CONFIG FILES --------------------------------------------------------------------------------------------------------------------- +-- begin REPLACE SCRAPING WITH API --------------------------------------------------------------------------------------------------------------------- +integrate API into libation. replace all authentication, audible communication + +IN-PROGRESS +----------- +download via api with UI integration +- functionality: episodes +library import UI +- disable main ui +- updates on which stage and how long it's expected to take +- error handling +- dialog/pop up when done. show how many new and total +move biz logic out of UI (Form1.scanLibraryToolStripMenuItem_Click) +- non-db dependent: InternalUtilities. see: SearchEngineActions + InternalUtilities.AudibleApiExtensions may not belong here. not sure +- db dependent: eg DtoImporterService + +extract IdentityTokens.json into FileManager +replace all hardcoded occurances + +FIX +// hardcoded blacklist + +datalayer stuff (eg: Book) need better ToString + +MOVE TO LEGACY +-------------- +- scraping code +- "legacy inAudible wire-up code" +Book.IsEpisodes +Book.HasBookDetails +LibraryBook.DownloadBookLink +humanizer +replace all scraping with audible api + public partial class ScanLibraryDialog : Form, IIndexLibraryDialog + public async Task DoMainWorkAsync() + using var pageRetriever = websiteProcessorControl1.GetPageRetriever(); + jsonFilepaths = await DownloadLibrary.DownloadLibraryAsync(pageRetriever).ConfigureAwait(false); +scraping stuff to remove. order matters. go from top to bottom: +REMOVED PROJECT REFERENCES +3.2 Domain Utilities (post database) + DomainServices + AudibleDotComAutomation +3.1 Domain Internal Utilities + InternalUtilities + Scraping +REMOVED PROJECTS +2 Utilities (domain ignorant) + Scraping + AudibleDotComAutomation + AudibleDotCom + CookieMonster +REMOVED FILES +\Libation\InternalUtilities\UNTESTED\DataConverter.cs +\Libation\DomainServices\UNTESTED\DownloadLibrary.cs +\Libation\DomainServices\UNTESTED\ScrapeBookDetails.cs +ADDED PROJECT REFERENCES +3.1 Domain Internal Utilities + InternalUtilities + DTOs +-- end REPLACE SCRAPING WITH API --------------------------------------------------------------------------------------------------------------------- + +-- begin ENHANCEMENT, PERFORMANCE: IMPORT --------------------------------------------------------------------------------------------------------------------- +imports are PAINFULLY slow for just a few hundred items. wtf is taking so long? +-- end ENHANCEMENT, PERFORMANCE: IMPORT --------------------------------------------------------------------------------------------------------------------- + +-- begin ENHANCEMENT, PERFORMANCE: GRID --------------------------------------------------------------------------------------------------------------------- +when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow +https://stackoverflow.com/a/12046333 +https://codereview.stackexchange.com/a/135074 +// do NOT use lock() or Monitor with async/await +private static int _lockFlag = 0; // 0 - free +if (Interlocked.CompareExchange(ref _lockFlag, 1, 0) != 0) return; +// only 1 thread will enter here without locking the object/put the other threads to sleep +try { await DoWorkAsync(); } +// free the lock +finally { Interlocked.Decrement(ref _lockFlag); } + +use stop light icons for liberated state: red=none, yellow=downloaded encrypted, green=liberated + +need a way to liberate ad hoc books and pdf.s + +use pdf icon with and without and X over it to indicate status +-- end ENHANCEMENT, PERFORMANCE: GRID --------------------------------------------------------------------------------------------------------------------- + +-- begin ENHANCEMENTS, GET LIBRARY --------------------------------------------------------------------------------------------------------------------- +Audible API. GET /1.0/library , GET /1.0/library/{asin} +TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryApiV10 +-- end ENHANCEMENTS, GET LIBRARY --------------------------------------------------------------------------------------------------------------------- + +-- begin ENHANCEMENTS, EPISODES --------------------------------------------------------------------------------------------------------------------- +grid: episodes need a better Download_Status and Length/LengthInMinutes +download: need a way to liberate episodes. show in grid 'x/y downloaded/liberated' etc +-- end ENHANCEMENTS, EPISODES --------------------------------------------------------------------------------------------------------------------- + +-- begin BUG, MOVING FILES --------------------------------------------------------------------------------------------------------------------- +with libation closed, move files +start libation +can get error below +fixed on restart + +Form1_Load ... await setBackupCountsAsync(); +Collection was modified; enumeration operation may not execute. +stack trace + at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() + at System.Collections.Generic.List`1.Enumerator.MoveNextRare() + at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate) + at FileManager.FilePathCache.GetPath(String id, FileType type) in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\FilePathCache.cs:line 33 + at FileManager.AudibleFileStorage.d__32.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 112 + at FileManager.AudibleFileStorage.d__31.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 107 + at FileManager.AudibleFileStorage.d__30.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 104 + at LibationWinForm.Form1.<g__getAudioFileStateAsync|15_1>d.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 110 + at LibationWinForm.Form1.d__15.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 117 + at LibationWinForm.Form1.d__13.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 81 + at LibationWinForm.Form1.d__11.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 60 +-- end BUG, MOVING FILES --------------------------------------------------------------------------------------------------------------------- + +-- begin CONFIG FILES --------------------------------------------------------------------------------------------------------------------- .\appsettings.json should only be a pointer to the real settings file location: LibationSettings.json replace complex config saving throughout with new way in my ConsoleDependencyInjection solution all settings should be strongly typed @@ -16,68 +133,21 @@ BasePath => recursively search directories upward-only until fild dir with .sln from here can set up a shared dir anywhere. use recursive upward search to find it. store shared files here. eg: identityTokens.json, ComputedTestValues -- end CONFIG FILES --------------------------------------------------------------------------------------------------------------------- --- begin REPLACE SCRAPING WITH API --------------------------------------------------------------------------------------------------------------------- -integrate API into libation. eventually entirely replace all other authentication methods, audible communication. incl book download -logged-in user's own ratings (aka rating-star(s)) -- is "provided_review" -unlike traditional scraping, audible api calls do not need to be humanized. can likely delete humanizer altogether. still needed for downloading pdf.s? +-- begin TAGS --------------------------------------------------------------------------------------------------------------------- +pulling previous tags into new Books. think: reloading db +move out of Book and into DtoMapper? -incl episodes. eg: "Bill Bryson's Appliance of Science" - refining episode retrieval might also get rid of the need for IsEpisodes property -replace all scraping with audible api - public partial class ScanLibraryDialog : Form, IIndexLibraryDialog - public async Task DoMainWorkAsync() - using var pageRetriever = websiteProcessorControl1.GetPageRetriever(); - jsonFilepaths = await DownloadLibrary.DownloadLibraryAsync(pageRetriever).ConfigureAwait(false); +Extract file and tag stuff from domain objects. This should exist only in data layer. If domain objects are able to call EF context, it should go through data layer +Why are tags in file AND database? -move old DTOs back into scraping so it's easier to move them all to new legacy area -library to return strongly typed LibraryApiV10 - -note: some json importing may still be relevant. might want to allow custom importing - -scraping stuff to remove: -order matters. go from top to bottom: - -REMOVED PROJECT REFERENCES -3.2 Domain Utilities (post database) - DomainServices - AudibleDotComAutomation -3.1 Domain Internal Utilities - InternalUtilities - Scraping - -REMOVED PROJECTS -2 Utilities (domain ignorant) - Scraping - AudibleDotComAutomation - AudibleDotCom - CookieMonster - -REMOVED FILES -\Libation\InternalUtilities\UNTESTED\DataConverter.cs -\Libation\DomainServices\UNTESTED\DownloadLibrary.cs -\Libation\DomainServices\UNTESTED\ScrapeBookDetails.cs - -ADDED PROJECT REFERENCES -3.1 Domain Internal Utilities - InternalUtilities - DTOs - -Audible API. GET /1.0/library , GET /1.0/library/{asin} -TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryApiV10 --- end REPLACE SCRAPING WITH API --------------------------------------------------------------------------------------------------------------------- +extract FileManager dependency from data layer +-- end TAGS --------------------------------------------------------------------------------------------------------------------- -- begin ENHANCEMENT, CATEGORIES --------------------------------------------------------------------------------------------------------------------- add support for multiple categories when i do this, learn about the different CategoryLadder.Root enums. probably only need Root.Genres -- end ENHANCEMENT, CATEGORIES --------------------------------------------------------------------------------------------------------------------- --- begin LEGACY --------------------------------------------------------------------------------------------------------------------- -retain legacy functionality in out-of-the-way area. turn on/off in settings? - -> scraping code -> "legacy inAudible wire-up code" --- end LEGACY --------------------------------------------------------------------------------------------------------------------- - -- begin CLEAN UP ARCHITECTURE --------------------------------------------------------------------------------------------------------------------- my ui sucks. it's also tightly coupled with biz logic. can't replace ui until biz logic is extracted and loosely. remove all biz logic from presentation/winforms layer -- end CLEAN UP ARCHITECTURE --------------------------------------------------------------------------------------------------------------------- @@ -98,6 +168,9 @@ Turn into unit tests or demos search 'example code' on: LibationWinForm\...\Form1.cs EnumerationFlagsExtensions.EXAMPLES() // examples + scratchpad + scratch pad + scratch_pad -- end UNIT TESTS --------------------------------------------------------------------------------------------------------------------- -- begin DECRYPTING --------------------------------------------------------------------------------------------------------------------- @@ -128,6 +201,7 @@ directly call ffmpeg (decrypt only) -- begin ENHANCEMENT, UI: LONG RUNNING TASKS --------------------------------------------------------------------------------------------------------------------- long running tasks are appropriately async. however there's no way for the user to see that the task is running (vs nothing happened) except to wait and see if the final notification ever comes +need events to update UI with progress -- end ENHANCEMENT, UI: LONG RUNNING TASKS --------------------------------------------------------------------------------------------------------------------- -- begin ENHANCEMENT, PERFORMANCE: TAGS --------------------------------------------------------------------------------------------------------------------- @@ -136,10 +210,6 @@ unlikely to be an issue with file write. in fact, should probably roll back this also touches parts of code which: db write via a hook, search engine re-index -- end ENHANCEMENT, PERFORMANCE: TAGS --------------------------------------------------------------------------------------------------------------------- --- begin ENHANCEMENT, PERFORMANCE: GRID --------------------------------------------------------------------------------------------------------------------- -when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow --- end ENHANCEMENT, PERFORMANCE: GRID --------------------------------------------------------------------------------------------------------------------- - -- begin ENHANCEMENT: REMOVE BOOK --------------------------------------------------------------------------------------------------------------------- how to remove a book? previously difficult due to implementation details regarding scraping and importing. should be trivial after api replaces scraping