Skip to content

Commit 23613ce

Browse files
committed
fix: add debouncing updates to artist metadata scan to prevent lag
1 parent c520e3e commit 23613ce

File tree

4 files changed

+103
-8
lines changed

4 files changed

+103
-8
lines changed

src/Nagi.Core/Services/Abstractions/ILibraryScanner.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Nagi.Core.Services.Abstractions;
99
public interface ILibraryScanner
1010
{
1111
event EventHandler<ArtistMetadataUpdatedEventArgs>? ArtistMetadataUpdated;
12+
event EventHandler<IEnumerable<ArtistMetadataUpdatedEventArgs>>? ArtistMetadataBatchUpdated;
1213
event EventHandler<bool>? ScanCompleted;
1314
event EventHandler<LibraryContentChangedEventArgs>? LibraryContentChanged;
1415

src/Nagi.Core/Services/Implementations/LibraryService.cs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Concurrent;
2+
using System.Diagnostics;
23
using System.Threading.Channels;
34
using Microsoft.EntityFrameworkCore;
45
using Microsoft.Extensions.DependencyInjection;
@@ -114,6 +115,9 @@ public LibraryService(
114115
/// </summary>
115116
public event EventHandler<ArtistMetadataUpdatedEventArgs>? ArtistMetadataUpdated;
116117

118+
/// <inheritdoc />
119+
public event EventHandler<IEnumerable<ArtistMetadataUpdatedEventArgs>>? ArtistMetadataBatchUpdated;
120+
117121
/// <inheritdoc />
118122
public event EventHandler<PlaylistUpdatedEventArgs>? PlaylistUpdated;
119123

@@ -931,7 +935,8 @@ public async Task<bool> RefreshAllFoldersAsync(bool forceFullScan, IProgress<Sca
931935
{
932936
try
933937
{
934-
await FetchAndUpdateArtistFromRemoteAsync(context, artist, cancellationToken).ConfigureAwait(false);
938+
// For single artist details, we want immediate save and events
939+
await FetchAndUpdateArtistFromRemoteAsync(context, artist, cancellationToken, saveChanges: true, suppressEvents: false).ConfigureAwait(false);
935940
}
936941
catch (Exception ex)
937942
{
@@ -990,16 +995,40 @@ public async Task StartArtistMetadataBackgroundFetchAsync()
990995
using var scope = _serviceScopeFactory.CreateScope();
991996
var scopedContextFactory =
992997
scope.ServiceProvider.GetRequiredService<IDbContextFactory<MusicDbContext>>();
998+
999+
// We need a context to fetch and update entities
9931000
await using var batchContext = await scopedContextFactory.CreateDbContextAsync().ConfigureAwait(false);
9941001

9951002
var artistsInBatch = await batchContext.Artists.Where(a => artistIdsToUpdate.Contains(a.Id))
9961003
.ToListAsync(token).ConfigureAwait(false);
1004+
1005+
var pendingUpdates = new List<ArtistMetadataUpdatedEventArgs>();
1006+
var stopwatch = Stopwatch.StartNew();
1007+
9971008
foreach (var artist in artistsInBatch)
9981009
{
9991010
if (token.IsCancellationRequested) break;
10001011
try
10011012
{
1002-
await FetchAndUpdateArtistFromRemoteAsync(batchContext, artist, token).ConfigureAwait(false);
1013+
// Pass saveChanges: false and suppressEvents: true to batch operations
1014+
var (updated, newImagePath) = await FetchAndUpdateArtistFromRemoteAsync(
1015+
batchContext, artist, token, saveChanges: false, suppressEvents: true).ConfigureAwait(false);
1016+
1017+
if (updated)
1018+
{
1019+
pendingUpdates.Add(new ArtistMetadataUpdatedEventArgs(artist.Id, newImagePath));
1020+
}
1021+
1022+
// Hybrid Flush Check: 10 items or 5 seconds
1023+
if (pendingUpdates.Count >= 10 || (pendingUpdates.Count > 0 && stopwatch.Elapsed.TotalSeconds >= 5))
1024+
{
1025+
await batchContext.SaveChangesAsync(token).ConfigureAwait(false);
1026+
_logger.LogInformation("Saved partial batch of {Count} artists metadata (Time: {Elapsed}s).", pendingUpdates.Count, stopwatch.Elapsed.TotalSeconds.ToString("F1"));
1027+
1028+
ArtistMetadataBatchUpdated?.Invoke(this, pendingUpdates.ToList());
1029+
pendingUpdates.Clear();
1030+
stopwatch.Restart();
1031+
}
10031032
}
10041033
catch (DbUpdateConcurrencyException)
10051034
{
@@ -1012,6 +1041,18 @@ public async Task StartArtistMetadataBackgroundFetchAsync()
10121041
_logger.LogError(ex, "Failed to update artist {ArtistId} in background.", artist.Id);
10131042
}
10141043
}
1044+
1045+
if (token.IsCancellationRequested) break;
1046+
1047+
// Final flush for remaining items in the batch
1048+
if (pendingUpdates.Count > 0)
1049+
{
1050+
await batchContext.SaveChangesAsync(token).ConfigureAwait(false);
1051+
_logger.LogInformation("Saved final batch of {Count} artists metadata.", pendingUpdates.Count);
1052+
1053+
// Fire batch event
1054+
ArtistMetadataBatchUpdated?.Invoke(this, pendingUpdates);
1055+
}
10151056
}
10161057
}
10171058
catch (OperationCanceledException)
@@ -4195,7 +4236,15 @@ public async Task<bool> RemoveArtistImageAsync(Guid artistId)
41954236
}
41964237
}
41974238

4198-
private async Task FetchAndUpdateArtistFromRemoteAsync(MusicDbContext context, Artist artist, CancellationToken cancellationToken = default)
4239+
/// <summary>
4240+
/// Fetches metadata from remote sources and updates availability status.
4241+
/// </summary>
4242+
private async Task<(bool Updated, string? NewImagePath)> FetchAndUpdateArtistFromRemoteAsync(
4243+
MusicDbContext context,
4244+
Artist artist,
4245+
CancellationToken cancellationToken = default,
4246+
bool saveChanges = true,
4247+
bool suppressEvents = false)
41994248
{
42004249
var wasMetadataFoundAndUpdated = false;
42014250

@@ -4205,7 +4254,7 @@ private async Task FetchAndUpdateArtistFromRemoteAsync(MusicDbContext context, A
42054254
if (enabledProviders.Count == 0)
42064255
{
42074256
_logger.LogDebug("No metadata providers enabled. Skipping remote fetch for artist '{ArtistName}'.", artist.Name);
4208-
return;
4257+
return (false, artist.LocalImageCachePath);
42094258
}
42104259

42114260
_logger.LogDebug("Using metadata providers for '{ArtistName}': {Providers}",
@@ -4355,11 +4404,17 @@ private async Task FetchAndUpdateArtistFromRemoteAsync(MusicDbContext context, A
43554404
}
43564405

43574406
artist.MetadataLastCheckedUtc = DateTime.UtcNow;
4358-
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
4407+
if (saveChanges)
4408+
{
4409+
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
4410+
}
4411+
4412+
if (!suppressEvents && wasMetadataFoundAndUpdated)
4413+
{
4414+
ArtistMetadataUpdated?.Invoke(this, new ArtistMetadataUpdatedEventArgs(artist.Id, artist.LocalImageCachePath));
4415+
}
43594416

4360-
if (wasMetadataFoundAndUpdated)
4361-
ArtistMetadataUpdated?.Invoke(this,
4362-
new ArtistMetadataUpdatedEventArgs(artist.Id, artist.LocalImageCachePath));
4417+
return (wasMetadataFoundAndUpdated, artist.LocalImageCachePath);
43634418
}
43644419

43654420
/// <summary>

src/Nagi.WinUI/ViewModels/ArtistViewModel.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,33 @@ private void OnArtistMetadataUpdated(object? sender, ArtistMetadataUpdatedEventA
332332
});
333333
}
334334

335+
/// <summary>
336+
/// Handles batched updates to artist metadata.
337+
/// </summary>
338+
private void OnArtistMetadataBatchUpdated(object? sender, IEnumerable<ArtistMetadataUpdatedEventArgs> updates)
339+
{
340+
// Ensure UI updates are performed on the main thread.
341+
_dispatcherService.TryEnqueue(() =>
342+
{
343+
var timestamp = DateTime.UtcNow;
344+
foreach (var update in updates)
345+
{
346+
if (_artistLookup.TryGetValue(update.ArtistId, out var artistVm))
347+
{
348+
// Force unique cache buster using current time to ensure UI updates immediately
349+
artistVm.LocalImageCachePath = ImageUriHelper.GetUriWithCacheBuster(update.NewLocalImageCachePath, timestamp);
350+
}
351+
}
352+
});
353+
}
354+
335355
/// <summary>
336356
/// Subscribes to necessary service events.
337357
/// </summary>
338358
public void SubscribeToEvents()
339359
{
340360
_libraryService.ArtistMetadataUpdated += OnArtistMetadataUpdated;
361+
_libraryService.ArtistMetadataBatchUpdated += OnArtistMetadataBatchUpdated;
341362
}
342363

343364
/// <summary>
@@ -346,6 +367,7 @@ public void SubscribeToEvents()
346367
public void UnsubscribeFromEvents()
347368
{
348369
_libraryService.ArtistMetadataUpdated -= OnArtistMetadataUpdated;
370+
_libraryService.ArtistMetadataBatchUpdated -= OnArtistMetadataBatchUpdated;
349371
}
350372

351373
/// <summary>

src/Nagi.WinUI/ViewModels/ArtistViewViewModel.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ public async Task LoadArtistDetailsAsync(Guid artistId)
123123

124124
// Ensure we don't attach multiple handlers if this method is called again.
125125
_libraryScanner.ArtistMetadataUpdated -= OnArtistMetadataUpdated;
126+
_libraryScanner.ArtistMetadataBatchUpdated -= OnArtistMetadataBatchUpdated;
126127
_libraryScanner.ArtistMetadataUpdated += OnArtistMetadataUpdated;
128+
_libraryScanner.ArtistMetadataBatchUpdated += OnArtistMetadataBatchUpdated;
127129

128130
try
129131
{
@@ -256,6 +258,20 @@ private void OnArtistMetadataUpdated(object? sender, ArtistMetadataUpdatedEventA
256258
}
257259
}
258260

261+
/// <summary>
262+
/// Handles batched updates to artist metadata.
263+
/// </summary>
264+
private void OnArtistMetadataBatchUpdated(object? sender, IEnumerable<ArtistMetadataUpdatedEventArgs> updates)
265+
{
266+
// Check if the current artist is in the batch
267+
var update = updates.FirstOrDefault(u => u.ArtistId == _artistId);
268+
if (update != null)
269+
{
270+
_logger.LogDebug("Received batch metadata update for artist ID {ArtistId}", _artistId);
271+
OnArtistMetadataUpdated(sender, update);
272+
}
273+
}
274+
259275

260276
protected override Task SaveSortOrderAsync(SongSortOrder sortOrder)
261277
{
@@ -273,6 +289,7 @@ public override void ResetState()
273289
base.ResetState();
274290
_logger.LogDebug("Cleaned up ArtistViewViewModel search resources");
275291
_libraryScanner.ArtistMetadataUpdated -= OnArtistMetadataUpdated;
292+
_libraryScanner.ArtistMetadataBatchUpdated -= OnArtistMetadataBatchUpdated;
276293

277294
// Properly unsubscribe using the stored handler reference.
278295
Albums.CollectionChanged -= _albumsChangedHandler;

0 commit comments

Comments
 (0)