From 9416f4e0403d421695a1adc9f867719e9536e666 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Tue, 22 Jun 2021 11:25:18 -0400 Subject: [PATCH 1/3] Persist keys to db. Necessary in the event that download succeeds then decrypt fails. --- ApplicationServices/LibraryCommands.cs | 8 ++++++++ FileLiberator/DownloadBook.cs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ApplicationServices/LibraryCommands.cs b/ApplicationServices/LibraryCommands.cs index 3ec19cc7..35313d81 100644 --- a/ApplicationServices/LibraryCommands.cs +++ b/ApplicationServices/LibraryCommands.cs @@ -119,5 +119,13 @@ namespace ApplicationServices throw; } } + + // I hate how unintuitive this is to use/remember. Will hopefully be cleaned up in a future comprehensive data strategy overhaul + public static void UpdateBook(Book book) + { + using var context = DbContexts.GetContext(); + context.Update(book); + context.SaveChanges(); + } } } diff --git a/FileLiberator/DownloadBook.cs b/FileLiberator/DownloadBook.cs index 7ef272d2..88423778 100644 --- a/FileLiberator/DownloadBook.cs +++ b/FileLiberator/DownloadBook.cs @@ -51,6 +51,8 @@ namespace FileLiberator libraryBook.Book.AudibleKey = dlLic.AudibleKey; libraryBook.Book.AudibleIV = dlLic.AudibleIV; + // persist changes + ApplicationServices.LibraryCommands.UpdateBook(libraryBook.Book); var client = new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent); From b1a033e16298fe942b100f954b25042753cc3117 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Tue, 22 Jun 2021 16:49:19 -0400 Subject: [PATCH 2/3] Keep download license details with aax file, not in db --- ApplicationServices/LibraryCommands.cs | 8 -------- DataLayer/EfClasses/Book.cs | 2 -- FileLiberator/DecryptBook.cs | 26 +++++++++++++++--------- FileLiberator/DownloadBook.cs | 25 ++++++++++++++++------- LibationLauncher/LibationLauncher.csproj | 2 +- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/ApplicationServices/LibraryCommands.cs b/ApplicationServices/LibraryCommands.cs index 35313d81..3ec19cc7 100644 --- a/ApplicationServices/LibraryCommands.cs +++ b/ApplicationServices/LibraryCommands.cs @@ -119,13 +119,5 @@ namespace ApplicationServices throw; } } - - // I hate how unintuitive this is to use/remember. Will hopefully be cleaned up in a future comprehensive data strategy overhaul - public static void UpdateBook(Book book) - { - using var context = DbContexts.GetContext(); - context.Update(book); - context.SaveChanges(); - } } } diff --git a/DataLayer/EfClasses/Book.cs b/DataLayer/EfClasses/Book.cs index dcce8c73..18d4f282 100644 --- a/DataLayer/EfClasses/Book.cs +++ b/DataLayer/EfClasses/Book.cs @@ -31,8 +31,6 @@ namespace DataLayer // mutable public string PictureId { get; set; } - public string AudibleKey { get; set; } - public string AudibleIV { get; set; } // book details public bool IsAbridged { get; private set; } diff --git a/FileLiberator/DecryptBook.cs b/FileLiberator/DecryptBook.cs index 9b2e8871..47ef3e3a 100644 --- a/FileLiberator/DecryptBook.cs +++ b/FileLiberator/DecryptBook.cs @@ -57,30 +57,33 @@ namespace FileLiberator if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; - var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale); + var chapters = await downloadChapterNamesAsync(libraryBook); - var chapters = await downloadChapterNames(libraryBook, api); - - var outputAudioFilename = await aaxToM4bConverterDecrypt(aaxFilename, libraryBook, chapters, api); + var outputAudioFilename = await aaxToM4bConverterDecryptAsync(aaxFilename, libraryBook, chapters); // decrypt failed if (outputAudioFilename == null) return new StatusHandler { "Decrypt failed" }; + // moves files and returns dest dir. Do not put inside of if(RetainAaxFiles) var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); - var config = Configuration.Instance; - if (config.RetainAaxFiles) + var jsonFilename = PathLib.ReplaceExtension(aaxFilename, "json"); + if (Configuration.Instance.RetainAaxFiles) { var newAaxFilename = FileUtility.GetValidFilename( destinationDir, Path.GetFileNameWithoutExtension(aaxFilename), "aax"); File.Move(aaxFilename, newAaxFilename); + + var newJsonFilename = PathLib.ReplaceExtension(newAaxFilename, "json"); + File.Move(jsonFilename, newJsonFilename); } else { Dinah.Core.IO.FileExt.SafeDelete(aaxFilename); + Dinah.Core.IO.FileExt.SafeDelete(jsonFilename); } var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId); @@ -95,10 +98,11 @@ namespace FileLiberator } } - private static async Task downloadChapterNames(LibraryBook libraryBook, Api api) + private static async Task downloadChapterNamesAsync(LibraryBook libraryBook) { try { + var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale); var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId); if (contentMetadata?.ChapterInfo is null) return null; @@ -111,15 +115,17 @@ namespace FileLiberator } } - private async Task aaxToM4bConverterDecrypt(string aaxFilename, LibraryBook libraryBook, Chapters chapters, Api api) + private async Task aaxToM4bConverterDecryptAsync(string aaxFilename, LibraryBook libraryBook, Chapters chapters) { DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}"); try { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var jsonPath = PathLib.ReplaceExtension(aaxFilename, "json"); + var jsonContents = File.ReadAllText(jsonPath); + var dlLic = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonContents); - var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, libraryBook.Book.AudibleKey, libraryBook.Book.AudibleIV, chapters); + var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, dlLic.AudibleKey, dlLic.AudibleIV, chapters); converter.AppName = "Libation"; TitleDiscovered?.Invoke(this, converter.tags.title); diff --git a/FileLiberator/DownloadBook.cs b/FileLiberator/DownloadBook.cs index 88423778..cd69c2c1 100644 --- a/FileLiberator/DownloadBook.cs +++ b/FileLiberator/DownloadBook.cs @@ -41,6 +41,7 @@ namespace FileLiberator libraryBook.Book.Title, "aax", libraryBook.Book.AudibleProductId); + private async Task downloadAaxcBookAsync(LibraryBook libraryBook, string tempAaxFilename) { validate(libraryBook); @@ -49,26 +50,30 @@ namespace FileLiberator var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); - libraryBook.Book.AudibleKey = dlLic.AudibleKey; - libraryBook.Book.AudibleIV = dlLic.AudibleIV; - // persist changes - ApplicationServices.LibraryCommands.UpdateBook(libraryBook.Book); - var client = new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent); var actualFilePath = await PerformDownloadAsync( tempAaxFilename, - (p) => client.DownloadFileAsync(dlLic.DownloadUri.AbsoluteUri, tempAaxFilename, p)); + (p) => client.DownloadFileAsync(new Uri(dlLic.DownloadUrl).AbsoluteUri, tempAaxFilename, p)); System.Threading.Thread.Sleep(100); // if bad file download, a 0-33 byte file will be created // if service unavailable, a 52 byte string will be saved as file var length = new FileInfo(actualFilePath).Length; + // success. save json and return if (length > 100) - return actualFilePath; + { + // save along side book + var jsonPath = PathLib.ReplaceExtension(actualFilePath, "json"); + var jsonContents = Newtonsoft.Json.JsonConvert.SerializeObject(dlLic, Newtonsoft.Json.Formatting.Indented); + File.WriteAllText(jsonPath, jsonContents); + return actualFilePath; + } + + // else: failure. clean up and throw var contents = File.ReadAllText(actualFilePath); File.Delete(actualFilePath); @@ -121,6 +126,12 @@ namespace FileLiberator "aax", libraryBook.Book.AudibleProductId); File.Move(actualFilePath, newAaxFilename); + + // also move DownloadLicense json file + var jsonPathOld = PathLib.ReplaceExtension(actualFilePath, "json"); + var jsonPathNew = PathLib.ReplaceExtension(newAaxFilename, "json"); + File.Move(jsonPathOld, jsonPathNew); + Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}"); } diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index ab60526f..af3b6287 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 4.4.0.35 + 4.4.0.60 From 305de34a769041e4dc05dafbd818598afce90f53 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Tue, 22 Jun 2021 17:02:00 -0400 Subject: [PATCH 3/3] db migration to remove license keys from Books table. They are not really data related to a book. Also, it was causing problems on update due to other persistence choices. For now, store decrypt keys along side of encrypted file instead. --- ...05558_RemoveAaxcDecryptionKeys.Designer.cs | 381 ++++++++++++++++++ ...20210622205558_RemoveAaxcDecryptionKeys.cs | 33 ++ .../LibationContextModelSnapshot.cs | 6 - LibationLauncher/LibationLauncher.csproj | 2 +- 4 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs create mode 100644 DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.cs diff --git a/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs b/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs new file mode 100644 index 00000000..bd492aec --- /dev/null +++ b/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs @@ -0,0 +1,381 @@ +// +using System; +using DataLayer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace DataLayer.Migrations +{ + [DbContext(typeof(LibationContext))] + [Migration("20210622205558_RemoveAaxcDecryptionKeys")] + partial class RemoveAaxcDecryptionKeys + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Property("BookId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleProductId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("DatePublished") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsAbridged") + .HasColumnType("INTEGER"); + + b.Property("LengthInMinutes") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("PictureId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("BookId"); + + b.HasIndex("AudibleProductId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("ContributorId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.HasKey("BookId", "ContributorId", "Role"); + + b.HasIndex("BookId"); + + b.HasIndex("ContributorId"); + + b.ToTable("BookContributor"); + }); + + modelBuilder.Entity("DataLayer.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleCategoryId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ParentCategoryCategoryId") + .HasColumnType("INTEGER"); + + b.HasKey("CategoryId"); + + b.HasIndex("AudibleCategoryId"); + + b.HasIndex("ParentCategoryCategoryId"); + + b.ToTable("Categories"); + + b.HasData( + new + { + CategoryId = -1, + AudibleCategoryId = "", + Name = "" + }); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Property("ContributorId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleContributorId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("ContributorId"); + + b.HasIndex("Name"); + + b.ToTable("Contributors"); + + b.HasData( + new + { + ContributorId = -1, + Name = "" + }); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("Account") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.HasKey("BookId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Property("SeriesId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleSeriesId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("SeriesId"); + + b.HasIndex("AudibleSeriesId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("REAL"); + + b.HasKey("SeriesId", "BookId"); + + b.HasIndex("BookId"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBook"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.HasOne("DataLayer.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("DataLayer.Rating", "Rating", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("OverallRating") + .HasColumnType("REAL"); + + b1.Property("PerformanceRating") + .HasColumnType("REAL"); + + b1.Property("StoryRating") + .HasColumnType("REAL"); + + b1.HasKey("BookId"); + + b1.ToTable("Books"); + + b1.WithOwner() + .HasForeignKey("BookId"); + }); + + b.OwnsMany("DataLayer.Supplement", "Supplements", b1 => + { + b1.Property("SupplementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("Url") + .HasColumnType("TEXT"); + + b1.HasKey("SupplementId"); + + b1.HasIndex("BookId"); + + b1.ToTable("Supplement"); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.Navigation("Book"); + }); + + b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("Tags") + .HasColumnType("TEXT"); + + b1.HasKey("BookId"); + + b1.ToTable("UserDefinedItem"); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.OwnsOne("DataLayer.Rating", "Rating", b2 => + { + b2.Property("UserDefinedItemBookId") + .HasColumnType("INTEGER"); + + b2.Property("OverallRating") + .HasColumnType("REAL"); + + b2.Property("PerformanceRating") + .HasColumnType("REAL"); + + b2.Property("StoryRating") + .HasColumnType("REAL"); + + b2.HasKey("UserDefinedItemBookId"); + + b2.ToTable("UserDefinedItem"); + + b2.WithOwner() + .HasForeignKey("UserDefinedItemBookId"); + }); + + b1.Navigation("Book"); + + b1.Navigation("Rating"); + }); + + b.Navigation("Category"); + + b.Navigation("Rating"); + + b.Navigation("Supplements"); + + b.Navigation("UserDefinedItem"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("ContributorsLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Contributor", "Contributor") + .WithMany("BooksLink") + .HasForeignKey("ContributorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Contributor"); + }); + + modelBuilder.Entity("DataLayer.Category", b => + { + b.HasOne("DataLayer.Category", "ParentCategory") + .WithMany() + .HasForeignKey("ParentCategoryCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithOne() + .HasForeignKey("DataLayer.LibraryBook", "BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("SeriesLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Series", "Series") + .WithMany("BooksLink") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Navigation("ContributorsLink"); + + b.Navigation("SeriesLink"); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Navigation("BooksLink"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Navigation("BooksLink"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.cs b/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.cs new file mode 100644 index 00000000..bab97a55 --- /dev/null +++ b/DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace DataLayer.Migrations +{ + public partial class RemoveAaxcDecryptionKeys : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AudibleIV", + table: "Books"); + + migrationBuilder.DropColumn( + name: "AudibleKey", + table: "Books"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AudibleIV", + table: "Books", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AudibleKey", + table: "Books", + type: "TEXT", + nullable: true); + } + } +} diff --git a/DataLayer/Migrations/LibationContextModelSnapshot.cs b/DataLayer/Migrations/LibationContextModelSnapshot.cs index 6d65ba5f..aa084c32 100644 --- a/DataLayer/Migrations/LibationContextModelSnapshot.cs +++ b/DataLayer/Migrations/LibationContextModelSnapshot.cs @@ -22,12 +22,6 @@ namespace DataLayer.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AudibleIV") - .HasColumnType("TEXT"); - - b.Property("AudibleKey") - .HasColumnType("TEXT"); - b.Property("AudibleProductId") .HasColumnType("TEXT"); diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index af3b6287..cd9acb48 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 4.4.0.60 + 4.4.0.63