All scraping code removed
This commit is contained in:
parent
c61bc27a7b
commit
df90fc5361
106 changed files with 666 additions and 5319 deletions
16
FileLiberator/FileLiberator.csproj
Normal file
16
FileLiberator/FileLiberator.csproj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
54
FileLiberator/UNTESTED/BackupBook.cs
Normal file
54
FileLiberator/UNTESTED/BackupBook.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class BackupBook : IProcessable
|
||||
{
|
||||
public event EventHandler<string> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> Completed;
|
||||
|
||||
public DownloadBook Download { get; } = new DownloadBook();
|
||||
public DecryptBook Decrypt { get; } = new DecryptBook();
|
||||
|
||||
// ValidateAsync() doesn't need UI context
|
||||
public async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
=> await validateAsync_ConfigureAwaitFalse(libraryBook.Book.AudibleProductId).ConfigureAwait(false);
|
||||
private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId)
|
||||
=> !await AudibleFileStorage.Audio.ExistsAsync(productId);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
|
||||
|
||||
Begin?.Invoke(this, displayMessage);
|
||||
|
||||
try
|
||||
{
|
||||
var aaxExists = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
if (!aaxExists)
|
||||
await Download.ProcessAsync(libraryBook);
|
||||
|
||||
return await Decrypt.ProcessAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
311
FileLiberator/UNTESTED/DecryptBook.cs
Normal file
311
FileLiberator/UNTESTED/DecryptBook.cs
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DecryptBook : IDecryptable
|
||||
{
|
||||
public event EventHandler<string> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> DecryptBegin;
|
||||
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
public event EventHandler<int> UpdateProgress;
|
||||
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<string> Completed;
|
||||
|
||||
// ValidateAsync() doesn't need UI context
|
||||
public async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
=> await validateAsync_ConfigureAwaitFalse(libraryBook.Book.AudibleProductId).ConfigureAwait(false);
|
||||
private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId)
|
||||
=> await AudibleFileStorage.AAX.ExistsAsync(productId)
|
||||
&& !await AudibleFileStorage.Audio.ExistsAsync(productId);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
|
||||
|
||||
Begin?.Invoke(this, displayMessage);
|
||||
|
||||
try
|
||||
{
|
||||
var aaxFilename = await AudibleFileStorage.AAX.GetAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (aaxFilename == null)
|
||||
return new StatusHandler { "aaxFilename parameter is null" };
|
||||
if (!FileUtility.FileExists(aaxFilename))
|
||||
return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" };
|
||||
if (await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
string proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
|
||||
|
||||
string outputAudioFilename;
|
||||
//outputAudioFilename = await inAudibleDecrypt(proposedOutputFile, aaxFilename);
|
||||
outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename == null)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
var finalAudioExists = await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
if (!finalAudioExists)
|
||||
statusHandler.AddError("Cannot find final audio file after decryption");
|
||||
return statusHandler;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecrypt(string proposedOutputFile, string aaxFilename)
|
||||
{
|
||||
DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}");
|
||||
|
||||
try
|
||||
{
|
||||
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, Configuration.Instance.DecryptKey);
|
||||
converter.AppName = "Libation";
|
||||
|
||||
TitleDiscovered?.Invoke(this, converter.tags.title);
|
||||
AuthorsDiscovered?.Invoke(this, converter.tags.author);
|
||||
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
|
||||
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
|
||||
|
||||
converter.SetOutputFilename(proposedOutputFile);
|
||||
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(() => converter.Run());
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("decrypt failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
Configuration.Instance.DecryptKey = converter.decryptKey;
|
||||
|
||||
return converter.outputFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
var m4bDir = new FileInfo(outputAudioFilename).Directory;
|
||||
var files = m4bDir
|
||||
.EnumerateFiles()
|
||||
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
|
||||
.ToList();
|
||||
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = product.Title.IndexOf(':');
|
||||
var titleDir = (underscoreIndex < 4) ? product.Title : product.Title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
|
||||
Directory.CreateDirectory(finalDir);
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
files = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
|
||||
var musicFileExt = musicFiles
|
||||
.Select(f => f.Extension)
|
||||
.Distinct()
|
||||
.Single()
|
||||
.Trim('.');
|
||||
|
||||
foreach (var f in files)
|
||||
{
|
||||
var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
? FileUtility.GetValidFilename(finalDir, product.Title, musicFileExt, product.AudibleProductId)
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(finalDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
}
|
||||
|
||||
#region legacy inAudible wire-up code
|
||||
//
|
||||
// instructions are in comments below for editing and interacting with inAudible. eg:
|
||||
// \_NET\Visual Studio 2017\inAudible197\decompiled - in progress\inAudible.csproj
|
||||
// first, add its project and put its exe path into inAudiblePath
|
||||
//
|
||||
#region placeholder code
|
||||
// this exists so the below legacy code will compile as-is. comment out placeholder code when actually connecting to inAudible
|
||||
|
||||
class Form
|
||||
{
|
||||
internal void Show() => throw new NotImplementedException();
|
||||
internal void Kill() => throw new NotImplementedException();
|
||||
}
|
||||
class TextBox
|
||||
{
|
||||
internal string Text { set => throw new NotImplementedException(); }
|
||||
}
|
||||
class Button
|
||||
{
|
||||
internal void PerformClick() => throw new NotImplementedException();
|
||||
}
|
||||
class AudibleConvertor
|
||||
{
|
||||
internal class GLOBALS
|
||||
{
|
||||
internal static string ExecutablePath { set => throw new NotImplementedException(); }
|
||||
}
|
||||
internal class Form1 : Form
|
||||
{
|
||||
internal Form1(Action<string> action) => throw new NotImplementedException();
|
||||
internal void LoadAudibleFiles(string[] arr) => throw new NotImplementedException();
|
||||
internal TextBox txtOutputFile { get => throw new NotImplementedException(); }
|
||||
internal Button btnConvert { get => throw new NotImplementedException(); }
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static string inAudiblePath { get; }
|
||||
= @"C:\"
|
||||
+ @"DEV_ROOT_EXAMPLE\"
|
||||
+ @"_NET\Visual Studio 2017\"
|
||||
+ @"inAudible197\decompiled - in progress\bin\Debug\inAudible.exe";
|
||||
private static async Task<string> inAudibleDecrypt(string proposedOutputFile, string aaxFilename)
|
||||
{
|
||||
#region // inAudible code to change:
|
||||
/*
|
||||
* Prevent "Path too long" error
|
||||
* =============================
|
||||
* BatchFiles.cs :: GenerateOutputFilepath()
|
||||
* Add this just before the bottom return statement
|
||||
*
|
||||
if (oneOff && !string.IsNullOrWhiteSpace(outputPath))
|
||||
return str + "\\" + Path.GetFileNameWithoutExtension(outputPath) + "." + fileType;
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#region init inAudible
|
||||
#region // suppress warnings
|
||||
// inAudible. project properties > Build > Warning level=2
|
||||
#endregion
|
||||
#region // instructions to create inAudible ExecutablePath
|
||||
/*
|
||||
* STEP 1
|
||||
* ======
|
||||
* do a PROJECT level find/replace within inAudible
|
||||
* find
|
||||
* Application.ExecutablePath
|
||||
* replace
|
||||
* AudibleConvertor.GLOBALS.ExecutablePath
|
||||
* STEP 2
|
||||
* ======
|
||||
* new inAudible root-level file
|
||||
* _GLOBALS.cs
|
||||
* contents:
|
||||
* namespace AudibleConvertor { public static class GLOBALS { public static string ExecutablePath { get; set; } = System.Windows.Forms.Application.ExecutablePath; } }
|
||||
*/
|
||||
#endregion
|
||||
AudibleConvertor.GLOBALS.ExecutablePath = inAudiblePath;
|
||||
// before using inAudible, set ini values
|
||||
setIniValues(new Dictionary<string, string> { ["selected_codec"] = "lossless", ["embed_cover"] = "True", ["copy_cover_art"] = "False", ["create_cue"] = "True", ["nfo"] = "True", ["strip_unabridged"] = "True", });
|
||||
#endregion
|
||||
|
||||
// this provides the async magic to keep all of the form calling code in one method instead of event callback pattern
|
||||
// TODO: error handling is not obvious:
|
||||
// https://deaddesk.top/don't-fall-for-TaskCompletionSource-traps/
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
|
||||
// to know when inAudible is complete. code to change:
|
||||
#region // code to preceed ctor
|
||||
/*
|
||||
Action<string> _conversionCompleteAction;
|
||||
public Form1(Action<string> conversionCompleteAction) : this() => _conversionCompleteAction = conversionCompleteAction;
|
||||
*/
|
||||
#endregion
|
||||
#region // code for the end of bgwAAX_Completed()
|
||||
/*
|
||||
if (this.myAdvancedOptions.beep && !this.myAdvancedOptions.cylon) this.SOXPlay(Form1.appPath + "\\beep.mp3", true);
|
||||
else if (myAdvancedOptions.cylon) SOXPlay(appPath + "\\inAudible-end.mp3", true);
|
||||
_conversionCompleteAction?.Invoke(outputFileName);
|
||||
}
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#region start inAudible
|
||||
var form = new AudibleConvertor.Form1(tcs.SetResult);
|
||||
form.Show();
|
||||
form.LoadAudibleFiles(new string[] { aaxFilename }); // inAudible: make public
|
||||
|
||||
// change output info to include asin. put in temp
|
||||
form.txtOutputFile.Text = proposedOutputFile; // inAudible: make public
|
||||
|
||||
// submit/process/decrypt
|
||||
form.btnConvert.PerformClick(); // inAudible: make public
|
||||
|
||||
// ta-da -- magic! we stop here until inAudible complete
|
||||
var outputAudioFilename = await tcs.Task;
|
||||
#endregion
|
||||
|
||||
#region when complete, close inAudible
|
||||
// use this instead of Dinah.Core.Windows.Forms.UIThread()
|
||||
form.Kill();
|
||||
#endregion
|
||||
|
||||
return outputAudioFilename;
|
||||
}
|
||||
|
||||
private static void setIniValues(Dictionary<string, string> settings)
|
||||
{
|
||||
// C:\Users\username\Documents\inAudible\config.ini
|
||||
var iniPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "inAudible", "config.ini");
|
||||
var iniContents = File.ReadAllText(iniPath);
|
||||
|
||||
foreach (var kvp in settings)
|
||||
iniContents = System.Text.RegularExpressions.Regex.Replace(
|
||||
iniContents,
|
||||
$@"\r\n{kvp.Key} = [^\r\n]+\r\n",
|
||||
$"\r\n{kvp.Key} = {kvp.Value}\r\n");
|
||||
|
||||
File.WriteAllText(iniPath, iniContents);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
75
FileLiberator/UNTESTED/DownloadBook.cs
Normal file
75
FileLiberator/UNTESTED/DownloadBook.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FileManager;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DownloadBook : DownloadableBase
|
||||
{
|
||||
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
=> !await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)
|
||||
&& !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var tempAaxFilename = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsInProgress,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
|
||||
// if getting from full title:
|
||||
// '?' is allowed
|
||||
// colons are inconsistent but not problematic to just leave them
|
||||
// - 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
|
||||
tempAaxFilename = await performApiDownloadAsync(libraryBook, tempAaxFilename);
|
||||
|
||||
// move
|
||||
var aaxFilename = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsFinal,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
File.Move(tempAaxFilename, aaxFilename);
|
||||
|
||||
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)
|
||||
{
|
||||
var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
FileLiberator/UNTESTED/DownloadPdf.cs
Normal file
105
FileLiberator/UNTESTED/DownloadPdf.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : DownloadableBase
|
||||
{
|
||||
static DownloadPdf()
|
||||
{
|
||||
// https://stackoverflow.com/a/15483698
|
||||
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
|
||||
}
|
||||
|
||||
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var product = libraryBook.Book;
|
||||
|
||||
if (!product.Supplements.Any())
|
||||
return false;
|
||||
|
||||
return !await AudibleFileStorage.PDF.ExistsAsync(product.AudibleProductId);
|
||||
}
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var product = libraryBook.Book;
|
||||
|
||||
if (product == null)
|
||||
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 url = urls.Single();
|
||||
|
||||
var destinationDir = await getDestinationDirectory(product.AudibleProductId);
|
||||
if (destinationDir == null)
|
||||
return new StatusHandler { "Destination directory not found for PDF download" };
|
||||
|
||||
var destinationFilename = Path.Combine(destinationDir, Path.GetFileName(url));
|
||||
|
||||
using var webClient = GetWebClient(destinationFilename);
|
||||
await webClient.DownloadFileTaskAsync(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> getDestinationDirectory(string productId)
|
||||
{
|
||||
// if audio file exists, get it's dir
|
||||
var audioFile = await AudibleFileStorage.Audio.GetAsync(productId);
|
||||
if (audioFile != null)
|
||||
return Path.GetDirectoryName(audioFile);
|
||||
|
||||
// else return base Book dir
|
||||
return AudibleFileStorage.PDF.StorageDirectory;
|
||||
}
|
||||
|
||||
// other user agents from my chrome. from: https://www.whoishostingthis.com/tools/user-agent/
|
||||
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",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
|
||||
};
|
||||
private WebClient GetWebClient(string downloadMessage)
|
||||
{
|
||||
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: {downloadMessage}");
|
||||
webClient.DownloadDataCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}");
|
||||
webClient.DownloadStringCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}");
|
||||
|
||||
Invoke_DownloadBegin(downloadMessage);
|
||||
|
||||
return webClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
FileLiberator/UNTESTED/DownloadableBase.cs
Normal file
45
FileLiberator/UNTESTED/DownloadableBase.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public abstract class DownloadableBase : IDownloadable
|
||||
{
|
||||
public event EventHandler<string> Begin;
|
||||
public event EventHandler<string> Completed;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> DownloadBegin;
|
||||
public event EventHandler<Dinah.Core.Net.Http.DownloadProgress> DownloadProgressChanged;
|
||||
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 abstract Task<bool> ValidateAsync(LibraryBook libraryBook);
|
||||
|
||||
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
|
||||
|
||||
Begin?.Invoke(this, displayMessage);
|
||||
|
||||
try
|
||||
{
|
||||
return await ProcessItemAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
FileLiberator/UNTESTED/IDecryptable.cs
Normal file
17
FileLiberator/UNTESTED/IDecryptable.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IDecryptable : IProcessable
|
||||
{
|
||||
event EventHandler<string> DecryptBegin;
|
||||
|
||||
event EventHandler<string> TitleDiscovered;
|
||||
event EventHandler<string> AuthorsDiscovered;
|
||||
event EventHandler<string> NarratorsDiscovered;
|
||||
event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
event EventHandler<int> UpdateProgress;
|
||||
|
||||
event EventHandler<string> DecryptCompleted;
|
||||
}
|
||||
}
|
||||
11
FileLiberator/UNTESTED/IDownloadable.cs
Normal file
11
FileLiberator/UNTESTED/IDownloadable.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IDownloadable : IProcessable
|
||||
{
|
||||
event EventHandler<string> DownloadBegin;
|
||||
event EventHandler<Dinah.Core.Net.Http.DownloadProgress> DownloadProgressChanged;
|
||||
event EventHandler<string> DownloadCompleted;
|
||||
}
|
||||
}
|
||||
23
FileLiberator/UNTESTED/IProcessable.cs
Normal file
23
FileLiberator/UNTESTED/IProcessable.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public interface IProcessable
|
||||
{
|
||||
event EventHandler<string> Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
event EventHandler<string> StatusUpdate;
|
||||
|
||||
event EventHandler<string> Completed;
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
Task<bool> ValidateAsync(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
}
|
||||
}
|
||||
43
FileLiberator/UNTESTED/IProcessableExt.cs
Normal file
43
FileLiberator/UNTESTED/IProcessableExt.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public static class IProcessableExt
|
||||
{
|
||||
//
|
||||
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
|
||||
// - ValidateAsync() doesn't need UI context. however, each class already uses ConfigureAwait(false)
|
||||
// - ProcessAsync() often does a lot with forms in the UI context
|
||||
//
|
||||
|
||||
|
||||
/// <summary>Process the first valid product. Create default context</summary>
|
||||
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
|
||||
public static async Task<StatusHandler> ProcessFirstValidAsync(this IProcessable processable)
|
||||
{
|
||||
var libraryBook = await processable.GetNextValidAsync();
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
|
||||
var status = await processable.ProcessAsync(libraryBook);
|
||||
if (status == null)
|
||||
throw new Exception("Processable should never return a null status");
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public static async Task<LibraryBook> GetNextValidAsync(this IProcessable processable)
|
||||
{
|
||||
var libraryBooks = LibraryQueries.GetLibrary_Flat_NoTracking();
|
||||
|
||||
foreach (var libraryBook in libraryBooks)
|
||||
if (await processable.ValidateAsync(libraryBook))
|
||||
return libraryBook;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue