Browse Source

show database and search index initialization in ui (#1325)

* unblock startup, show database initialization message

* wait on search index to be ready (rebuild)

* clean logging and fake delay
pull/1326/head
Jason Dove 3 years ago committed by GitHub
parent
commit
8277894f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs
  3. 33
      ErsatzTV.Core/SystemStartup.cs
  4. 79
      ErsatzTV/Pages/Index.razor
  5. 11
      ErsatzTV/Services/EmbyService.cs
  6. 20
      ErsatzTV/Services/FFmpegLocatorService.cs
  7. 2
      ErsatzTV/Services/FFmpegWorkerService.cs
  8. 11
      ErsatzTV/Services/JellyfinService.cs
  9. 11
      ErsatzTV/Services/PlexService.cs
  10. 17
      ErsatzTV/Services/RunOnce/CacheCleanerService.cs
  11. 16
      ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs
  12. 10
      ErsatzTV/Services/RunOnce/EndpointValidatorService.cs
  13. 21
      ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs
  14. 11
      ErsatzTV/Services/RunOnce/PlatformSettingsService.cs
  15. 21
      ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs
  16. 8
      ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
  17. 2
      ErsatzTV/Services/ScannerService.cs
  18. 21
      ErsatzTV/Services/SchedulerService.cs
  19. 2
      ErsatzTV/Services/SearchIndexService.cs
  20. 2
      ErsatzTV/Services/WorkerService.cs
  21. 26
      ErsatzTV/Shared/MainLayout.razor
  22. 1
      ErsatzTV/Startup.cs

6
CHANGELOG.md

@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable
### Changed
- Rework startup process to show UI as early as possible
- A minimal UI will indicate when the database and search index are initializing
- The UI will automatically refresh when the initialization processes have completed
## [0.8.0-beta] - 2023-06-23
### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)

5
ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs

@ -14,6 +14,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex> @@ -14,6 +14,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly SystemStartup _systemStartup;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex;
@ -25,6 +26,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex> @@ -25,6 +26,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider,
SystemStartup systemStartup,
ILogger<RebuildSearchIndexHandler> logger)
{
_searchIndex = searchIndex;
@ -33,6 +35,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex> @@ -33,6 +35,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
_fallbackMetadataProvider = fallbackMetadataProvider;
_systemStartup = systemStartup;
}
public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
@ -63,5 +66,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex> @@ -63,5 +66,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
}
_systemStartup.SearchIndexIsReady();
}
}

33
ErsatzTV.Core/SystemStartup.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
namespace ErsatzTV.Core;
public class SystemStartup
{
private readonly SemaphoreSlim _databaseStartup = new(0, 100);
private readonly SemaphoreSlim _searchIndexStartup = new(0, 100);
public event EventHandler OnDatabaseReady;
public event EventHandler OnSearchIndexReady;
public bool IsDatabaseReady { get; private set; }
public bool IsSearchIndexReady { get; private set; }
public async Task WaitForDatabase(CancellationToken cancellationToken) =>
await _databaseStartup.WaitAsync(cancellationToken);
public async Task WaitForSearchIndex(CancellationToken cancellationToken) =>
await _searchIndexStartup.WaitAsync(cancellationToken);
public void DatabaseIsReady()
{
_databaseStartup.Release(100);
IsDatabaseReady = true;
OnDatabaseReady?.Invoke(this, EventArgs.Empty);
}
public void SearchIndexIsReady()
{
_searchIndexStartup.Release(100);
IsSearchIndexReady = true;
OnSearchIndexReady?.Invoke(this, EventArgs.Empty);
}
}

79
ErsatzTV/Pages/Index.razor

@ -5,11 +5,48 @@ @@ -5,11 +5,48 @@
@using ErsatzTV.Core.Interfaces.GitHub
@using ErsatzTV.Application.Health
@implements IDisposable
@inject IGitHubApiClient _gitHubApiClient
@inject IMemoryCache _memoryCache
@inject IMediator _mediator
@inject IGitHubApiClient GitHubApiClient
@inject IMemoryCache MemoryCache
@inject IMediator Mediator
@inject SystemStartup SystemStartup;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@if (!SystemStartup.IsDatabaseReady)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h4">Database is initializing</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText>Please wait, this may take a few minutes.</MudText>
<MudText>This page will automatically refresh when the database is ready.</MudText>
</MudCardContent>
<MudCardActions>
<MudProgressCircular Color="Color.Primary" Size="Size.Medium" Indeterminate="true"/>
</MudCardActions>
</MudCard>
}
else if (!SystemStartup.IsSearchIndexReady)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h4">Search Index is initializing</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText>Please wait, this may take a few minutes.</MudText>
<MudText>This page will automatically refresh when the search index is ready.</MudText>
</MudCardContent>
<MudCardActions>
<MudProgressCircular Color="Color.Primary" Size="Size.Medium" Indeterminate="true"/>
</MudCardActions>
</MudCard>
}
else
{
<MudCard>
<MudCardContent Class="release-notes mud-typography mud-typography-body1">
<MarkdownView Content="@_releaseNotes"/>
@ -68,7 +105,7 @@ @@ -68,7 +105,7 @@
}
</MudTd>
</RowTemplate>
</MudTable>
</MudTable> }
</MudContainer>
@code {
@ -77,17 +114,31 @@ @@ -77,17 +114,31 @@
private string _releaseNotes;
private MudTable<HealthCheckResult> _table;
protected override void OnInitialized()
{
SystemStartup.OnDatabaseReady += OnStartupProgress;
SystemStartup.OnSearchIndexReady += OnStartupProgress;
}
public void Dispose()
{
SystemStartup.OnDatabaseReady -= OnStartupProgress;
SystemStartup.OnSearchIndexReady -= OnStartupProgress;
_cts.Cancel();
_cts.Dispose();
}
private async void OnStartupProgress(object sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
protected override async Task OnParametersSetAsync()
{
try
{
if (_memoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml))
if (MemoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml))
{
_releaseNotes = releaseNotesHtml;
}
@ -109,20 +160,26 @@ @@ -109,20 +160,26 @@
gitHubVersion = $"v{gitHubVersion}";
}
maybeNotes = await _gitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes);
maybeNotes = await GitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token);
foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
}
}
else
{
maybeNotes = await _gitHubApiClient.GetLatestReleaseNotes(_cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes);
maybeNotes = await GitHubApiClient.GetLatestReleaseNotes(_cts.Token);
foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
}
}
}
}
if (_releaseNotes != null)
{
_memoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes);
MemoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes);
}
}
}
@ -134,7 +191,7 @@ @@ -134,7 +191,7 @@
private async Task<TableData<HealthCheckResult>> ServerReload(TableState state)
{
List<HealthCheckResult> healthCheckResults = await _mediator.Send(new GetAllHealthCheckResults(), _cts.Token);
List<HealthCheckResult> healthCheckResults = await Mediator.Send(new GetAllHealthCheckResults(), _cts.Token);
return new TableData<HealthCheckResult>
{

11
ErsatzTV/Services/EmbyService.cs

@ -13,19 +13,30 @@ public class EmbyService : BackgroundService @@ -13,19 +13,30 @@ public class EmbyService : BackgroundService
private readonly ChannelReader<IEmbyBackgroundServiceRequest> _channel;
private readonly ILogger<EmbyService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public EmbyService(
ChannelReader<IEmbyBackgroundServiceRequest> channel,
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<EmbyService> logger)
{
_channel = channel;
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
if (!File.Exists(FileSystemLayout.EmbySecretsPath))

20
ErsatzTV/Services/FFmpegLocatorService.cs

@ -1,23 +1,35 @@ @@ -1,23 +1,35 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Services;
public class FFmpegLocatorService : IHostedService
public class FFmpegLocatorService : BackgroundService
{
private readonly ILogger<FFmpegLocatorService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public FFmpegLocatorService(
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<FFmpegLocatorService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IFFmpegLocator ffmpegLocator = scope.ServiceProvider.GetRequiredService<IFFmpegLocator>();
@ -34,6 +46,4 @@ public class FFmpegLocatorService : IHostedService @@ -34,6 +46,4 @@ public class FFmpegLocatorService : IHostedService
path => _logger.LogInformation("Located ffprobe at {Path}", path),
() => _logger.LogWarning("Failed to locate ffprobe executable"));
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

2
ErsatzTV/Services/FFmpegWorkerService.cs

@ -27,6 +27,8 @@ public class FFmpegWorkerService : BackgroundService @@ -27,6 +27,8 @@ public class FFmpegWorkerService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
try
{
_logger.LogInformation("FFmpeg worker service started");

11
ErsatzTV/Services/JellyfinService.cs

@ -13,19 +13,30 @@ public class JellyfinService : BackgroundService @@ -13,19 +13,30 @@ public class JellyfinService : BackgroundService
private readonly ChannelReader<IJellyfinBackgroundServiceRequest> _channel;
private readonly ILogger<JellyfinService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public JellyfinService(
ChannelReader<IJellyfinBackgroundServiceRequest> channel,
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<JellyfinService> logger)
{
_channel = channel;
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
if (!File.Exists(FileSystemLayout.JellyfinSecretsPath))

11
ErsatzTV/Services/PlexService.cs

@ -13,19 +13,30 @@ public class PlexService : BackgroundService @@ -13,19 +13,30 @@ public class PlexService : BackgroundService
private readonly ChannelReader<IPlexBackgroundServiceRequest> _channel;
private readonly ILogger<PlexService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public PlexService(
ChannelReader<IPlexBackgroundServiceRequest> channel,
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<PlexService> logger)
{
_channel = channel;
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
if (!File.Exists(FileSystemLayout.PlexSecretsPath))

17
ErsatzTV/Services/RunOnce/CacheCleanerService.cs

@ -6,21 +6,32 @@ using Microsoft.EntityFrameworkCore; @@ -6,21 +6,32 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Services.RunOnce;
public class CacheCleanerService : IHostedService
public class CacheCleanerService : BackgroundService
{
private readonly ILogger<CacheCleanerService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public CacheCleanerService(
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<CacheCleanerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
using IServiceScope scope = _serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
@ -57,6 +68,4 @@ public class CacheCleanerService : IHostedService @@ -57,6 +68,4 @@ public class CacheCleanerService : IHostedService
_logger.LogInformation("Done emptying transcode cache folder");
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

16
ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs

@ -1,23 +1,29 @@ @@ -1,23 +1,29 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Services.RunOnce;
public class DatabaseMigratorService : IHostedService
public class DatabaseMigratorService : BackgroundService
{
private readonly ILogger<DatabaseMigratorService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public DatabaseMigratorService(
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<DatabaseMigratorService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
_logger.LogInformation("Applying database migrations");
using IServiceScope scope = _serviceScopeFactory.CreateScope();
@ -25,8 +31,8 @@ public class DatabaseMigratorService : IHostedService @@ -25,8 +31,8 @@ public class DatabaseMigratorService : IHostedService
await dbContext.Database.MigrateAsync(cancellationToken);
await DbInitializer.Initialize(dbContext, cancellationToken);
_systemStartup.DatabaseIsReady();
_logger.LogInformation("Done applying database migrations");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

10
ErsatzTV/Services/RunOnce/EndpointValidatorService.cs

@ -4,7 +4,7 @@ using ErsatzTV.Core; @@ -4,7 +4,7 @@ using ErsatzTV.Core;
namespace ErsatzTV.Services.RunOnce;
public class EndpointValidatorService : IHostedService
public class EndpointValidatorService : BackgroundService
{
private readonly IConfiguration _configuration;
private readonly ILogger<EndpointValidatorService> _logger;
@ -15,8 +15,10 @@ public class EndpointValidatorService : IHostedService @@ -15,8 +15,10 @@ public class EndpointValidatorService : IHostedService
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
string urls = _configuration.GetValue<string>("Kestrel:Endpoints:Http:Url");
if (urls.Split(";").Length > 1)
{
@ -50,9 +52,5 @@ public class EndpointValidatorService : IHostedService @@ -50,9 +52,5 @@ public class EndpointValidatorService : IHostedService
"Server will listen on port {Port} - try UI at {UI}",
Settings.ListenPort,
$"http://localhost:{Settings.ListenPort}{baseUrl}");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

21
ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Serilog.Core;
@ -5,15 +6,27 @@ using Serilog.Events; @@ -5,15 +6,27 @@ using Serilog.Events;
namespace ErsatzTV.Services.RunOnce;
public class LoadLoggingLevelService : IHostedService
public class LoadLoggingLevelService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public LoadLoggingLevelService(IServiceScopeFactory serviceScopeFactory) =>
public LoadLoggingLevelService(IServiceScopeFactory serviceScopeFactory, SystemStartup systemStartup)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
public async Task StartAsync(CancellationToken cancellationToken)
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IConfigElementRepository configElementRepository =
scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
@ -26,6 +39,4 @@ public class LoadLoggingLevelService : IHostedService @@ -26,6 +39,4 @@ public class LoadLoggingLevelService : IHostedService
loggingLevelSwitch.MinimumLevel = logLevel;
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

11
ErsatzTV/Services/RunOnce/PlatformSettingsService.cs

@ -1,12 +1,11 @@ @@ -1,12 +1,11 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Services.RunOnce;
public class PlatformSettingsService : IHostedService
public class PlatformSettingsService : BackgroundService
{
private readonly ILogger<PlatformSettingsService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
@ -19,11 +18,11 @@ public class PlatformSettingsService : IHostedService @@ -19,11 +18,11 @@ public class PlatformSettingsService : IHostedService
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
await Task.Yield();
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService<IRuntimeInfo>();
if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux) &&
Directory.Exists("/dev/dri"))
@ -38,6 +37,4 @@ public class PlatformSettingsService : IHostedService @@ -38,6 +37,4 @@ public class PlatformSettingsService : IHostedService
memoryCache.Set("ffmpeg.render_devices", devices);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

21
ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs

@ -1,21 +1,32 @@ @@ -1,21 +1,32 @@
using ErsatzTV.Application.Search;
using ErsatzTV.Core;
using MediatR;
namespace ErsatzTV.Services.RunOnce;
public class RebuildSearchIndexService : IHostedService
public class RebuildSearchIndexService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public RebuildSearchIndexService(IServiceScopeFactory serviceScopeFactory) =>
public RebuildSearchIndexService(IServiceScopeFactory serviceScopeFactory, SystemStartup systemStartup)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
public async Task StartAsync(CancellationToken cancellationToken)
await _systemStartup.WaitForDatabase(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(new RebuildSearchIndex(), cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

8
ErsatzTV/Services/RunOnce/ResourceExtractorService.cs

@ -3,10 +3,12 @@ using ErsatzTV.Core; @@ -3,10 +3,12 @@ using ErsatzTV.Core;
namespace ErsatzTV.Services.RunOnce;
public class ResourceExtractorService : IHostedService
public class ResourceExtractorService : BackgroundService
{
public async Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
if (!Directory.Exists(FileSystemLayout.ResourcesCacheFolder))
{
Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder);
@ -60,8 +62,6 @@ public class ResourceExtractorService : IHostedService @@ -60,8 +62,6 @@ public class ResourceExtractorService : IHostedService
cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
{
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");

2
ErsatzTV/Services/ScannerService.cs

@ -30,6 +30,8 @@ public class ScannerService : BackgroundService @@ -30,6 +30,8 @@ public class ScannerService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
try
{
_logger.LogInformation("Scanner service started");

21
ErsatzTV/Services/SchedulerService.cs

@ -9,6 +9,7 @@ using ErsatzTV.Application.MediaCollections; @@ -9,6 +9,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaSources;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.Plex;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Scheduling;
@ -20,6 +21,7 @@ namespace ErsatzTV.Services; @@ -20,6 +21,7 @@ namespace ErsatzTV.Services;
public class SchedulerService : BackgroundService
{
private readonly IEntityLocker _entityLocker;
private readonly SystemStartup _systemStartup;
private readonly ILogger<SchedulerService> _logger;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IServiceScopeFactory _serviceScopeFactory;
@ -30,17 +32,31 @@ public class SchedulerService : BackgroundService @@ -30,17 +32,31 @@ public class SchedulerService : BackgroundService
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,
IEntityLocker entityLocker,
SystemStartup systemStartup,
ILogger<SchedulerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_workerChannel = workerChannel;
_scannerWorkerChannel = scannerWorkerChannel;
_entityLocker = entityLocker;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
await _systemStartup.WaitForSearchIndex(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
_logger.LogInformation("Scheduler service started");
DateTime firstRun = DateTime.Now;
// run once immediately at startup
@ -86,6 +102,11 @@ public class SchedulerService : BackgroundService @@ -86,6 +102,11 @@ public class SchedulerService : BackgroundService
}
}
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
_logger.LogInformation("Scheduler service shutting down");
}
}
private async Task DoWork(CancellationToken cancellationToken)
{

2
ErsatzTV/Services/SearchIndexService.cs

@ -24,6 +24,8 @@ public class SearchIndexService : BackgroundService @@ -24,6 +24,8 @@ public class SearchIndexService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
try
{
_logger.LogInformation("Search index worker service started");

2
ErsatzTV/Services/WorkerService.cs

@ -29,6 +29,8 @@ public class WorkerService : BackgroundService @@ -29,6 +29,8 @@ public class WorkerService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
try
{
_logger.LogInformation("Worker service started");

26
ErsatzTV/Shared/MainLayout.razor

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
@implements IDisposable
@inject NavigationManager NavigationManager
@inject IMediator Mediator
@inject SystemStartup SystemStartup
<MudThemeProvider Theme="_ersatzTvTheme"/>
<MudDialogProvider DisableBackdropClick="true"/>
@ -17,6 +18,8 @@ @@ -17,6 +18,8 @@
<img src="images/ersatztv.png" alt="ErsatzTV"/>
</a>
</div>
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady)
{
<div class="search-form">
<EditForm Model="@_dummyModel" OnSubmit="@(_ => PerformSearch())">
<MudTextField T="string"
@ -62,9 +65,13 @@ @@ -62,9 +65,13 @@
</MudPopover>
</EditForm>
</div>
}
<MudSpacer/>
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady)
{
<MudLink Color="Color.Info" Href="iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink>
<MudLink Color="Color.Info" Href="iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink>
}
@* <MudLink Color="Color.Info" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink> *@
<MudTooltip Text="Documentation">
<MudIconButton Icon="@Icons.Material.Filled.Help" Color="Color.Primary" Link="https://ersatztv.org" Target="_blank"/>
@ -83,6 +90,8 @@ @@ -83,6 +90,8 @@
</form>
</AuthorizeView>
</MudAppBar>
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady)
{
<MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
<MudNavMenu>
<MudNavLink Href="channels">Channels</MudNavLink>
@ -122,6 +131,7 @@ @@ -122,6 +131,7 @@
</MudContainer>
</MudNavMenu>
</MudDrawer>
}
<MudMainContent>
@Body
</MudMainContent>
@ -140,8 +150,17 @@ @@ -140,8 +150,17 @@
private bool _isOpen;
private List<SearchTargetViewModel> _searchTargets;
protected override void OnInitialized()
{
SystemStartup.OnDatabaseReady += OnStartupProgress;
SystemStartup.OnSearchIndexReady += OnStartupProgress;
}
public void Dispose()
{
SystemStartup.OnDatabaseReady -= OnStartupProgress;
SystemStartup.OnSearchIndexReady -= OnStartupProgress;
_cts.Cancel();
_cts.Dispose();
}
@ -185,12 +204,17 @@ @@ -185,12 +204,17 @@
}
}
private async void OnStartupProgress(object sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
_query = NavigationManager.Uri.GetSearchQuery();
if (_searchTargets is null)
if (SystemStartup.IsDatabaseReady && _searchTargets is null)
{
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
}

1
ErsatzTV/Startup.cs

@ -521,6 +521,7 @@ public class Startup @@ -521,6 +521,7 @@ public class Startup
services.AddSingleton<ITempFilePool, TempFilePool>();
services.AddSingleton<IHlsPlaylistFilter, HlsPlaylistFilter>();
services.AddSingleton<RecyclableMemoryStreamManager>();
services.AddSingleton<SystemStartup>();
AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services);
AddChannel<IJellyfinBackgroundServiceRequest>(services);

Loading…
Cancel
Save