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 2 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/).
- Only allow a single instance of ErsatzTV to run - Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable - 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 ## [0.8.0-beta] - 2023-06-23
### Added ### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted) - 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>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly SystemStartup _systemStartup;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RebuildSearchIndexHandler> _logger; private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
@ -25,6 +26,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
SystemStartup systemStartup,
ILogger<RebuildSearchIndexHandler> logger) ILogger<RebuildSearchIndexHandler> logger)
{ {
_searchIndex = searchIndex; _searchIndex = searchIndex;
@ -33,6 +35,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_systemStartup = systemStartup;
} }
public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken) public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
@ -63,5 +66,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{ {
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version); _logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
} }
_systemStartup.SearchIndexIsReady();
} }
} }

33
ErsatzTV.Core/SystemStartup.cs

@ -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 @@
@using ErsatzTV.Core.Interfaces.GitHub @using ErsatzTV.Core.Interfaces.GitHub
@using ErsatzTV.Application.Health @using ErsatzTV.Application.Health
@implements IDisposable @implements IDisposable
@inject IGitHubApiClient _gitHubApiClient @inject IGitHubApiClient GitHubApiClient
@inject IMemoryCache _memoryCache @inject IMemoryCache MemoryCache
@inject IMediator _mediator @inject IMediator Mediator
@inject SystemStartup SystemStartup;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <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> <MudCard>
<MudCardContent Class="release-notes mud-typography mud-typography-body1"> <MudCardContent Class="release-notes mud-typography mud-typography-body1">
<MarkdownView Content="@_releaseNotes"/> <MarkdownView Content="@_releaseNotes"/>
@ -68,7 +105,7 @@
} }
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable> }
</MudContainer> </MudContainer>
@code { @code {
@ -77,17 +114,31 @@
private string _releaseNotes; private string _releaseNotes;
private MudTable<HealthCheckResult> _table; private MudTable<HealthCheckResult> _table;
protected override void OnInitialized()
{
SystemStartup.OnDatabaseReady += OnStartupProgress;
SystemStartup.OnSearchIndexReady += OnStartupProgress;
}
public void Dispose() public void Dispose()
{ {
SystemStartup.OnDatabaseReady -= OnStartupProgress;
SystemStartup.OnSearchIndexReady -= OnStartupProgress;
_cts.Cancel(); _cts.Cancel();
_cts.Dispose(); _cts.Dispose();
} }
private async void OnStartupProgress(object sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
if (_memoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml)) if (MemoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml))
{ {
_releaseNotes = releaseNotesHtml; _releaseNotes = releaseNotesHtml;
} }
@ -109,20 +160,26 @@
gitHubVersion = $"v{gitHubVersion}"; gitHubVersion = $"v{gitHubVersion}";
} }
maybeNotes = await _gitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token); maybeNotes = await GitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes); foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
}
} }
else else
{ {
maybeNotes = await _gitHubApiClient.GetLatestReleaseNotes(_cts.Token); maybeNotes = await GitHubApiClient.GetLatestReleaseNotes(_cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes); foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
}
} }
} }
} }
if (_releaseNotes != null) if (_releaseNotes != null)
{ {
_memoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes); MemoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes);
} }
} }
} }
@ -134,7 +191,7 @@
private async Task<TableData<HealthCheckResult>> ServerReload(TableState state) 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> return new TableData<HealthCheckResult>
{ {

11
ErsatzTV/Services/EmbyService.cs

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

20
ErsatzTV/Services/FFmpegLocatorService.cs

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

11
ErsatzTV/Services/JellyfinService.cs

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

11
ErsatzTV/Services/PlexService.cs

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

17
ErsatzTV/Services/RunOnce/CacheCleanerService.cs

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

16
ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs

@ -1,23 +1,29 @@
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Services.RunOnce; namespace ErsatzTV.Services.RunOnce;
public class DatabaseMigratorService : IHostedService public class DatabaseMigratorService : BackgroundService
{ {
private readonly ILogger<DatabaseMigratorService> _logger; private readonly ILogger<DatabaseMigratorService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public DatabaseMigratorService( public DatabaseMigratorService(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<DatabaseMigratorService> logger) ILogger<DatabaseMigratorService> logger)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger; _logger = logger;
} }
public async Task StartAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
await Task.Yield();
_logger.LogInformation("Applying database migrations"); _logger.LogInformation("Applying database migrations");
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
@ -25,8 +31,8 @@ public class DatabaseMigratorService : IHostedService
await dbContext.Database.MigrateAsync(cancellationToken); await dbContext.Database.MigrateAsync(cancellationToken);
await DbInitializer.Initialize(dbContext, cancellationToken); await DbInitializer.Initialize(dbContext, cancellationToken);
_systemStartup.DatabaseIsReady();
_logger.LogInformation("Done applying database migrations"); _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;
namespace ErsatzTV.Services.RunOnce; namespace ErsatzTV.Services.RunOnce;
public class EndpointValidatorService : IHostedService public class EndpointValidatorService : BackgroundService
{ {
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly ILogger<EndpointValidatorService> _logger; private readonly ILogger<EndpointValidatorService> _logger;
@ -15,8 +15,10 @@ public class EndpointValidatorService : IHostedService
_logger = logger; _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"); string urls = _configuration.GetValue<string>("Kestrel:Endpoints:Http:Url");
if (urls.Split(";").Length > 1) if (urls.Split(";").Length > 1)
{ {
@ -50,9 +52,5 @@ public class EndpointValidatorService : IHostedService
"Server will listen on port {Port} - try UI at {UI}", "Server will listen on port {Port} - try UI at {UI}",
Settings.ListenPort, Settings.ListenPort,
$"http://localhost:{Settings.ListenPort}{baseUrl}"); $"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 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using Serilog.Core; using Serilog.Core;
@ -5,15 +6,27 @@ using Serilog.Events;
namespace ErsatzTV.Services.RunOnce; namespace ErsatzTV.Services.RunOnce;
public class LoadLoggingLevelService : IHostedService public class LoadLoggingLevelService : BackgroundService
{ {
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public LoadLoggingLevelService(IServiceScopeFactory serviceScopeFactory) => public LoadLoggingLevelService(IServiceScopeFactory serviceScopeFactory, SystemStartup systemStartup)
{
_serviceScopeFactory = serviceScopeFactory; _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(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IConfigElementRepository configElementRepository = IConfigElementRepository configElementRepository =
scope.ServiceProvider.GetRequiredService<IConfigElementRepository>(); scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
@ -26,6 +39,4 @@ public class LoadLoggingLevelService : IHostedService
loggingLevelSwitch.MinimumLevel = logLevel; loggingLevelSwitch.MinimumLevel = logLevel;
} }
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }

11
ErsatzTV/Services/RunOnce/PlatformSettingsService.cs

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

21
ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs

@ -1,21 +1,32 @@
using ErsatzTV.Application.Search; using ErsatzTV.Application.Search;
using ErsatzTV.Core;
using MediatR; using MediatR;
namespace ErsatzTV.Services.RunOnce; namespace ErsatzTV.Services.RunOnce;
public class RebuildSearchIndexService : IHostedService public class RebuildSearchIndexService : BackgroundService
{ {
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public RebuildSearchIndexService(IServiceScopeFactory serviceScopeFactory) => public RebuildSearchIndexService(IServiceScopeFactory serviceScopeFactory, SystemStartup systemStartup)
{
_serviceScopeFactory = serviceScopeFactory; _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(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(new RebuildSearchIndex(), cancellationToken); 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;
namespace ErsatzTV.Services.RunOnce; 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)) if (!Directory.Exists(FileSystemLayout.ResourcesCacheFolder))
{ {
Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder); Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder);
@ -60,8 +62,6 @@ public class ResourceExtractorService : IHostedService
cancellationToken); cancellationToken);
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken) private static async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
{ {
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}"); await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");

2
ErsatzTV/Services/ScannerService.cs

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

21
ErsatzTV/Services/SchedulerService.cs

@ -9,6 +9,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaSources; using ErsatzTV.Application.MediaSources;
using ErsatzTV.Application.Playouts; using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.Plex; using ErsatzTV.Application.Plex;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Scheduling;
@ -20,6 +21,7 @@ namespace ErsatzTV.Services;
public class SchedulerService : BackgroundService public class SchedulerService : BackgroundService
{ {
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly SystemStartup _systemStartup;
private readonly ILogger<SchedulerService> _logger; private readonly ILogger<SchedulerService> _logger;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel; private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
@ -30,17 +32,31 @@ public class SchedulerService : BackgroundService
ChannelWriter<IBackgroundServiceRequest> workerChannel, ChannelWriter<IBackgroundServiceRequest> workerChannel,
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel, ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,
IEntityLocker entityLocker, IEntityLocker entityLocker,
SystemStartup systemStartup,
ILogger<SchedulerService> logger) ILogger<SchedulerService> logger)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_workerChannel = workerChannel; _workerChannel = workerChannel;
_scannerWorkerChannel = scannerWorkerChannel; _scannerWorkerChannel = scannerWorkerChannel;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_systemStartup = systemStartup;
_logger = logger; _logger = logger;
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) 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; DateTime firstRun = DateTime.Now;
// run once immediately at startup // run once immediately at startup
@ -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) private async Task DoWork(CancellationToken cancellationToken)
{ {

2
ErsatzTV/Services/SearchIndexService.cs

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

2
ErsatzTV/Services/WorkerService.cs

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

26
ErsatzTV/Shared/MainLayout.razor

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

1
ErsatzTV/Startup.cs

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

Loading…
Cancel
Save