Browse Source

rework emby collection scanning (#1205)

* optimize emby collection scan frequency

* add button to sync emby collections

* update changelog

* fix scanning; add progress indicator
pull/1206/head
Jason Dove 2 years ago committed by GitHub
parent
commit
78745de0ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 82
      ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs
  3. 6
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyCollections.cs
  4. 3
      ErsatzTV.Application/Libraries/Queries/GetExternalCollections.cs
  5. 35
      ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs
  6. 1
      ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs
  7. 4
      ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
  8. 1
      ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs
  9. 8
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  10. 28
      ErsatzTV.Infrastructure/Locking/EntityLocker.cs
  11. 4417
      ErsatzTV.Infrastructure/Migrations/20230312015133_Add_EmbyMediaSource_LastCollectionScan.Designer.cs
  12. 29
      ErsatzTV.Infrastructure/Migrations/20230312015133_Add_EmbyMediaSource_LastCollectionScan.cs
  13. 5
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  14. 2
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollections.cs
  15. 63
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollectionsHandler.cs
  16. 16
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  17. 3
      ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs
  18. 24
      ErsatzTV.Scanner/Worker.cs
  19. 72
      ErsatzTV/Pages/Libraries.razor
  20. 32
      ErsatzTV/Services/ScannerService.cs
  21. 11
      ErsatzTV/Services/SchedulerService.cs

3
CHANGELOG.md

@ -21,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

82
ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs

@ -0,0 +1,82 @@ @@ -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);
}
}

6
ErsatzTV.Application/Emby/Commands/SynchronizeEmbyCollections.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest;

3
ErsatzTV.Application/Libraries/Queries/GetExternalCollections.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Libraries;
public record GetExternalCollections : IRequest<List<LibraryViewModel>>;

35
ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs

@ -0,0 +1,35 @@ @@ -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();
}
}

1
ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs

@ -6,4 +6,5 @@ public class EmbyMediaSource : MediaSource @@ -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; }
}

4
ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs

@ -6,6 +6,7 @@ public interface IEntityLocker @@ -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 @@ -18,4 +19,7 @@ public interface IEntityLocker
bool IsTraktLocked();
bool LockTrakt();
bool UnlockTrakt();
bool LockEmbyCollections();
bool UnlockEmbyCollections();
bool AreEmbyCollectionsLocked();
}

1
ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs

@ -83,4 +83,5 @@ public interface IMediaSourceRepository @@ -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);
}

8
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -865,4 +865,12 @@ public class MediaSourceRepository : IMediaSourceRepository @@ -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();
}
}

28
ErsatzTV.Infrastructure/Locking/EntityLocker.cs

@ -9,6 +9,7 @@ public class EntityLocker : IEntityLocker @@ -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 @@ -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 @@ -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;
}

4417
ErsatzTV.Infrastructure/Migrations/20230312015133_Add_EmbyMediaSource_LastCollectionScan.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure/Migrations/20230312015133_Add_EmbyMediaSource_LastCollectionScan.cs

@ -0,0 +1,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");
}
}
}

5
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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 @@ -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
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollections.cs

@ -2,4 +2,4 @@ @@ -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>>;

63
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollectionsHandler.cs

@ -11,32 +11,50 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby @@ -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 @@ -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 @@ -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);
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)
private record ConnectionParameters(EmbyConnection ActiveConnection, EmbyMediaSource MediaSource)
{
public string? ApiKey { get; init; }
}

16
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -88,22 +88,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -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())

3
ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs

@ -30,6 +30,9 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner @@ -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)

24
ErsatzTV.Scanner/Worker.cs

@ -59,6 +59,7 @@ public class Worker : BackgroundService @@ -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 @@ -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 @@ -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 @@ -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;

72
ErsatzTV/Pages/Libraries.razor

@ -83,15 +83,61 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -151,6 +222,7 @@
void IDisposable.Dispose()
{
_locker.OnLibraryChanged -= LockChanged;
_locker.OnEmbyCollectionsChanged -= LockChanged;
_courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress);
_cts.Cancel();

32
ErsatzTV/Services/ScannerService.cs

@ -59,6 +59,9 @@ public class ScannerService : BackgroundService @@ -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 @@ -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();
}
}
}

11
ErsatzTV/Services/SchedulerService.cs

@ -236,8 +236,12 @@ public class SchedulerService : BackgroundService @@ -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 @@ -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)

Loading…
Cancel
Save