Download logic in DownloadPdf should look more like DownloadBook. Extract common d/l pattern to base class

This commit is contained in:
Robert McRackan 2019-11-15 12:50:00 -05:00
parent 9076fae6f6
commit d53a617bc8
6 changed files with 94 additions and 134 deletions

View File

@ -31,9 +31,9 @@ namespace FileLiberator
private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId) private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId)
=> !await AudibleFileStorage.Audio.ExistsAsync(productId); => !await AudibleFileStorage.Audio.ExistsAsync(productId);
// do NOT use ConfigureAwait(false) on ProcessUnregistered() // do NOT use ConfigureAwait(false) on ProcessAsync()
// often does a lot with forms in the UI context // often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
var productId = libraryBook.Book.AudibleProductId; var productId = libraryBook.Book.AudibleProductId;
var displayMessage = $"[{productId}] {libraryBook.Book.Title}"; var displayMessage = $"[{productId}] {libraryBook.Book.Title}";
@ -44,19 +44,19 @@ namespace FileLiberator
{ {
{ {
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.AAX, DownloadBook); var statusHandler = await processAsync(libraryBook, AudibleFileStorage.AAX, DownloadBook);
if (statusHandler.Any()) if (statusHandler.HasErrors)
return statusHandler; return statusHandler;
} }
{ {
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.Audio, DecryptBook); var statusHandler = await processAsync(libraryBook, AudibleFileStorage.Audio, DecryptBook);
if (statusHandler.Any()) if (statusHandler.HasErrors)
return statusHandler; return statusHandler;
} }
{ {
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.PDF, DownloadPdf); var statusHandler = await processAsync(libraryBook, AudibleFileStorage.PDF, DownloadPdf);
if (statusHandler.Any()) if (statusHandler.HasErrors)
return statusHandler; return statusHandler;
} }

View File

@ -41,9 +41,9 @@ namespace FileLiberator
=> await AudibleFileStorage.AAX.ExistsAsync(productId) => await AudibleFileStorage.AAX.ExistsAsync(productId)
&& !await AudibleFileStorage.Audio.ExistsAsync(productId); && !await AudibleFileStorage.Audio.ExistsAsync(productId);
// do NOT use ConfigureAwait(false) on ProcessUnregistered() // do NOT use ConfigureAwait(false) on ProcessAsync()
// often does a lot with forms in the UI context // often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"; var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
@ -60,11 +60,8 @@ namespace FileLiberator
if (await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)) if (await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
string proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b"); var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
var outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
string outputAudioFilename;
//outputAudioFilename = await inAudibleDecrypt(proposedOutputFile, aaxFilename);
outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
// decrypt failed // decrypt failed
if (outputAudioFilename == null) if (outputAudioFilename == null)

View File

@ -21,55 +21,46 @@ namespace FileLiberator
=> !await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId) => !await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)
&& !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId); && !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook) public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{ {
var tempAaxFilename = FileUtility.GetValidFilename( var tempAaxFilename = getDownloadPath(libraryBook);
var actualFilePath = await downloadBookAsync(libraryBook, tempAaxFilename);
moveBook(libraryBook, actualFilePath);
return await verifyDownloadAsync(libraryBook);
}
private static string getDownloadPath(LibraryBook libraryBook)
=> FileUtility.GetValidFilename(
AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DownloadsInProgress,
libraryBook.Book.Title, libraryBook.Book.Title,
"aax", "aax",
libraryBook.Book.AudibleProductId); libraryBook.Book.AudibleProductId);
// if getting from full title: private async Task<string> downloadBookAsync(LibraryBook libraryBook, string tempAaxFilename)
// '?' is allowed {
// colons are inconsistent but not problematic to just leave them var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile);
// - 1 colon: sometimes full title is used. sometimes only the part before the colon is used
// - multple colons: only the part before the final colon is used
// e.g. Alien: Out of the Shadows: An Audible Original Drama => Alien: Out of the Shadows
// in cases where title includes '&', just use everything before the '&' and ignore the rest
//// var adhTitle = product.Title.Split('&')[0]
// new/api method var actualFilePath = await PerformDownloadAsync(
tempAaxFilename = await performApiDownloadAsync(libraryBook, tempAaxFilename); tempAaxFilename,
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
// move return actualFilePath;
var aaxFilename = FileUtility.GetValidFilename( }
private void moveBook(LibraryBook libraryBook, string actualFilePath)
{
var newAaxFilename = FileUtility.GetValidFilename(
AudibleFileStorage.DownloadsFinal, AudibleFileStorage.DownloadsFinal,
libraryBook.Book.Title, libraryBook.Book.Title,
"aax", "aax",
libraryBook.Book.AudibleProductId); libraryBook.Book.AudibleProductId);
File.Move(tempAaxFilename, aaxFilename); File.Move(actualFilePath, newAaxFilename);
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
var statusHandler = new StatusHandler();
var isDownloaded = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
if (isDownloaded)
Invoke_StatusUpdate($"Downloaded: {aaxFilename}");
else
statusHandler.AddError("Downloaded AAX file cannot be found");
return statusHandler;
} }
private async Task<string> performApiDownloadAsync(LibraryBook libraryBook, string tempAaxFilename) private static async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
{ => !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId)
var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile); ? new StatusHandler { "Downloaded AAX file cannot be found" }
: new StatusHandler();
var progress = new Progress<Dinah.Core.Net.Http.DownloadProgress>();
progress.ProgressChanged += (_, e) => Invoke_DownloadProgressChanged(this, e);
Invoke_DownloadBegin(tempAaxFilename);
var actualFilePath = await api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, progress);
Invoke_DownloadCompleted(this, $"Completed: {actualFilePath}");
return actualFilePath;
}
} }
} }

View File

@ -1,102 +1,57 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using DataLayer; using DataLayer;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
using FileManager; using FileManager;
namespace FileLiberator namespace FileLiberator
{ {
public class DownloadPdf : DownloadableBase public class DownloadPdf : DownloadableBase
{ {
static DownloadPdf()
{
// https://stackoverflow.com/a/15483698
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
}
public override async Task<bool> ValidateAsync(LibraryBook libraryBook) public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
{ {
var product = libraryBook.Book; if (string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)))
if (!product.Supplements.Any())
return false; return false;
return !await AudibleFileStorage.PDF.ExistsAsync(product.AudibleProductId); return !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
} }
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook) public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{ {
var product = libraryBook.Book; var proposedDownloadFilePath = await getProposedDownloadFilePathAsync(libraryBook);
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
if (product == null) return await verifyDownloadAsync(libraryBook);
return new StatusHandler { "Book not found" };
var urls = product.Supplements.Select(d => d.Url).ToList();
if (urls.Count == 0)
return new StatusHandler { "PDF download url not found" };
// sanity check
if (urls.Count > 1)
throw new Exception("Multiple PDF downloads are not currently supported. Typically indicates an error");
var destinationDir = await getDestinationDirectoryAsync(product.AudibleProductId);
if (destinationDir == null)
return new StatusHandler { "Destination directory not found for PDF download" };
var url = urls.Single();
var destinationFilename = Path.Combine(destinationDir, Path.GetFileName(url));
await performDownloadAsync(url, destinationFilename);
var statusHandler = new StatusHandler();
var exists = await AudibleFileStorage.PDF.ExistsAsync(product.AudibleProductId);
if (!exists)
statusHandler.AddError("Downloaded PDF cannot be found");
return statusHandler;
} }
private async Task<string> getDestinationDirectoryAsync(string productId) private static async Task<string> getProposedDownloadFilePathAsync(LibraryBook libraryBook)
{ {
// if audio file exists, get it's dir // if audio file exists, get it's dir. else return base Book dir
var audioFile = await AudibleFileStorage.Audio.GetAsync(productId); var destinationDir =
if (audioFile != null) // this is safe b/c GetDirectoryName(null) == null
return Path.GetDirectoryName(audioFile); Path.GetDirectoryName(await AudibleFileStorage.Audio.GetAsync(libraryBook.Book.AudibleProductId))
?? AudibleFileStorage.PDF.StorageDirectory;
// else return base Book dir return Path.Combine(destinationDir, Path.GetFileName(getdownloadUrl(libraryBook)));
return AudibleFileStorage.PDF.StorageDirectory;
} }
// other user agents from my chrome. from: https://www.whoishostingthis.com/tools/user-agent/ private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
private static string[] userAgents { get; } = new[]
{ {
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", var client = new HttpClient();
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36", var actualDownloadedFilePath = await PerformDownloadAsync(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36", proposedDownloadFilePath,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", (p) => client.DownloadFileAsync(getdownloadUrl(libraryBook), proposedDownloadFilePath, p));
};
private async Task performDownloadAsync(string url, string destinationFilename)
{
using var webClient = new WebClient();
var userAgentIndex = new Random().Next(0, userAgents.Length); // upper bound is exclusive
webClient.Headers["User-Agent"] = userAgents[userAgentIndex];
webClient.Headers["Referer"] = "https://google.com";
webClient.Headers["Upgrade-Insecure-Requests"] = "1";
webClient.Headers["DNT"] = "1";
webClient.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";
webClient.Headers["Accept-Language"] = "en-US,en;q=0.9";
webClient.DownloadProgressChanged += (s, e) => Invoke_DownloadProgressChanged(s, new Dinah.Core.Net.Http.DownloadProgress { BytesReceived = e.BytesReceived, ProgressPercentage = e.ProgressPercentage, TotalBytesToReceive = e.TotalBytesToReceive });
webClient.DownloadFileCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {destinationFilename}");
webClient.DownloadDataCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {destinationFilename}");
webClient.DownloadStringCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {destinationFilename}");
Invoke_DownloadBegin(destinationFilename);
await webClient.DownloadFileTaskAsync(url, destinationFilename);
} }
private static async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
} }
} }

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using DataLayer; using DataLayer;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
namespace FileLiberator namespace FileLiberator
{ {
@ -10,23 +11,20 @@ namespace FileLiberator
public event EventHandler<string> Begin; public event EventHandler<string> Begin;
public event EventHandler<string> Completed; public event EventHandler<string> Completed;
public event EventHandler<string> StatusUpdate;
public event EventHandler<string> DownloadBegin; public event EventHandler<string> DownloadBegin;
public event EventHandler<Dinah.Core.Net.Http.DownloadProgress> DownloadProgressChanged; public event EventHandler<DownloadProgress> DownloadProgressChanged;
public event EventHandler<string> DownloadCompleted; public event EventHandler<string> DownloadCompleted;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
protected void Invoke_DownloadBegin(string downloadMessage) => DownloadBegin?.Invoke(this, downloadMessage);
protected void Invoke_DownloadProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress progress) => DownloadProgressChanged?.Invoke(sender, progress);
protected void Invoke_DownloadCompleted(object sender, string str) => DownloadCompleted?.Invoke(sender, str);
public event EventHandler<string> StatusUpdate;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
public abstract Task<bool> ValidateAsync(LibraryBook libraryBook); public abstract Task<bool> ValidateAsync(LibraryBook libraryBook);
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook); public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
// do NOT use ConfigureAwait(false) on ProcessUnregistered() // do NOT use ConfigureAwait(false) on ProcessAsync()
// often does a lot with forms in the UI context // often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"; var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
@ -41,5 +39,25 @@ namespace FileLiberator
Completed?.Invoke(this, displayMessage); Completed?.Invoke(this, displayMessage);
} }
} }
}
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
{
var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e);
DownloadBegin?.Invoke(this, proposedDownloadFilePath);
try
{
var result = await func(progress);
StatusUpdate?.Invoke(this, result);
return result;
}
finally
{
DownloadCompleted?.Invoke(this, proposedDownloadFilePath);
}
}
}
} }

View File

@ -1,6 +1,5 @@
-- begin BETA --------------------------------------------------------------------------------------------------------------------- -- begin BETA ---------------------------------------------------------------------------------------------------------------------
OLD WEB DOWNLOADER Replace StatusHandler with CSharpFunctionalExtensions
download logic in DownloadPdf should look more like DownloadBook
TESTING BUG TESTING BUG
dbl clk. long pause. exception: dbl clk. long pause. exception: