using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using AaxDecrypter; using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; using FileManager; 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; // ValidateAsync() doesn't need UI context public async Task ValidateAsync(LibraryBook libraryBook) => await validateAsync_ConfigureAwaitFalse(libraryBook.Book.AudibleProductId).ConfigureAwait(false); private async Task validateAsync_ConfigureAwaitFalse(string productId) => await AudibleFileStorage.AAX.ExistsAsync(productId) && !await AudibleFileStorage.Audio.ExistsAsync(productId); // do NOT use ConfigureAwait(false) on ProcessAsync() // often calls events which prints to forms in the UI context public async Task ProcessAsync(LibraryBook libraryBook) { var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"; Begin?.Invoke(this, displayMessage); try { var aaxFilename = await AudibleFileStorage.AAX.GetAsync(libraryBook.Book.AudibleProductId); if (aaxFilename == null) return new StatusHandler { "aaxFilename parameter is null" }; if (!FileUtility.FileExists(aaxFilename)) return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" }; if (await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b"); var outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename); // decrypt failed if (outputAudioFilename == null) return new StatusHandler { "Decrypt failed" }; moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); Dinah.Core.IO.FileExt.SafeDelete(aaxFilename); var statusHandler = new StatusHandler(); var finalAudioExists = await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId); if (!finalAudioExists) statusHandler.AddError("Cannot find final audio file after decryption"); return statusHandler; } finally { Completed?.Invoke(this, displayMessage); } } private async Task aaxToM4bConverterDecrypt(string proposedOutputFile, string aaxFilename) { DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}"); try { var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, Configuration.Instance.DecryptKey); 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); converter.SetOutputFilename(proposedOutputFile); converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); // REAL WORK DONE HERE var success = await Task.Run(() => converter.Run()); if (!success) { Console.WriteLine("decrypt failed"); return null; } Configuration.Instance.DecryptKey = converter.decryptKey; return converter.outputFileName; } finally { DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}"); } } private static void moveFilesToBooksDir(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(); // create final directory. move each file into it. MOVE AUDIO FILE LAST // new dir: safetitle_limit50char + " [" + productId + "]" // to prevent the paths from getting too long, we don't need after the 1st ":" for the folder var underscoreIndex = product.Title.IndexOf(':'); var titleDir = (underscoreIndex < 4) ? product.Title : product.Title.Substring(0, underscoreIndex); var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId); Directory.CreateDirectory(finalDir); // move audio files to the end of the collection so these files are moved last var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f)); files = files .Except(musicFiles) .Concat(musicFiles) .ToList(); var musicFileExt = musicFiles .Select(f => f.Extension) .Distinct() .Single() .Trim('.'); foreach (var f in files) { var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f) // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext ? FileUtility.GetValidFilename(finalDir, product.Title, musicFileExt, product.AudibleProductId) // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext : FileUtility.GetValidFilename(finalDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt); File.Move(f.FullName, dest); } } } }