mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b966f6962 | ||
|
|
c0e955d5ef | ||
|
|
bc6f53c8ea | ||
|
|
cefab86ce1 | ||
|
|
249a2f3b59 | ||
|
|
0e9f2c7681 | ||
|
|
d25c32ff45 | ||
|
|
642a500f87 | ||
|
|
0e2469db64 |
@@ -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
|
||||
{
|
||||
@@ -137,7 +137,7 @@ namespace FileLiberator
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
var destinationDir = getDestDir(product);
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
@@ -158,18 +158,6 @@ namespace FileLiberator
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static string getDestDir(Book product)
|
||||
{
|
||||
// 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);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
@@ -16,38 +17,33 @@ namespace FileLiberator
|
||||
//
|
||||
|
||||
|
||||
/// <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 = processable.getNextValidBook();
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
|
||||
=> DbContexts.GetContext()
|
||||
.GetLibrary_Flat_NoTracking()
|
||||
.Where(libraryBook => processable.Validate(libraryBook));
|
||||
|
||||
return await processBookAsync(processable, 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, string productId)
|
||||
public static LibraryBook GetSingleLibraryBook(string productId)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBook = context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
return libraryBook;
|
||||
}
|
||||
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
if (!processable.Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
|
||||
return await processBookAsync(processable, libraryBook);
|
||||
return await processable.ProcessBookAsync_NoValidation(libraryBook);
|
||||
}
|
||||
|
||||
private static async Task<StatusHandler> processBookAsync(IProcessable processable, LibraryBook libraryBook)
|
||||
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(processBookAsync) + " {@DebugInfo}", new
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
@@ -55,25 +51,13 @@ 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;
|
||||
}
|
||||
|
||||
private static LibraryBook getNextValidBook(this IProcessable processable)
|
||||
{
|
||||
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
|
||||
|
||||
foreach (var libraryBook in libraryBooks)
|
||||
if (processable.Validate(libraryBook))
|
||||
return libraryBook;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
=> processable.Validate(libraryBook)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
|
||||
@@ -11,76 +11,77 @@ namespace FileManager
|
||||
// could add images here, but for now images are stored in a well-known location
|
||||
public enum FileType { Unknown, Audio, AAX, PDF }
|
||||
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
#region static
|
||||
public static AudibleFileStorage Audio { get; }
|
||||
public static AudibleFileStorage AAX { get; }
|
||||
public static AudibleFileStorage PDF { get; }
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
public abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
public static string DownloadsInProgress { get; }
|
||||
public static string DecryptInProgress { get; }
|
||||
public static string BooksDirectory => Configuration.Instance.Books;
|
||||
// not customizable. don't move to config
|
||||
public static string DownloadsFinal { get; }
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
|
||||
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
|
||||
|
||||
static AudibleFileStorage()
|
||||
public static string DownloadsInProgress
|
||||
{
|
||||
#region init DecryptInProgress
|
||||
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
|
||||
var M4bRootDir
|
||||
= Configuration.Instance.DecryptInProgressEnum == "WinTemp" // else "LibationFiles"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
DecryptInProgress = Path.Combine(M4bRootDir, "DecryptInProgress");
|
||||
Directory.CreateDirectory(DecryptInProgress);
|
||||
#endregion
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
|
||||
var AaxRootDir
|
||||
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
#region init DownloadsInProgress
|
||||
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
|
||||
var AaxRootDir
|
||||
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp" // else "LibationFiles"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
DownloadsInProgress = Path.Combine(AaxRootDir, "DownloadsInProgress");
|
||||
Directory.CreateDirectory(DownloadsInProgress);
|
||||
#endregion
|
||||
return Directory.CreateDirectory(Path.Combine(AaxRootDir, "DownloadsInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
#region init BooksDirectory
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
|
||||
Directory.CreateDirectory(Configuration.Instance.Books);
|
||||
#endregion
|
||||
public static string DecryptInProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
|
||||
|
||||
// must do this in static ctor, not w/inline properties
|
||||
// static properties init before static ctor so these dir.s would still be null
|
||||
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory, "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac");
|
||||
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal, "aax");
|
||||
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory, "pdf", "zip");
|
||||
var M4bRootDir
|
||||
= Configuration.Instance.DecryptInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(M4bRootDir, "DecryptInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
// not customizable. don't move to config
|
||||
public static string DownloadsFinal => new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
public string StorageDirectory => DisplayName;
|
||||
|
||||
private IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
private AudibleFileStorage(FileType fileType, string storageDirectory, params string[] extensions) : base((int)fileType, storageDirectory)
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
extensions_noDots = extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
}
|
||||
|
||||
@@ -90,30 +91,89 @@ namespace FileManager
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId)
|
||||
=> GetPath(productId) != null;
|
||||
public bool Exists(string productId) => GetPath(productId) != null;
|
||||
|
||||
public string GetPath(string productId)
|
||||
{
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
}
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
var firstOrNull =
|
||||
var firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
|
||||
|
||||
if (firstOrNull is null)
|
||||
return null;
|
||||
|
||||
FilePathCache.Upsert(productId, FileType, firstOrNull);
|
||||
return firstOrNull;
|
||||
}
|
||||
|
||||
public string GetDestDir(string title, string asin)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? title
|
||||
: title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
public const string SKIP_FILE_EXT = "libhack";
|
||||
|
||||
public override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public string CreateSkipFile(string title, string asin, string contents = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public class AaxFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "aax" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => DownloadsFinal;
|
||||
|
||||
public AaxFileStorage() : base(FileType.AAX) { }
|
||||
}
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public PdfFileStorage() : base(FileType.PDF) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>4.1.3.1</Version>
|
||||
<Version>4.2.0.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,73 +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)
|
||||
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();
|
||||
AutomatedBackupsForm.Show();
|
||||
|
||||
try
|
||||
{
|
||||
var shouldContinue = true;
|
||||
while (shouldContinue)
|
||||
{
|
||||
var statusHandler = await processable.ProcessFirstValidAsync();
|
||||
shouldContinue = validateStatus(statusHandler, automatedBackupsForm);
|
||||
}
|
||||
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 statusHandler = await processable.ProcessSingleAsync(productId);
|
||||
validateStatus(statusHandler, automatedBackupsForm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
automatedBackupsForm.AppendError(ex);
|
||||
}
|
||||
var statusHandler = await func(libraryBook);
|
||||
|
||||
automatedBackupsForm.FinalizeUI();
|
||||
}
|
||||
if (statusHandler.IsSuccess)
|
||||
return true;
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace LibationWinForms.Dialogs
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
throw new Exception("Please enter an account id for all accounts");
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
throw new Exception("Please select a local name for all accounts");
|
||||
throw new Exception("Please select a locale (i.e.: country or region) for all accounts");
|
||||
|
||||
var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName);
|
||||
acct.LibraryScan = dto.LibraryScan;
|
||||
|
||||
Reference in New Issue
Block a user