mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -04:00
bugfix #1925: show orphaned episodes as standalone rows
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
This commit is contained in:
6 files changed
+135
-9
No files matched your search
@@ -143,8 +143,10 @@ public class ApiExtended
|
||||
var importEpisodes = Configuration.Instance.ImportEpisodes;
|
||||
var importPlusTitles = Configuration.Instance.ImportPlusTitles;
|
||||
|
||||
List<Item> items = new();
|
||||
int libraryItemCount = 0, episodeItemsExcluded = 0, plusTitlesExcluded = 0;
|
||||
var items = new List<Item>();
|
||||
var libraryItemCount = 0;
|
||||
var episodeItemsExcluded = 0;
|
||||
var plusTitlesExcluded = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
var totalTime = TimeSpan.Zero;
|
||||
using var semaphore = new SemaphoreSlim(MaxConcurrency);
|
||||
@@ -378,7 +380,7 @@ public class ApiExtended
|
||||
var items = await fetch(asins);
|
||||
var missing = GetMissingAsins(asins, items);
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries && missing.Count > 0; attempt++)
|
||||
for (var attempt = 1; attempt <= maxRetries && missing.Count > 0; attempt++)
|
||||
{
|
||||
onRetry?.Invoke(attempt);
|
||||
|
||||
|
||||
@@ -108,6 +108,16 @@ public static class LibraryBookQueries
|
||||
.ParentedEpisodes()
|
||||
.Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId);
|
||||
|
||||
/// <summary>
|
||||
/// Books displayed as top-level rows: ordinary products plus episodes whose series parent
|
||||
/// is not in the database. Showing orphaned episodes here keeps a successfully imported
|
||||
/// title discoverable instead of silently hiding it from both grids.
|
||||
/// </summary>
|
||||
public IEnumerable<LibraryBook> StandaloneBooks()
|
||||
=> libraryBooks
|
||||
.Where(lb => lb.Book.IsProduct())
|
||||
.Concat(libraryBooks.FindOrphanedEpisodes());
|
||||
|
||||
public IEnumerable<LibraryBook> FindChildren(LibraryBook parent)
|
||||
=> libraryBooks.Where(lb => lb.Book.IsEpisodeChild() && lb.HasSeriesId(parent.Book.AudibleProductId));
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ public class ProductsDisplayViewModel : ViewModelBase
|
||||
var allEntries = SOURCE.BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||
var orphanedEpisodes = dbBooks.FindOrphanedEpisodes().ToHashSet();
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
@@ -169,11 +170,31 @@ public class ProductsDisplayViewModel : ViewModelBase
|
||||
{
|
||||
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||
|
||||
if (libraryBook.Book.IsProduct())
|
||||
if (libraryBook.Book.IsProduct() || orphanedEpisodes.Contains(libraryBook))
|
||||
{
|
||||
// An episode can become orphaned when its parent is removed. Move it out from under
|
||||
// the old parent and keep it visible as a standalone row.
|
||||
if (existingEntry?.Parent is not null)
|
||||
{
|
||||
existingEntry.Parent.RemoveChild(existingEntry);
|
||||
RemoveBooks([existingEntry], []);
|
||||
existingEntry = null;
|
||||
}
|
||||
|
||||
UpsertBook(libraryBook, existingEntry);
|
||||
}
|
||||
else if (parentedEpisodes.Contains(libraryBook))
|
||||
//Only try to add or update is this LibraryBook is a know child of a parent
|
||||
{
|
||||
// If a formerly orphaned episode now has a parent, remove its standalone row
|
||||
// before recreating it beneath that parent.
|
||||
if (existingEntry is { Parent: null })
|
||||
{
|
||||
RemoveBooks([existingEntry], []);
|
||||
existingEntry = null;
|
||||
}
|
||||
|
||||
UpsertEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,11 +32,14 @@ public class LibraryBookEntry : GridEntry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for all non-episode books in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for ordinary books and episodes whose series parent is missing.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="System.Threading.SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<GridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
=> await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsProduct(), lb => new LibraryBookEntry(lb) as GridEntry);
|
||||
=> await GetAllProductsAsync(
|
||||
libraryBooks.StandaloneBooks(),
|
||||
_ => true,
|
||||
lb => new LibraryBookEntry(lb) as GridEntry);
|
||||
|
||||
protected override string? GetBookTags()
|
||||
=> Book is null ? null : string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
|
||||
@@ -350,6 +350,7 @@ public partial class ProductsGrid : UserControl
|
||||
var allEntries = bindingList.AllItems().BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||
var orphanedEpisodes = dbBooks.FindOrphanedEpisodes().ToHashSet();
|
||||
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for creation of new IGridEntry items during upsert
|
||||
@@ -361,14 +362,30 @@ public partial class ProductsGrid : UserControl
|
||||
{
|
||||
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||
|
||||
if (libraryBook.Book.IsProduct())
|
||||
if (libraryBook.Book.IsProduct() || orphanedEpisodes.Contains(libraryBook))
|
||||
{
|
||||
// An episode can become orphaned when its parent is removed. Move it out from under
|
||||
// the old parent and keep it visible as a standalone row.
|
||||
if (existingEntry?.Parent is not null)
|
||||
{
|
||||
existingEntry.Parent.RemoveChild(existingEntry);
|
||||
bindingList.Remove(existingEntry);
|
||||
existingEntry = null;
|
||||
}
|
||||
|
||||
AddOrUpdateBook(libraryBook, existingEntry);
|
||||
continue;
|
||||
}
|
||||
if (parentedEpisodes.Contains(libraryBook))
|
||||
{
|
||||
//Only try to add or update is this LibraryBook is a know child of a parent
|
||||
// If a formerly orphaned episode now has a parent, remove its standalone row
|
||||
// before recreating it beneath that parent.
|
||||
if (existingEntry is { Parent: null })
|
||||
{
|
||||
bindingList.Remove(existingEntry);
|
||||
existingEntry = null;
|
||||
}
|
||||
|
||||
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationUiBase.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class StandaloneBooksTests
|
||||
{
|
||||
private static LibraryBook libraryBook(string asin, ContentType contentType, string? seriesAsin = null)
|
||||
{
|
||||
var contributor = Contributor.GetEmpty();
|
||||
var book = new Book(
|
||||
new AudibleProductId(asin),
|
||||
asin,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
contentType,
|
||||
[contributor],
|
||||
[contributor],
|
||||
"us");
|
||||
|
||||
if (seriesAsin is not null)
|
||||
book.UpsertSeries(new Series(new AudibleSeriesId(seriesAsin), $"Series {seriesAsin}"), "1");
|
||||
|
||||
return new LibraryBook(book, new DateTime(2026, 8, 10), "account");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ordinary_books_are_standalone()
|
||||
{
|
||||
var product = libraryBook("BOOK", ContentType.Product);
|
||||
|
||||
var standalone = new[] { product }.StandaloneBooks().ToList();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "BOOK" }, standalone.Select(lb => lb.Book.AudibleProductId).ToList());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void episode_without_its_parent_is_standalone()
|
||||
{
|
||||
var orphan = libraryBook("EPISODE", ContentType.Episode, "SHOW");
|
||||
|
||||
var standalone = new[] { orphan }.StandaloneBooks().ToList();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "EPISODE" }, standalone.Select(lb => lb.Book.AudibleProductId).ToList());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void episode_with_its_parent_is_not_standalone()
|
||||
{
|
||||
var parent = libraryBook("SHOW", ContentType.Parent, "SHOW");
|
||||
var episode = libraryBook("EPISODE", ContentType.Episode, "SHOW");
|
||||
|
||||
var standalone = new[] { parent, episode }.StandaloneBooks().ToList();
|
||||
|
||||
Assert.AreEqual(0, standalone.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void products_and_orphans_are_returned_but_parents_and_parented_episodes_are_not()
|
||||
{
|
||||
var product = libraryBook("BOOK", ContentType.Product);
|
||||
var parent = libraryBook("SHOW", ContentType.Parent, "SHOW");
|
||||
var child = libraryBook("CHILD", ContentType.Episode, "SHOW");
|
||||
var orphan = libraryBook("ORPHAN", ContentType.Episode, "MISSING_SHOW");
|
||||
|
||||
var standalone = new[] { product, parent, child, orphan }.StandaloneBooks().ToList();
|
||||
|
||||
CollectionAssert.AreEquivalent(
|
||||
new[] { "BOOK", "ORPHAN" },
|
||||
standalone.Select(lb => lb.Book.AudibleProductId).ToList());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user