86 lines
2.8 KiB
C#
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();
|
|
}
|
|
}
|