Improve download/decrypt cancellation
This commit is contained in:
parent
80b86086ca
commit
40b4915b65
@ -25,9 +25,8 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
public override async Task CancelAsync()
|
public override async Task CancelAsync()
|
||||||
{
|
{
|
||||||
IsCanceled = true;
|
await base.CancelAsync();
|
||||||
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
|
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
|
||||||
FinalizeDownload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mp4File Open()
|
private Mp4File Open()
|
||||||
|
|||||||
@ -120,7 +120,12 @@ namespace AaxDecrypter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task CancelAsync();
|
public virtual Task CancelAsync()
|
||||||
|
{
|
||||||
|
IsCanceled = true;
|
||||||
|
FinalizeDownload();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||||
|
|
||||||
public virtual void SetCoverArt(byte[] coverArt) { }
|
public virtual void SetCoverArt(byte[] coverArt) { }
|
||||||
|
|||||||
@ -17,13 +17,6 @@ namespace AaxDecrypter
|
|||||||
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
|
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task CancelAsync()
|
|
||||||
{
|
|
||||||
IsCanceled = true;
|
|
||||||
FinalizeDownload();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||||
{
|
{
|
||||||
await InputFileStream.DownloadTask;
|
await InputFileStream.DownloadTask;
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AaxDecrypter;
|
using AaxDecrypter;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
@ -18,10 +19,15 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
public override string Name => "Download & Decrypt";
|
public override string Name => "Download & Decrypt";
|
||||||
private AudiobookDownloadBase abDownloader;
|
private AudiobookDownloadBase abDownloader;
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||||
|
public override async Task CancelAsync()
|
||||||
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask;
|
{
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
if (abDownloader is not null)
|
||||||
|
await abDownloader.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
@ -41,6 +47,7 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnBegin(libraryBook);
|
OnBegin(libraryBook);
|
||||||
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -50,7 +57,7 @@ namespace FileLiberator
|
|||||||
downloadValidation(libraryBook);
|
downloadValidation(libraryBook);
|
||||||
var api = await libraryBook.GetApiAsync();
|
var api = await libraryBook.GetApiAsync();
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook);
|
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook, cancellationToken);
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
try
|
try
|
||||||
@ -74,38 +81,32 @@ namespace FileLiberator
|
|||||||
.Where(f => f.FileType != FileType.AAXC)
|
.Where(f => f.FileType != FileType.AAXC)
|
||||||
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
|
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
|
||||||
|
|
||||||
return
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
abDownloader?.IsCanceled is true
|
return new StatusHandler { "Decrypt failed" };
|
||||||
? new StatusHandler { "Cancelled" }
|
|
||||||
: new StatusHandler { "Decrypt failed" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||||
|
|
||||||
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken));
|
||||||
Task[] finalTasks =
|
Task[] finalTasks =
|
||||||
[
|
[
|
||||||
Task.Run(() => downloadCoverArt(downloadOptions)),
|
Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)),
|
||||||
moveFilesTask,
|
moveFilesTask,
|
||||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
|
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken))
|
||||||
];
|
];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.WhenAll(finalTasks);
|
await Task.WhenAll(finalTasks);
|
||||||
}
|
}
|
||||||
catch
|
catch when (!moveFilesTask.IsFaulted)
|
||||||
{
|
{
|
||||||
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
|
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
|
||||||
//Only fail if the downloaded audio files failed to move to Books directory
|
//Only fail if the downloaded audio files failed to move to Books directory
|
||||||
if (moveFilesTask.IsFaulted)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (moveFilesTask.IsCompletedSuccessfully)
|
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
|
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
|
||||||
|
|
||||||
@ -115,6 +116,10 @@ namespace FileLiberator
|
|||||||
|
|
||||||
return new StatusHandler();
|
return new StatusHandler();
|
||||||
}
|
}
|
||||||
|
catch when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new StatusHandler { "Cancelled" };
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
@ -257,10 +262,11 @@ namespace FileLiberator
|
|||||||
|
|
||||||
/// <summary>Move new files to 'Books' directory</summary>
|
/// <summary>Move new files to 'Books' directory</summary>
|
||||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||||
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// create final directory. move each file into it
|
// create final directory. move each file into it
|
||||||
var destinationDir = getDestinationDirectory(libraryBook);
|
var destinationDir = getDestinationDirectory(libraryBook);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
for (var i = 0; i < entries.Count; i++)
|
for (var i = 0; i < entries.Count; i++)
|
||||||
{
|
{
|
||||||
@ -278,6 +284,7 @@ namespace FileLiberator
|
|||||||
|
|
||||||
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
// propagate 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 };
|
entries[i] = entry with { Path = realDest };
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||||
@ -287,6 +294,7 @@ namespace FileLiberator
|
|||||||
SetFileTime(libraryBook, cue.Path);
|
SetFileTime(libraryBook, cue.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
AudibleFileStorage.Audio.Refresh();
|
AudibleFileStorage.Audio.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +309,7 @@ namespace FileLiberator
|
|||||||
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
||||||
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||||
|
|
||||||
private static void downloadCoverArt(DownloadOptions options)
|
private static void downloadCoverArt(DownloadOptions options, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!Configuration.Instance.DownloadCoverArt) return;
|
if (!Configuration.Instance.DownloadCoverArt) return;
|
||||||
|
|
||||||
@ -316,7 +324,7 @@ namespace FileLiberator
|
|||||||
if (File.Exists(coverPath))
|
if (File.Exists(coverPath))
|
||||||
FileUtility.SaferDelete(coverPath);
|
FileUtility.SaferDelete(coverPath);
|
||||||
|
|
||||||
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native));
|
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken);
|
||||||
if (picBytes.Length > 0)
|
if (picBytes.Length > 0)
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(coverPath, picBytes);
|
File.WriteAllBytes(coverPath, picBytes);
|
||||||
@ -327,6 +335,7 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
//Failure to download cover art should not be considered a failure to download the book
|
//Failure to download cover art should not be considered a failure to download the book
|
||||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -24,9 +25,10 @@ public partial class DownloadOptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initiate an audiobook download from the audible api.
|
/// Initiate an audiobook download from the audible api.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook)
|
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
|
||||||
{
|
{
|
||||||
var license = await ChooseContent(api, libraryBook, config);
|
var license = await ChooseContent(api, libraryBook, config, token);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
//Some audiobooks will have incorrect chapters in the metadata returned from the license request,
|
//Some audiobooks will have incorrect chapters in the metadata returned from the license request,
|
||||||
//but the metadata returned by the content metadata endpoint will be correct. Call the content
|
//but the metadata returned by the content metadata endpoint will be correct. Call the content
|
||||||
@ -36,9 +38,8 @@ public partial class DownloadOptions
|
|||||||
if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
|
if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
|
||||||
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
|
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
|
||||||
|
|
||||||
var options = BuildDownloadOptions(libraryBook, config, license);
|
token.ThrowIfCancellationRequested();
|
||||||
|
return BuildDownloadOptions(libraryBook, config, license);
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LicenseInfo
|
private class LicenseInfo
|
||||||
@ -57,16 +58,18 @@ public partial class DownloadOptions
|
|||||||
=> voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)];
|
=> voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
|
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
|
||||||
{
|
{
|
||||||
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
|
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
|
||||||
|
|
||||||
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||||
return new LicenseInfo(license);
|
return new LicenseInfo(license);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//try to request a widevine content license using the user's spatial audio settings
|
//try to request a widevine content license using the user's spatial audio settings
|
||||||
@ -85,8 +88,8 @@ public partial class DownloadOptions
|
|||||||
return new LicenseInfo(contentLic);
|
return new LicenseInfo(contentLic);
|
||||||
|
|
||||||
using var client = new HttpClient();
|
using var client = new HttpClient();
|
||||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse, token);
|
||||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
var dash = new MpegDash(mpdResponse.Content.ReadAsStream(token));
|
||||||
|
|
||||||
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
|
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
|
||||||
throw new InvalidDataException("Failed to get mpeg-dash content download url.");
|
throw new InvalidDataException("Failed to get mpeg-dash content download url.");
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -78,13 +79,13 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetPicturePathSynchronously(PictureDefinition def)
|
public static string GetPicturePathSynchronously(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
GetPictureSynchronously(def);
|
GetPictureSynchronously(def, cancellationToken);
|
||||||
return getPath(def);
|
return getPath(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
public static byte[] GetPictureSynchronously(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
lock (cacheLocker)
|
lock (cacheLocker)
|
||||||
{
|
{
|
||||||
@ -94,7 +95,7 @@ namespace LibationFileManager
|
|||||||
var bytes
|
var bytes
|
||||||
= File.Exists(path)
|
= File.Exists(path)
|
||||||
? File.ReadAllBytes(path)
|
? File.ReadAllBytes(path)
|
||||||
: downloadBytes(def);
|
: downloadBytes(def, cancellationToken);
|
||||||
cache[def] = bytes;
|
cache[def] = bytes;
|
||||||
}
|
}
|
||||||
return cache[def];
|
return cache[def];
|
||||||
@ -124,7 +125,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
||||||
private static byte[] downloadBytes(PictureDefinition def)
|
private static byte[] downloadBytes(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (def.PictureId is null)
|
if (def.PictureId is null)
|
||||||
return GetDefaultImage(def.Size);
|
return GetDefaultImage(def.Size);
|
||||||
@ -132,7 +133,7 @@ namespace LibationFileManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
|
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
|
||||||
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg").Result;
|
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg", cancellationToken).Result;
|
||||||
|
|
||||||
// save image file. make sure to not save default image
|
// save image file. make sure to not save default image
|
||||||
var path = getPath(def);
|
var path = getPath(def);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
@ -10,7 +11,7 @@ namespace LibationFileManager
|
|||||||
public static class WindowsDirectory
|
public static class WindowsDirectory
|
||||||
{
|
{
|
||||||
|
|
||||||
public static void SetCoverAsFolderIcon(string pictureId, string directory)
|
public static void SetCoverAsFolderIcon(string pictureId, string directory, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -19,8 +20,7 @@ namespace LibationFileManager
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// get path of cover art in Images dir. Download first if not exists
|
// get path of cover art in Images dir. Download first if not exists
|
||||||
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300));
|
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300), cancellationToken);
|
||||||
|
|
||||||
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
|
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user