rework emby collection scanning (#1205)
* optimize emby collection scan frequency * add button to sync emby collections * update changelog * fix scanning; add progress indicator
This commit is contained in:
@@ -21,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Playout builds are no longer blocked by library scans
|
||||
- Adding Trakt lists is no longer blocked by library scans
|
||||
- All library scans (local and media servers) run sequentially
|
||||
- Emby collection scanning will no longer happen after every (automatic or forced) library scan
|
||||
- Automatic/periodic scans will check collections one time after all libraries have been scanned
|
||||
- There is a new table in the `Media` > `Libraries` page with a button to manually re-scan Emby collections as needed
|
||||
|
||||
## [0.7.5-beta] - 2023-03-05
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<SynchronizeEmbyCollections>,
|
||||
IRequestHandler<SynchronizeEmbyCollections, Either<BaseError, Unit>>
|
||||
{
|
||||
public CallEmbyCollectionScannerHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||
IMediator mediator,
|
||||
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeEmbyCollections request)
|
||||
{
|
||||
DateTime minDateTime = await dbContext.EmbyMediaSources
|
||||
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyMediaSourceId)
|
||||
.Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||
|
||||
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
protected override bool ScanIsRequired(
|
||||
DateTimeOffset lastScan,
|
||||
int libraryRefreshInterval,
|
||||
SynchronizeEmbyCollections request)
|
||||
{
|
||||
if (lastScan == SystemTime.MaxValueUtc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>>
|
||||
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, string> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
scanner => PerformScan(scanner, request, cancellationToken),
|
||||
error =>
|
||||
{
|
||||
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||
{
|
||||
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
|
||||
}
|
||||
|
||||
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, Unit>> PerformScan(
|
||||
string scanner,
|
||||
SynchronizeEmbyCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"scan-emby-collections", request.EmbyMediaSourceId.ToString()
|
||||
};
|
||||
|
||||
if (request.ForceScan)
|
||||
{
|
||||
arguments.Add("--force");
|
||||
}
|
||||
|
||||
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
|
||||
IScannerBackgroundServiceRequest;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public record GetExternalCollections : IRequest<List<LibraryViewModel>>;
|
||||
@@ -0,0 +1,35 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollections, List<LibraryViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetExternalCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<List<LibraryViewModel>> Handle(
|
||||
GetExternalCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
List<int> mediaSourceIds = await dbContext.EmbyMediaSources
|
||||
.Filter(ems => ems.Libraries.Any(l => ((EmbyLibrary)l).ShouldSyncItems))
|
||||
.Map(ems => ems.Id)
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
|
||||
return mediaSourceIds.Map(
|
||||
id => new LibraryViewModel(
|
||||
"Emby",
|
||||
0,
|
||||
"Collections",
|
||||
0,
|
||||
id))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ public class EmbyMediaSource : MediaSource
|
||||
public string OperatingSystem { get; set; }
|
||||
public List<EmbyConnection> Connections { get; set; }
|
||||
public List<EmbyPathReplacement> PathReplacements { get; set; }
|
||||
public DateTime? LastCollectionsScan { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface IEntityLocker
|
||||
event EventHandler OnPlexChanged;
|
||||
event EventHandler<Type> OnRemoteMediaSourceChanged;
|
||||
event EventHandler OnTraktChanged;
|
||||
event EventHandler OnEmbyCollectionsChanged;
|
||||
bool LockLibrary(int libraryId);
|
||||
bool UnlockLibrary(int libraryId);
|
||||
bool IsLibraryLocked(int libraryId);
|
||||
@@ -18,4 +19,7 @@ public interface IEntityLocker
|
||||
bool IsTraktLocked();
|
||||
bool LockTrakt();
|
||||
bool UnlockTrakt();
|
||||
bool LockEmbyCollections();
|
||||
bool UnlockEmbyCollections();
|
||||
bool AreEmbyCollectionsLocked();
|
||||
}
|
||||
|
||||
@@ -83,4 +83,5 @@ public interface IMediaSourceRepository
|
||||
Task<List<int>> DeleteAllEmby();
|
||||
Task<Unit> EnableEmbyLibrarySync(IEnumerable<int> libraryIds);
|
||||
Task<List<int>> DisableEmbyLibrarySync(List<int> libraryIds);
|
||||
Task<Unit> UpdateLastScan(EmbyMediaSource embyMediaSource);
|
||||
}
|
||||
|
||||
@@ -865,4 +865,12 @@ public class MediaSourceRepository : IMediaSourceRepository
|
||||
|
||||
return deletedMediaIds;
|
||||
}
|
||||
|
||||
public async Task<Unit> UpdateLastScan(EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
"UPDATE EmbyMediaSource SET LastCollectionsScan = @LastCollectionsScan WHERE Id = @Id",
|
||||
new { embyMediaSource.LastCollectionsScan, embyMediaSource.Id }).ToUnit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public class EntityLocker : IEntityLocker
|
||||
private readonly ConcurrentDictionary<Type, byte> _lockedRemoteMediaSourceTypes;
|
||||
private bool _plex;
|
||||
private bool _trakt;
|
||||
private bool _embyCollections;
|
||||
|
||||
public EntityLocker()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class EntityLocker : IEntityLocker
|
||||
public event EventHandler OnPlexChanged;
|
||||
public event EventHandler<Type> OnRemoteMediaSourceChanged;
|
||||
public event EventHandler OnTraktChanged;
|
||||
public event EventHandler OnEmbyCollectionsChanged;
|
||||
|
||||
public bool LockLibrary(int libraryId)
|
||||
{
|
||||
@@ -127,4 +129,30 @@ public class EntityLocker : IEntityLocker
|
||||
}
|
||||
|
||||
public bool IsTraktLocked() => _trakt;
|
||||
|
||||
public bool LockEmbyCollections()
|
||||
{
|
||||
if (!_embyCollections)
|
||||
{
|
||||
_embyCollections = true;
|
||||
OnEmbyCollectionsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool UnlockEmbyCollections()
|
||||
{
|
||||
if (_embyCollections)
|
||||
{
|
||||
_embyCollections = false;
|
||||
OnEmbyCollectionsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool AreEmbyCollectionsLocked() => _embyCollections;
|
||||
}
|
||||
|
||||
ErsatzTV.Infrastructure/Migrations/20230312015133_Add_EmbyMediaSource_LastCollectionScan.Designer.cs
Generated
+4417
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_EmbyMediaSource_LastCollectionScan : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastCollectionsScan",
|
||||
table: "EmbyMediaSource",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastCollectionsScan",
|
||||
table: "EmbyMediaSource");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
@@ -2445,6 +2445,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
|
||||
|
||||
b.Property<DateTime?>("LastCollectionsScan")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Scanner.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyCollections(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>;
|
||||
public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -11,32 +11,50 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IEmbyCollectionScanner _scanner;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public SynchronizeEmbyCollectionsHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEmbyCollectionScanner scanner)
|
||||
IEmbyCollectionScanner scanner,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
_scanner = scanner;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
SynchronizeEmbyCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, ConnectionParameters> validation = await Validate(request);
|
||||
Validation<BaseError, RequestParameters> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
SynchronizeCollections,
|
||||
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyCollections request) =>
|
||||
MediaSourceMustExist(request)
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizeEmbyCollections request)
|
||||
{
|
||||
Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveApiKey);
|
||||
|
||||
return (await mediaSource, await ValidateLibraryRefreshInterval())
|
||||
.Apply(
|
||||
(connectionParameters, libraryRefreshInterval) => new RequestParameters(
|
||||
connectionParameters,
|
||||
connectionParameters.MediaSource,
|
||||
request.ForceScan,
|
||||
libraryRefreshInterval));
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
||||
.FilterT(lri => lri is >= 0 and < 1_000_000)
|
||||
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
|
||||
SynchronizeEmbyCollections request) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||
@@ -46,7 +64,7 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
|
||||
EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(connection))
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(connection, embyMediaSource))
|
||||
.ToValidation<BaseError>("Emby media source requires an active connection");
|
||||
}
|
||||
|
||||
@@ -60,12 +78,37 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, Unit>> SynchronizeCollections(ConnectionParameters connectionParameters) =>
|
||||
await _scanner.ScanCollections(
|
||||
connectionParameters.ActiveConnection.Address,
|
||||
connectionParameters.ApiKey);
|
||||
private async Task<Either<BaseError, Unit>> SynchronizeCollections(RequestParameters parameters)
|
||||
{
|
||||
var lastScan = new DateTimeOffset(
|
||||
parameters.MediaSource.LastCollectionsScan ?? SystemTime.MinValueUtc,
|
||||
TimeSpan.Zero);
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
||||
if (parameters.ForceScan || (parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now))
|
||||
{
|
||||
Either<BaseError, Unit> result = await _scanner.ScanCollections(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey);
|
||||
|
||||
private record ConnectionParameters(EmbyConnection ActiveConnection)
|
||||
if (result.IsRight)
|
||||
{
|
||||
parameters.MediaSource.LastCollectionsScan = DateTime.UtcNow;
|
||||
await _mediaSourceRepository.UpdateLastScan(parameters.MediaSource);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private record RequestParameters(
|
||||
ConnectionParameters ConnectionParameters,
|
||||
EmbyMediaSource MediaSource,
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval);
|
||||
|
||||
private record ConnectionParameters(EmbyConnection ActiveConnection, EmbyMediaSource MediaSource)
|
||||
{
|
||||
public string? ApiKey { get; init; }
|
||||
}
|
||||
|
||||
@@ -88,22 +88,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
|
||||
{
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
|
||||
// need to call get libraries to find library that contains collections (box sets)
|
||||
await _embyApiClient.GetLibraries(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey);
|
||||
|
||||
Either<BaseError, Unit> collectionResult = await _mediator.Send(
|
||||
new SynchronizeEmbyCollections(parameters.Library.MediaSourceId),
|
||||
cancellationToken);
|
||||
|
||||
collectionResult.BiIter(
|
||||
_ => _logger.LogDebug("Done synchronizing emby collections"),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize emby collections for source {MediaSourceId}: {Error}",
|
||||
parameters.Library.MediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
foreach (BaseError error in result.LeftToSeq())
|
||||
|
||||
@@ -30,6 +30,9 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
|
||||
{
|
||||
try
|
||||
{
|
||||
// need to call get libraries to find library that contains collections (box sets)
|
||||
await _embyApiClient.GetLibraries(address, apiKey);
|
||||
|
||||
var incomingItemIds = new List<string>();
|
||||
|
||||
// get all collections from db (item id, etag)
|
||||
|
||||
@@ -59,6 +59,7 @@ public class Worker : BackgroundService
|
||||
};
|
||||
|
||||
var libraryIdArgument = new Argument<int>("library-id", "The library id to scan");
|
||||
var mediaSourceIdArgument = new Argument<int>("media-source-id", "The media source id to scan");
|
||||
|
||||
var scanLocalCommand = new Command("scan-local", "Scan a local library");
|
||||
scanLocalCommand.AddArgument(libraryIdArgument);
|
||||
@@ -74,6 +75,10 @@ public class Worker : BackgroundService
|
||||
scanEmbyCommand.AddOption(forceOption);
|
||||
scanEmbyCommand.AddOption(deepOption);
|
||||
|
||||
var scanEmbyCollectionsCommand = new Command("scan-emby-collections", "Scan Emby collections");
|
||||
scanEmbyCollectionsCommand.AddArgument(mediaSourceIdArgument);
|
||||
scanEmbyCollectionsCommand.AddOption(forceOption);
|
||||
|
||||
var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library");
|
||||
scanJellyfinCommand.AddArgument(libraryIdArgument);
|
||||
scanJellyfinCommand.AddOption(forceOption);
|
||||
@@ -135,6 +140,24 @@ public class Worker : BackgroundService
|
||||
}
|
||||
});
|
||||
|
||||
scanEmbyCollectionsCommand.SetHandler(
|
||||
async context =>
|
||||
{
|
||||
if (IsScanningEnabled())
|
||||
{
|
||||
bool force = context.ParseResult.GetValueForOption(forceOption);
|
||||
SetProcessPriority(force);
|
||||
|
||||
int mediaSourceId = context.ParseResult.GetValueForArgument(mediaSourceIdArgument);
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
var scan = new SynchronizeEmbyCollections(mediaSourceId, force);
|
||||
await mediator.Send(scan, context.GetCancellationToken());
|
||||
}
|
||||
});
|
||||
|
||||
scanJellyfinCommand.SetHandler(
|
||||
async context =>
|
||||
{
|
||||
@@ -158,6 +181,7 @@ public class Worker : BackgroundService
|
||||
rootCommand.AddCommand(scanLocalCommand);
|
||||
rootCommand.AddCommand(scanPlexCommand);
|
||||
rootCommand.AddCommand(scanEmbyCommand);
|
||||
rootCommand.AddCommand(scanEmbyCollectionsCommand);
|
||||
rootCommand.AddCommand(scanJellyfinCommand);
|
||||
|
||||
return rootCommand;
|
||||
|
||||
@@ -83,15 +83,61 @@
|
||||
</MudTable>
|
||||
</MudContainer>
|
||||
|
||||
@if (_externalCollections.Any())
|
||||
{
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudTable Hover="true" Items="_externalCollections" Dense="true">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">External Collections</MudText>
|
||||
</ToolBarContent>
|
||||
<ColGroup>
|
||||
<col/>
|
||||
<col style="width: 180px;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>Library Kind</MudTh>
|
||||
<MudTh/>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Library Kind">@context.LibraryKind</MudTd>
|
||||
<MudTd>
|
||||
<div style="align-items: center; display: flex;">
|
||||
<div style="width: 48px"></div>
|
||||
@if (AreCollectionsLocked(context.LibraryKind))
|
||||
{
|
||||
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="width: 48px"></div>
|
||||
<MudTooltip Text="Scan Collections">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Disabled="@AreCollectionsLocked(context.LibraryKind)"
|
||||
OnClick="@(_ => ScanExternalCollections(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
<div style="width: 48px"></div>
|
||||
</div>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
private IList<LibraryViewModel> _libraries;
|
||||
private IList<LibraryViewModel> _externalCollections;
|
||||
private Dictionary<int, int> _progressByLibrary;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_locker.OnLibraryChanged += LockChanged;
|
||||
_locker.OnEmbyCollectionsChanged += LockChanged;
|
||||
_courier.Subscribe<LibraryScanProgress>(HandleScanProgress);
|
||||
}
|
||||
|
||||
@@ -100,6 +146,7 @@
|
||||
private async Task LoadLibraries(CancellationToken cancellationToken)
|
||||
{
|
||||
_libraries = await _mediator.Send(new GetConfiguredLibraries(), cancellationToken);
|
||||
_externalCollections = await _mediator.Send(new GetExternalCollections(), cancellationToken);
|
||||
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
|
||||
}
|
||||
|
||||
@@ -129,9 +176,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanExternalCollections(LibraryViewModel library)
|
||||
{
|
||||
switch (library.LibraryKind.ToLowerInvariant())
|
||||
{
|
||||
case "emby":
|
||||
if (_locker.LockEmbyCollections())
|
||||
{
|
||||
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyCollections(library.MediaSourceId, true));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void LockChanged(object sender, EventArgs e) =>
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
private bool AreCollectionsLocked(string libraryKind)
|
||||
{
|
||||
switch (libraryKind.ToLowerInvariant())
|
||||
{
|
||||
case "emby":
|
||||
return _locker.AreEmbyCollectionsLocked();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleScanProgress(LibraryScanProgress libraryScanProgress, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -151,6 +222,7 @@
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_locker.OnLibraryChanged -= LockChanged;
|
||||
_locker.OnEmbyCollectionsChanged -= LockChanged;
|
||||
_courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress);
|
||||
|
||||
_cts.Cancel();
|
||||
|
||||
@@ -59,6 +59,9 @@ public class ScannerService : BackgroundService
|
||||
case ISynchronizeEmbyLibraryById synchronizeEmbyLibraryById:
|
||||
requestTask = SynchronizeEmbyLibrary(synchronizeEmbyLibraryById, cancellationToken);
|
||||
break;
|
||||
case SynchronizeEmbyCollections synchronizeEmbyCollections:
|
||||
requestTask = SynchronizeEmbyCollections(synchronizeEmbyCollections, cancellationToken);
|
||||
break;
|
||||
case IScanLocalLibrary scanLocalLibrary:
|
||||
requestTask = SynchronizeLocalLibrary(scanLocalLibrary, cancellationToken);
|
||||
break;
|
||||
@@ -274,5 +277,34 @@ public class ScannerService : BackgroundService
|
||||
entityLocker.UnlockLibrary(request.EmbyLibraryId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeEmbyCollections(
|
||||
SynchronizeEmbyCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
IEntityLocker entityLocker = scope.ServiceProvider.GetRequiredService<IEntityLocker>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogDebug("Done synchronizing emby collections"),
|
||||
error =>
|
||||
{
|
||||
if (error is ScanIsNotRequired)
|
||||
{
|
||||
_logger.LogDebug("Scan is not required for emby collections at this time");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Unable to synchronize emby collections: {Error}", error.Value);
|
||||
}
|
||||
});
|
||||
|
||||
if (entityLocker.AreEmbyCollectionsLocked())
|
||||
{
|
||||
entityLocker.UnlockEmbyCollections();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,8 +236,12 @@ public class SchedulerService : BackgroundService
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
var mediaSourceIds = new System.Collections.Generic.HashSet<int>();
|
||||
|
||||
foreach (EmbyLibrary library in dbContext.EmbyLibraries.Filter(l => l.ShouldSyncItems))
|
||||
{
|
||||
mediaSourceIds.Add(library.MediaSourceId);
|
||||
|
||||
if (_entityLocker.LockLibrary(library.Id))
|
||||
{
|
||||
await _scannerWorkerChannel.WriteAsync(
|
||||
@@ -245,6 +249,13 @@ public class SchedulerService : BackgroundService
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int mediaSourceId in mediaSourceIds)
|
||||
{
|
||||
await _scannerWorkerChannel.WriteAsync(
|
||||
new SynchronizeEmbyCollections(mediaSourceId, false),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MatchTraktLists(CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user