commit
cdb6c9a1a4
@ -59,6 +59,7 @@ namespace AppScaffolding
|
|||||||
//
|
//
|
||||||
|
|
||||||
Migrations.migrate_to_v6_6_9(config);
|
Migrations.migrate_to_v6_6_9(config);
|
||||||
|
Migrations.migrate_from_7_10_1(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PopulateMissingConfigValues(Configuration config)
|
public static void PopulateMissingConfigValues(Configuration config)
|
||||||
@ -401,5 +402,28 @@ namespace AppScaffolding
|
|||||||
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void migrate_from_7_10_1(Configuration config)
|
||||||
|
{
|
||||||
|
//This migration removes books and series with SERIES_ prefix that were created
|
||||||
|
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
|
||||||
|
|
||||||
|
var migrated = config.GetNonString<bool>(nameof(migrate_from_7_10_1));
|
||||||
|
|
||||||
|
if (migrated) return;
|
||||||
|
|
||||||
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
|
var booksToRemove = context.Books.Where(b => b.AudibleProductId.StartsWith("SERIES_")).ToArray();
|
||||||
|
var seriesToRemove = context.Series.Where(s => s.AudibleSeriesId.StartsWith("SERIES_")).ToArray();
|
||||||
|
var lbToRemove = context.LibraryBooks.Where(lb => booksToRemove.Any(b => b == lb.Book)).ToArray();
|
||||||
|
|
||||||
|
context.LibraryBooks.RemoveRange(lbToRemove);
|
||||||
|
context.Books.RemoveRange(booksToRemove);
|
||||||
|
context.Series.RemoveRange(seriesToRemove);
|
||||||
|
|
||||||
|
LibraryCommands.SaveContext(context);
|
||||||
|
config.SetObject(nameof(migrate_from_7_10_1), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,10 @@ namespace ApplicationServices
|
|||||||
=> LibationContext.Create(SqliteStorage.ConnectionString);
|
=> LibationContext.Create(SqliteStorage.ConnectionString);
|
||||||
|
|
||||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
|
||||||
{
|
{
|
||||||
using var context = GetContext();
|
using var context = GetContext();
|
||||||
return context.GetLibrary_Flat_NoTracking();
|
return context.GetLibrary_Flat_NoTracking(includeParents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -200,7 +200,7 @@ namespace ApplicationServices
|
|||||||
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 = saveChanges(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
|
||||||
@ -211,7 +211,7 @@ namespace ApplicationServices
|
|||||||
return newCount;
|
return newCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int saveChanges(LibationContext context)
|
public static int SaveContext(LibationContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,7 +29,7 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public static EventHandler SearchEngineUpdated;
|
public static event EventHandler SearchEngineUpdated;
|
||||||
|
|
||||||
#region Update
|
#region Update
|
||||||
private static bool isUpdating;
|
private static bool isUpdating;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AudibleApi;
|
using AudibleApi;
|
||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Retry;
|
using Polly.Retry;
|
||||||
|
|
||||||
@ -129,7 +132,7 @@ namespace AudibleUtilities
|
|||||||
|
|
||||||
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
|
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
|
||||||
{
|
{
|
||||||
if (item.IsEpisodes && importEpisodes)
|
if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes)
|
||||||
{
|
{
|
||||||
//Get child episodes asynchronously and await all at the end
|
//Get child episodes asynchronously and await all at the end
|
||||||
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
||||||
@ -173,17 +176,66 @@ namespace AudibleUtilities
|
|||||||
{
|
{
|
||||||
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
||||||
|
|
||||||
var children = await getEpisodeChildrenAsync(parent);
|
List<Item> children;
|
||||||
|
|
||||||
if (!children.Any())
|
if (parent.IsEpisodes)
|
||||||
{
|
{
|
||||||
//The parent is the only episode in the podcase series,
|
//The 'parent' is a single episode that was added to the library.
|
||||||
//so the parent is its own child.
|
//Get the episode's parent and add it to the database.
|
||||||
parent.Series = new Series[] { new Series { Asin = parent.Asin, Sequence = RelationshipToProduct.Parent, Title = parent.TitleWithSubtitle } };
|
|
||||||
children.Add(parent);
|
Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", parent);
|
||||||
return children;
|
|
||||||
|
children = new() { parent };
|
||||||
|
|
||||||
|
var parentAsins = parent.Relationships
|
||||||
|
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||||
|
.Select(p => p.Asin);
|
||||||
|
|
||||||
|
var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
|
|
||||||
|
int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent);
|
||||||
|
if (numSeriesParents != 1)
|
||||||
|
{
|
||||||
|
//There should only ever be 1 top-level parent per episode. If not, log
|
||||||
|
//and throw so we can figure out what to do about those special cases.
|
||||||
|
JsonSerializerSettings Settings = new()
|
||||||
|
{
|
||||||
|
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
||||||
|
DateParseHandling = DateParseHandling.None,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var ex = new ApplicationException($"Found {numSeriesParents} parents for {parent.Asin}");
|
||||||
|
Serilog.Log.Logger.Error(ex, $"Episode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}");
|
||||||
|
throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realParent = seriesParents.Single(p => p.IsSeriesParent);
|
||||||
|
realParent.PurchaseDate = parent.PurchaseDate;
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Debug("Completed parent scan for {parent}", parent);
|
||||||
|
parent = realParent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
children = await getEpisodeChildrenAsync(parent);
|
||||||
|
if (!children.Any())
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
//A series parent will always have exactly 1 Series
|
||||||
|
parent.Series = new Series[]
|
||||||
|
{
|
||||||
|
new Series
|
||||||
|
{
|
||||||
|
Asin = parent.Asin,
|
||||||
|
Sequence = "-1",
|
||||||
|
Title = parent.TitleWithSubtitle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||||
@ -199,17 +251,10 @@ namespace AudibleUtilities
|
|||||||
Title = parent.TitleWithSubtitle
|
Title = parent.TitleWithSubtitle
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// overload (read: abuse) IsEpisodes flag
|
|
||||||
child.Relationships = new Relationship[]
|
|
||||||
{
|
|
||||||
new Relationship
|
|
||||||
{
|
|
||||||
RelationshipToProduct = RelationshipToProduct.Child,
|
|
||||||
RelationshipType = RelationshipType.Episode
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children.Add(parent);
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
|
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="3.0.2.1" />
|
<PackageReference Include="AudibleApi" Version="3.1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -16,8 +16,14 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// enum will be easier than bool to extend later
|
// enum will be easier than bool to extend later.
|
||||||
public enum ContentType { Unknown = 0, Product = 1, Episode = 2 }
|
public enum ContentType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Product = 1,
|
||||||
|
Episode = 2,
|
||||||
|
Parent = 4,
|
||||||
|
}
|
||||||
|
|
||||||
public class Book
|
public class Book
|
||||||
{
|
{
|
||||||
|
|||||||
@ -35,5 +35,15 @@ namespace DataLayer
|
|||||||
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||||
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||||
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
|
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
|
||||||
|
|
||||||
|
public static bool IsProduct(this Book book)
|
||||||
|
=> book.ContentType is not ContentType.Episode and not ContentType.Parent;
|
||||||
|
|
||||||
|
public static bool IsEpisodeChild(this Book book)
|
||||||
|
=> book.ContentType is ContentType.Episode;
|
||||||
|
|
||||||
|
public static bool IsEpisodeParent(this Book book)
|
||||||
|
=> book.ContentType is ContentType.Parent;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,13 @@ namespace DataLayer
|
|||||||
// .GetLibrary()
|
// .GetLibrary()
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
|
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context, bool includeParents = false)
|
||||||
=> context
|
=> context
|
||||||
.LibraryBooks
|
.LibraryBooks
|
||||||
.AsNoTrackingWithIdentityResolution()
|
.AsNoTrackingWithIdentityResolution()
|
||||||
.GetLibrary()
|
.GetLibrary()
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||||
@ -40,5 +42,32 @@ namespace DataLayer
|
|||||||
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||||
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||||
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
|
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode)
|
||||||
|
{
|
||||||
|
if (seriesEpisode.Book.SeriesLink is null) return null;
|
||||||
|
|
||||||
|
//Parent books will always have exactly 1 SeriesBook due to how
|
||||||
|
//they are imported in ApiExtended.getChildEpisodesAsync()
|
||||||
|
return libraryBooks.FirstOrDefault(
|
||||||
|
lb =>
|
||||||
|
lb.Book.IsEpisodeParent() &&
|
||||||
|
seriesEpisode.Book.SeriesLink.Any(
|
||||||
|
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
|
||||||
|
}
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
||||||
|
=> bookList
|
||||||
|
.Where(
|
||||||
|
lb =>
|
||||||
|
lb.Book.IsEpisodeChild() &&
|
||||||
|
lb.Book.SeriesLink?
|
||||||
|
.Any(
|
||||||
|
s =>
|
||||||
|
s.Series.AudibleSeriesId == parent.Book.AudibleProductId
|
||||||
|
) == true
|
||||||
|
).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ namespace DtoImporterService
|
|||||||
{
|
{
|
||||||
var item = importItem.DtoItem;
|
var item = importItem.DtoItem;
|
||||||
|
|
||||||
var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;
|
var contentType = GetContentType(item);
|
||||||
|
|
||||||
// absence of authors is very rare, but possible
|
// absence of authors is very rare, but possible
|
||||||
if (!item.Authors?.Any() ?? true)
|
if (!item.Authors?.Any() ?? true)
|
||||||
@ -184,5 +184,15 @@ namespace DtoImporterService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DataLayer.ContentType GetContentType(Item item)
|
||||||
|
{
|
||||||
|
if (item.IsEpisodes)
|
||||||
|
return DataLayer.ContentType.Episode;
|
||||||
|
else if (item.IsSeriesParent)
|
||||||
|
return DataLayer.ContentType.Parent;
|
||||||
|
else
|
||||||
|
return DataLayer.ContentType.Product;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ namespace FileLiberator
|
|||||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||||
=> library.Where(libraryBook =>
|
=> library.Where(libraryBook =>
|
||||||
Validate(libraryBook)
|
Validate(libraryBook)
|
||||||
&& (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes)
|
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes)
|
||||||
);
|
);
|
||||||
|
|
||||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||||
|
|||||||
@ -121,12 +121,12 @@ namespace LibationSearchEngine
|
|||||||
["Liberated"] = lb => isLiberated(lb.Book),
|
["Liberated"] = lb => isLiberated(lb.Book),
|
||||||
["LiberatedError"] = lb => liberatedError(lb.Book),
|
["LiberatedError"] = lb => liberatedError(lb.Book),
|
||||||
|
|
||||||
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["Podcast"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
["Podcasts"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["Podcasts"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["IsPodcast"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["Episode"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
["Episodes"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["Episodes"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
["IsEpisode"] = lb => lb.Book.IsEpisodeChild(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,30 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using Dinah.Core;
|
||||||
using Dinah.Core.DataBinding;
|
using Dinah.Core.DataBinding;
|
||||||
using Dinah.Core.Drawing;
|
using Dinah.Core.Drawing;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
|
/// <summary>The View Model base for the DataGridView</summary>
|
||||||
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||||
{
|
{
|
||||||
protected abstract Book Book { get; }
|
[Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
|
||||||
|
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
|
||||||
|
[Browsable(false)] public float SeriesIndex { get; protected set; }
|
||||||
|
[Browsable(false)] public string LongDescription { get; protected set; }
|
||||||
|
[Browsable(false)] public abstract DateTime DateAdded { get; }
|
||||||
|
[Browsable(false)] protected Book Book => LibraryBook.Book;
|
||||||
|
|
||||||
private Image _cover;
|
|
||||||
#region Model properties exposed to the view
|
#region Model properties exposed to the view
|
||||||
|
|
||||||
|
public abstract LiberateButtonStatus Liberate { get; }
|
||||||
public Image Cover
|
public Image Cover
|
||||||
{
|
{
|
||||||
get => _cover;
|
get => _cover;
|
||||||
@ -25,59 +34,31 @@ namespace LibationWinForms.GridView
|
|||||||
NotifyPropertyChanged();
|
NotifyPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public new bool InvokeRequired => base.InvokeRequired;
|
public string PurchaseDate { get; protected set; }
|
||||||
public abstract DateTime DateAdded { get; }
|
public string Series { get; protected set; }
|
||||||
public abstract float SeriesIndex { get; }
|
public string Title { get; protected set; }
|
||||||
public abstract string ProductRating { get; protected set; }
|
public string Length { get; protected set; }
|
||||||
public abstract string PurchaseDate { get; protected set; }
|
public string Authors { get; protected set; }
|
||||||
public abstract string MyRating { get; protected set; }
|
public string Narrators { get; protected set; }
|
||||||
public abstract string Series { get; protected set; }
|
public string Category { get; protected set; }
|
||||||
public abstract string Title { get; protected set; }
|
public string Misc { get; protected set; }
|
||||||
public abstract string Length { get; protected set; }
|
public string Description { get; protected set; }
|
||||||
public abstract string Authors { get; protected set; }
|
public string ProductRating { get; protected set; }
|
||||||
public abstract string Narrators { get; protected set; }
|
public string MyRating { get; protected set; }
|
||||||
public abstract string Category { get; protected set; }
|
|
||||||
public abstract string Misc { get; protected set; }
|
|
||||||
public abstract string Description { get; protected set; }
|
|
||||||
public abstract string DisplayTags { get; }
|
public abstract string DisplayTags { get; }
|
||||||
public abstract LiberateButtonStatus Liberate { get; }
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Sorting
|
#region Sorting
|
||||||
|
|
||||||
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
||||||
private Dictionary<string, Func<object>> _memberValues { get; set; }
|
|
||||||
protected abstract Dictionary<string, Func<object>> CreateMemberValueDictionary();
|
|
||||||
|
|
||||||
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
||||||
// Used by GridEntryBindingList for all sorting
|
// Used by GridEntryBindingList for all sorting
|
||||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||||
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||||
|
protected abstract Dictionary<string, Func<object>> CreateMemberValueDictionary();
|
||||||
#endregion
|
private Dictionary<string, Func<object>> _memberValues { get; set; }
|
||||||
|
|
||||||
protected void LoadCover()
|
|
||||||
{
|
|
||||||
// Get cover art. If it's default, subscribe to PictureCached
|
|
||||||
{
|
|
||||||
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
|
||||||
|
|
||||||
if (isDefault)
|
|
||||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
|
||||||
|
|
||||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
|
||||||
_cover = ImageReader.ToImage(picture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Definition.PictureId == Book.PictureId)
|
|
||||||
{
|
|
||||||
Cover = ImageReader.ToImage(e.Picture);
|
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instantiate comparers for every exposed member object type.
|
// Instantiate comparers for every exposed member object type.
|
||||||
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
||||||
@ -90,25 +71,87 @@ namespace LibationWinForms.GridView
|
|||||||
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
|
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Cover Art
|
||||||
|
|
||||||
|
private Image _cover;
|
||||||
|
protected void LoadCover()
|
||||||
|
{
|
||||||
|
// Get cover art. If it's default, subscribe to PictureCached
|
||||||
|
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||||
|
|
||||||
|
if (isDefault)
|
||||||
|
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||||
|
|
||||||
|
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||||
|
_cover = ImageReader.ToImage(picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Definition.PictureId == Book.PictureId)
|
||||||
|
{
|
||||||
|
Cover = ImageReader.ToImage(e.Picture);
|
||||||
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Static library display functions
|
||||||
|
|
||||||
|
/// <summary>This information should not change during <see cref="GridEntry"/> lifetime, so call only once.</summary>
|
||||||
|
protected static string GetDescriptionDisplay(Book book)
|
||||||
|
{
|
||||||
|
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||||
|
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
|
||||||
|
return doc.DocumentNode.InnerText.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static string TrimTextToWord(string text, int maxLength)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
text.Length <= maxLength ?
|
||||||
|
text :
|
||||||
|
text.Substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
|
||||||
|
/// Maximum of 5 text rows will fit in 80-pixel row height.
|
||||||
|
/// </summary>
|
||||||
|
protected static string GetMiscDisplay(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
var details = new List<string>();
|
||||||
|
|
||||||
|
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
|
||||||
|
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
|
||||||
|
|
||||||
|
details.Add($"Account: {locale} - {acct}");
|
||||||
|
|
||||||
|
if (libraryBook.Book.HasPdf())
|
||||||
|
details.Add("Has PDF");
|
||||||
|
if (libraryBook.Book.IsAbridged)
|
||||||
|
details.Add("Abridged");
|
||||||
|
if (libraryBook.Book.DatePublished.HasValue)
|
||||||
|
details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
|
||||||
|
// this goes last since it's most likely to have a line-break
|
||||||
|
if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
|
||||||
|
details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
|
||||||
|
|
||||||
|
if (!details.Any())
|
||||||
|
return "[details not imported]";
|
||||||
|
|
||||||
|
return string.Join("\r\n", details);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
~GridEntry()
|
~GridEntry()
|
||||||
{
|
{
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class GridEntryExtensions
|
|
||||||
{
|
|
||||||
#nullable enable
|
|
||||||
public static IEnumerable<SeriesEntry> Series(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.OfType<SeriesEntry>();
|
|
||||||
public static IEnumerable<LibraryBookEntry> LibraryBooks(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.OfType<LibraryBookEntry>();
|
|
||||||
public static LibraryBookEntry? FindBookByAsin(this IEnumerable<LibraryBookEntry> gridEntries, string audibleProductID)
|
|
||||||
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
|
||||||
public static SeriesEntry? FindBookSeriesEntry(this IEnumerable<GridEntry> gridEntries, IEnumerable<SeriesBook> matchSeries)
|
|
||||||
=> gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
|
|
||||||
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
|
|
||||||
=> gridEntries.Series().Where(i => i.Children.Count == 0);
|
|
||||||
public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,10 +73,10 @@ namespace LibationWinForms.GridView
|
|||||||
FilterString = filterString;
|
FilterString = filterString;
|
||||||
SearchResults = SearchEngineCommands.Search(filterString);
|
SearchResults = SearchEngineCommands.Search(filterString);
|
||||||
|
|
||||||
var booksFilteredIn = Items.LibraryBooks().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
||||||
|
|
||||||
//Find all series containing children that match the search criteria
|
//Find all series containing children that match the search criteria
|
||||||
var seriesFilteredIn = Items.Series().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||||
|
|
||||||
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
||||||
|
|
||||||
@ -89,19 +89,19 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
public void CollapseAll()
|
public void CollapseAll()
|
||||||
{
|
{
|
||||||
foreach (var series in Items.Series().ToList())
|
foreach (var series in Items.SeriesEntries().ToList())
|
||||||
CollapseItem(series);
|
CollapseItem(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExpandAll()
|
public void ExpandAll()
|
||||||
{
|
{
|
||||||
foreach (var series in Items.Series().ToList())
|
foreach (var series in Items.SeriesEntries().ToList())
|
||||||
ExpandItem(series);
|
ExpandItem(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CollapseItem(SeriesEntry sEntry)
|
public void CollapseItem(SeriesEntry sEntry)
|
||||||
{
|
{
|
||||||
foreach (var episode in Items.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
|
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).ToList())
|
||||||
{
|
{
|
||||||
FilterRemoved.Add(episode);
|
FilterRemoved.Add(episode);
|
||||||
base.Remove(episode);
|
base.Remove(episode);
|
||||||
@ -114,7 +114,7 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var sindex = Items.IndexOf(sEntry);
|
var sindex = Items.IndexOf(sEntry);
|
||||||
|
|
||||||
foreach (var episode in FilterRemoved.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
|
foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).ToList())
|
||||||
{
|
{
|
||||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||||
{
|
{
|
||||||
@ -174,7 +174,7 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var itemsList = (List<GridEntry>)Items;
|
var itemsList = (List<GridEntry>)Items;
|
||||||
|
|
||||||
var children = itemsList.LibraryBooks().Where(i => i.Parent is not null).ToList();
|
var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList();
|
||||||
|
|
||||||
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
||||||
|
|
||||||
|
|||||||
@ -8,23 +8,11 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
|
||||||
/// The View Model for a LibraryBook
|
|
||||||
/// </summary>
|
|
||||||
public class LibraryBookEntry : GridEntry
|
public class LibraryBookEntry : GridEntry
|
||||||
{
|
{
|
||||||
#region implementation properties NOT exposed to the view
|
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
|
||||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
[Browsable(false)] public SeriesEntry Parent { get; init; }
|
||||||
|
|
||||||
[Browsable(false)]
|
|
||||||
public string AudibleProductId => Book.AudibleProductId;
|
|
||||||
[Browsable(false)]
|
|
||||||
public LibraryBook LibraryBook { get; private set; }
|
|
||||||
[Browsable(false)]
|
|
||||||
public string LongDescription { get; private set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
protected override Book Book => LibraryBook.Book;
|
|
||||||
|
|
||||||
#region Model properties exposed to the view
|
#region Model properties exposed to the view
|
||||||
|
|
||||||
@ -32,22 +20,6 @@ namespace LibationWinForms.GridView
|
|||||||
private LiberatedStatus _bookStatus;
|
private LiberatedStatus _bookStatus;
|
||||||
private LiberatedStatus? _pdfStatus;
|
private LiberatedStatus? _pdfStatus;
|
||||||
|
|
||||||
public override DateTime DateAdded => LibraryBook.DateAdded;
|
|
||||||
public override float SeriesIndex => Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
|
||||||
public override string ProductRating { get; protected set; }
|
|
||||||
public override string PurchaseDate { get; protected set; }
|
|
||||||
public override string MyRating { get; protected set; }
|
|
||||||
public override string Series { get; protected set; }
|
|
||||||
public override string Title { get; protected set; }
|
|
||||||
public override string Length { get; protected set; }
|
|
||||||
public override string Authors { get; protected set; }
|
|
||||||
public override string Narrators { get; protected set; }
|
|
||||||
public override string Category { get; protected set; }
|
|
||||||
public override string Misc { get; protected set; }
|
|
||||||
public override string Description { get; protected set; }
|
|
||||||
public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
|
||||||
|
|
||||||
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
|
|
||||||
public override LiberateButtonStatus Liberate
|
public override LiberateButtonStatus Liberate
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -62,6 +34,8 @@ namespace LibationWinForms.GridView
|
|||||||
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
|
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public LibraryBookEntry(LibraryBook libraryBook)
|
public LibraryBookEntry(LibraryBook libraryBook)
|
||||||
@ -70,7 +44,6 @@ namespace LibationWinForms.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SeriesEntry Parent { get; init; }
|
|
||||||
public void UpdateLibraryBook(LibraryBook libraryBook)
|
public void UpdateLibraryBook(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
if (AudibleProductId != libraryBook.Book.AudibleProductId)
|
if (AudibleProductId != libraryBook.Book.AudibleProductId)
|
||||||
@ -86,8 +59,6 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
LibraryBook = libraryBook;
|
LibraryBook = libraryBook;
|
||||||
|
|
||||||
// Immutable properties
|
|
||||||
{
|
|
||||||
Title = Book.Title;
|
Title = Book.Title;
|
||||||
Series = Book.SeriesNames();
|
Series = Book.SeriesNames();
|
||||||
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
||||||
@ -100,12 +71,11 @@ namespace LibationWinForms.GridView
|
|||||||
Misc = GetMiscDisplay(libraryBook);
|
Misc = GetMiscDisplay(libraryBook);
|
||||||
LongDescription = GetDescriptionDisplay(Book);
|
LongDescription = GetDescriptionDisplay(Book);
|
||||||
Description = TrimTextToWord(LongDescription, 62);
|
Description = TrimTextToWord(LongDescription, 62);
|
||||||
}
|
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
||||||
|
|
||||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#region detect changes to the model, update the view, and save to database.
|
#region detect changes to the model, update the view, and save to database.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -169,58 +139,6 @@ namespace LibationWinForms.GridView
|
|||||||
{ nameof(DateAdded), () => DateAdded },
|
{ nameof(DateAdded), () => DateAdded },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static library display functions
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetDescriptionDisplay(Book book)
|
|
||||||
{
|
|
||||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
|
||||||
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
|
|
||||||
return doc.DocumentNode.InnerText.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TrimTextToWord(string text, int maxLength)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
text.Length <= maxLength ?
|
|
||||||
text :
|
|
||||||
text.Substring(0, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
|
||||||
/// Maximum of 5 text rows will fit in 80-pixel row height.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetMiscDisplay(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
var details = new List<string>();
|
|
||||||
|
|
||||||
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
|
|
||||||
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
|
|
||||||
|
|
||||||
details.Add($"Account: {locale} - {acct}");
|
|
||||||
|
|
||||||
if (libraryBook.Book.HasPdf())
|
|
||||||
details.Add("Has PDF");
|
|
||||||
if (libraryBook.Book.IsAbridged)
|
|
||||||
details.Add("Abridged");
|
|
||||||
if (libraryBook.Book.DatePublished.HasValue)
|
|
||||||
details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
|
|
||||||
// this goes last since it's most likely to have a line-break
|
|
||||||
if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
|
|
||||||
details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
|
|
||||||
|
|
||||||
if (!details.Any())
|
|
||||||
return "[details not imported]";
|
|
||||||
|
|
||||||
return string.Join("\r\n", details);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
~LibraryBookEntry()
|
~LibraryBookEntry()
|
||||||
|
|||||||
@ -39,10 +39,10 @@
|
|||||||
this.productsGrid.Name = "productsGrid";
|
this.productsGrid.Name = "productsGrid";
|
||||||
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
||||||
this.productsGrid.TabIndex = 0;
|
this.productsGrid.TabIndex = 0;
|
||||||
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
||||||
this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
||||||
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
||||||
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
||||||
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
||||||
//
|
//
|
||||||
// ProductsDisplay
|
// ProductsDisplay
|
||||||
|
|||||||
@ -29,7 +29,7 @@ namespace LibationWinForms.GridView
|
|||||||
#region Button controls
|
#region Button controls
|
||||||
|
|
||||||
private ImageDisplay imageDisplay;
|
private ImageDisplay imageDisplay;
|
||||||
private async void productsGrid_CoverClicked(LibraryBookEntry liveGridEntry)
|
private async void productsGrid_CoverClicked(GridEntry liveGridEntry)
|
||||||
{
|
{
|
||||||
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
||||||
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
||||||
@ -52,7 +52,7 @@ namespace LibationWinForms.GridView
|
|||||||
imageDisplay.CoverPicture = await picDlTask;
|
imageDisplay.CoverPicture = await picDlTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
|
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
|
||||||
{
|
{
|
||||||
var displayWindow = new DescriptionDisplay
|
var displayWindow = new DescriptionDisplay
|
||||||
{
|
{
|
||||||
@ -87,7 +87,7 @@ namespace LibationWinForms.GridView
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
||||||
var lib = DbContexts.GetLibrary_Flat_NoTracking();
|
var lib = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||||
|
|
||||||
if (!hasBeenDisplayed)
|
if (!hasBeenDisplayed)
|
||||||
{
|
{
|
||||||
@ -103,7 +103,6 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
|
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -115,7 +114,7 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisible().Select(v => v.LibraryBook).ToList();
|
internal List<LibraryBook> GetVisible() => productsGrid.GetVisibleBooks().ToList();
|
||||||
|
|
||||||
private void productsGrid_VisibleCountChanged(object sender, int count)
|
private void productsGrid_VisibleCountChanged(object sender, int count)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,23 +10,25 @@ using System.Windows.Forms;
|
|||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
|
public delegate void GridEntryClickedEventHandler(GridEntry liveGridEntry);
|
||||||
|
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
|
||||||
|
public delegate void GridEntryRectangleClickedEventHandler(GridEntry liveGridEntry, Rectangle cellRectangle);
|
||||||
|
|
||||||
public partial class ProductsGrid : UserControl
|
public partial class ProductsGrid : UserControl
|
||||||
{
|
{
|
||||||
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
|
|
||||||
public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
|
|
||||||
|
|
||||||
/// <summary>Number of visible rows has changed</summary>
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler CoverClicked;
|
public event GridEntryClickedEventHandler CoverClicked;
|
||||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||||
public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
|
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||||
public new event EventHandler<ScrollEventArgs> Scroll;
|
public new event EventHandler<ScrollEventArgs> Scroll;
|
||||||
|
|
||||||
private GridEntryBindingList bindingList;
|
private GridEntryBindingList bindingList;
|
||||||
internal IEnumerable<LibraryBookEntry> GetVisible()
|
internal IEnumerable<LibraryBook> GetVisibleBooks()
|
||||||
=> bindingList
|
=> bindingList
|
||||||
.LibraryBooks();
|
.BookEntries()
|
||||||
|
.Select(lbe => lbe.LibraryBook);
|
||||||
|
|
||||||
public ProductsGrid()
|
public ProductsGrid()
|
||||||
{
|
{
|
||||||
@ -61,7 +63,9 @@ namespace LibationWinForms.GridView
|
|||||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||||
CoverClicked?.Invoke(lbEntry);
|
CoverClicked?.Invoke(lbEntry);
|
||||||
}
|
}
|
||||||
else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
|
else if (entry is SeriesEntry sEntry)
|
||||||
|
{
|
||||||
|
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||||
{
|
{
|
||||||
if (sEntry.Liberate.Expanded)
|
if (sEntry.Liberate.Expanded)
|
||||||
bindingList.CollapseItem(sEntry);
|
bindingList.CollapseItem(sEntry);
|
||||||
@ -70,7 +74,12 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
|
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
|
||||||
|
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
|
}
|
||||||
|
else if (e.ColumnIndex == descriptionGVColumn.Index)
|
||||||
|
DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
|
||||||
|
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||||
|
CoverClicked?.Invoke(sEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,14 +91,17 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
internal void BindToGrid(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
var geList = dbBooks.Where(lb => lb.Book.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
||||||
|
|
||||||
var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList();
|
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
|
||||||
|
|
||||||
var allSeries = episodes.SelectMany(lb => lb.Book.SeriesLink.Where(s => !s.Series.AudibleSeriesId.StartsWith("SERIES_"))).DistinctBy(s => s.Series).ToList();
|
foreach (var parent in dbBooks.Where(lb => lb.Book.IsEpisodeParent()))
|
||||||
foreach (var series in allSeries)
|
|
||||||
{
|
{
|
||||||
var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.Any(s => s.Series == series.Series)));
|
var seriesEpisodes = episodes.FindChildren(parent);
|
||||||
|
|
||||||
|
if (!seriesEpisodes.Any()) continue;
|
||||||
|
|
||||||
|
var seriesEntry = new SeriesEntry(parent, seriesEpisodes);
|
||||||
|
|
||||||
geList.Add(seriesEntry);
|
geList.Add(seriesEntry);
|
||||||
geList.AddRange(seriesEntry.Children);
|
geList.AddRange(seriesEntry.Children);
|
||||||
@ -98,79 +110,47 @@ namespace LibationWinForms.GridView
|
|||||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
||||||
bindingList.CollapseAll();
|
bindingList.CollapseAll();
|
||||||
syncBindingSource.DataSource = bindingList;
|
syncBindingSource.DataSource = bindingList;
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
|
#region Add new or update existing grid entries
|
||||||
|
|
||||||
|
//Remove filter prior to adding/updating boooks
|
||||||
string existingFilter = syncBindingSource.Filter;
|
string existingFilter = syncBindingSource.Filter;
|
||||||
Filter(null);
|
Filter(null);
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = true;
|
bindingList.SuspendFilteringOnUpdate = true;
|
||||||
|
|
||||||
//Add absent books to grid, or update current books
|
//Add absent entries to grid, or update existing entry
|
||||||
|
|
||||||
var allItmes = bindingList.AllItems().LibraryBooks();
|
var allEntries = bindingList.AllItems().BookEntries();
|
||||||
foreach (var libraryBook in dbBooks)
|
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||||
{
|
|
||||||
var existingItem = allItmes.FindBookByAsin(libraryBook.Book.AudibleProductId);
|
|
||||||
|
|
||||||
// add new to top
|
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||||
if (existingItem is null)
|
|
||||||
{
|
{
|
||||||
if (libraryBook.IsEpisodeChild())
|
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
||||||
{
|
|
||||||
LibraryBookEntry lbe;
|
|
||||||
//Find the series that libraryBook belongs to, if it exists
|
|
||||||
var series = bindingList.AllItems().FindBookSeriesEntry(libraryBook.Book.SeriesLink);
|
|
||||||
|
|
||||||
if (series is null)
|
if (libraryBook.Book.IsProduct())
|
||||||
{
|
AddOrUpdateBook(libraryBook, existingEntry);
|
||||||
//Series doesn't exist yet, so create and add it
|
else if(libraryBook.Book.IsEpisodeChild())
|
||||||
var newSeries = new SeriesEntry(libraryBook.Book.SeriesLink.First(), libraryBook);
|
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||||
lbe = newSeries.Children[0];
|
|
||||||
newSeries.Liberate.Expanded = true;
|
|
||||||
bindingList.Insert(0, newSeries);
|
|
||||||
series = newSeries;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lbe = new(libraryBook) { Parent = series };
|
|
||||||
series.Children.Add(lbe);
|
|
||||||
}
|
|
||||||
//Add episode beneath the parent
|
|
||||||
int seriesIndex = bindingList.IndexOf(series);
|
|
||||||
bindingList.Insert(seriesIndex + 1, lbe);
|
|
||||||
|
|
||||||
if (series.Liberate.Expanded)
|
|
||||||
bindingList.ExpandItem(series);
|
|
||||||
else
|
|
||||||
bindingList.CollapseItem(series);
|
|
||||||
|
|
||||||
series.NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
else if (libraryBook.Book.ContentType is not ContentType.Episode)
|
|
||||||
//Add the new product
|
|
||||||
bindingList.Insert(0, new LibraryBookEntry(libraryBook));
|
|
||||||
}
|
|
||||||
// update existing
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingItem.UpdateLibraryBook(libraryBook);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = false;
|
bindingList.SuspendFilteringOnUpdate = false;
|
||||||
|
|
||||||
//Re-filter after updating existing / adding new books to capture any changes
|
//Re-apply filter after adding new/updating existing books to capture any changes
|
||||||
Filter(existingFilter);
|
Filter(existingFilter);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// remove deleted from grid.
|
// remove deleted from grid.
|
||||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||||
var removedBooks =
|
var removedBooks =
|
||||||
bindingList
|
bindingList
|
||||||
.AllItems()
|
.AllItems()
|
||||||
.LibraryBooks()
|
.BookEntries()
|
||||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||||
|
|
||||||
//Remove books in series from their parents' Children list
|
//Remove books in series from their parents' Children list
|
||||||
@ -190,7 +170,70 @@ namespace LibationWinForms.GridView
|
|||||||
//no need to re-filter for removed books
|
//no need to re-filter for removed books
|
||||||
bindingList.Remove(removed);
|
bindingList.Remove(removed);
|
||||||
|
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry existingBookEntry)
|
||||||
|
{
|
||||||
|
if (existingBookEntry is null)
|
||||||
|
// Add the new product to top
|
||||||
|
bindingList.Insert(0, new LibraryBookEntry(book));
|
||||||
|
else
|
||||||
|
// update existing
|
||||||
|
existingBookEntry.UpdateLibraryBook(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry existingEpisodeEntry, List<SeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||||
|
{
|
||||||
|
if (existingEpisodeEntry is null)
|
||||||
|
{
|
||||||
|
LibraryBookEntry episodeEntry;
|
||||||
|
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
|
||||||
|
|
||||||
|
if (seriesEntry is null)
|
||||||
|
{
|
||||||
|
//Series doesn't exist yet, so create and add it
|
||||||
|
var seriesBook = dbBooks.FindSeriesParent(episodeBook);
|
||||||
|
|
||||||
|
if (seriesBook is null)
|
||||||
|
{
|
||||||
|
//This should be impossible because the importer ensures every episode has a parent.
|
||||||
|
var ex = new ApplicationException($"Episode's series parent not found in database.");
|
||||||
|
var seriesLinks = string.Join("\r\n", episodeBook.Book.SeriesLink?.Select(sb => $"{nameof(sb.Series.Name)}={sb.Series.Name}, {nameof(sb.Series.AudibleSeriesId)}={sb.Series.AudibleSeriesId}"));
|
||||||
|
Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesEntry = new SeriesEntry(seriesBook, episodeBook);
|
||||||
|
seriesEntries.Add(seriesEntry);
|
||||||
|
|
||||||
|
episodeEntry = seriesEntry.Children[0];
|
||||||
|
seriesEntry.Liberate.Expanded = true;
|
||||||
|
bindingList.Insert(0, seriesEntry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Series exists. Create and add episode child then update the SeriesEntry
|
||||||
|
episodeEntry = new(episodeBook) { Parent = seriesEntry };
|
||||||
|
seriesEntry.Children.Add(episodeEntry);
|
||||||
|
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
|
||||||
|
seriesEntry.UpdateSeries(seriesBook);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add episode to the grid beneath the parent
|
||||||
|
int seriesIndex = bindingList.IndexOf(seriesEntry);
|
||||||
|
bindingList.Insert(seriesIndex + 1, episodeEntry);
|
||||||
|
|
||||||
|
if (seriesEntry.Liberate.Expanded)
|
||||||
|
bindingList.ExpandItem(seriesEntry);
|
||||||
|
else
|
||||||
|
bindingList.CollapseItem(seriesEntry);
|
||||||
|
|
||||||
|
seriesEntry.NotifyPropertyChanged();
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -207,7 +250,7 @@ namespace LibationWinForms.GridView
|
|||||||
syncBindingSource.Filter = searchString;
|
syncBindingSource.Filter = searchString;
|
||||||
|
|
||||||
if (visibleCount != bindingList.Count)
|
if (visibleCount != bindingList.Count)
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
Source/LibationWinForms/GridView/QueryExtensions.cs
Normal file
36
Source/LibationWinForms/GridView/QueryExtensions.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using DataLayer;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LibationWinForms.GridView
|
||||||
|
{
|
||||||
|
#nullable enable
|
||||||
|
internal static class QueryExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
|
||||||
|
=> gridEntries.OfType<LibraryBookEntry>();
|
||||||
|
|
||||||
|
public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
|
||||||
|
=> gridEntries.OfType<SeriesEntry>();
|
||||||
|
|
||||||
|
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry
|
||||||
|
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
||||||
|
|
||||||
|
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
|
||||||
|
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
|
||||||
|
|
||||||
|
public static SeriesEntry? FindSeriesParent(this IEnumerable<GridEntry> gridEntries, LibraryBook seriesEpisode)
|
||||||
|
{
|
||||||
|
if (seriesEpisode.Book.SeriesLink is null) return null;
|
||||||
|
|
||||||
|
//Parent books will always have exactly 1 SeriesBook due to how
|
||||||
|
//they are imported in ApiExtended.getChildEpisodesAsync()
|
||||||
|
return gridEntries.SeriesEntries().FirstOrDefault(
|
||||||
|
lb =>
|
||||||
|
seriesEpisode.Book.SeriesLink.Any(
|
||||||
|
s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#nullable disable
|
||||||
|
}
|
||||||
@ -2,108 +2,90 @@
|
|||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
{
|
{
|
||||||
|
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
|
||||||
public class SeriesEntry : GridEntry
|
public class SeriesEntry : GridEntry
|
||||||
{
|
{
|
||||||
public List<LibraryBookEntry> Children { get; init; }
|
[Browsable(false)] public List<LibraryBookEntry> Children { get; }
|
||||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
[Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
||||||
public override float SeriesIndex { get; }
|
|
||||||
public override string ProductRating
|
#region Model properties exposed to the view
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var productAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.Rating.StoryRating));
|
|
||||||
return productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
|
||||||
}
|
|
||||||
protected set => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public override string PurchaseDate { get; protected set; }
|
|
||||||
public override string MyRating
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var myAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating));
|
|
||||||
return myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
|
||||||
}
|
|
||||||
protected set => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public override string Series { get; protected set; }
|
|
||||||
public override string Title { get; protected set; }
|
|
||||||
public override string Length
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
|
||||||
return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
|
|
||||||
}
|
|
||||||
protected set => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public override string Authors { get; protected set; }
|
|
||||||
public override string Narrators { get; protected set; }
|
|
||||||
public override string Category { get; protected set; }
|
|
||||||
public override string Misc { get; protected set; } = string.Empty;
|
|
||||||
public override string Description { get; protected set; } = string.Empty;
|
|
||||||
public override string DisplayTags { get; } = string.Empty;
|
|
||||||
|
|
||||||
public override LiberateButtonStatus Liberate { get; }
|
public override LiberateButtonStatus Liberate { get; }
|
||||||
|
public override string DisplayTags { get; } = string.Empty;
|
||||||
|
|
||||||
protected override Book Book => SeriesBook.Book;
|
#endregion
|
||||||
|
|
||||||
private SeriesBook SeriesBook { get; set; }
|
private SeriesEntry(LibraryBook parent)
|
||||||
|
|
||||||
private SeriesEntry(SeriesBook seriesBook)
|
|
||||||
{
|
{
|
||||||
Liberate = new LiberateButtonStatus { IsSeries = true };
|
Liberate = new LiberateButtonStatus { IsSeries = true };
|
||||||
SeriesIndex = seriesBook.Index;
|
SeriesIndex = -1;
|
||||||
|
LibraryBook = parent;
|
||||||
|
LoadCover();
|
||||||
}
|
}
|
||||||
public SeriesEntry(SeriesBook seriesBook, IEnumerable<LibraryBook> children) : this(seriesBook)
|
|
||||||
|
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children) : this(parent)
|
||||||
{
|
{
|
||||||
Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList();
|
Children = children
|
||||||
SetSeriesBook(seriesBook);
|
.Select(c => new LibraryBookEntry(c) { Parent = this })
|
||||||
|
.OrderBy(c => c.SeriesIndex)
|
||||||
|
.ToList();
|
||||||
|
UpdateSeries(parent);
|
||||||
}
|
}
|
||||||
public SeriesEntry(SeriesBook seriesBook, LibraryBook child) : this(seriesBook)
|
|
||||||
|
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent)
|
||||||
{
|
{
|
||||||
Children = new() { new LibraryBookEntry(child) { Parent = this } };
|
Children = new() { new LibraryBookEntry(child) { Parent = this } };
|
||||||
SetSeriesBook(seriesBook);
|
UpdateSeries(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSeriesBook(SeriesBook seriesBook)
|
public void UpdateSeries(LibraryBook parent)
|
||||||
{
|
{
|
||||||
SeriesBook = seriesBook;
|
LibraryBook = parent;
|
||||||
LoadCover();
|
|
||||||
|
|
||||||
// Immutable properties
|
Title = Book.Title;
|
||||||
{
|
Series = Book.SeriesNames();
|
||||||
Title = SeriesBook.Series.Name;
|
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||||
Series = SeriesBook.Series.Name;
|
|
||||||
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
|
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
|
||||||
|
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||||
Authors = Book.AuthorNames();
|
Authors = Book.AuthorNames();
|
||||||
Narrators = Book.NarratorNames();
|
Narrators = Book.NarratorNames();
|
||||||
Category = string.Join(" > ", Book.CategoriesNames());
|
Category = string.Join(" > ", Book.CategoriesNames());
|
||||||
}
|
Misc = GetMiscDisplay(LibraryBook);
|
||||||
|
LongDescription = GetDescriptionDisplay(Book);
|
||||||
|
Description = TrimTextToWord(LongDescription, 62);
|
||||||
|
|
||||||
|
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
||||||
|
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
|
||||||
|
|
||||||
|
NotifyPropertyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Data Sorting
|
||||||
|
|
||||||
/// <summary>Create getters for all member object values by name</summary>
|
/// <summary>Create getters for all member object values by name</summary>
|
||||||
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
||||||
{
|
{
|
||||||
{ nameof(Title), () => Book.SeriesSortable() },
|
{ nameof(Title), () => Book.TitleSortable() },
|
||||||
{ nameof(Series), () => Book.SeriesSortable() },
|
{ nameof(Series), () => Book.SeriesSortable() },
|
||||||
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
|
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
|
||||||
{ nameof(MyRating), () => Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
|
{ nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
|
||||||
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
|
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
|
||||||
{ nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) },
|
{ nameof(ProductRating), () => Book.Rating.FirstScore() },
|
||||||
{ nameof(Authors), () => string.Empty },
|
{ nameof(Authors), () => Authors },
|
||||||
{ nameof(Narrators), () => string.Empty },
|
{ nameof(Narrators), () => Narrators },
|
||||||
{ nameof(Description), () => string.Empty },
|
{ nameof(Description), () => Description },
|
||||||
{ nameof(Category), () => string.Empty },
|
{ nameof(Category), () => Category },
|
||||||
{ nameof(Misc), () => string.Empty },
|
{ nameof(Misc), () => Misc },
|
||||||
{ nameof(DisplayTags), () => string.Empty },
|
{ nameof(DisplayTags), () => string.Empty },
|
||||||
{ nameof(Liberate), () => Liberate },
|
{ nameof(Liberate), () => Liberate },
|
||||||
{ nameof(DateAdded), () => DateAdded },
|
{ nameof(DateAdded), () => DateAdded },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,59 +77,71 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
CompletedCount = 0;
|
CompletedCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
|
||||||
|
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||||
|
|
||||||
|
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
|
||||||
|
=> AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook });
|
||||||
|
|
||||||
|
public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook)
|
||||||
|
=> AddDownloadDecrypt(new List<DataLayer.LibraryBook>() { libraryBook });
|
||||||
|
|
||||||
|
public void AddConvertMp3(DataLayer.LibraryBook libraryBook)
|
||||||
|
=> AddConvertMp3(new List<DataLayer.LibraryBook>() { libraryBook });
|
||||||
|
|
||||||
public void AddDownloadPdf(IEnumerable<DataLayer.LibraryBook> entries)
|
public void AddDownloadPdf(IEnumerable<DataLayer.LibraryBook> entries)
|
||||||
{
|
{
|
||||||
|
List<ProcessBook> procs = new();
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
AddDownloadPdf(entry);
|
{
|
||||||
|
if (isBookInQueue(entry))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ProcessBook pbook = new(entry, Logger);
|
||||||
|
pbook.PropertyChanged += Pbook_DataAvailable;
|
||||||
|
pbook.AddDownloadPdf();
|
||||||
|
procs.Add(pbook);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddToQueue(procs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddDownloadDecrypt(IEnumerable<DataLayer.LibraryBook> entries)
|
public void AddDownloadDecrypt(IEnumerable<DataLayer.LibraryBook> entries)
|
||||||
{
|
{
|
||||||
|
List<ProcessBook> procs = new();
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
AddDownloadDecrypt(entry);
|
{
|
||||||
|
if (isBookInQueue(entry))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ProcessBook pbook = new(entry, Logger);
|
||||||
|
pbook.PropertyChanged += Pbook_DataAvailable;
|
||||||
|
pbook.AddDownloadDecryptBook();
|
||||||
|
pbook.AddDownloadPdf();
|
||||||
|
procs.Add(pbook);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddToQueue(procs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddConvertMp3(IEnumerable<DataLayer.LibraryBook> entries)
|
public void AddConvertMp3(IEnumerable<DataLayer.LibraryBook> entries)
|
||||||
{
|
{
|
||||||
|
List<ProcessBook> procs = new();
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
AddConvertMp3(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
|
|
||||||
{
|
{
|
||||||
if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
|
if (isBookInQueue(entry))
|
||||||
return;
|
continue;
|
||||||
|
|
||||||
ProcessBook pbook = new(libraryBook, Logger);
|
ProcessBook pbook = new(entry, Logger);
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
AddToQueue(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ProcessBook pbook = new(libraryBook, Logger);
|
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
|
||||||
pbook.AddDownloadDecryptBook();
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
AddToQueue(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddConvertMp3(DataLayer.LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ProcessBook pbook = new(libraryBook, Logger);
|
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
pbook.PropertyChanged += Pbook_DataAvailable;
|
||||||
pbook.AddConvertToMp3();
|
pbook.AddConvertToMp3();
|
||||||
AddToQueue(pbook);
|
procs.Add(pbook);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddToQueue(ProcessBook pbook)
|
AddToQueue(procs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToQueue(IEnumerable<ProcessBook> pbook)
|
||||||
{
|
{
|
||||||
syncContext.Post(_ =>
|
syncContext.Post(_ =>
|
||||||
{
|
{
|
||||||
@ -140,10 +152,11 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime StartintTime;
|
|
||||||
|
DateTime StartingTime;
|
||||||
private async Task QueueLoop()
|
private async Task QueueLoop()
|
||||||
{
|
{
|
||||||
StartintTime = DateTime.Now;
|
StartingTime = DateTime.Now;
|
||||||
counterTimer.Start();
|
counterTimer.Start();
|
||||||
|
|
||||||
while (Queue.MoveNext())
|
while (Queue.MoveNext())
|
||||||
@ -225,7 +238,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Running)
|
if (Running)
|
||||||
runningTimeLbl.Text = timeToStr(DateTime.Now - StartintTime);
|
runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearLogBtn_Click(object sender, EventArgs e)
|
private void clearLogBtn_Click(object sender, EventArgs e)
|
||||||
|
|||||||
@ -85,13 +85,18 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
public bool RemoveQueued(T item)
|
public bool RemoveQueued(T item)
|
||||||
{
|
{
|
||||||
|
bool itemsRemoved;
|
||||||
|
int queuedCount;
|
||||||
|
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
bool removed = _queued.Remove(item);
|
itemsRemoved = _queued.Remove(item);
|
||||||
if (removed)
|
queuedCount = _queued.Count;
|
||||||
QueuededCountChanged?.Invoke(this, _queued.Count);
|
|
||||||
return removed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemsRemoved)
|
||||||
|
QueuededCountChanged?.Invoke(this, queuedCount);
|
||||||
|
return itemsRemoved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCurrent()
|
public void ClearCurrent()
|
||||||
@ -102,32 +107,33 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
public bool RemoveCompleted(T item)
|
public bool RemoveCompleted(T item)
|
||||||
{
|
{
|
||||||
|
bool itemsRemoved;
|
||||||
|
int completedCount;
|
||||||
|
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
bool removed = _completed.Remove(item);
|
itemsRemoved = _completed.Remove(item);
|
||||||
if (removed)
|
completedCount = _completed.Count;
|
||||||
CompletedCountChanged?.Invoke(this, _completed.Count);
|
|
||||||
return removed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemsRemoved)
|
||||||
|
CompletedCountChanged?.Invoke(this, completedCount);
|
||||||
|
return itemsRemoved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearQueue()
|
public void ClearQueue()
|
||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
|
||||||
_queued.Clear();
|
_queued.Clear();
|
||||||
QueuededCountChanged?.Invoke(this, 0);
|
QueuededCountChanged?.Invoke(this, 0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearCompleted()
|
public void ClearCompleted()
|
||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
|
||||||
_completed.Clear();
|
_completed.Clear();
|
||||||
CompletedCountChanged?.Invoke(this, 0);
|
CompletedCountChanged?.Invoke(this, 0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool Any(Func<T, bool> predicate)
|
public bool Any(Func<T, bool> predicate)
|
||||||
{
|
{
|
||||||
@ -174,13 +180,18 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool MoveNext()
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
int completedCount = 0, queuedCount = 0;
|
||||||
|
bool completedChanged = false;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
if (Current != null)
|
if (Current != null)
|
||||||
{
|
{
|
||||||
_completed.Add(Current);
|
_completed.Add(Current);
|
||||||
CompletedCountChanged?.Invoke(this, _completed.Count);
|
completedCount = _completed.Count;
|
||||||
|
completedChanged = true;
|
||||||
}
|
}
|
||||||
if (_queued.Count == 0)
|
if (_queued.Count == 0)
|
||||||
{
|
{
|
||||||
@ -190,10 +201,17 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
Current = _queued[0];
|
Current = _queued[0];
|
||||||
_queued.RemoveAt(0);
|
_queued.RemoveAt(0);
|
||||||
|
|
||||||
QueuededCountChanged?.Invoke(this, _queued.Count);
|
queuedCount = _queued.Count;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (completedChanged)
|
||||||
|
CompletedCountChanged?.Invoke(this, completedCount);
|
||||||
|
QueuededCountChanged?.Invoke(this, queuedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryPeek(out T item)
|
public bool TryPeek(out T item)
|
||||||
{
|
{
|
||||||
@ -218,13 +236,15 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enqueue(T item)
|
public void Enqueue(IEnumerable<T> item)
|
||||||
{
|
{
|
||||||
|
int queueCount;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
_queued.Add(item);
|
_queued.AddRange(item);
|
||||||
QueuededCountChanged?.Invoke(this, _queued.Count);
|
queueCount = _queued.Count;
|
||||||
}
|
}
|
||||||
|
QueuededCountChanged?.Invoke(this, queueCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user