From b27325cdcbc2d78657384e8096742a07fde77027 Mon Sep 17 00:00:00 2001 From: MBucari Date: Fri, 25 Jul 2025 15:35:03 -0600 Subject: [PATCH] Improve comvert to mp3 task - Improve progress reporting and cancellation performance - Clear current book from queue before queueing single convert to mp3 task --- Source/FileLiberator/ConvertToMp3.cs | 110 +++++++++++------- .../ProcessQueue/ProcessQueueViewModel.cs | 2 + 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index 2639c23f..6fc97d7c 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AAXClean; using AAXClean.Codecs; @@ -19,7 +20,13 @@ namespace FileLiberator private readonly AaxDecrypter.AverageSpeed averageSpeed = new(); private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3"); - public override Task CancelAsync() => Mp4Operation?.CancelAsync() ?? Task.CompletedTask; + private CancellationTokenSource CancellationTokenSource { get; set; } + public override async Task CancelAsync() + { + await CancellationTokenSource.CancelAsync(); + if (Mp4Operation is not null) + await Mp4Operation.CancelAsync(); + } public static bool ValidateMp3(LibraryBook libraryBook) { @@ -32,17 +39,29 @@ namespace FileLiberator public override async Task ProcessAsync(LibraryBook libraryBook) { OnBegin(libraryBook); + var cancellationToken = (CancellationTokenSource = new()).Token; try { - var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId); + var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId) + .Where(m4bPath => File.Exists(m4bPath)) + .Select(m4bPath => new { m4bPath, proposedMp3Path = Mp3FileName(m4bPath), m4bSize = new FileInfo(m4bPath).Length }) + .Where(p => !File.Exists(p.proposedMp3Path)) + .ToArray(); - foreach (var m4bPath in m4bPaths) + long totalInputSize = m4bPaths.Sum(p => p.m4bSize); + long sizeOfCompletedFiles = 0L; + foreach (var entry in m4bPaths) { - var proposedMp3Path = Mp3FileName(m4bPath); - if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue; + cancellationToken.ThrowIfCancellationRequested(); + if (File.Exists(entry.proposedMp3Path) || !File.Exists(entry.m4bPath)) + { + sizeOfCompletedFiles += entry.m4bSize; + continue; + } - var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read)); + using var m4bFileStream = File.Open(entry.m4bPath, FileMode.Open, FileAccess.Read, FileShare.Read); + var m4bBook = new Mp4File(m4bFileStream); //AAXClean.Codecs only supports decoding AAC and E-AC-3 audio. if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null) @@ -69,74 +88,85 @@ namespace FileLiberator lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString(); } - using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite); + long currentFileNumBytesProcessed = 0; try { - Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters); - Mp4Operation.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; - await Mp4Operation; - - if (Mp4Operation.IsCanceled) + var tempPath = Path.GetTempFileName(); + using (var mp3File = File.Open(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { - FileUtility.SaferDelete(mp3File.Name); - return new StatusHandler { "Cancelled" }; + Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters); + Mp4Operation.ConversionProgressUpdate += m4bBook_ConversionProgressUpdate; + await Mp4Operation; } - else - { - var realMp3Path + + if (cancellationToken.IsCancellationRequested) + FileUtility.SaferDelete(tempPath); + + cancellationToken.ThrowIfCancellationRequested(); + + var realMp3Path = FileUtility.SaferMoveToValidPath( - mp3File.Name, - proposedMp3Path, + tempPath, + entry.proposedMp3Path, Configuration.Instance.ReplacementCharacters, extension: "mp3", Configuration.Instance.OverwriteExisting); - SetFileTime(libraryBook, realMp3Path); - SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path)); - - OnFileCreated(libraryBook, realMp3Path); - } - } - catch (Exception ex) - { - Serilog.Log.Error(ex, "AAXClean error"); - return new StatusHandler { "Conversion failed" }; + SetFileTime(libraryBook, realMp3Path); + SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path)); + OnFileCreated(libraryBook, realMp3Path); } finally { if (Mp4Operation is not null) - Mp4Operation.ConversionProgressUpdate -= M4bBook_ConversionProgressUpdate; + Mp4Operation.ConversionProgressUpdate -= m4bBook_ConversionProgressUpdate; - m4bBook.InputStream.Close(); - mp3File.Close(); + sizeOfCompletedFiles += entry.m4bSize; + } + void m4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) + { + currentFileNumBytesProcessed = (long)(e.FractionCompleted * entry.m4bSize); + var bytesCompleted = sizeOfCompletedFiles + currentFileNumBytesProcessed; + ConversionProgressUpdate(totalInputSize, bytesCompleted); } } + return new StatusHandler(); + } + catch (Exception ex) + { + if (!cancellationToken.IsCancellationRequested) + { + Serilog.Log.Error(ex, "AAXClean error"); + return new StatusHandler { "Conversion failed" }; + } + return new StatusHandler { "Cancelled" }; } finally { OnCompleted(libraryBook); + CancellationTokenSource.Dispose(); + CancellationTokenSource = null; } - return new StatusHandler(); } - private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) + private void ConversionProgressUpdate(long totalInputSize, long bytesCompleted) { - averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds); + averageSpeed.AddPosition(bytesCompleted); - var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds; - var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average; + var remainingBytes = (totalInputSize - bytesCompleted); + var estTimeRemaining = remainingBytes / averageSpeed.Average; if (double.IsNormal(estTimeRemaining)) OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); - double progressPercent = 100 * e.FractionCompleted; + double progressPercent = 100 * bytesCompleted / totalInputSize; OnStreamingProgressChanged( new DownloadProgress { ProgressPercentage = progressPercent, - BytesReceived = (long)(e.ProcessPosition - e.StartTime).TotalSeconds, - TotalBytesToReceive = (long)(e.EndTime - e.StartTime).TotalSeconds + BytesReceived = bytesCompleted, + TotalBytesToReceive = totalInputSize }); } } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index d3172b40..b9c1619a 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -111,6 +111,8 @@ public class ProcessQueueViewModel : ReactiveObject var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray(); if (preLiberated.Length > 0) { + if (preLiberated.Length == 1) + RemoveCompleted(preLiberated[0]); Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length); AddConvertMp3(preLiberated); return true;