using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PLib.Application.Library; namespace PLib.Desktop.Services; /// /// Fetches the pictures a metadata source pointed at, behind whatever is already on screen. /// /// /// Nothing waits on this. A candidate is shown the moment the source answers, and its cover /// appears when it appears — the alternative, fetching first, put a stranger's picture host on /// the critical path of showing a result, and one that stalled froze the whole run. /// /// Concurrency is capped because a run can produce results faster than pictures download, and /// an unbounded fan-out would open a connection per candidate to the same host. /// public sealed class RemoteImageLoader( IServiceScopeFactory scopeFactory, ILogger logger) : IDisposable { private readonly SemaphoreSlim _slots = new(4); private readonly Subject _problems = new(); /// /// Reasons pictures are not arriving, for whatever page is on screen to show. /// /// /// An empty square tells the user nothing — it looks the same whether the source has no /// picture or the whole host is unreachable. The cache raises one of these only once it /// has given up on a host, so this is a handful of lines, not one per candidate. /// public IObservable Problems => _problems.AsObservable(); public async Task LoadAsync(string? imageUrl, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(imageUrl)) { return null; } try { await _slots.WaitAsync(cancellationToken); try { await using var scope = scopeFactory.CreateAsyncScope(); var library = scope.ServiceProvider.GetRequiredService(); var image = await library.FetchImageAsync(imageUrl, cancellationToken); if (image.Problem is { } problem) { _problems.OnNext(problem); } return image.Path; } finally { _slots.Release(); } } catch (OperationCanceledException) { return null; } catch (Exception ex) { // A missing cover costs a thumbnail. Nothing above this is waiting for an answer, // so there is nobody to report it to but the log. logger.LogDebug(ex, "Could not fetch the picture at {Url}", imageUrl); return null; } } public void Dispose() { _problems.Dispose(); _slots.Dispose(); } }