Add lame options to ConvertToMp3
This commit is contained in:
parent
2bc74d5378
commit
159f5cbd00
@ -21,6 +21,7 @@ namespace AaxDecrypter
|
|||||||
public event EventHandler<string> FileCreated;
|
public event EventHandler<string> FileCreated;
|
||||||
|
|
||||||
public bool IsCanceled { get; set; }
|
public bool IsCanceled { get; set; }
|
||||||
|
public string TempFilePath { get; }
|
||||||
|
|
||||||
protected string OutputFileName { get; private set; }
|
protected string OutputFileName { get; private set; }
|
||||||
protected DownloadOptions DownloadOptions { get; }
|
protected DownloadOptions DownloadOptions { get; }
|
||||||
@ -33,7 +34,6 @@ namespace AaxDecrypter
|
|||||||
private NetworkFileStreamPersister nfsPersister;
|
private NetworkFileStreamPersister nfsPersister;
|
||||||
|
|
||||||
private string jsonDownloadState { get; }
|
private string jsonDownloadState { get; }
|
||||||
public string TempFilePath { get; }
|
|
||||||
|
|
||||||
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
||||||
{
|
{
|
||||||
@ -65,7 +65,7 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
public bool Run()
|
public bool Run()
|
||||||
{
|
{
|
||||||
var (IsSuccess, Elapsed) = Steps.Run();
|
var (IsSuccess, _) = Steps.Run();
|
||||||
|
|
||||||
if (!IsSuccess)
|
if (!IsSuccess)
|
||||||
Serilog.Log.Logger.Error("Conversion failed");
|
Serilog.Log.Logger.Error("Conversion failed");
|
||||||
@ -79,10 +79,8 @@ namespace AaxDecrypter
|
|||||||
=> RetrievedAuthors?.Invoke(this, authors);
|
=> RetrievedAuthors?.Invoke(this, authors);
|
||||||
protected void OnRetrievedNarrators(string narrators)
|
protected void OnRetrievedNarrators(string narrators)
|
||||||
=> RetrievedNarrators?.Invoke(this, narrators);
|
=> RetrievedNarrators?.Invoke(this, narrators);
|
||||||
|
|
||||||
protected void OnRetrievedCoverArt(byte[] coverArt)
|
protected void OnRetrievedCoverArt(byte[] coverArt)
|
||||||
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
||||||
|
|
||||||
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
||||||
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
||||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using LibationFileManager;
|
||||||
|
using NAudio.Lame;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
@ -12,6 +14,30 @@ namespace FileLiberator
|
|||||||
public event EventHandler<byte[]> CoverImageDiscovered;
|
public event EventHandler<byte[]> CoverImageDiscovered;
|
||||||
public abstract void Cancel();
|
public abstract void Cancel();
|
||||||
|
|
||||||
|
protected LameConfig GetLameOptions(Configuration config)
|
||||||
|
{
|
||||||
|
LameConfig lameConfig = new();
|
||||||
|
lameConfig.Mode = MPEGMode.Mono;
|
||||||
|
|
||||||
|
if (config.LameTargetBitrate)
|
||||||
|
{
|
||||||
|
if (config.LameConstantBitrate)
|
||||||
|
lameConfig.BitRate = config.LameBitrate;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lameConfig.ABRRateKbps = config.LameBitrate;
|
||||||
|
lameConfig.VBR = VBRMode.ABR;
|
||||||
|
lameConfig.WriteVBRTag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lameConfig.VBR = VBRMode.Default;
|
||||||
|
lameConfig.VBRQuality = config.LameVBRQuality;
|
||||||
|
lameConfig.WriteVBRTag = true;
|
||||||
|
}
|
||||||
|
return lameConfig;
|
||||||
|
}
|
||||||
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
||||||
protected void OnTitleDiscovered(object _, string title)
|
protected void OnTitleDiscovered(object _, string title)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using System.Threading.Tasks;
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using AAXClean.Codecs;
|
using AAXClean.Codecs;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
@ -12,85 +11,85 @@ using LibationFileManager;
|
|||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public class ConvertToMp3 : AudioDecodable
|
public class ConvertToMp3 : AudioDecodable
|
||||||
{
|
{
|
||||||
public override string Name => "Convert to Mp3";
|
public override string Name => "Convert to Mp3";
|
||||||
private Mp4File m4bBook;
|
private Mp4File m4bBook;
|
||||||
|
|
||||||
private long fileSize;
|
private long fileSize;
|
||||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||||
|
|
||||||
public override void Cancel()
|
public override void Cancel()
|
||||||
{
|
|
||||||
m4bBook?.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
|
||||||
{
|
{
|
||||||
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
m4bBook?.Cancel();
|
||||||
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||||
|
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
||||||
{
|
|
||||||
OnBegin(libraryBook);
|
|
||||||
|
|
||||||
try
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
OnBegin(libraryBook);
|
||||||
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
|
||||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
|
||||||
|
|
||||||
fileSize = m4bBook.InputStream.Length;
|
try
|
||||||
|
{
|
||||||
|
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||||
|
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
||||||
|
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||||
|
|
||||||
OnTitleDiscovered(m4bBook.AppleTags.Title);
|
fileSize = m4bBook.InputStream.Length;
|
||||||
OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor);
|
|
||||||
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
|
||||||
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
|
||||||
|
|
||||||
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
OnTitleDiscovered(m4bBook.AppleTags.Title);
|
||||||
|
OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor);
|
||||||
|
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
||||||
|
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
||||||
|
|
||||||
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File));
|
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
||||||
m4bBook.InputStream.Close();
|
var lameConfig = GetLameOptions(Configuration.Instance);
|
||||||
mp3File.Close();
|
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File, lameConfig));
|
||||||
|
m4bBook.InputStream.Close();
|
||||||
|
mp3File.Close();
|
||||||
|
|
||||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
var proposedMp3Path = Mp3FileName(m4bPath);
|
||||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
||||||
OnFileCreated(libraryBook, realMp3Path);
|
OnFileCreated(libraryBook, realMp3Path);
|
||||||
|
|
||||||
if (result == ConversionResult.Failed)
|
if (result == ConversionResult.Failed)
|
||||||
return new StatusHandler { "Conversion failed" };
|
return new StatusHandler { "Conversion failed" };
|
||||||
else if (result == ConversionResult.Cancelled)
|
else if (result == ConversionResult.Cancelled)
|
||||||
return new StatusHandler { "Cancelled" };
|
return new StatusHandler { "Cancelled" };
|
||||||
else
|
else
|
||||||
return new StatusHandler();
|
return new StatusHandler();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||||
{
|
{
|
||||||
var duration = m4bBook.Duration;
|
var duration = m4bBook.Duration;
|
||||||
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||||
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||||
|
|
||||||
if (double.IsNormal(estTimeRemaining))
|
if (double.IsNormal(estTimeRemaining))
|
||||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||||
|
|
||||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||||
|
|
||||||
OnStreamingProgressChanged(
|
OnStreamingProgressChanged(
|
||||||
new DownloadProgress
|
new DownloadProgress
|
||||||
{
|
{
|
||||||
ProgressPercentage = progressPercent,
|
ProgressPercentage = progressPercent,
|
||||||
BytesReceived = (long)(fileSize * progressPercent),
|
BytesReceived = (long)(fileSize * progressPercent),
|
||||||
TotalBytesToReceive = fileSize
|
TotalBytesToReceive = fileSize
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,241 +14,222 @@ using LibationFileManager;
|
|||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public class DownloadDecryptBook : AudioDecodable
|
public class DownloadDecryptBook : AudioDecodable
|
||||||
{
|
{
|
||||||
public override string Name => "Download & Decrypt";
|
public override string Name => "Download & Decrypt";
|
||||||
private AudiobookDownloadBase abDownloader;
|
private AudiobookDownloadBase abDownloader;
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||||
|
|
||||||
public override void Cancel() => abDownloader?.Cancel();
|
public override void Cancel() => abDownloader?.Cancel();
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
var entries = new List<FilePathCache.CacheEntry>();
|
var entries = new List<FilePathCache.CacheEntry>();
|
||||||
// these only work so minimally b/c CacheEntry is a record.
|
// these only work so minimally b/c CacheEntry is a record.
|
||||||
// in case of parallel decrypts, only capture the ones for this book id.
|
// 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
|
// 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)
|
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 abDownloader?.IsCanceled == true ?
|
|
||||||
new StatusHandler { "Cancelled" } :
|
|
||||||
new StatusHandler { "Decrypt failed" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// moves new files from temp dir to final dest.
|
|
||||||
// This could take a few seconds if moving hundreds of files.
|
|
||||||
var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
|
||||||
|
|
||||||
// decrypt failed
|
|
||||||
if (!movedAudioFile)
|
|
||||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
|
||||||
|
|
||||||
if (Configuration.Instance.DownloadCoverArt)
|
|
||||||
DownloadCoverArt(libraryBook);
|
|
||||||
|
|
||||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
|
||||||
|
|
||||||
return new StatusHandler();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
OnCompleted(libraryBook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||||
audiobookDlLic.LameConfig.BitRate = config.LameBitrate;
|
entries.Add(e);
|
||||||
else
|
|
||||||
{
|
|
||||||
audiobookDlLic.LameConfig.ABRRateKbps = config.LameBitrate;
|
|
||||||
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.ABR;
|
|
||||||
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 abDownloader?.IsCanceled == true ?
|
||||||
|
new StatusHandler { "Cancelled" } :
|
||||||
|
new StatusHandler { "Decrypt failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves new files from temp dir to final dest.
|
||||||
|
// This could take a few seconds if moving hundreds of files.
|
||||||
|
var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||||
|
|
||||||
|
// decrypt failed
|
||||||
|
if (!movedAudioFile)
|
||||||
|
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||||
|
|
||||||
|
if (Configuration.Instance.DownloadCoverArt)
|
||||||
|
DownloadCoverArt(libraryBook);
|
||||||
|
|
||||||
|
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||||
|
|
||||||
|
return new StatusHandler();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
OnCompleted(libraryBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.Default;
|
AaxcDownloadConvertBase converter
|
||||||
audiobookDlLic.LameConfig.VBRQuality = config.LameVBRQuality;
|
= config.SplitFilesByChapter ?
|
||||||
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
new AaxcDownloadMultiConverter(
|
||||||
}
|
outFileName, cacheDir, audiobookDlLic,
|
||||||
|
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)) :
|
||||||
|
new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic);
|
||||||
|
|
||||||
return audiobookDlLic;
|
if (config.AllowLibationFixup)
|
||||||
}
|
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
|
||||||
|
|
||||||
private static void downloadValidation(LibraryBook libraryBook)
|
abDownloader = converter;
|
||||||
{
|
}
|
||||||
string errorString(string field)
|
|
||||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
|
||||||
|
|
||||||
string errorTitle()
|
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
||||||
{
|
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
||||||
var title
|
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
||||||
= (libraryBook.Book.Title.Length > 53)
|
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
||||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
||||||
: libraryBook.Book.Title;
|
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||||
return errorBookTitle;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
// REAL WORK DONE HERE
|
||||||
throw new Exception(errorString("Account"));
|
var success = await Task.Run(abDownloader.Run);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
return success;
|
||||||
throw new Exception(errorString("Locale"));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
private DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||||
{
|
{
|
||||||
if (e is not null)
|
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
||||||
OnCoverImageDiscovered(e);
|
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
||||||
else if (Configuration.Instance.AllowLibationFixup)
|
//These assumptions may be wrong, and only time and bug reports will tell.
|
||||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Move new files to 'Books' directory</summary>
|
bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm;
|
||||||
/// <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);
|
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
||||||
|
OutputFormat.Mp3 : OutputFormat.M4b;
|
||||||
|
|
||||||
|
var dlOptions = 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,
|
||||||
|
LameConfig = GetLameOptions(config)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
|
||||||
|
{
|
||||||
|
long startMs = dlOptions.TrimOutputToChapterLength ?
|
||||||
|
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
||||||
|
|
||||||
|
dlOptions.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;
|
||||||
|
|
||||||
|
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dlOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
abDownloader.SetCoverArt(OnRequestCoverArt());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
if (getFirstAudio() == default)
|
||||||
return false;
|
return false;
|
||||||
@ -273,33 +254,33 @@ namespace FileLiberator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadCoverArt(LibraryBook libraryBook)
|
private void DownloadCoverArt(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||||
var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
||||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(coverPath))
|
if (File.Exists(coverPath))
|
||||||
FileUtility.SaferDelete(coverPath);
|
FileUtility.SaferDelete(coverPath);
|
||||||
|
|
||||||
(string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ?
|
(string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ?
|
||||||
(libraryBook.Book.PictureId, PictureSize.Native) :
|
(libraryBook.Book.PictureId, PictureSize.Native) :
|
||||||
(libraryBook.Book.PictureLarge, PictureSize.Native);
|
(libraryBook.Book.PictureLarge, PictureSize.Native);
|
||||||
|
|
||||||
var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size));
|
var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size));
|
||||||
|
|
||||||
if (picBytes.Length > 0)
|
if (picBytes.Length > 0)
|
||||||
File.WriteAllBytes(coverPath, picBytes);
|
File.WriteAllBytes(coverPath, picBytes);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
//Failure to download cover art should not be
|
//Failure to download cover art should not be
|
||||||
//considered a failure to download the book
|
//considered a failure to download the book
|
||||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user