Files
PLib/src/PLib.Desktop/Services/RemoteImageLoader.cs
T

86 lines
2.8 KiB
C#

using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PLib.Application.Library;
namespace PLib.Desktop.Services;
/// <summary>
/// Fetches the pictures a metadata source pointed at, behind whatever is already on screen.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed class RemoteImageLoader(
IServiceScopeFactory scopeFactory,
ILogger<RemoteImageLoader> logger) : IDisposable
{
private readonly SemaphoreSlim _slots = new(4);
private readonly Subject<string> _problems = new();
/// <summary>
/// Reasons pictures are not arriving, for whatever page is on screen to show.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IObservable<string> Problems => _problems.AsObservable();
public async Task<string?> 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<ILibraryService>();
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();
}
}