Improve comvert to mp3 task

- Improve progress reporting and cancellation performance
- Clear current book from queue before queueing single convert to mp3 task
This commit is contained in:
MBucari 2025-07-25 15:35:03 -06:00
parent accedeb1b1
commit b27325cdcb
2 changed files with 72 additions and 40 deletions

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AAXClean; using AAXClean;
using AAXClean.Codecs; using AAXClean.Codecs;
@ -19,7 +20,13 @@ namespace FileLiberator
private readonly AaxDecrypter.AverageSpeed averageSpeed = new(); private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3"); 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) public static bool ValidateMp3(LibraryBook libraryBook)
{ {
@ -32,17 +39,29 @@ namespace FileLiberator
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
OnBegin(libraryBook); OnBegin(libraryBook);
var cancellationToken = (CancellationTokenSource = new()).Token;
try 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); cancellationToken.ThrowIfCancellationRequested();
if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue; 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. //AAXClean.Codecs only supports decoding AAC and E-AC-3 audio.
if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null) 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(); lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString();
} }
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite); long currentFileNumBytesProcessed = 0;
try try
{ {
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters); var tempPath = Path.GetTempFileName();
Mp4Operation.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; using (var mp3File = File.Open(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
await Mp4Operation;
if (Mp4Operation.IsCanceled)
{ {
FileUtility.SaferDelete(mp3File.Name); Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
return new StatusHandler { "Cancelled" }; Mp4Operation.ConversionProgressUpdate += m4bBook_ConversionProgressUpdate;
await Mp4Operation;
} }
else
{ if (cancellationToken.IsCancellationRequested)
var realMp3Path FileUtility.SaferDelete(tempPath);
cancellationToken.ThrowIfCancellationRequested();
var realMp3Path
= FileUtility.SaferMoveToValidPath( = FileUtility.SaferMoveToValidPath(
mp3File.Name, tempPath,
proposedMp3Path, entry.proposedMp3Path,
Configuration.Instance.ReplacementCharacters, Configuration.Instance.ReplacementCharacters,
extension: "mp3", extension: "mp3",
Configuration.Instance.OverwriteExisting); Configuration.Instance.OverwriteExisting);
SetFileTime(libraryBook, realMp3Path); SetFileTime(libraryBook, realMp3Path);
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path)); SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
OnFileCreated(libraryBook, realMp3Path);
OnFileCreated(libraryBook, realMp3Path);
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "AAXClean error");
return new StatusHandler { "Conversion failed" };
} }
finally finally
{ {
if (Mp4Operation is not null) if (Mp4Operation is not null)
Mp4Operation.ConversionProgressUpdate -= M4bBook_ConversionProgressUpdate; Mp4Operation.ConversionProgressUpdate -= m4bBook_ConversionProgressUpdate;
m4bBook.InputStream.Close(); sizeOfCompletedFiles += entry.m4bSize;
mp3File.Close(); }
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 finally
{ {
OnCompleted(libraryBook); 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 remainingBytes = (totalInputSize - bytesCompleted);
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average; var estTimeRemaining = remainingBytes / averageSpeed.Average;
if (double.IsNormal(estTimeRemaining)) if (double.IsNormal(estTimeRemaining))
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.FractionCompleted; double progressPercent = 100 * bytesCompleted / totalInputSize;
OnStreamingProgressChanged( OnStreamingProgressChanged(
new DownloadProgress new DownloadProgress
{ {
ProgressPercentage = progressPercent, ProgressPercentage = progressPercent,
BytesReceived = (long)(e.ProcessPosition - e.StartTime).TotalSeconds, BytesReceived = bytesCompleted,
TotalBytesToReceive = (long)(e.EndTime - e.StartTime).TotalSeconds TotalBytesToReceive = totalInputSize
}); });
} }
} }

View File

@ -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(); 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 > 0)
{ {
if (preLiberated.Length == 1)
RemoveCompleted(preLiberated[0]);
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length); Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
AddConvertMp3(preLiberated); AddConvertMp3(preLiberated);
return true; return true;