Compare commits

...

6 Commits

Author SHA1 Message Date
Robert McRackan
7b966f6962 Add IsLiberated option to search engine. Reindex search after download and decrypt 2021-03-31 21:50:32 -04:00
Robert McRackan
c0e955d5ef Better logging and error handling during login 2021-03-15 13:30:33 -04:00
Robert McRackan
bc6f53c8ea bug fix around latest skip-bad-book feature 2020-12-23 16:07:47 -05:00
Robert McRackan
cefab86ce1 bug fixes around new skip-bad-book feature 2020-12-23 14:05:18 -05:00
Robert McRackan
249a2f3b59 bug fix: libhack files: directory not found 2020-12-22 15:54:46 -05:00
Robert McRackan
0e9f2c7681 Truncate too-long error message 2020-12-22 11:38:03 -05:00
7 changed files with 207 additions and 148 deletions

View File

@@ -78,11 +78,11 @@ namespace FileLiberator
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
}
var statusHandler = new StatusHandler();
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
statusHandler.AddError("Cannot find final audio file after decryption");
return statusHandler;
return new StatusHandler { "Cannot find final audio file after decryption" };
return new StatusHandler();
}
finally
{

View File

@@ -33,12 +33,8 @@ namespace FileLiberator
return libraryBook;
}
/// <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> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
{
if (libraryBook == null)
return null;
if (!processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" };
@@ -55,10 +51,9 @@ namespace FileLiberator
Account = libraryBook.Account?.ToMask() ?? "[empty]"
});
// this should never happen. check anyway. ProcessFirstValidAsync returning null is the signal that we're done. we can't let another IProcessable accidentally send this command
var status = await processable.ProcessAsync(libraryBook);
if (status == null)
throw new Exception("Processable should never return a null status");
var status
= (await processable.ProcessAsync(libraryBook))
?? new StatusHandler { "Processable should never return a null status" };
return status;
}

View File

@@ -141,10 +141,15 @@ namespace FileManager
public AudioFileStorage() : base(FileType.Audio) { }
public void CreateSkipFile(string title, string asin, string contents)
public string CreateSkipFile(string title, string asin, string contents = null)
{
var path = FileUtility.GetValidFilename(GetDestDir(title, asin), title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents);
var destinationDir = GetDestDir(title, asin);
Directory.CreateDirectory(destinationDir);
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents ?? string.Empty);
return path;
}
}

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>4.1.5.1</Version>
<Version>4.2.0.1</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -119,6 +119,10 @@ namespace LibationSearchEngine
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
["Abridged"] = lb => lb.Book.IsAbridged,
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
["IsLiberated"] = lb => AudibleFileStorage.Audio.Exists(lb.Book.AudibleProductId),
["Liberated"] = lb => AudibleFileStorage.Audio.Exists(lb.Book.AudibleProductId),
});
private static bool isAuthorNarrated(LibraryBook lb)

View File

@@ -24,24 +24,13 @@ namespace LibationWinForms.BookLiberation
InitializeComponent();
}
public void AppendError(Exception ex)
{
Serilog.Log.Logger.Error(ex, "Automated backup: error");
appendText("ERROR: " + ex.Message);
}
public void AppendText(string text)
{
Serilog.Log.Logger.Information($"Automated backup: {text}");
appendText(text);
}
private void appendText(string text)
public void WriteLine(string text)
=> logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
public void FinalizeUI()
{
keepGoingCb.Enabled = false;
logTb.AppendText("");
AppendText("DONE");
}
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;

View File

@@ -1,11 +1,45 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileLiberator;
namespace LibationWinForms.BookLiberation
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
public static LogMe RegisterForm(AutomatedBackupsForm form)
{
var logMe = new LogMe();
logMe.LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
logMe.LogInfo += (_, text) => form.WriteLine(text);
logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
logMe.LogErrorString += (_, text) => form.WriteLine(text);
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
logMe.LogError += (_, tuple) =>
{
form.WriteLine(tuple.Item2 ?? "Automated backup: error");
form.WriteLine("ERROR: " + tuple.Item1.Message);
};
return logMe;
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(string productId, EventHandler<LibraryBook> completedAction = null)
@@ -14,10 +48,12 @@ namespace LibationWinForms.BookLiberation
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = attachToBackupsForm(backupBook);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
automatedBackupsForm.KeepGoingVisible = false;
await runSingleBackupAsync(backupBook, automatedBackupsForm, productId);
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, automatedBackupsForm, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
@@ -26,9 +62,8 @@ namespace LibationWinForms.BookLiberation
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = attachToBackupsForm(backupBook);
await runBackupLoopAsync(backupBook, automatedBackupsForm);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
}
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
@@ -37,7 +72,7 @@ namespace LibationWinForms.BookLiberation
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook);
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
if (completedAction != null)
{
@@ -46,25 +81,34 @@ namespace LibationWinForms.BookLiberation
backupBook.DownloadPdf.Completed += completedAction;
}
// enables search engine to index for things like "IsLiberated"
backupBook.DownloadBook.Completed += reindex;
backupBook.DecryptBook.Completed += reindex;
backupBook.DownloadPdf.Completed += reindex;
return backupBook;
}
private static AutomatedBackupsForm attachToBackupsForm(BackupBook backupBook)
private static async void reindex(object sender, LibraryBook e)
=> await Task.Run(() => ApplicationServices.SearchEngineCommands.FullReIndex());
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(BackupBook backupBook)
{
#region create form
#region create form and logger
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void downloadBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Begin: {libraryBook.Book}");
void downloadBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void decryptBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Begin: {libraryBook.Book}");
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void downloadPdfCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfCompleted(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
@@ -95,7 +139,7 @@ namespace LibationWinForms.BookLiberation
};
#endregion
return automatedBackupsForm;
return (automatedBackupsForm, logMe);
}
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
@@ -104,9 +148,8 @@ namespace LibationWinForms.BookLiberation
var downloadPdf = getWiredUpDownloadPdf(completedAction);
var automatedBackupsForm = attachToBackupsForm(downloadPdf);
await runBackupLoopAsync(downloadPdf, automatedBackupsForm);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction)
@@ -250,17 +293,18 @@ namespace LibationWinForms.BookLiberation
#endregion
}
private static AutomatedBackupsForm attachToBackupsForm(IDownloadableProcessable downloadable)
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
{
#region create form
#region create form and logger
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void begin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str);
// extra line after book is completely finished
void completed(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Completed: {libraryBook.Book}{Environment.NewLine}");
void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
@@ -279,128 +323,150 @@ namespace LibationWinForms.BookLiberation
};
#endregion
return automatedBackupsForm;
return (automatedBackupsForm, logMe);
}
}
abstract class BackupRunner
{
protected LogMe LogMe { get; }
protected IProcessable Processable { get; }
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
{
LogMe = logMe;
Processable = processable;
AutomatedBackupsForm = automatedBackupsForm;
}
// automated backups looper feels like a composible IProcessable: logic, UI, begin + process child + end
// however the process step doesn't follow the pattern: Validate(product) + Process(product)
private static async Task runBackupLoopAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
{
automatedBackupsForm.Show();
protected abstract Task RunAsync();
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract DialogResult CreateSkipFileResult { get; }
public async Task RunBackupAsync()
{
AutomatedBackupsForm.Show();
// processable.ProcessFirstValidAsync used to be encapsulated elsewhere. however, support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
try
{
foreach (var libraryBook in processable.GetValidLibraryBooks())
{
try
{
var statusHandler = await processable.ProcessBookAsync_NoValidation(libraryBook);
if (!validateStatus(statusHandler, automatedBackupsForm))
break;
}
catch (Exception e)
{
automatedBackupsForm.AppendError(e);
DisplaySkipDialog(automatedBackupsForm, libraryBook, e);
}
}
await RunAsync();
}
catch (Exception ex)
{
automatedBackupsForm.AppendError(ex);
LogMe.Error(ex);
}
automatedBackupsForm.FinalizeUI();
AutomatedBackupsForm.FinalizeUI();
LogMe.Info("DONE");
}
private static async Task runSingleBackupAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm, string productId)
protected async Task<bool> ProcessOneAsync(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook)
{
automatedBackupsForm.Show();
string logMessage;
try
{
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
var statusHandler = await func(libraryBook);
try
{
var statusHandler = await processable.ProcessSingleAsync(libraryBook);
validateStatus(statusHandler, automatedBackupsForm);
}
catch (Exception ex)
{
automatedBackupsForm.AppendError(ex);
DisplaySkipDialog(automatedBackupsForm, libraryBook, ex);
}
}
catch (Exception e)
{
automatedBackupsForm.AppendError(e);
}
if (statusHandler.IsSuccess)
return true;
automatedBackupsForm.FinalizeUI();
}
private static void DisplaySkipDialog(AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook, Exception ex)
{
try
{
var text = @$"
The below error occurred while trying to process this book. Skip this book permanently?
- Click YES to skip this book permanently.
- Click NO to skip the book this time only. We'll try again later.
Error:
{ex.Message}
".Trim();
var dialogResult = System.Windows.Forms.MessageBox.Show(
text,
"Skip importing this book?",
System.Windows.Forms.MessageBoxButtons.YesNo,
System.Windows.Forms.MessageBoxIcon.Question);
if (dialogResult != System.Windows.Forms.DialogResult.Yes)
{
FileManager.AudibleFileStorage.Audio.CreateSkipFile(
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,
ex.Message + "\r\n|\r\n" + ex.StackTrace);
}
}
catch (Exception exc)
{
automatedBackupsForm.AppendText($"Error attempting to display {nameof(DisplaySkipDialog)}");
automatedBackupsForm.AppendError(exc);
}
}
private static bool validateStatus(StatusHandler statusHandler, AutomatedBackupsForm automatedBackupsForm)
{
if (statusHandler == null)
{
automatedBackupsForm.AppendText("Done. All books have been processed");
return false;
}
if (statusHandler.HasErrors)
{
automatedBackupsForm.AppendText("ERROR. All books have not been processed. Most recent valid book: processing failed");
foreach (var errorMessage in statusHandler.Errors)
automatedBackupsForm.AppendText(errorMessage);
return false;
LogMe.Error(errorMessage);
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
}
catch (Exception ex)
{
LogMe.Error(ex);
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
}
if (!automatedBackupsForm.KeepGoing)
{
if (automatedBackupsForm.KeepGoingVisible && !automatedBackupsForm.KeepGoingChecked)
automatedBackupsForm.AppendText("'Keep going' is unchecked");
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
var dialogResult = MessageBox.Show(SkipDialogText, "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == CreateSkipFileResult)
{
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
LogMe.Info($@"
Created new 'skip' file
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
{path}
".Trim());
}
return true;
}
}
class BackupSingle : BackupRunner
{
private LibraryBook _libraryBook { get; }
protected override string SkipDialogText => @"
An error occurred while trying to process this book. Skip this book permanently?
- Click YES to skip this book permanently.
- Click NO to skip the book this time only. We'll try again later.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook)
: base(logMe, processable, automatedBackupsForm)
{
_libraryBook = libraryBook;
}
protected override async Task RunAsync()
{
if (_libraryBook is not null)
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
}
}
class BackupLoop : BackupRunner
{
protected override string SkipDialogText => @"
An error occurred while trying to process this book
- ABORT: stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
: base(logMe, processable, automatedBackupsForm) { }
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks())
{
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
if (!keepGoing)
return;
if (!AutomatedBackupsForm.KeepGoing)
{
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
LogMe.Info("'Keep going' is unchecked");
return;
}
}
LogMe.Info("Done. All books have been processed");
}
}
}