Scan queue handler now returns a QueueLibraryScanResult enum (Queued|NotFound|SyncDisabled|AlreadyScanning) instead of a lying bool; LibrariesController.ScanLibrary maps them to 202/404/422/409 with ProblemDetails. Guard the lock->enqueue with the EnqueueWithTraktLock compensating-unlock pattern. ScannerService now releases every library/collection lock in a finally so a handler exception can't leak the lock. Plex "Shows" scheduler batch (one lock, two messages) now has only the trailing SynchronizePlexNetworks carry the single release (Unlock flag), mirroring the scheduler Trakt tail-token precedent. Guard the other lock->enqueue producers (Create/UpdateLocalLibrary, UpdateTraktList) with compensating unlock. SPA drops the PENDING_GRACE_TICKS heuristic now that the POST reports 202/409/404/422 directly: 202 -> pending+poll, 409 -> reconcile (no error toast), 404/422 -> surface error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
4.3 KiB
C#
105 lines
4.3 KiB
C#
using System.Threading.Channels;
|
|
using ErsatzTV.Application.Emby;
|
|
using ErsatzTV.Application.Jellyfin;
|
|
using ErsatzTV.Application.MediaSources;
|
|
using ErsatzTV.Application.Plex;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Interfaces.Locking;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Infrastructure.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ErsatzTV.Application.Libraries;
|
|
|
|
public class QueueLibraryScanByLibraryIdHandler(
|
|
IDbContextFactory<TvContext> dbContextFactory,
|
|
IEntityLocker locker,
|
|
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorker,
|
|
ILogger<QueueLibraryScanByLibraryIdHandler> logger)
|
|
: IRequestHandler<QueueLibraryScanByLibraryId, QueueLibraryScanResult>
|
|
{
|
|
public async Task<QueueLibraryScanResult> Handle(
|
|
QueueLibraryScanByLibraryId request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
|
|
Option<Library> maybeLibrary = await dbContext.Libraries
|
|
.AsNoTracking()
|
|
.SelectOneAsync(l => l.Id, l => l.Id == request.LibraryId, cancellationToken);
|
|
|
|
foreach (Library library in maybeLibrary)
|
|
{
|
|
bool shouldSyncItems = library switch
|
|
{
|
|
PlexLibrary plexLibrary => plexLibrary.ShouldSyncItems,
|
|
JellyfinLibrary jellyfinLibrary => jellyfinLibrary.ShouldSyncItems,
|
|
EmbyLibrary embyLibrary => embyLibrary.ShouldSyncItems,
|
|
_ => true
|
|
};
|
|
|
|
if (!shouldSyncItems)
|
|
{
|
|
logger.LogWarning("Library sync is disabled for library id {Id}", library.Id);
|
|
return QueueLibraryScanResult.SyncDisabled;
|
|
}
|
|
|
|
// A true from LockLibrary confers ownership of exactly one release; a false means a scan
|
|
// is already in progress and we own no release.
|
|
if (!locker.LockLibrary(library.Id))
|
|
{
|
|
return QueueLibraryScanResult.AlreadyScanning;
|
|
}
|
|
|
|
try
|
|
{
|
|
logger.LogDebug("Queued library scan for library id {Id}", library.Id);
|
|
|
|
switch (library)
|
|
{
|
|
case LocalLibrary:
|
|
await scannerWorker.WriteAsync(new ForceScanLocalLibrary(library.Id), cancellationToken);
|
|
break;
|
|
case PlexLibrary:
|
|
await scannerWorker.WriteAsync(
|
|
new SynchronizePlexLibraries(library.MediaSourceId),
|
|
cancellationToken);
|
|
await scannerWorker.WriteAsync(
|
|
new ForceSynchronizePlexLibraryById(library.Id, false),
|
|
cancellationToken);
|
|
break;
|
|
case JellyfinLibrary:
|
|
await scannerWorker.WriteAsync(
|
|
new SynchronizeJellyfinLibraries(library.MediaSourceId),
|
|
cancellationToken);
|
|
await scannerWorker.WriteAsync(
|
|
new ForceSynchronizeJellyfinLibraryById(library.Id, false),
|
|
cancellationToken);
|
|
break;
|
|
case EmbyLibrary:
|
|
await scannerWorker.WriteAsync(
|
|
new SynchronizeEmbyLibraries(library.MediaSourceId),
|
|
cancellationToken);
|
|
await scannerWorker.WriteAsync(
|
|
new ForceSynchronizeEmbyLibraryById(library.Id, false),
|
|
cancellationToken);
|
|
break;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// the scanner only unlocks when it receives the message; if enqueueing fails
|
|
// (e.g. request aborted / channel completed) after we acquired the lock, release
|
|
// it here or it is held forever (EnqueueWithTraktLock pattern).
|
|
locker.UnlockLibrary(library.Id);
|
|
throw;
|
|
}
|
|
|
|
return QueueLibraryScanResult.Queued;
|
|
}
|
|
|
|
return QueueLibraryScanResult.NotFound;
|
|
}
|
|
}
|