When there's a problem downloading a book, you get the option to skip the file temporarily or permanently. This can be useful with extremely old audible titles where the modern download may no longer be supported

This commit is contained in:
Robert McRackan 2020-12-21 16:25:42 -05:00
parent 642a500f87
commit d25c32ff45
5 changed files with 198 additions and 111 deletions

View File

@ -137,7 +137,7 @@ namespace FileLiberator
// create final directory. move each file into it. MOVE AUDIO FILE LAST // create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]" // new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = getDestDir(product); var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
Directory.CreateDirectory(destinationDir); Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename); var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
@ -158,18 +158,6 @@ namespace FileLiberator
return destinationDir; 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) private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{ {
// files are: temp path\author\[asin].ext // files are: temp path\author\[asin].ext

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices; using ApplicationServices;
@ -16,38 +17,37 @@ namespace FileLiberator
// //
/// <summary>Process the first valid product. Create default context</summary> // when used in foreach: stateful. deferred execution
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns> public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
public static async Task<StatusHandler> ProcessFirstValidAsync(this IProcessable processable) => DbContexts.GetContext()
{ .GetLibrary_Flat_NoTracking()
var libraryBook = processable.getNextValidBook(); .Where(libraryBook => processable.Validate(libraryBook));
if (libraryBook == null)
return null;
return await processBookAsync(processable, libraryBook); public static LibraryBook GetSingleLibraryBook(string productId)
}
/// <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)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var libraryBook = context var libraryBook = context
.Library .Library
.GetLibrary() .GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId); .SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
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) if (libraryBook == null)
return null; return null;
if (!processable.Validate(libraryBook)) if (!processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" }; 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.Title,
libraryBook.Book.AudibleProductId, libraryBook.Book.AudibleProductId,
@ -63,17 +63,6 @@ namespace FileLiberator
return 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) public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
=> processable.Validate(libraryBook) => processable.Validate(libraryBook)
? await processable.ProcessAsync(libraryBook) ? await processable.ProcessAsync(libraryBook)

View File

@ -18,69 +18,70 @@ namespace FileManager
/// Paths are read at app launch and 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 /// Many files are often looked up at once
/// </summary> /// </summary>
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage> public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
{ {
public abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
#region static #region static
public static AudibleFileStorage Audio { get; } public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAX { get; } public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
public static AudibleFileStorage PDF { get; } public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
public static string DownloadsInProgress { get; } public static string DownloadsInProgress
public static string DecryptInProgress { get; } {
public static string BooksDirectory => Configuration.Instance.Books; get
// not customizable. don't move to config
public static string DownloadsFinal { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
static AudibleFileStorage()
{ {
#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
#region init DownloadsInProgress
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles")) if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DownloadsInProgressEnum = "WinTemp"; Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
var AaxRootDir var AaxRootDir
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp" // else "LibationFiles" = Configuration.Instance.DownloadsInProgressEnum == "WinTemp"
? Configuration.WinTemp ? Configuration.WinTemp
: Configuration.Instance.LibationFiles; : Configuration.Instance.LibationFiles;
DownloadsInProgress = Path.Combine(AaxRootDir, "DownloadsInProgress");
Directory.CreateDirectory(DownloadsInProgress);
#endregion
#region init BooksDirectory return Directory.CreateDirectory(Path.Combine(AaxRootDir, "DownloadsInProgress")).FullName;
}
}
public static string DecryptInProgress
{
get
{
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
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)) if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books"); Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
Directory.CreateDirectory(Configuration.Instance.Books); return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
#endregion }
// 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");
} }
#endregion #endregion
#region instance #region instance
public FileType FileType => (FileType)Value; public FileType FileType => (FileType)Value;
public string StorageDirectory => DisplayName;
private IEnumerable<string> extensions_noDots { get; } private IEnumerable<string> extensions_noDots { get; }
private string extAggr { 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}"); extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
} }
@ -90,16 +91,13 @@ namespace FileManager
/// - a directory name has the product id and an audio file is immediately inside /// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id /// - any audio filename contains the product id
/// </summary> /// </summary>
public bool Exists(string productId) public bool Exists(string productId) => GetPath(productId) != null;
=> GetPath(productId) != null;
public string GetPath(string productId) public string GetPath(string productId)
{
{ {
var cachedFile = FilePathCache.GetPath(productId, FileType); var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null) if (cachedFile != null)
return cachedFile; return cachedFile;
}
var firstOrNull = var firstOrNull =
Directory Directory
@ -108,12 +106,69 @@ namespace FileManager
if (firstOrNull is null) if (firstOrNull is null)
return null; return null;
FilePathCache.Upsert(productId, FileType, firstOrNull); FilePathCache.Upsert(productId, FileType, firstOrNull);
return 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) public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.')); => extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion #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 void CreateSkipFile(string title, string asin, string contents)
{
var path = FileUtility.GetValidFilename(GetDestDir(title, asin), title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents);
}
}
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) { }
}
} }

View File

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

View File

@ -288,13 +288,22 @@ namespace LibationWinForms.BookLiberation
{ {
automatedBackupsForm.Show(); 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 try
{ {
var shouldContinue = true; foreach (var libraryBook in processable.GetValidLibraryBooks())
while (shouldContinue)
{ {
var statusHandler = await processable.ProcessFirstValidAsync(); try
shouldContinue = validateStatus(statusHandler, automatedBackupsForm); {
var statusHandler = await processable.ProcessBookAsync_NoValidation(libraryBook);
if (!validateStatus(statusHandler, automatedBackupsForm))
break;
}
catch (Exception e)
{
automatedBackupsForm.AppendError(e);
DisplaySkipDialog(automatedBackupsForm, libraryBook, e);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -311,17 +320,63 @@ namespace LibationWinForms.BookLiberation
try try
{ {
var statusHandler = await processable.ProcessSingleAsync(productId); var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
try
{
var statusHandler = await processable.ProcessSingleAsync(libraryBook);
validateStatus(statusHandler, automatedBackupsForm); validateStatus(statusHandler, automatedBackupsForm);
} }
catch (Exception ex) catch (Exception ex)
{ {
automatedBackupsForm.AppendError(ex); automatedBackupsForm.AppendError(ex);
DisplaySkipDialog(automatedBackupsForm, libraryBook, ex);
}
}
catch (Exception e)
{
automatedBackupsForm.AppendError(e);
} }
automatedBackupsForm.FinalizeUI(); 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) private static bool validateStatus(StatusHandler statusHandler, AutomatedBackupsForm automatedBackupsForm)
{ {
if (statusHandler == null) if (statusHandler == null)