New feature #430 : bulk set pdf-downloaded status

This commit is contained in:
Robert McRackan 2022-12-29 09:33:32 -05:00
parent c98664d584
commit 83fb2cd1d0
10 changed files with 441 additions and 371 deletions

View File

@ -13,221 +13,221 @@ using static DtoImporterService.PerfLogger;
namespace ApplicationServices namespace ApplicationServices
{ {
public static class LibraryCommands public static class LibraryCommands
{ {
public static event EventHandler<int> ScanBegin; public static event EventHandler<int> ScanBegin;
public static event EventHandler ScanEnd; public static event EventHandler ScanEnd;
public static bool Scanning { get; private set; } public static bool Scanning { get; private set; }
private static object _lock { get; } = new(); private static object _lock { get; } = new();
static LibraryCommands() static LibraryCommands()
{ {
ScanBegin += (_, __) => Scanning = true; ScanBegin += (_, __) => Scanning = true;
ScanEnd += (_, __) => Scanning = false; ScanEnd += (_, __) => Scanning = false;
} }
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, IEnumerable<LibraryBook> existingLibrary, params Account[] accounts) public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
{ {
logRestart(); logRestart();
lock (_lock) lock (_lock)
{
if (Scanning)
return new();
}
ScanBegin?.Invoke(null, accounts.Length);
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
var libraryOptions = new LibraryOptions
{ {
ResponseGroups if (Scanning)
= LibraryOptions.ResponseGroupOptions.ProductAttrs return new();
| LibraryOptions.ResponseGroupOptions.ProductDesc }
| LibraryOptions.ResponseGroupOptions.Relationships ScanBegin?.Invoke(null, accounts.Length);
};
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
var libraryOptions = new LibraryOptions
{
ResponseGroups
= LibraryOptions.ResponseGroupOptions.ProductAttrs
| LibraryOptions.ResponseGroupOptions.ProductDesc
| LibraryOptions.ResponseGroupOptions.Relationships
};
if (accounts is null || accounts.Length == 0) if (accounts is null || accounts.Length == 0)
return new List<LibraryBook>(); return new List<LibraryBook>();
try try
{ {
logTime($"pre {nameof(scanAccountsAsync)} all"); logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all"); logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = libraryItems.Count; var totalCount = libraryItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList(); var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
return missingBookList; return missingBookList;
} }
catch (AudibleApi.Authentication.LoginFailedException lfEx) catch (AudibleApi.Authentication.LoginFailedException lfEx)
{ {
lfEx.SaveFiles(Configuration.Instance.LibationFiles); lfEx.SaveFiles(Configuration.Instance.LibationFiles);
// nuget Serilog.Exceptions would automatically log custom properties // nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
// https://github.com/RehanSaeed/Serilog.Exceptions // https://github.com/RehanSaeed/Serilog.Exceptions
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new
{
lfEx.RequestUrl,
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
lfEx.ResponseInputFields,
lfEx.ResponseBodyFilePaths
});
throw;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error scanning library");
throw;
}
finally
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
}
}
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
{
logRestart();
if (accounts is null || accounts.Length == 0)
return (0, 0);
try
{
lock (_lock)
{ {
if (Scanning) lfEx.RequestUrl,
return (0, 0); ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
} ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
ScanBegin?.Invoke(null, accounts.Length); lfEx.ResponseInputFields,
lfEx.ResponseBodyFilePaths
});
throw;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error scanning library");
throw;
}
finally
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
}
}
logTime($"pre {nameof(scanAccountsAsync)} all"); #region FULL LIBRARY scan and import
var libraryOptions = new LibraryOptions public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
{ {
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, logRestart();
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
}; if (accounts is null || accounts.Length == 0)
return (0, 0);
try
{
lock (_lock)
{
if (Scanning)
return (0, 0);
}
ScanBegin?.Invoke(null, accounts.Length);
logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryOptions = new LibraryOptions
{
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
};
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all"); logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count; var totalCount = importItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
if (totalCount == 0) if (totalCount == 0)
return default; return default;
Log.Logger.Information("Begin scan for orphaned episode parents"); Log.Logger.Information("Begin scan for orphaned episode parents");
var newParents = await findAndAddMissingParents(accounts); var newParents = await findAndAddMissingParents(accounts);
Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}"); Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}");
if (newParents >= 0) if (newParents >= 0)
{ {
//If any episodes are still orphaned, their series have been //If any episodes are still orphaned, their series have been
//removed from the catalog and we'll never be able to find them. //removed from the catalog and we'll never be able to find them.
//only do this if findAndAddMissingParents returned >= 0. If it //only do this if findAndAddMissingParents returned >= 0. If it
//returned < 0, an error happened and there's still a chance that //returned < 0, an error happened and there's still a chance that
//a future successful run will find missing parents. //a future successful run will find missing parents.
removedOrphanedEpisodes(); removedOrphanedEpisodes();
} }
Log.Logger.Information("Begin long-running import"); Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}"); logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems); var newCount = await importIntoDbAsync(importItems);
logTime($"post {nameof(importIntoDbAsync)}"); logTime($"post {nameof(importIntoDbAsync)}");
Log.Logger.Information($"Import complete. New count {newCount}"); Log.Logger.Information($"Import complete. New count {newCount}");
return (totalCount, newCount); return (totalCount, newCount);
} }
catch (AudibleApi.Authentication.LoginFailedException lfEx) catch (AudibleApi.Authentication.LoginFailedException lfEx)
{ {
lfEx.SaveFiles(Configuration.Instance.LibationFiles); lfEx.SaveFiles(Configuration.Instance.LibationFiles);
// nuget Serilog.Exceptions would automatically log custom properties // nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
// https://github.com/RehanSaeed/Serilog.Exceptions // https://github.com/RehanSaeed/Serilog.Exceptions
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new
{ {
lfEx.RequestUrl, lfEx.RequestUrl,
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
ResponseStatusCodeDesc = lfEx.ResponseStatusCode, ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
lfEx.ResponseInputFields, lfEx.ResponseInputFields,
lfEx.ResponseBodyFilePaths lfEx.ResponseBodyFilePaths
}); });
throw; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "Error importing library"); Log.Logger.Error(ex, "Error importing library");
throw; throw;
} }
finally finally
{ {
stop(); stop();
var putBreakPointHere = logOutput; var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null); ScanEnd?.Invoke(null, null);
} }
} }
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions) private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
{ {
var tasks = new List<Task<List<ImportItem>>>(); var tasks = new List<Task<List<ImportItem>>>();
foreach (var account in accounts) foreach (var account in accounts)
{ {
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll) // get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
var apiExtended = await apiExtendedfunc(account); var apiExtended = await apiExtendedfunc(account);
// add scanAccountAsync as a TASK: do not await // add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions)); tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
} }
// import library in parallel // import library in parallel
var arrayOfLists = await Task.WhenAll(tasks); var arrayOfLists = await Task.WhenAll(tasks);
var importItems = arrayOfLists.SelectMany(a => a).ToList(); var importItems = arrayOfLists.SelectMany(a => a).ToList();
return importItems; return importItems;
} }
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions) private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
{ {
ArgumentValidator.EnsureNotNull(account, nameof(account)); ArgumentValidator.EnsureNotNull(account, nameof(account));
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
{ {
Account = account?.MaskedLogEntry ?? "[null]" Account = account?.MaskedLogEntry ?? "[null]"
}); });
logTime($"pre scanAccountAsync {account.AccountName}"); logTime($"pre scanAccountAsync {account.AccountName}");
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes); var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}"); logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList(); return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
} }
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems) private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{ {
logTime("importIntoDbAsync -- pre db"); logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context); var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems)); var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()"); logTime("importIntoDbAsync -- post Import()");
int qtyChanges = SaveContext(context); int qtyChanges = SaveContext(context);
logTime("importIntoDbAsync -- post SaveChanges"); logTime("importIntoDbAsync -- post SaveChanges");
// this is any changes at all to the database, not just new books // this is any changes at all to the database, not just new books
if (qtyChanges > 0) if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange()); await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
@ -235,112 +235,112 @@ namespace ApplicationServices
return newCount; return newCount;
} }
static void removedOrphanedEpisodes() static void removedOrphanedEpisodes()
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
try try
{ {
var orphanedEpisodes = var orphanedEpisodes =
context context
.GetLibrary_Flat_NoTracking(includeParents: true) .GetLibrary_Flat_NoTracking(includeParents: true)
.FindOrphanedEpisodes(); .FindOrphanedEpisodes();
context.LibraryBooks.RemoveRange(orphanedEpisodes); context.LibraryBooks.RemoveRange(orphanedEpisodes);
context.Books.RemoveRange(orphanedEpisodes.Select(lb => lb.Book)); context.Books.RemoveRange(orphanedEpisodes.Select(lb => lb.Book));
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database"); Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database");
} }
} }
static async Task<int> findAndAddMissingParents(Account[] accounts) static async Task<int> findAndAddMissingParents(Account[] accounts)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var library = context.GetLibrary_Flat_NoTracking(includeParents: true); var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
try try
{ {
var orphanedEpisodes = library.FindOrphanedEpisodes().ToList(); var orphanedEpisodes = library.FindOrphanedEpisodes().ToList();
if (!orphanedEpisodes.Any()) if (!orphanedEpisodes.Any())
return -1; return -1;
var orphanedSeries = var orphanedSeries =
orphanedEpisodes orphanedEpisodes
.SelectMany(lb => lb.Book.SeriesLink) .SelectMany(lb => lb.Book.SeriesLink)
.DistinctBy(s => s.Series.AudibleSeriesId) .DistinctBy(s => s.Series.AudibleSeriesId)
.ToList(); .ToList();
// The Catalog endpoint does not require authentication. // The Catalog endpoint does not require authentication.
var api = new ApiUnauthenticated(accounts[0].Locale); var api = new ApiUnauthenticated(accounts[0].Locale);
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList(); var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
var items = await api.GetCatalogProductsAsync(seriesParents, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); var items = await api.GetCatalogProductsAsync(seriesParents, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
List<ImportItem> newParentsImportItems = new(); List<ImportItem> newParentsImportItems = new();
foreach (var sp in orphanedSeries) foreach (var sp in orphanedSeries)
{ {
var seriesItem = items.First(i => i.Asin == sp.Series.AudibleSeriesId); var seriesItem = items.First(i => i.Asin == sp.Series.AudibleSeriesId);
if (seriesItem.Relationships is null) if (seriesItem.Relationships is null)
continue; continue;
var episode = orphanedEpisodes.First(l => l.Book.AudibleProductId == sp.Book.AudibleProductId); var episode = orphanedEpisodes.First(l => l.Book.AudibleProductId == sp.Book.AudibleProductId);
seriesItem.PurchaseDate = new DateTimeOffset(episode.DateAdded); seriesItem.PurchaseDate = new DateTimeOffset(episode.DateAdded);
seriesItem.Series = new AudibleApi.Common.Series[] seriesItem.Series = new AudibleApi.Common.Series[]
{ {
new AudibleApi.Common.Series{ Asin = seriesItem.Asin, Title = seriesItem.TitleWithSubtitle, Sequence = "-1"} new AudibleApi.Common.Series{ Asin = seriesItem.Asin, Title = seriesItem.TitleWithSubtitle, Sequence = "-1"}
}; };
newParentsImportItems.Add(new ImportItem { DtoItem = seriesItem, AccountId = episode.Account, LocaleName = episode.Book.Locale }); newParentsImportItems.Add(new ImportItem { DtoItem = seriesItem, AccountId = episode.Account, LocaleName = episode.Book.Locale });
} }
var newCount = new LibraryBookImporter(context) var newCount = new LibraryBookImporter(context)
.Import(newParentsImportItems); .Import(newParentsImportItems);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
return newCount; return newCount;
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents."); Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents.");
return -1; return -1;
} }
} }
public static int SaveContext(LibationContext context) public static int SaveContext(LibationContext context)
{ {
try try
{ {
return context.SaveChanges(); return context.SaveChanges();
} }
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{ {
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension // DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"; static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException"; var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
if (ex.InnerException is null) if (ex.InnerException is null)
throw new Exception($"{msg}{format(ex)}"); throw new Exception($"{msg}{format(ex)}");
throw new Exception( throw new Exception(
$"{msg}{format(ex)}", $"{msg}{format(ex)}",
new Exception($"Inner Exception{format(ex.InnerException)}")); new Exception($"Inner Exception{format(ex.InnerException)}"));
} }
} }
#endregion #endregion
#region remove/restore books #region remove/restore books
public static Task<int> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove)); public static Task<int> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
public static int RemoveBook(string idToRemove) => removeBooks(new() { idToRemove }); public static int RemoveBook(string idToRemove) => removeBooks(new() { idToRemove });
private static int removeBooks(List<string> idsToRemove) private static int removeBooks(List<string> idsToRemove)
{ {
try try
{ {
if (idsToRemove is null || !idsToRemove.Any()) if (idsToRemove is null || !idsToRemove.Any())
return 0; return 0;
@ -370,8 +370,8 @@ namespace ApplicationServices
} }
public static int RestoreBooks(this List<LibraryBook> libraryBooks) public static int RestoreBooks(this List<LibraryBook> libraryBooks)
{ {
try try
{ {
if (libraryBooks is null || !libraryBooks.Any()) if (libraryBooks is null || !libraryBooks.Any())
return 0; return 0;
@ -402,59 +402,58 @@ namespace ApplicationServices
// call this whenever books are added or removed from library // call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null); private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary> /// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged; public static event EventHandler LibrarySizeChanged;
/// <summary> /// <summary>
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted. /// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
/// </summary> /// </summary>
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted; public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
#region Update book details #region Update book details
public static int UpdateUserDefinedItem( public static int UpdateUserDefinedItem(
this Book book, this Book book,
string tags = null, string tags = null,
LiberatedStatus? bookStatus = null, LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null) LiberatedStatus? pdfStatus = null)
=> new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus); => new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus);
public static int UpdateUserDefinedItem( public static int UpdateUserDefinedItem(
this IEnumerable<Book> books, this IEnumerable<Book> books,
string tags = null, string tags = null,
LiberatedStatus? bookStatus = null, LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null) LiberatedStatus? pdfStatus = null)
=> updateUserDefinedItem( => updateUserDefinedItem(
books, books,
udi => { udi => {
// blank tags are expected. null tags are not // blank tags are expected. null tags are not
if (tags is not null && udi.Tags != tags) if (tags is not null)
udi.Tags = tags; udi.Tags = tags;
if (bookStatus is not null && udi.BookStatus != bookStatus.Value) if (bookStatus.HasValue)
udi.BookStatus = bookStatus.Value; udi.BookStatus = bookStatus.Value;
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null // method handles null logic
if (pdfStatus is not null && udi.PdfStatus != pdfStatus.Value) udi.SetPdfStatus(pdfStatus);
udi.PdfStatus = pdfStatus.Value;
}); });
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus) public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); => book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus) public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus)
=> books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); => books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus) public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); => libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus) public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); => libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus) public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
=> book.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus); => book.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this IEnumerable<Book> books, LiberatedStatus pdfStatus) public static int UpdatePdfStatus(this IEnumerable<Book> books, LiberatedStatus pdfStatus)
=> books.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus); => books.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus) public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
=> libraryBook.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus); => libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus) public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus); => libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdateTags(this Book book, string tags) public static int UpdateTags(this Book book, string tags)
=> book.UpdateUserDefinedItem(udi => udi.Tags = tags); => book.UpdateUserDefinedItem(udi => udi.Tags = tags);
@ -466,9 +465,9 @@ namespace ApplicationServices
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags); => libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action) public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
=> libraryBook.Book.updateUserDefinedItem(action); => libraryBook.Book.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action) public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
=> libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action); => libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => book.updateUserDefinedItem(action); public static int UpdateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => book.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action) => books.updateUserDefinedItem(action); public static int UpdateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action) => books.updateUserDefinedItem(action);
@ -481,7 +480,7 @@ namespace ApplicationServices
if (books is null || !books.Any()) if (books is null || !books.Any())
return 0; return 0;
foreach (var book in books) foreach (var book in books)
action?.Invoke(book.UserDefinedItem); action?.Invoke(book.UserDefinedItem);
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
@ -506,49 +505,49 @@ namespace ApplicationServices
// must be here instead of in db layer due to AaxcExists // must be here instead of in db layer due to AaxcExists
public static LiberatedStatus Liberated_Status(Book book) public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus => book.Audio_Exists() ? book.UserDefinedItem.BookStatus
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload : AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated; : LiberatedStatus.NotLiberated;
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is // exists here for feature predictability. It makes sense for this to be where Liberated_Status is
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus; public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
// 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 record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
{ {
public int PendingBooks => booksNoProgress + booksDownloadedOnly; public int PendingBooks => booksNoProgress + booksDownloadedOnly;
public bool HasPendingBooks => PendingBooks > 0; public bool HasPendingBooks => PendingBooks > 0;
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError); public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded); public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
} }
public static LibraryStats GetCounts() public static LibraryStats GetCounts()
{ {
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking(); var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
var results = libraryBooks var results = libraryBooks
.AsParallel() .AsParallel()
.Select(lb => Liberated_Status(lb.Book)) .Select(lb => Liberated_Status(lb.Book))
.ToList(); .ToList();
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated); var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload); var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated); var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
var booksError = results.Count(r => r == LiberatedStatus.Error); var booksError = results.Count(r => r == LiberatedStatus.Error);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError }); Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
var boolResults = libraryBooks var boolResults = libraryBooks
.AsParallel() .AsParallel()
.Where(lb => lb.Book.HasPdf()) .Where(lb => lb.Book.HasPdf())
.Select(lb => Pdf_Status(lb.Book)) .Select(lb => Pdf_Status(lb.Book))
.ToList(); .ToList();
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated); var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated); 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 });
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded); return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
} }
} }
} }

View File

@ -146,10 +146,19 @@ namespace DataLayer
} }
} }
} }
public void SetPdfStatus(LiberatedStatus? pdfStatus)
{
// don't change whether pdf is actually available. if null, leave as null. if not null, only assign non-null
// null => non-null : only when adding a supplement
if (pdfStatus.HasValue && PdfStatus.HasValue)
PdfStatus = pdfStatus;
}
public LiberatedStatus? PdfStatus public LiberatedStatus? PdfStatus
{ {
get => _pdfStatus; get => _pdfStatus;
set internal set
{ {
if (_pdfStatus != value) if (_pdfStatus != value)
{ {

View File

@ -34,7 +34,13 @@ namespace LibationAvalonia.Dialogs
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" }, new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
}; };
public LiberatedStatusBatchManualDialog() public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Title = this.Title.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog()
{ {
InitializeComponent(); InitializeComponent();
SelectedItem = BookStatuses[0] as liberatedComboBoxItem; SelectedItem = BookStatuses[0] as liberatedComboBoxItem;

View File

@ -108,7 +108,7 @@ namespace LibationAvalonia.ViewModels
this.RaisePropertyChanged(nameof(Liberate)); this.RaisePropertyChanged(nameof(Liberate));
break; break;
case nameof(udi.PdfStatus): case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus; Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
_pdfStatus = udi.PdfStatus; _pdfStatus = udi.PdfStatus;
this.RaisePropertyChanged(nameof(Liberate)); this.RaisePropertyChanged(nameof(Liberate));
break; break;

View File

@ -62,7 +62,7 @@ namespace LibationAvalonia.Views
visibleLibraryBooks.UpdateTags(dialog.NewTags); visibleLibraryBooks.UpdateTags(dialog.NewTags);
} }
public async void setDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) public async void setBookDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{ {
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(); var dialog = new Dialogs.LiberatedStatusBatchManualDialog();
var result = await dialog.ShowDialog<DialogResult>(this); var result = await dialog.ShowDialog<DialogResult>(this);
@ -75,7 +75,7 @@ namespace LibationAvalonia.Views
this, this,
visibleLibraryBooks, visibleLibraryBooks,
// do not use `$` string interpolation. See impl. // do not use `$` string interpolation. See impl.
"Are you sure you want to replace downloaded status in {0}?", "Are you sure you want to replace book downloaded status in {0}?",
"Replace downloaded status?"); "Replace downloaded status?");
if (confirmationResult != DialogResult.Yes) if (confirmationResult != DialogResult.Yes)
@ -84,7 +84,29 @@ namespace LibationAvalonia.Views
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
} }
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true);
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace PDF downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
}
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{ {
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog(); var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
var result = await dialog.ShowDialog<DialogResult>(this); var result = await dialog.ShowDialog<DialogResult>(this);

View File

@ -110,7 +110,8 @@
</MenuItem.Styles> </MenuItem.Styles>
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" /> <MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
<MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." /> <MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
<MenuItem Click="setDownloadedManualToolStripMenuItem_Click" Header="Set '_Downloaded' status manually..." /> <MenuItem Click="setBookDownloadedManualToolStripMenuItem_Click" Header="Set book '_Downloaded' status manually..." />
<MenuItem Click="setPdfDownloadedManualToolStripMenuItem_Click" Header="Set _PDF 'Downloaded' status manually..." />
<MenuItem Click="setDownloadedAutoToolStripMenuItem_Click" Header="Set '_Downloaded' status automatically..." /> <MenuItem Click="setDownloadedAutoToolStripMenuItem_Click" Header="Set '_Downloaded' status automatically..." />
<MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." /> <MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
</MenuItem> </MenuItem>

View File

@ -1,10 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using DataLayer; using DataLayer;
using Dinah.Core;
using LibationFileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
@ -19,6 +15,12 @@ namespace LibationWinForms.Dialogs
public override string ToString() => Text; public override string ToString() => Text;
} }
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Text = this.Text.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog() public LiberatedStatusBatchManualDialog()
{ {
InitializeComponent(); InitializeComponent();

View File

@ -57,7 +57,9 @@
this.visibleBooksToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem(); this.visibleBooksToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu = new LibationWinForms.FormattableToolStripMenuItem(); this.liberateVisibleToolStripMenuItem_VisibleBooksMenu = new LibationWinForms.FormattableToolStripMenuItem();
this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.setBookDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setPdfDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@ -77,7 +79,6 @@
this.doneRemovingBtn = new System.Windows.Forms.Button(); this.doneRemovingBtn = new System.Windows.Forms.Button();
this.removeBooksBtn = new System.Windows.Forms.Button(); this.removeBooksBtn = new System.Windows.Forms.Button();
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl(); this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menuStrip1.SuspendLayout(); this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout(); this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
@ -101,7 +102,7 @@
// filterBtn // filterBtn
// //
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(892, 3); this.filterBtn.Location = new System.Drawing.Point(884, 3);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn"; this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27); this.filterBtn.Size = new System.Drawing.Size(88, 27);
@ -118,7 +119,7 @@
this.filterSearchTb.Location = new System.Drawing.Point(195, 5); this.filterSearchTb.Location = new System.Drawing.Point(195, 5);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb"; this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(689, 25); this.filterSearchTb.Size = new System.Drawing.Size(681, 25);
this.filterSearchTb.TabIndex = 1; this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress); this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
// //
@ -136,7 +137,7 @@
this.menuStrip1.Location = new System.Drawing.Point(0, 0); this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1"; this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2); this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1037, 24); this.menuStrip1.Size = new System.Drawing.Size(1025, 24);
this.menuStrip1.TabIndex = 0; this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1"; this.menuStrip1.Text = "menuStrip1";
// //
@ -316,7 +317,8 @@
this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu, this.liberateVisibleToolStripMenuItem_VisibleBooksMenu,
this.replaceTagsToolStripMenuItem, this.replaceTagsToolStripMenuItem,
this.setDownloadedManualToolStripMenuItem, this.setBookDownloadedManualToolStripMenuItem,
this.setPdfDownloadedManualToolStripMenuItem,
this.setDownloadedAutoToolStripMenuItem, this.setDownloadedAutoToolStripMenuItem,
this.removeToolStripMenuItem}); this.removeToolStripMenuItem});
this.visibleBooksToolStripMenuItem.FormatText = "&Visible Books: {0}"; this.visibleBooksToolStripMenuItem.FormatText = "&Visible Books: {0}";
@ -328,28 +330,42 @@
// //
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.FormatText = "&Liberate: {0}"; this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.FormatText = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Name = "liberateVisibleToolStripMenuItem_VisibleBooksMenu"; this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Name = "liberateVisibleToolStripMenuItem_VisibleBooksMenu";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(284, 22); this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(314, 22);
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "&Liberate: {0}"; this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Click += new System.EventHandler(this.liberateVisible); this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Click += new System.EventHandler(this.liberateVisible);
// //
// replaceTagsToolStripMenuItem // replaceTagsToolStripMenuItem
// //
this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem"; this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem";
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(284, 22); this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
this.replaceTagsToolStripMenuItem.Text = "Replace &Tags..."; this.replaceTagsToolStripMenuItem.Text = "Replace &Tags...";
this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click); this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click);
// //
// setDownloadedManualToolStripMenuItem // setBookDownloadedManualToolStripMenuItem
// //
this.setDownloadedManualToolStripMenuItem.Name = "setDownloadedManualToolStripMenuItem"; this.setBookDownloadedManualToolStripMenuItem.Name = "setBookDownloadedManualToolStripMenuItem";
this.setDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(284, 22); this.setBookDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
this.setDownloadedManualToolStripMenuItem.Text = "Set \'&Downloaded\' status manually..."; this.setBookDownloadedManualToolStripMenuItem.Text = "Set book \'&Downloaded\' status manually...";
this.setDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedManualToolStripMenuItem_Click); this.setBookDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setBookDownloadedManualToolStripMenuItem_Click);
//
// setPdfDownloadedManualToolStripMenuItem
//
this.setPdfDownloadedManualToolStripMenuItem.Name = "setPdfDownloadedManualToolStripMenuItem";
this.setPdfDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
this.setPdfDownloadedManualToolStripMenuItem.Text = "Set &PDF \'Downloaded\' status manually...";
this.setPdfDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setPdfDownloadedManualToolStripMenuItem_Click);
//
// setDownloadedAutoToolStripMenuItem
//
this.setDownloadedAutoToolStripMenuItem.Name = "setDownloadedAutoToolStripMenuItem";
this.setDownloadedAutoToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
this.setDownloadedAutoToolStripMenuItem.Text = "Set book \'Downloaded\' status &automatically...";
this.setDownloadedAutoToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedAutoToolStripMenuItem_Click);
// //
// removeToolStripMenuItem // removeToolStripMenuItem
// //
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem"; this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
this.removeToolStripMenuItem.Size = new System.Drawing.Size(284, 22); this.removeToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
this.removeToolStripMenuItem.Text = "&Remove from library..."; this.removeToolStripMenuItem.Text = "&Remove from library...";
this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click); this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click);
// //
@ -402,7 +418,7 @@
this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.ShowItemToolTips = true; this.statusStrip1.ShowItemToolTips = true;
this.statusStrip1.Size = new System.Drawing.Size(1033, 22); this.statusStrip1.Size = new System.Drawing.Size(1025, 22);
this.statusStrip1.TabIndex = 6; this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1"; this.statusStrip1.Text = "statusStrip1";
// //
@ -416,7 +432,7 @@
// springLbl // springLbl
// //
this.springLbl.Name = "springLbl"; this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(519, 17); this.springLbl.Size = new System.Drawing.Size(511, 17);
this.springLbl.Spring = true; this.springLbl.Spring = true;
// //
// backupsCountsLbl // backupsCountsLbl
@ -460,7 +476,7 @@
// //
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1); this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
this.splitContainer1.Size = new System.Drawing.Size(1463, 640); this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
this.splitContainer1.SplitterDistance = 1033; this.splitContainer1.SplitterDistance = 1025;
this.splitContainer1.SplitterWidth = 8; this.splitContainer1.SplitterWidth = 8;
this.splitContainer1.TabIndex = 7; this.splitContainer1.TabIndex = 7;
// //
@ -479,19 +495,19 @@
this.panel1.Location = new System.Drawing.Point(0, 24); this.panel1.Location = new System.Drawing.Point(0, 24);
this.panel1.Margin = new System.Windows.Forms.Padding(0); this.panel1.Margin = new System.Windows.Forms.Padding(0);
this.panel1.Name = "panel1"; this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1033, 594); this.panel1.Size = new System.Drawing.Size(1025, 594);
this.panel1.TabIndex = 7; this.panel1.TabIndex = 7;
// //
// productsDisplay // productsDisplay
// //
this.productsDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) this.productsDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.productsDisplay.AutoScroll = true; this.productsDisplay.AutoScroll = true;
this.productsDisplay.Location = new System.Drawing.Point(15, 36); this.productsDisplay.Location = new System.Drawing.Point(15, 36);
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.productsDisplay.Name = "productsDisplay"; this.productsDisplay.Name = "productsDisplay";
this.productsDisplay.Size = new System.Drawing.Size(1007, 555); this.productsDisplay.Size = new System.Drawing.Size(999, 555);
this.productsDisplay.TabIndex = 9; this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged); this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged); this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
@ -501,7 +517,7 @@
// toggleQueueHideBtn // toggleQueueHideBtn
// //
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.toggleQueueHideBtn.Location = new System.Drawing.Point(985, 3); this.toggleQueueHideBtn.Location = new System.Drawing.Point(977, 3);
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3); this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
this.toggleQueueHideBtn.Name = "toggleQueueHideBtn"; this.toggleQueueHideBtn.Name = "toggleQueueHideBtn";
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27); this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
@ -542,16 +558,9 @@
this.processBookQueue1.Location = new System.Drawing.Point(0, 0); this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.processBookQueue1.Name = "processBookQueue1"; this.processBookQueue1.Name = "processBookQueue1";
this.processBookQueue1.Size = new System.Drawing.Size(422, 640); this.processBookQueue1.Size = new System.Drawing.Size(430, 640);
this.processBookQueue1.TabIndex = 0; this.processBookQueue1.TabIndex = 0;
// //
// setDownloadedAutoToolStripMenuItem
//
this.setDownloadedAutoToolStripMenuItem.Name = "setDownloadedAutoToolStripMenuItem";
this.setDownloadedAutoToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
this.setDownloadedAutoToolStripMenuItem.Text = "Set \'&Downloaded\' status automatically...";
this.setDownloadedAutoToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedAutoToolStripMenuItem_Click);
//
// Form1 // Form1
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
@ -618,7 +627,7 @@
private LibationWinForms.FormattableToolStripMenuItem visibleBooksToolStripMenuItem; private LibationWinForms.FormattableToolStripMenuItem visibleBooksToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu; private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu;
private System.Windows.Forms.ToolStripMenuItem replaceTagsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem replaceTagsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem setDownloadedManualToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem setBookDownloadedManualToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem setDownloadedAutoToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem setDownloadedAutoToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu; private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
@ -629,5 +638,6 @@
private LibationWinForms.GridView.ProductsDisplay productsDisplay; private LibationWinForms.GridView.ProductsDisplay productsDisplay;
private System.Windows.Forms.Button removeBooksBtn; private System.Windows.Forms.Button removeBooksBtn;
private System.Windows.Forms.Button doneRemovingBtn; private System.Windows.Forms.Button doneRemovingBtn;
private System.Windows.Forms.ToolStripMenuItem setPdfDownloadedManualToolStripMenuItem;
} }
} }

View File

@ -90,7 +90,7 @@ namespace LibationWinForms
visibleLibraryBooks.UpdateTags(dialog.NewTags); visibleLibraryBooks.UpdateTags(dialog.NewTags);
} }
private void setDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e) private void setBookDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e)
{ {
var dialog = new LiberatedStatusBatchManualDialog(); var dialog = new LiberatedStatusBatchManualDialog();
var result = dialog.ShowDialog(); var result = dialog.ShowDialog();
@ -102,7 +102,7 @@ namespace LibationWinForms
var confirmationResult = MessageBoxLib.ShowConfirmationDialog( var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks, visibleLibraryBooks,
// do not use `$` string interpolation. See impl. // do not use `$` string interpolation. See impl.
"Are you sure you want to replace downloaded status in {0}?", "Are you sure you want to replace book downloaded status in {0}?",
"Replace downloaded status?"); "Replace downloaded status?");
if (confirmationResult != DialogResult.Yes) if (confirmationResult != DialogResult.Yes)
@ -111,6 +111,27 @@ namespace LibationWinForms
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
} }
private void setPdfDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e)
{
var dialog = new LiberatedStatusBatchManualDialog(isPdf: true);
var result = dialog.ShowDialog();
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace PDF downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
}
private async void setDownloadedAutoToolStripMenuItem_Click(object sender, EventArgs e) private async void setDownloadedAutoToolStripMenuItem_Click(object sender, EventArgs e)
{ {
var dialog = new LiberatedStatusBatchAutoDialog(); var dialog = new LiberatedStatusBatchAutoDialog();

View File

@ -122,7 +122,7 @@ namespace LibationWinForms.GridView
NotifyPropertyChanged(nameof(Liberate)); NotifyPropertyChanged(nameof(Liberate));
break; break;
case nameof(udi.PdfStatus): case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus; Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
_pdfStatus = udi.PdfStatus; _pdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate)); NotifyPropertyChanged(nameof(Liberate));
break; break;