diff --git a/FileLiberator/DecryptBook.cs b/FileLiberator/DecryptBook.cs deleted file mode 100644 index 47ef3e3a..00000000 --- a/FileLiberator/DecryptBook.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using AaxDecrypter; -using AudibleApi; -using DataLayer; -using Dinah.Core; -using Dinah.Core.ErrorHandling; -using FileManager; -using InternalUtilities; - -namespace FileLiberator -{ - /// - /// Decrypt audiobook files - /// - /// Processes: - /// Download: download aax file: the DRM encrypted audiobook - /// Decrypt: remove DRM encryption from audiobook. Store final book - /// Backup: perform all steps (downloaded, decrypt) still needed to get final book - /// - public class DecryptBook : IDecryptable - { - public event EventHandler Begin; - public event EventHandler StatusUpdate; - public event EventHandler DecryptBegin; - - public event EventHandler TitleDiscovered; - public event EventHandler AuthorsDiscovered; - public event EventHandler NarratorsDiscovered; - public event EventHandler CoverImageFilepathDiscovered; - public event EventHandler UpdateProgress; - - public event EventHandler DecryptCompleted; - public event EventHandler Completed; - - public bool Validate(LibraryBook libraryBook) - => AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId) - && !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId); - - // do NOT use ConfigureAwait(false) on ProcessAsync() - // often calls events which prints to forms in the UI context - public async Task ProcessAsync(LibraryBook libraryBook) - { - Begin?.Invoke(this, libraryBook); - - try - { - var aaxFilename = AudibleFileStorage.AAX.GetPath(libraryBook.Book.AudibleProductId); - - if (aaxFilename == null) - return new StatusHandler { "aaxFilename parameter is null" }; - if (!File.Exists(aaxFilename)) - return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" }; - if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)) - return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; - - var chapters = await downloadChapterNamesAsync(libraryBook); - - 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 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); - if (!finalAudioExists) - return new StatusHandler { "Cannot find final audio file after decryption" }; - - return new StatusHandler(); - } - finally - { - Completed?.Invoke(this, libraryBook); - } - } - - 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; - - return new DownloadedChapters(contentMetadata.ChapterInfo); - } - catch - { - return null; - } - } - - private async Task aaxToM4bConverterDecryptAsync(string aaxFilename, LibraryBook libraryBook, Chapters chapters) - { - DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}"); - - try - { - var jsonPath = PathLib.ReplaceExtension(aaxFilename, "json"); - var jsonContents = File.ReadAllText(jsonPath); - var dlLic = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonContents); - - var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, dlLic.AudibleKey, dlLic.AudibleIV, chapters); - converter.AppName = "Libation"; - - TitleDiscovered?.Invoke(this, converter.tags.title); - AuthorsDiscovered?.Invoke(this, converter.tags.author); - NarratorsDiscovered?.Invoke(this, converter.tags.narrator); - CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes); - - // override default which was set in CreateAsync - var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b"); - converter.SetOutputFilename(proposedOutputFile); - converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); - - // REAL WORK DONE HERE - var success = await Task.Run(() => converter.Run()); - - // decrypt failed - if (!success) - return null; - - return converter.outputFileName; - } - finally - { - DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}"); - } - } - - private static string moveFilesToBooksDir(Book product, string outputAudioFilename) - { - // create final directory. move each file into it. MOVE AUDIO FILE LAST - // new dir: safetitle_limit50char + " [" + productId + "]" - - var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId); - Directory.CreateDirectory(destinationDir); - - var sortedFiles = getProductFilesSorted(product, outputAudioFilename); - - var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.'); - - // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext - var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId); - - foreach (var f in sortedFiles) - { - var dest - = AudibleFileStorage.Audio.IsFileTypeMatch(f) - ? audioFileName - // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext - : FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt); - - if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") - Cue.UpdateFileName(f, audioFileName); - - File.Move(f.FullName, dest); - } - - return destinationDir; - } - - private static List getProductFilesSorted(Book product, string outputAudioFilename) - { - // files are: temp path\author\[asin].ext - var m4bDir = new FileInfo(outputAudioFilename).Directory; - var files = m4bDir - .EnumerateFiles() - .Where(f => f.Name.ContainsInsensitive(product.AudibleProductId)) - .ToList(); - - // move audio files to the end of the collection so these files are moved last - var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f)); - var sortedFiles = files - .Except(musicFiles) - .Concat(musicFiles) - .ToList(); - - return sortedFiles; - } - } -} diff --git a/FileLiberator/DownloadBook.cs b/FileLiberator/DownloadBook.cs deleted file mode 100644 index c32aa51e..00000000 --- a/FileLiberator/DownloadBook.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using AudibleApi; -using DataLayer; -using Dinah.Core; -using Dinah.Core.ErrorHandling; -using FileManager; -using System.Net.Http; -using Dinah.Core.Net.Http; - -namespace FileLiberator -{ - /// - /// Download DRM book - /// - /// Processes: - /// Download: download aax file: the DRM encrypted audiobook - /// Decrypt: remove DRM encryption from audiobook. Store final book - /// Backup: perform all steps (downloaded, decrypt) still needed to get final book - /// - public class DownloadBook : DownloadableBase - { - private const string SERVICE_UNAVAILABLE = "Content Delivery Companion Service is not available."; - - public override bool Validate(LibraryBook libraryBook) - => !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId) - && !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId); - - public override async Task ProcessItemAsync(LibraryBook libraryBook) - { - var tempAaxFilename = getDownloadPath(libraryBook); - var actualFilePath = await downloadAaxcBookAsync(libraryBook, tempAaxFilename); - moveBook(libraryBook, actualFilePath); - return verifyDownload(libraryBook); - } - - private static string getDownloadPath(LibraryBook libraryBook) - => FileUtility.GetValidFilename( - AudibleFileStorage.DownloadsInProgress, - libraryBook.Book.Title, - "aaxc", - libraryBook.Book.AudibleProductId); - - private async Task downloadAaxcBookAsync(LibraryBook libraryBook, string tempAaxFilename) - { - validate(libraryBook); - - var api = await GetApiAsync(libraryBook); - - var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); - - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent); - - var actualFilePath = await PerformDownloadAsync( - tempAaxFilename, - (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) - { - // 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); - - var exMsg = contents.StartsWithInsensitive(SERVICE_UNAVAILABLE) - ? SERVICE_UNAVAILABLE - : "Error downloading file"; - - var ex = new Exception(exMsg); - Serilog.Log.Logger.Error(ex, "Download error {@DebugInfo}", new - { - libraryBook.Book.Title, - libraryBook.Book.AudibleProductId, - libraryBook.Book.Locale, - Account = libraryBook.Account?.ToMask() ?? "[empty]", - tempAaxFilename, - actualFilePath, - length, - contents - }); - throw ex; - } - - private static void validate(LibraryBook libraryBook) - { - string errorString(string field) - => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; - - string errorTitle() - { - var title - = (libraryBook.Book.Title.Length > 53) - ? $"{libraryBook.Book.Title.Truncate(50)}..." - : libraryBook.Book.Title; - var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; - return errorBookTitle; - }; - - if (string.IsNullOrWhiteSpace(libraryBook.Account)) - throw new Exception(errorString("Account")); - - if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) - throw new Exception(errorString("Locale")); - } - - private void moveBook(LibraryBook libraryBook, string actualFilePath) - { - var newAaxFilename = FileUtility.GetValidFilename( - AudibleFileStorage.DownloadsFinal, - libraryBook.Book.Title, - "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}"); - } - - private static StatusHandler verifyDownload(LibraryBook libraryBook) - => !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId) - ? new StatusHandler { "Downloaded AAX file cannot be found" } - : new StatusHandler(); - } -} diff --git a/FileLiberator/DownloadedChapters.cs b/FileLiberator/DownloadedChapters.cs deleted file mode 100644 index 7a95f83c..00000000 --- a/FileLiberator/DownloadedChapters.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Text; -using AaxDecrypter; -using AudibleApiDTOs; -using Dinah.Core.Diagnostics; - - -namespace FileLiberator -{ - public class DownloadedChapters : Chapters - { - public DownloadedChapters(ChapterInfo chapterInfo) - { - AddChapters(chapterInfo.Chapters - .Select(c => new AaxDecrypter.Chapter(c.StartOffsetMs / 1000d, (c.StartOffsetMs + c.LengthMs) / 1000d, c.Title))); - } - } -}