309 lines
13 KiB
C#
309 lines
13 KiB
C#
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 LibationFileManager;
|
|
|
|
namespace FileLiberator
|
|
{
|
|
public class DownloadDecryptBook : AudioDecodable
|
|
{
|
|
private AudiobookDownloadBase abDownloader;
|
|
|
|
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
|
|
|
|
public override void Cancel() => abDownloader?.Cancel();
|
|
|
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
|
{
|
|
var entries = new List<FilePathCache.CacheEntry>();
|
|
// these only work so minimally b/c CacheEntry is a record.
|
|
// in case of parallel decrypts, only capture the ones for this book id.
|
|
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
|
|
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
|
|
{
|
|
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
|
entries.Add(e);
|
|
}
|
|
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
|
|
{
|
|
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
|
entries.Remove(e);
|
|
}
|
|
|
|
OnBegin(libraryBook);
|
|
|
|
try
|
|
{
|
|
if (libraryBook.Book.Audio_Exists)
|
|
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
|
|
|
bool success = false;
|
|
try
|
|
{
|
|
FilePathCache.Inserted += FilePathCache_Inserted;
|
|
FilePathCache.Removed += FilePathCache_Removed;
|
|
|
|
success = await downloadAudiobookAsync(libraryBook);
|
|
}
|
|
finally
|
|
{
|
|
FilePathCache.Inserted -= FilePathCache_Inserted;
|
|
FilePathCache.Removed -= FilePathCache_Removed;
|
|
}
|
|
|
|
// decrypt failed
|
|
if (!success)
|
|
{
|
|
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
|
|
FileUtility.SaferDelete(tmpFile.Path);
|
|
|
|
return new StatusHandler { "Decrypt failed" };
|
|
}
|
|
|
|
// moves new files from temp dir to final dest
|
|
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
|
|
|
|
DownloadCoverArt(libraryBook);
|
|
|
|
// decrypt failed
|
|
if (!movedAudioFile)
|
|
return new StatusHandler { "Cannot find final audio file after decryption" };
|
|
|
|
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
|
|
|
return new StatusHandler();
|
|
}
|
|
finally
|
|
{
|
|
OnCompleted(libraryBook);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
|
{
|
|
OnStreamingBegin($"Begin decrypting {libraryBook}");
|
|
|
|
try
|
|
{
|
|
var config = Configuration.Instance;
|
|
|
|
downloadValidation(libraryBook);
|
|
|
|
var api = await libraryBook.GetApiAsync();
|
|
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
|
var audiobookDlLic = BuildDownloadOptions(config, contentLic);
|
|
|
|
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.OutputFormat.ToString().ToLower());
|
|
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
|
|
|
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
|
|
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
|
|
else
|
|
{
|
|
AaxcDownloadConvertBase converter
|
|
= config.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
|
|
outFileName, cacheDir, audiobookDlLic,
|
|
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook))
|
|
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic);
|
|
|
|
if (config.AllowLibationFixup)
|
|
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames);
|
|
|
|
abDownloader = converter;
|
|
}
|
|
|
|
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
|
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
|
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
|
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
|
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
|
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
|
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
|
|
|
// REAL WORK DONE HERE
|
|
var success = await Task.Run(abDownloader.Run);
|
|
return success;
|
|
}
|
|
finally
|
|
{
|
|
OnStreamingCompleted($"Completed downloading and decrypting {libraryBook.Book.Title}");
|
|
}
|
|
}
|
|
|
|
private static DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
|
{
|
|
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
|
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
|
//These assumptions may be wrong, and only time and bug reports will tell.
|
|
|
|
bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm;
|
|
|
|
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
|
OutputFormat.Mp3 : OutputFormat.M4b;
|
|
|
|
var audiobookDlLic = new DownloadOptions
|
|
(
|
|
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
|
Resources.USER_AGENT
|
|
)
|
|
{
|
|
AudibleKey = contentLic?.Voucher?.Key,
|
|
AudibleIV = contentLic?.Voucher?.Iv,
|
|
OutputFormat = outputFormat,
|
|
TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio,
|
|
RetainEncryptedFile = config.RetainAaxFile && encrypted,
|
|
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
|
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
|
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
|
CreateCueSheet = config.CreateCueSheet
|
|
};
|
|
|
|
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
|
|
{
|
|
|
|
long startMs = audiobookDlLic.TrimOutputToChapterLength ?
|
|
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
|
|
|
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs));
|
|
|
|
for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++)
|
|
{
|
|
var chapter = contentLic.ContentMetadata.ChapterInfo.Chapters[i];
|
|
long chapLenMs = chapter.LengthMs;
|
|
|
|
if (i == 0)
|
|
chapLenMs -= startMs;
|
|
|
|
if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1)
|
|
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
|
|
|
audiobookDlLic.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
|
}
|
|
}
|
|
|
|
audiobookDlLic.LameConfig = new();
|
|
audiobookDlLic.LameConfig.Mode = NAudio.Lame.MPEGMode.Mono;
|
|
|
|
if (config.LameTargetBitrate)
|
|
{
|
|
if (config.LameConstantBitrate)
|
|
audiobookDlLic.LameConfig.BitRate = config.LameBitrate;
|
|
else
|
|
{
|
|
audiobookDlLic.LameConfig.ABRRateKbps = config.LameBitrate;
|
|
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.ABR;
|
|
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.Default;
|
|
audiobookDlLic.LameConfig.VBRQuality = config.LameVBRQuality;
|
|
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
|
}
|
|
|
|
return audiobookDlLic;
|
|
}
|
|
|
|
private static void downloadValidation(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 AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
|
{
|
|
if (e is not null)
|
|
OnCoverImageDiscovered(e);
|
|
else if (Configuration.Instance.AllowLibationFixup)
|
|
OnRequestCoverArt(abDownloader.SetCoverArt);
|
|
}
|
|
|
|
/// <summary>Move new files to 'Books' directory</summary>
|
|
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
|
|
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
|
{
|
|
// create final directory. move each file into it
|
|
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
|
Directory.CreateDirectory(destinationDir);
|
|
|
|
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
|
|
|
if (getFirstAudio() == default)
|
|
return false;
|
|
|
|
for (var i = 0; i < entries.Count; i++)
|
|
{
|
|
var entry = entries[i];
|
|
|
|
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
|
|
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
|
|
|
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
|
entries[i] = entry with { Path = realDest };
|
|
}
|
|
|
|
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
|
if (cue != default)
|
|
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
|
|
|
AudibleFileStorage.Audio.Refresh();
|
|
|
|
return true;
|
|
}
|
|
|
|
private void DownloadCoverArt(LibraryBook libraryBook)
|
|
{
|
|
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
|
var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
|
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
|
|
|
try
|
|
{
|
|
if (File.Exists(coverPath))
|
|
FileUtility.SaferDelete(coverPath);
|
|
|
|
(string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ?
|
|
(libraryBook.Book.PictureId, PictureSize.Native) :
|
|
(libraryBook.Book.PictureLarge, PictureSize.Native);
|
|
|
|
var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size));
|
|
|
|
if (picBytes.Length > 0)
|
|
File.WriteAllBytes(coverPath, picBytes);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Failure to download cover art should not be
|
|
//considered a failure to download the book
|
|
Serilog.Log.Logger.Error(ex.Message);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|