Merge pull request #90 from Mbucari/master

Fully implemented the MVVM pattern
This commit is contained in:
rmcrackan 2021-08-22 21:06:00 -04:00 committed by GitHub
commit 343c3b62d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 251 additions and 207 deletions

View File

@ -11,13 +11,6 @@ using Serilog;
namespace ApplicationServices namespace ApplicationServices
{ {
// subtly different from DataLayer.LiberatedStatus
// - DataLayer.LiberatedStatus: has no concept of partially downloaded
// - ApplicationServices.LiberatedState: has no concept of Error/skipped
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
public static class LibraryCommands public static class LibraryCommands
{ {
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS; private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
@ -162,94 +155,24 @@ namespace ApplicationServices
#endregion #endregion
#region Update book details #region Update book details
public static int UpdateUserDefinedItem(Book book, string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
public static int UpdateUserDefinedItem(Book book)
{ {
try try
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var udi = book.UserDefinedItem;
var tagsChanged = udi.Tags != newTags;
var bookStatusChanged = udi.BookStatus != bookStatus;
var pdfStatusChanged = udi.PdfStatus != pdfStatus;
if (!tagsChanged && !bookStatusChanged && !pdfStatusChanged)
return 0;
udi.Tags = newTags;
udi.BookStatus = bookStatus;
udi.PdfStatus = pdfStatus;
// Attach() NoTracking entities before SaveChanges() // Attach() NoTracking entities before SaveChanges()
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified; context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
if (qtyChanges == 0)
return 0;
if (tagsChanged)
SearchEngineCommands.UpdateBookTags(book);
if (bookStatusChanged || pdfStatusChanged)
SearchEngineCommands.UpdateLiberatedStatus(book); SearchEngineCommands.UpdateLiberatedStatus(book);
return qtyChanges; return qtyChanges;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "Error updating tags"); Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
throw;
}
}
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
if (udi.BookStatus == liberatedStatus)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.BookStatus = liberatedStatus;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
if (udi.PdfStatus == liberatedStatus)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.PdfStatus = liberatedStatus;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw; throw;
} }
} }
@ -257,15 +180,15 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those... // below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public static LiberatedState Liberated_Status(Book book) public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists ? LiberatedState.Liberated => book.Audio_Exists ? LiberatedStatus.Liberated
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload : FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedState.NotDownloaded; : LiberatedStatus.NotLiberated;
public static PdfState Pdf_Status(Book book) public static LiberatedStatus? Pdf_Status(Book book)
=> !book.Supplements.Any() ? PdfState.NoPdf => !book.Supplements.Any() ? null
: book.PDF_Exists ? PdfState.Downloaded : book.PDF_Exists ? LiberatedStatus.Liberated
: PdfState.NotDownloaded; : LiberatedStatus.NotLiberated;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { } public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
public static LibraryStats GetCounts() public static LibraryStats GetCounts()
@ -276,9 +199,9 @@ namespace ApplicationServices
.AsParallel() .AsParallel()
.Select(lb => Liberated_Status(lb.Book)) .Select(lb => Liberated_Status(lb.Book))
.ToList(); .ToList();
var booksFullyBackedUp = results.Count(r => r == LiberatedState.Liberated); var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedState.PartialDownload); var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedState.NotDownloaded); var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress }); Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
@ -287,8 +210,8 @@ namespace ApplicationServices
.Where(lb => lb.Book.Supplements.Any()) .Where(lb => lb.Book.Supplements.Any())
.Select(lb => Pdf_Status(lb.Book)) .Select(lb => Pdf_Status(lb.Book))
.ToList(); .ToList();
var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded); var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded); var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded }); Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });

View File

@ -14,7 +14,11 @@ namespace DataLayer
NotLiberated = 0, NotLiberated = 0,
Liberated = 1, Liberated = 1,
/// <summary>Error occurred during liberation. Don't retry</summary> /// <summary>Error occurred during liberation. Don't retry</summary>
Error = 2 Error = 2,
/// <summary>Application-state only. Not a valid persistence state.</summary>
PartialDownload = 0x1000
} }
public class UserDefinedItem public class UserDefinedItem
@ -38,7 +42,15 @@ namespace DataLayer
public string Tags public string Tags
{ {
get => _tags; get => _tags;
set => _tags = sanitize(value); set
{
var newTags = sanitize(value);
if (_tags != newTags)
{
_tags = newTags;
ItemChanged?.Invoke(this, nameof(Tags));
}
}
} }
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
@ -95,10 +107,38 @@ namespace DataLayer
#endregion #endregion
#region LiberatedStatuses #region LiberatedStatuses
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
#endregion
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public LiberatedStatus BookStatus
{
get => _bookStatus;
set
{
if (_bookStatus != value)
{
_bookStatus = value;
ItemChanged?.Invoke(this, nameof(BookStatus));
}
}
}
public LiberatedStatus? PdfStatus
{
get => _pdfStatus;
set
{
if (_pdfStatus != value)
{
_pdfStatus = value;
ItemChanged?.Invoke(this, nameof(PdfStatus));
}
}
}
#endregion
/// <summary>
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
/// </summary>
public static event EventHandler<string> ItemChanged;
public override string ToString() => $"{Book} {Rating} {Tags}"; public override string ToString() => $"{Book} {Rating} {Tags}";
} }
} }

View File

@ -16,22 +16,22 @@ namespace FileLiberator
public class DownloadDecryptBook : IAudioDecodable public class DownloadDecryptBook : IAudioDecodable
{ {
private AaxcDownloadConverter aaxcDownloader; private AaxcDownloadConverter aaxcDownloader;
public event EventHandler<TimeSpan> StreamingTimeRemaining; public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt; public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered; public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered; public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered; public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered; public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin; public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged; public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted; public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin; public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate; public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed; public event EventHandler<LibraryBook> Completed;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
Begin?.Invoke(this, libraryBook); Begin?.Invoke(this, libraryBook);
@ -47,10 +47,12 @@ namespace FileLiberator
return new StatusHandler { "Decrypt failed" }; return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir // moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
// only need to update if success. if failure, it will remain at 0 == NotLiberated if (!moveResults.movedAudioFile)
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated); return new StatusHandler { "Cannot find final audio file after decryption" };
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
return new StatusHandler(); return new StatusHandler();
} }
@ -123,7 +125,7 @@ namespace FileLiberator
} }
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{ {
if (e is null && Configuration.Instance.AllowLibationFixup) if (e is null && Configuration.Instance.AllowLibationFixup)
{ {
@ -143,7 +145,7 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]"); NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
} }
private static string moveFilesToBooksDir(Book product, string outputAudioFilename) private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
{ {
// 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 + "]"
@ -158,6 +160,7 @@ namespace FileLiberator
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId); var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
bool movedAudioFile = false;
foreach (var f in sortedFiles) foreach (var f in sortedFiles)
{ {
var dest var dest
@ -170,11 +173,14 @@ namespace FileLiberator
Cue.UpdateFileName(f, audioFileName); Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest); File.Move(f.FullName, dest);
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
} }
AudibleFileStorage.Audio.Refresh(); AudibleFileStorage.Audio.Refresh();
return destinationDir; return (destinationDir, movedAudioFile);
} }
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename) private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
@ -197,26 +203,26 @@ namespace FileLiberator
} }
private static void validate(LibraryBook libraryBook) private static void validate(LibraryBook libraryBook)
{ {
string errorString(string field) string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle() string errorTitle()
{ {
var title var title
= (libraryBook.Book.Title.Length > 53) = (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..." ? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title; : libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle; return errorBookTitle;
}; };
if (string.IsNullOrWhiteSpace(libraryBook.Account)) if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account")); throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale")); throw new Exception(errorString("Locale"));
} }
public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists; public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;

View File

@ -23,8 +23,7 @@ namespace FileLiberator
await downloadPdfAsync(libraryBook, proposedDownloadFilePath); await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(libraryBook); var result = verifyDownload(libraryBook);
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated; libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
return result; return result;
} }

View File

@ -7,7 +7,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" /> <ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" /> <ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" /> <ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" /> <ProjectReference Include="..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" /> <ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />

View File

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
@ -18,10 +17,8 @@ namespace FileLiberator
// when used in foreach: stateful. deferred execution // when used in foreach: stateful. deferred execution
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable) public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
=> DbContexts.GetContext() => library.Where(libraryBook => processable.Validate(libraryBook));
.GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook));
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate) public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
{ {

View File

@ -86,7 +86,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.T
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -222,7 +222,7 @@ Global
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E} {59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF} {393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249} {06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF} {2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2} {9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}

View File

@ -50,24 +50,24 @@ namespace LibationWinForms.BookLiberation
public static class ProcessorAutomationController public static class ProcessorAutomationController
{ {
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null) public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
{ {
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId }); Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm(); var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(completedAction, logMe); var backupBook = CreateBackupBook(logMe);
// continue even if libraryBook is null. we'll display even that in the processing box // continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync(); await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
} }
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null) public static async Task BackupAllBooksAsync()
{ {
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync)); Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm(); var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm); var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(completedAction, logMe); var backupBook = CreateBackupBook(logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync(); await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
} }
@ -84,19 +84,19 @@ namespace LibationWinForms.BookLiberation
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync(); await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
} }
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null) public static async Task BackupAllPdfsAsync()
{ {
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync)); Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var automatedBackupsForm = new AutomatedBackupsForm(); var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm); var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction); var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
} }
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe) private static IProcessable CreateBackupBook(LogMe logMe)
{ {
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe); var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
@ -104,7 +104,6 @@ namespace LibationWinForms.BookLiberation
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e) async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{ {
await downloadPdf.TryProcessAsync(e); await downloadPdf.TryProcessAsync(e);
completedAction(sender, e);
} }
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted); var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
@ -246,7 +245,7 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == SkipResult) if (dialogResult == SkipResult)
{ {
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error); libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"); LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
} }
@ -305,7 +304,7 @@ An error occurred while trying to process this book.
protected override async Task RunAsync() protected override async Task RunAsync()
{ {
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here // 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()) foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetContext().GetLibrary_Flat_NoTracking()))
{ {
var keepGoing = await ProcessOneAsync(libraryBook, validate: false); var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing) if (!keepGoing)

View File

@ -384,15 +384,14 @@ namespace LibationWinForms
#region liberate menu #region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow); => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow); => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync(); => await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
#endregion #endregion
#region Export menu #region Export menu

View File

@ -12,6 +12,9 @@ using Dinah.Core.Drawing;
namespace LibationWinForms namespace LibationWinForms
{ {
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{ {
#region implementation properties #region implementation properties
@ -26,12 +29,15 @@ namespace LibationWinForms
private Book Book => LibraryBook.Book; private Book Book => LibraryBook.Book;
private Image _cover; private Image _cover;
private Action Refilter { get; }
public GridEntry(LibraryBook libraryBook) public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
{ {
LibraryBook = libraryBook; LibraryBook = libraryBook;
Refilter = refilterOnChanged;
_memberValues = CreateMemberValueDictionary(); _memberValues = CreateMemberValueDictionary();
//Get cover art. If it's default, subscribe to PictureCached //Get cover art. If it's default, subscribe to PictureCached
{ {
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80)); (bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
@ -58,7 +64,7 @@ namespace LibationWinForms
Description = GetDescriptionDisplay(Book); Description = GetDescriptionDisplay(Book);
} }
//DisplayTags and Liberate properties are live. UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
} }
private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e) private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e)
@ -70,8 +76,77 @@ namespace LibationWinForms
} }
} }
#region Data Source properties #region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
var udi = sender as UserDefinedItem;
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
switch (itemName)
{
case nameof(udi.Tags):
{
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(DisplayTags));
}
break;
case nameof(udi.BookStatus):
{
Book.UserDefinedItem.BookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
case nameof(udi.PdfStatus):
{
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
}
if (!suspendCommit)
Commit();
}
private bool suspendCommit = false;
/// <summary>
/// Begin editing the model, suspending commits until <see cref="EndEdit"/> is called.
/// </summary>
public void BeginEdit() => suspendCommit = true;
/// <summary>
/// Save all edits to the database.
/// </summary>
public void EndEdit()
{
Commit();
suspendCommit = false;
}
private void Commit()
{
//We don't want LiberatedStatus.PartialDownload to be a persistent status.
var bookStatus = Book.UserDefinedItem.BookStatus;
var saveStatus = bookStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : bookStatus;
Book.UserDefinedItem.BookStatus = saveStatus;
LibraryCommands.UpdateUserDefinedItem(Book);
Book.UserDefinedItem.BookStatus = bookStatus;
Refilter?.Invoke();
}
#endregion
#region Model properties exposed to the view
public Image Cover public Image Cover
{ {
get get
@ -96,8 +171,22 @@ namespace LibationWinForms
public string Category { get; } public string Category { get; }
public string Misc { get; } public string Misc { get; }
public string Description { get; } public string Description { get; }
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); public string DisplayTags
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book)); {
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
set => Book.UserDefinedItem.Tags = value;
}
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
{
get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book));
set
{
LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
}
}
#endregion #endregion
#region Data Sorting #region Data Sorting
@ -121,7 +210,7 @@ namespace LibationWinForms
{ nameof(Category), () => Category }, { nameof(Category), () => Category },
{ nameof(Misc), () => Misc }, { nameof(Misc), () => Misc },
{ nameof(DisplayTags), () => DisplayTags }, { nameof(DisplayTags), () => DisplayTags },
{ nameof(Liberate), () => Liberate.Item1 } { nameof(Liberate), () => Liberate.BookStatus }
}; };
// Instantiate comparers for every exposed member object type. // Instantiate comparers for every exposed member object type.
@ -131,7 +220,7 @@ namespace LibationWinForms
{ typeof(int), new ObjectComparer<int>() }, { typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() }, { typeof(float), new ObjectComparer<float>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() }, { typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberatedState), new ObjectComparer<LiberatedState>() }, { typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
}; };
public virtual object GetMemberValue(string memberName) => _memberValues[memberName](); public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
@ -200,5 +289,11 @@ namespace LibationWinForms
} }
#endregion #endregion
~GridEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
} }
} }

View File

@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" /> <ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" /> <ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -3,6 +3,7 @@ using System;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using System.Linq; using System.Linq;
using DataLayer;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -20,9 +21,11 @@ namespace LibationWinForms
{ {
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts); base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is (LiberatedState liberatedState, PdfState pdfState)) if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
{ {
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(liberatedState, pdfState); var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
DrawButtonImage(graphics, buttonImage, cellBounds); DrawButtonImage(graphics, buttonImage, cellBounds);
@ -30,29 +33,31 @@ namespace LibationWinForms
} }
} }
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus) private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
{ {
(string libState, string image_lib) = liberatedStatus switch (string libState, string image_lib) = liberatedStatus switch
{ {
LiberatedState.Liberated => ("Liberated", "green"), LiberatedStatus.Liberated => ("Liberated", "green"),
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"), LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"), LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"),
LiberatedStatus.Error => ("Book downloaded ERROR", "red"),
_ => throw new Exception("Unexpected liberation state") _ => throw new Exception("Unexpected liberation state")
}; };
(string pdfState, string image_pdf) = pdfStatus switch (string pdfState, string image_pdf) = pdfStatus switch
{ {
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"), LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"),
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"), LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"),
PdfState.NoPdf => ("", ""), LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"),
null => ("", ""),
_ => throw new Exception("Unexpected PDF state") _ => throw new Exception("Unexpected PDF state")
}; };
var mouseoverText = libState + pdfState; var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedState.NotDownloaded || if (liberatedStatus == LiberatedStatus.NotLiberated ||
liberatedStatus == LiberatedState.PartialDownload || liberatedStatus == LiberatedStatus.PartialDownload ||
pdfStatus == PdfState.NotDownloaded) pdfStatus == LiberatedStatus.NotLiberated)
mouseoverText += "\r\nClick to complete"; mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}"); var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");

View File

@ -86,7 +86,7 @@ namespace LibationWinForms
} }
// else: liberate // else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId)); await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
} }
private void Details_Click(GridEntry liveGridEntry) private void Details_Click(GridEntry liveGridEntry)
@ -95,15 +95,14 @@ namespace LibationWinForms
if (bookDetailsForm.ShowDialog() != DialogResult.OK) if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return; return;
var qtyChanges = LibraryCommands.UpdateUserDefinedItem(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus); liveGridEntry.BeginEdit();
if (qtyChanges == 0)
return;
//Re-apply filters liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
Filter(); liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
//Update whole GridEntry row liveGridEntry.EndEdit();
liveGridEntry.NotifyPropertyChanged();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
} }
#endregion #endregion
@ -132,7 +131,7 @@ namespace LibationWinForms
} }
var orderedGridEntries = lib var orderedGridEntries = lib
.Select(lb => new GridEntry(lb)).ToList() .Select(lb => new GridEntry(lb, Filter)).ToList()
// default load order // default load order
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
//// more advanced example: sort by author, then series, then title //// more advanced example: sort by author, then series, then title
@ -150,19 +149,6 @@ namespace LibationWinForms
BackupCountsChanged?.Invoke(this, EventArgs.Empty); BackupCountsChanged?.Invoke(this, EventArgs.Empty);
} }
public void RefreshRow(string productId)
{
var liveGridEntry = getGridEntry((ge) => ge.AudibleProductId == productId);
// update GridEntry Liberate cell
liveGridEntry?.NotifyPropertyChanged(nameof(liveGridEntry.Liberate));
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
Filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#endregion #endregion
#region Filter #region Filter
@ -195,12 +181,7 @@ namespace LibationWinForms
#endregion #endregion
#region DataGridView Macro #region DataGridView Macro
private GridEntry getGridEntry(Func<GridEntry, bool> predicate)
=> ((SortableBindingList<GridEntry>)gridEntryBindingSource.DataSource).FirstOrDefault(predicate);
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex); private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion #endregion
} }
} }