diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5fcb8b3..50f6909e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,13 +51,8 @@ jobs: with: name: Libation ${{ needs.prerelease.outputs.version }} body: + token: ${{ secrets.GITHUB_TOKEN }} draft: true prerelease: false - - - name: Upload release assets - uses: dwenegar/upload-release-assets@v2 - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - with: - release_id: "${{ steps.release.outputs.id }}" - assets_path: ./artifacts + files: | + artifacts/*/* diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 229cdce4..9a0b385d 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -279,8 +279,11 @@ namespace AppScaffolding private static void wireUpSystemEvents(Configuration configuration) { - LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex(); - LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books); + LibraryCommands.LibrarySizeChanged += (object _, List libraryBooks) + => SearchEngineCommands.FullReIndex(libraryBooks); + + LibraryCommands.BookUserDefinedItemCommitted += (_, books) + => SearchEngineCommands.UpdateBooks(books); } public static UpgradeProperties GetLatestRelease() diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 8e56e9b3..45556dfb 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -222,7 +222,7 @@ namespace ApplicationServices { int qtyChanged = await Task.Run(() => SaveContext(context)); if (qtyChanged > 0) - await Task.Run(finalizeLibrarySizeChange); + await Task.Run(() => finalizeLibrarySizeChange(context)); return qtyChanged; } catch (Exception ex) @@ -329,7 +329,7 @@ namespace ApplicationServices // this is any changes at all to the database, not just new books if (qtyChanges > 0) - await Task.Run(() => finalizeLibrarySizeChange()); + await Task.Run(() => finalizeLibrarySizeChange(context)); logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); return newCount; @@ -369,16 +369,16 @@ namespace ApplicationServices using var context = DbContexts.GetContext(); - // Attach() NoTracking entities before SaveChanges() - foreach (var lb in removeLibraryBooks) + // Entry() NoTracking entities before SaveChanges() + foreach (var lb in removeLibraryBooks) { - lb.IsDeleted = true; - context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - } + lb.IsDeleted = true; + context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + } var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) - finalizeLibrarySizeChange(); + finalizeLibrarySizeChange(context); return qtyChanges; } @@ -398,16 +398,16 @@ namespace ApplicationServices using var context = DbContexts.GetContext(); - // Attach() NoTracking entities before SaveChanges() - foreach (var lb in libraryBooks) - { - lb.IsDeleted = false; - context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - } + // Entry() NoTracking entities before SaveChanges() + foreach (var lb in libraryBooks) + { + lb.IsDeleted = false; + context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + } var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) - finalizeLibrarySizeChange(); + finalizeLibrarySizeChange(context); return qtyChanges; } @@ -432,7 +432,7 @@ namespace ApplicationServices var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) - finalizeLibrarySizeChange(); + finalizeLibrarySizeChange(context); return qtyChanges; } @@ -445,10 +445,14 @@ namespace ApplicationServices #endregion // call this whenever books are added or removed from library - private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null); + private static void finalizeLibrarySizeChange(LibationContext context) + { + var library = context.GetLibrary_Flat_NoTracking(includeParents: true); + LibrarySizeChanged?.Invoke(null, library); + } /// Occurs when the size of the library changes. ie: books are added or removed - public static event EventHandler LibrarySizeChanged; + public static event EventHandler> LibrarySizeChanged; /// /// Occurs when the size of the library does not change but book(s) details do. Especially when , , or changed values are successfully persisted. @@ -518,17 +522,18 @@ namespace ApplicationServices if (libraryBooks is null || !libraryBooks.Any()) return 0; - foreach (var book in libraryBooks) - action?.Invoke(book.Book.UserDefinedItem); - using var context = DbContexts.GetContext(); - // Attach() NoTracking entities before SaveChanges() - foreach (var book in libraryBooks) + // Entry() instead of Attach() due to possible stack overflow with large tables + foreach (var book in libraryBooks) { - context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - } + action?.Invoke(book.Book.UserDefinedItem); + + var udiEntity = context.Entry(book.Book.UserDefinedItem); + + udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + } var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) @@ -599,7 +604,8 @@ namespace ApplicationServices var results = libraryBooks .AsParallel() - .Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) }) + .WithoutParents() + .Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) }) .ToList(); var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated); diff --git a/Source/ApplicationServices/SearchEngineCommands.cs b/Source/ApplicationServices/SearchEngineCommands.cs index 377248d5..7b292e44 100644 --- a/Source/ApplicationServices/SearchEngineCommands.cs +++ b/Source/ApplicationServices/SearchEngineCommands.cs @@ -48,6 +48,8 @@ namespace ApplicationServices } public static void FullReIndex() => performSafeCommand(fullReIndex); + public static void FullReIndex(List libraryBooks) + => performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents())); internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e => { @@ -94,8 +96,11 @@ namespace ApplicationServices private static void fullReIndex(SearchEngine engine) { var library = DbContexts.GetLibrary_Flat_NoTracking(); - engine.CreateNewIndex(library); + fullReIndex(engine, library); } + + private static void fullReIndex(SearchEngine engine, IEnumerable libraryBooks) + => engine.CreateNewIndex(libraryBooks); #endregion } } diff --git a/Source/DataLayer/LibationContextFactory.cs b/Source/DataLayer/LibationContextFactory.cs index abf9b656..92f1f7d1 100644 --- a/Source/DataLayer/LibationContextFactory.cs +++ b/Source/DataLayer/LibationContextFactory.cs @@ -6,6 +6,7 @@ namespace DataLayer public class LibationContextFactory : DesignTimeDbContextFactoryBase { protected override LibationContext CreateNewInstance(DbContextOptions options) => new LibationContext(options); - protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString); + protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) + => optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); } } diff --git a/Source/DataLayer/QueryObjects/BookQueries.cs b/Source/DataLayer/QueryObjects/BookQueries.cs index 4a3c9171..81cdc162 100644 --- a/Source/DataLayer/QueryObjects/BookQueries.cs +++ b/Source/DataLayer/QueryObjects/BookQueries.cs @@ -44,7 +44,11 @@ namespace DataLayer public static bool IsEpisodeParent(this Book book) => book.ContentType is ContentType.Parent; - public static bool HasLiberated(this Book book) + + public static IEnumerable WithoutParents(this IEnumerable libraryBooks) + => libraryBooks.Where(lb => !lb.Book.IsEpisodeParent()); + + public static bool HasLiberated(this Book book) => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated || book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated; } diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 476e617d..91f89995 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -21,8 +21,8 @@ namespace DataLayer .AsNoTrackingWithIdentityResolution() .GetLibrary() .AsEnumerable() - .Where(lb => !lb.Book.IsEpisodeParent() || includeParents) - .ToList(); + .Where(c => !c.Book.IsEpisodeParent() || includeParents) + .ToList(); public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) => context @@ -91,7 +91,7 @@ namespace DataLayer } #nullable disable - public static IEnumerable FindChildren(this IEnumerable bookList, LibraryBook parent) + public static List FindChildren(this IEnumerable bookList, LibraryBook parent) => bookList .Where( lb => diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index 59fe3f75..70b091d6 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -1,8 +1,6 @@ using ApplicationServices; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Platform; @@ -14,14 +12,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using ReactiveUI; -using DataLayer; +using Avalonia.Threading; namespace LibationAvalonia { public class App : Application { - public static Window MainWindow { get; private set; } + public static MainWindow MainWindow { get; private set; } public static IBrush ProcessQueueBookFailedBrush { get; private set; } public static IBrush ProcessQueueBookCompletedBrush { get; private set; } public static IBrush ProcessQueueBookCancelledBrush { get; private set; } @@ -216,11 +213,17 @@ namespace LibationAvalonia LoadStyles(); var mainWindow = new MainWindow(); desktop.MainWindow = MainWindow = mainWindow; - mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult()); + mainWindow.Loaded += MainWindow_Loaded; mainWindow.RestoreSizeAndLocation(Configuration.Instance); mainWindow.Show(); } + private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var library = await LibraryTask; + await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); + } + private static void LoadStyles() { ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush)); diff --git a/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs index b0a103ea..658ef683 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs @@ -44,16 +44,18 @@ namespace LibationAvalonia.ViewModels private void Configure_BackupCounts() { - MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent())); - LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts(); - LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts(); + LibraryCommands.LibrarySizeChanged += async (object _, List libraryBooks) + => await SetBackupCountsAsync(libraryBooks); + + //Pass null to the setup count to get the whole library. + LibraryCommands.BookUserDefinedItemCommitted += async (_, _) + => await SetBackupCountsAsync(null); } - private async void setBackupCounts(IEnumerable libraryBooks = null) + public async Task SetBackupCountsAsync(IEnumerable libraryBooks) { if (updateCountsTask?.IsCompleted ?? true) { - libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(); updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks)); var stats = await updateCountsTask; await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats); diff --git a/Source/LibationAvalonia/ViewModels/MainVM.cs b/Source/LibationAvalonia/ViewModels/MainVM.cs index 86c69d9d..027f13d9 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.cs @@ -1,7 +1,9 @@ using ApplicationServices; +using DataLayer; using LibationAvalonia.Views; using LibationFileManager; using ReactiveUI; +using System.Collections.Generic; namespace LibationAvalonia.ViewModels { @@ -20,7 +22,7 @@ namespace LibationAvalonia.ViewModels MainWindow = mainWindow; ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation"; - LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged; Configure_NonUI(); Configure_BackupCounts(); @@ -34,6 +36,11 @@ namespace LibationAvalonia.ViewModels Configure_VisibleBooks(); } + private async void LibraryCommands_LibrarySizeChanged(object sender, List fullLibrary) + { + await ProductsDisplay.UpdateGridAsync(fullLibrary); + } + private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}"; } } diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 419b387a..d8bfee95 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -28,7 +28,13 @@ namespace LibationAvalonia.ViewModels /// Grid entries included in the filter set. If null, all grid entries are shown private HashSet FilteredInGridEntries; public string FilterString { get; private set; } - public DataGridCollectionView GridEntries { get; private set; } + + private DataGridCollectionView _gridEntries; + public DataGridCollectionView GridEntries + { + get => _gridEntries; + private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); + } private bool _removeColumnVisible; public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); } diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 87823b17..f9687d2a 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -13,7 +13,6 @@ namespace LibationAvalonia.Views { public partial class MainWindow : ReactiveWindow { - public event EventHandler> LibraryLoaded; public MainWindow() { DataContext = new MainVM(this); @@ -23,7 +22,6 @@ namespace LibationAvalonia.Views Opened += MainWindow_Opened; Closing += MainWindow_Closing; - LibraryLoaded += MainWindow_LibraryLoaded; KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) }); @@ -56,21 +54,21 @@ namespace LibationAvalonia.Views this.SaveSizeAndLocation(Configuration.Instance); } - private async void MainWindow_LibraryLoaded(object sender, List dbBooks) - { - if (QuickFilters.UseDefault) - await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault()); - - await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks); - } - private void selectAndFocusSearchBox() { filterSearchTb.SelectAll(); filterSearchTb.Focus(); } - public void OnLibraryLoaded(List initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary); + public async System.Threading.Tasks.Task OnLibraryLoadedAsync(List initialLibrary) + { + if (QuickFilters.UseDefault) + await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault()); + + await ViewModel.SetBackupCountsAsync(initialLibrary); + await ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary); + } + public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook); public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series); public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook); diff --git a/Source/LibationFileManager/QuickFilters.cs b/Source/LibationFileManager/QuickFilters.cs index 82a5c1ae..d8f40181 100644 --- a/Source/LibationFileManager/QuickFilters.cs +++ b/Source/LibationFileManager/QuickFilters.cs @@ -24,14 +24,14 @@ namespace LibationFileManager // load json into memory. if file doesn't exist, nothing to do. save() will create if needed - public static FilterState InMemoryState { get; set; } = null!; + public static FilterState? InMemoryState { get; set; } public static bool UseDefault { - get => InMemoryState.UseDefault; + get => InMemoryState?.UseDefault ?? false; set { - if (UseDefault == value) + if (InMemoryState is null || UseDefault == value) return; lock (locker) @@ -52,7 +52,8 @@ namespace LibationFileManager public string? Name { get; set; } = Name; } - public static IEnumerable Filters => InMemoryState.Filters.AsReadOnly(); + public static IEnumerable Filters + => InMemoryState?.Filters.AsReadOnly() ?? Enumerable.Empty(); public static void Add(NamedFilter namedFilter) { @@ -64,10 +65,11 @@ namespace LibationFileManager namedFilter.Filter = namedFilter.Filter?.Trim() ?? string.Empty; namedFilter.Name = namedFilter.Name?.Trim() ?? null; - lock (locker) - { - // check for duplicates - if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter)) + lock (locker) + { + InMemoryState ??= new(); + // check for duplicates + if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter)) return; InMemoryState.Filters.Add(namedFilter); @@ -79,6 +81,8 @@ namespace LibationFileManager { lock (locker) { + if (InMemoryState is null) + return; InMemoryState.Filters.Remove(filter); save(); } @@ -88,8 +92,7 @@ namespace LibationFileManager { lock (locker) { - var index = InMemoryState.Filters.IndexOf(oldFilter); - if (index < 0) + if (InMemoryState is null || InMemoryState.Filters.IndexOf(oldFilter) < 0) return; InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList(); @@ -107,7 +110,8 @@ namespace LibationFileManager filter.Filter = filter.Filter.Trim(); lock (locker) { - InMemoryState.Filters = new List(filters); + InMemoryState ??= new(); + InMemoryState.Filters = new List(filters); save(); } } diff --git a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs index 5866ae40..85fbb9ea 100644 --- a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs @@ -33,37 +33,25 @@ namespace LibationUiBase.GridView LoadCover(); } - public static async Task> GetAllProductsAsync(IEnumerable libraryBooks) { var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray(); + if (products.Length == 0) + return []; int parallelism = int.Max(1, Environment.ProcessorCount - 1); - (int numPer, int rem) = int.DivRem(products.Length, parallelism); - if (rem != 0) numPer++; + (int batchSize, int rem) = int.DivRem(products.Length, parallelism); + if (rem != 0) batchSize++; - var tasks = new Task[parallelism]; var syncContext = SynchronizationContext.Current; - for (int i = 0; i < parallelism; i++) + //Asynchronously create an ILibraryBookEntry for every book in the library + var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() => { - int start = i * numPer; - tasks[i] = Task.Run(() => - { - SynchronizationContext.SetSynchronizationContext(syncContext); - - int length = int.Min(numPer, products.Length - start); - if (length < 1) return Array.Empty(); - - var result = new IGridEntry[length]; - - for (int j = 0; j < length; j++) - result[j] = new LibraryBookEntry(products[start + j]); - - return result; - }); - } + SynchronizationContext.SetSynchronizationContext(syncContext); + return batch.Select(lb => new LibraryBookEntry(lb) as IGridEntry); + })); return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList(); } diff --git a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs index 0c140946..ba3fef4f 100644 --- a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs @@ -1,6 +1,5 @@ using DataLayer; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -62,53 +61,54 @@ namespace LibationUiBase.GridView var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray(); var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray(); - int parallelism = int.Max(1, Environment.ProcessorCount - 1); - - var tasks = new Task[parallelism]; - var syncContext = SynchronizationContext.Current; - - var q = new BlockingCollection<(int, LibraryBook episode)>(); - var seriesEntries = new ISeriesEntry[seriesBooks.Length]; - var seriesEpisodes = new ConcurrentBag[seriesBooks.Length]; + var seriesEpisodes = new ILibraryBookEntry[seriesBooks.Length][]; - for (int i = 0; i < parallelism; i++) - { - tasks[i] = Task.Run(() => - { - SynchronizationContext.SetSynchronizationContext(syncContext); + var syncContext = SynchronizationContext.Current; + var options = new ParallelOptions { MaxDegreeOfParallelism = int.Max(1, Environment.ProcessorCount - 1) }; - while (q.TryTake(out var entry, -1)) - { - var parent = seriesEntries[entry.Item1]; - var episodeBag = seriesEpisodes[entry.Item1]; - episodeBag.Add(new LibraryBookEntry(entry.episode, parent)); - } - }); - } + //Asynchronously create an ILibraryBookEntry for every episode in the library + await Parallel.ForEachAsync(getAllEpisodes(), options, createEpisodeEntry); - for (int i = 0; i (series, Enumerable.Empty()); - seriesEpisodes[i] = new ConcurrentBag(); - - foreach (var ep in allEpisodes.FindChildren(series)) - q.Add((i, ep)); - } - - q.CompleteAdding(); - - await Task.WhenAll(tasks); - - for (int i = 0; i < seriesBooks.Length; i++) + //Match all episode entries to their corresponding parents + for (int i = seriesEntries.Length - 1; i >= 0; i--) { var series = seriesEntries[i]; - series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder)); + + //Sort episodes by series order descending, then add them to their parent's entry + Array.Sort(seriesEpisodes[i], (a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder)); + series.Children.AddRange(seriesEpisodes[i]); series.UpdateLibraryBook(series.LibraryBook); } - return seriesEntries.Where(s => s.Children.Count != 0).ToList(); + return seriesEntries.Where(s => s.Children.Count != 0).Cast().ToList(); + + //Create a LibraryBookEntry for a single episode + ValueTask createEpisodeEntry((int seriesIndex, int episodeIndex, LibraryBook episode) data, CancellationToken cancellationToken) + { + SynchronizationContext.SetSynchronizationContext(syncContext); + var parent = seriesEntries[data.seriesIndex]; + seriesEpisodes[data.seriesIndex][data.episodeIndex] = new LibraryBookEntry(data.episode, parent); + return ValueTask.CompletedTask; + } + + //Enumeration all series episodes, along with the index to its seriesEntries entry + //and an index to its seriesEpisodes entry + IEnumerable<(int seriesIndex, int episodeIndex, LibraryBook episode)> getAllEpisodes() + { + for (int i = 0; i < seriesBooks.Length; i++) + { + var series = seriesBooks[i]; + var childEpisodes = allEpisodes.FindChildren(series); + + SynchronizationContext.SetSynchronizationContext(syncContext); + seriesEntries[i] = new SeriesEntry(series, []); + seriesEpisodes[i] = new ILibraryBookEntry[childEpisodes.Count]; + + for (int j = 0; j < childEpisodes.Count; j++) + yield return (i, j, childEpisodes[j]); + } + } } public void RemoveChild(ILibraryBookEntry lbe) diff --git a/Source/LibationWinForms/Form1.BackupCounts.cs b/Source/LibationWinForms/Form1.BackupCounts.cs index 941cedf4..8c8ee876 100644 --- a/Source/LibationWinForms/Form1.BackupCounts.cs +++ b/Source/LibationWinForms/Form1.BackupCounts.cs @@ -17,7 +17,9 @@ namespace LibationWinForms beginPdfBackupsToolStripMenuItem.Format(0); LibraryCommands.LibrarySizeChanged += setBackupCounts; - LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; + //Pass null to the runner to get the whole library. + LibraryCommands.BookUserDefinedItemCommitted += (_, _) + => setBackupCounts(null, null); updateCountsBw.DoWork += UpdateCountsBw_DoWork; updateCountsBw.RunWorkerCompleted += exportMenuEnable; @@ -28,12 +30,12 @@ namespace LibationWinForms private bool runBackupCountsAgain; - private void setBackupCounts(object _, object __) + private void setBackupCounts(object _, List libraryBooks) { runBackupCountsAgain = true; if (!updateCountsBw.IsBusy) - updateCountsBw.RunWorkerAsync(); + updateCountsBw.RunWorkerAsync(libraryBooks); } private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) @@ -41,11 +43,7 @@ namespace LibationWinForms while (runBackupCountsAgain) { runBackupCountsAgain = false; - - if (e.Argument is not IEnumerable lbs) - lbs = DbContexts.GetLibrary_Flat_NoTracking(); - - e.Result = LibraryCommands.GetCounts(lbs); + e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable); } } diff --git a/Source/LibationWinForms/Form1.cs b/Source/LibationWinForms/Form1.cs index b7031136..50232c5c 100644 --- a/Source/LibationWinForms/Form1.cs +++ b/Source/LibationWinForms/Form1.cs @@ -52,7 +52,8 @@ namespace LibationWinForms // Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1' { - LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync()); + LibraryCommands.LibrarySizeChanged += (object _, List fullLibrary) + => Invoke(() => productsDisplay.DisplayAsync(fullLibrary)); } Shown += Form1_Shown; } @@ -75,7 +76,7 @@ namespace LibationWinForms public async Task InitLibraryAsync(List libraryBooks) { runBackupCountsAgain = true; - updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent())); + updateCountsBw.RunWorkerAsync(libraryBooks); await productsDisplay.DisplayAsync(libraryBooks); }