refactor namespaces and imports (#670)
* re-namespace * optimize usings * more usings * more of the same * more implicit/global usings * cleanup all usings * minor fixes
This commit is contained in:
+129
-141
@@ -1,159 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Emby.Commands;
|
||||
using ErsatzTV.Application.Emby;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class EmbyService : BackgroundService
|
||||
{
|
||||
public class EmbyService : BackgroundService
|
||||
private readonly ChannelReader<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<EmbyService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public EmbyService(
|
||||
ChannelReader<IEmbyBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<EmbyService> logger)
|
||||
{
|
||||
private readonly ChannelReader<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<EmbyService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public EmbyService(
|
||||
ChannelReader<IEmbyBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<EmbyService> logger)
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.EmbySecretsPath))
|
||||
{
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
await File.WriteAllTextAsync(FileSystemLayout.EmbySecretsPath, "{}", cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
_logger.LogInformation(
|
||||
"Emby service started; secrets are at {EmbySecretsPath}",
|
||||
FileSystemLayout.EmbySecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizeEmbyMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IEmbyBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.EmbySecretsPath))
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(FileSystemLayout.EmbySecretsPath, "{}", cancellationToken);
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case SynchronizeEmbyMediaSources synchronizeEmbyMediaSources:
|
||||
requestTask = SynchronizeSources(synchronizeEmbyMediaSources, cancellationToken);
|
||||
break;
|
||||
// case SynchronizeEmbyAdminUserId synchronizeEmbyAdminUserId:
|
||||
// requestTask = SynchronizeAdminUserId(synchronizeEmbyAdminUserId, cancellationToken);
|
||||
// break;
|
||||
case SynchronizeEmbyLibraries synchronizeEmbyLibraries:
|
||||
requestTask = SynchronizeLibraries(synchronizeEmbyLibraries, cancellationToken);
|
||||
break;
|
||||
case ISynchronizeEmbyLibraryById synchronizeEmbyLibraryById:
|
||||
requestTask = SynchronizeEmbyLibrary(synchronizeEmbyLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Emby service started; secrets are at {EmbySecretsPath}",
|
||||
FileSystemLayout.EmbySecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizeEmbyMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IEmbyBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case SynchronizeEmbyMediaSources synchronizeEmbyMediaSources:
|
||||
requestTask = SynchronizeSources(synchronizeEmbyMediaSources, cancellationToken);
|
||||
break;
|
||||
// case SynchronizeEmbyAdminUserId synchronizeEmbyAdminUserId:
|
||||
// requestTask = SynchronizeAdminUserId(synchronizeEmbyAdminUserId, cancellationToken);
|
||||
// break;
|
||||
case SynchronizeEmbyLibraries synchronizeEmbyLibraries:
|
||||
requestTask = SynchronizeLibraries(synchronizeEmbyLibraries, cancellationToken);
|
||||
break;
|
||||
case ISynchronizeEmbyLibraryById synchronizeEmbyLibraryById:
|
||||
requestTask = SynchronizeEmbyLibrary(synchronizeEmbyLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process Emby background service request");
|
||||
}
|
||||
_logger.LogWarning(ex, "Failed to process Emby background service request");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeSources(
|
||||
SynchronizeEmbyMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<EmbyMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized emby media sources");
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize emby media sources: {Error}",
|
||||
error.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(
|
||||
SynchronizeEmbyLibraries request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Emby libraries for source {MediaSourceId}",
|
||||
request.EmbyMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Emby libraries for source {MediaSourceId}: {Error}",
|
||||
request.EmbyMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
// private async Task SynchronizeAdminUserId(
|
||||
// SynchronizeEmbyAdminUserId request,
|
||||
// CancellationToken cancellationToken)
|
||||
// {
|
||||
// using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
// IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
//
|
||||
// Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
// result.BiIter(
|
||||
// _ => _logger.LogInformation(
|
||||
// "Successfully synchronized Emby admin user id for source {MediaSourceId}",
|
||||
// request.EmbyMediaSourceId),
|
||||
// error => _logger.LogWarning(
|
||||
// "Unable to synchronize Emby admin user id for source {MediaSourceId}: {Error}",
|
||||
// request.EmbyMediaSourceId,
|
||||
// error.Value));
|
||||
// }
|
||||
|
||||
private async Task SynchronizeEmbyLibrary(
|
||||
ISynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing emby library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize emby library {LibraryId}: {Error}",
|
||||
request.EmbyLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeSources(
|
||||
SynchronizeEmbyMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<EmbyMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized emby media sources");
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize emby media sources: {Error}",
|
||||
error.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(
|
||||
SynchronizeEmbyLibraries request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Emby libraries for source {MediaSourceId}",
|
||||
request.EmbyMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Emby libraries for source {MediaSourceId}: {Error}",
|
||||
request.EmbyMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
// private async Task SynchronizeAdminUserId(
|
||||
// SynchronizeEmbyAdminUserId request,
|
||||
// CancellationToken cancellationToken)
|
||||
// {
|
||||
// using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
// IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
//
|
||||
// Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
// result.BiIter(
|
||||
// _ => _logger.LogInformation(
|
||||
// "Successfully synchronized Emby admin user id for source {MediaSourceId}",
|
||||
// request.EmbyMediaSourceId),
|
||||
// error => _logger.LogWarning(
|
||||
// "Unable to synchronize Emby admin user id for source {MediaSourceId}: {Error}",
|
||||
// request.EmbyMediaSourceId,
|
||||
// error.Value));
|
||||
// }
|
||||
|
||||
private async Task SynchronizeEmbyLibrary(
|
||||
ISynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing emby library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize emby library {LibraryId}: {Error}",
|
||||
request.EmbyLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,39 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class FFmpegLocatorService : IHostedService
|
||||
{
|
||||
public class FFmpegLocatorService : IHostedService
|
||||
private readonly ILogger<FFmpegLocatorService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public FFmpegLocatorService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<FFmpegLocatorService> logger)
|
||||
{
|
||||
private readonly ILogger<FFmpegLocatorService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public FFmpegLocatorService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<FFmpegLocatorService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IFFmpegLocator ffmpegLocator = scope.ServiceProvider.GetRequiredService<IFFmpegLocator>();
|
||||
|
||||
// check for ffmpeg and ffprobe in the last known/configured location
|
||||
// otherwise search using which/where and save any located executables
|
||||
Option<string> maybeFFmpegPath = await ffmpegLocator.ValidatePath("ffmpeg", ConfigElementKey.FFmpegPath);
|
||||
maybeFFmpegPath.Match(
|
||||
path => _logger.LogInformation("Located ffmpeg at {Path}", path),
|
||||
() => _logger.LogWarning("Failed to locate ffmpeg executable"));
|
||||
|
||||
Option<string> maybeFFprobePath =
|
||||
await ffmpegLocator.ValidatePath("ffprobe", ConfigElementKey.FFprobePath);
|
||||
maybeFFprobePath.Match(
|
||||
path => _logger.LogInformation("Located ffprobe at {Path}", path),
|
||||
() => _logger.LogWarning("Failed to locate ffprobe executable"));
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IFFmpegLocator ffmpegLocator = scope.ServiceProvider.GetRequiredService<IFFmpegLocator>();
|
||||
|
||||
// check for ffmpeg and ffprobe in the last known/configured location
|
||||
// otherwise search using which/where and save any located executables
|
||||
Option<string> maybeFFmpegPath = await ffmpegLocator.ValidatePath("ffmpeg", ConfigElementKey.FFmpegPath);
|
||||
maybeFFmpegPath.Match(
|
||||
path => _logger.LogInformation("Located ffmpeg at {Path}", path),
|
||||
() => _logger.LogWarning("Failed to locate ffmpeg executable"));
|
||||
|
||||
Option<string> maybeFFprobePath =
|
||||
await ffmpegLocator.ValidatePath("ffprobe", ConfigElementKey.FFprobePath);
|
||||
maybeFFprobePath.Match(
|
||||
path => _logger.LogInformation("Located ffprobe at {Path}", path),
|
||||
() => _logger.LogWarning("Failed to locate ffprobe executable"));
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,64 +1,55 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Streaming.Commands;
|
||||
using ErsatzTV.Application.Streaming;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class FFmpegWorkerService : BackgroundService
|
||||
{
|
||||
public class FFmpegWorkerService : BackgroundService
|
||||
private readonly ChannelReader<IFFmpegWorkerRequest> _channel;
|
||||
private readonly ILogger<FFmpegWorkerService> _logger;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public FFmpegWorkerService(
|
||||
ChannelReader<IFFmpegWorkerRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<FFmpegWorkerService> logger,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService)
|
||||
{
|
||||
private readonly ChannelReader<IFFmpegWorkerRequest> _channel;
|
||||
private readonly ILogger<FFmpegWorkerService> _logger;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
}
|
||||
|
||||
public FFmpegWorkerService(
|
||||
ChannelReader<IFFmpegWorkerRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<FFmpegWorkerService> logger,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService)
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("FFmpeg worker service started");
|
||||
|
||||
await foreach (IFFmpegWorkerRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("FFmpeg worker service started");
|
||||
|
||||
await foreach (IFFmpegWorkerRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
// IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
// IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
switch (request)
|
||||
{
|
||||
case TouchFFmpegSession touchFFmpegSession:
|
||||
foreach (DirectoryInfo parent in Optional(Directory.GetParent(touchFFmpegSession.Path)))
|
||||
{
|
||||
_ffmpegSegmenterService.TouchChannel(parent.Name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
switch (request)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to handle ffmpeg worker request");
|
||||
case TouchFFmpegSession touchFFmpegSession:
|
||||
foreach (DirectoryInfo parent in Optional(Directory.GetParent(touchFFmpegSession.Path)))
|
||||
{
|
||||
_ffmpegSegmenterService.TouchChannel(parent.Name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to handle ffmpeg worker request");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Jellyfin.Commands;
|
||||
using ErsatzTV.Application.Jellyfin;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class JellyfinService : BackgroundService
|
||||
{
|
||||
public class JellyfinService : BackgroundService
|
||||
private readonly ChannelReader<IJellyfinBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<JellyfinService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public JellyfinService(
|
||||
ChannelReader<IJellyfinBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<JellyfinService> logger)
|
||||
{
|
||||
private readonly ChannelReader<IJellyfinBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<JellyfinService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public JellyfinService(
|
||||
ChannelReader<IJellyfinBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<JellyfinService> logger)
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.JellyfinSecretsPath))
|
||||
{
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
await File.WriteAllTextAsync(FileSystemLayout.JellyfinSecretsPath, "{}", cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
_logger.LogInformation(
|
||||
"Jellyfin service started; secrets are at {JellyfinSecretsPath}",
|
||||
FileSystemLayout.JellyfinSecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizeJellyfinMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IJellyfinBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.JellyfinSecretsPath))
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(FileSystemLayout.JellyfinSecretsPath, "{}", cancellationToken);
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case SynchronizeJellyfinMediaSources synchronizeJellyfinMediaSources:
|
||||
requestTask = SynchronizeSources(synchronizeJellyfinMediaSources, cancellationToken);
|
||||
break;
|
||||
case SynchronizeJellyfinAdminUserId synchronizeJellyfinAdminUserId:
|
||||
requestTask = SynchronizeAdminUserId(synchronizeJellyfinAdminUserId, cancellationToken);
|
||||
break;
|
||||
case SynchronizeJellyfinLibraries synchronizeJellyfinLibraries:
|
||||
requestTask = SynchronizeLibraries(synchronizeJellyfinLibraries, cancellationToken);
|
||||
break;
|
||||
case ISynchronizeJellyfinLibraryById synchronizeJellyfinLibraryById:
|
||||
requestTask = SynchronizeJellyfinLibrary(synchronizeJellyfinLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Jellyfin service started; secrets are at {JellyfinSecretsPath}",
|
||||
FileSystemLayout.JellyfinSecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizeJellyfinMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IJellyfinBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case SynchronizeJellyfinMediaSources synchronizeJellyfinMediaSources:
|
||||
requestTask = SynchronizeSources(synchronizeJellyfinMediaSources, cancellationToken);
|
||||
break;
|
||||
case SynchronizeJellyfinAdminUserId synchronizeJellyfinAdminUserId:
|
||||
requestTask = SynchronizeAdminUserId(synchronizeJellyfinAdminUserId, cancellationToken);
|
||||
break;
|
||||
case SynchronizeJellyfinLibraries synchronizeJellyfinLibraries:
|
||||
requestTask = SynchronizeLibraries(synchronizeJellyfinLibraries, cancellationToken);
|
||||
break;
|
||||
case ISynchronizeJellyfinLibraryById synchronizeJellyfinLibraryById:
|
||||
requestTask = SynchronizeJellyfinLibrary(synchronizeJellyfinLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process Jellyfin background service request");
|
||||
}
|
||||
_logger.LogWarning(ex, "Failed to process Jellyfin background service request");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeSources(
|
||||
SynchronizeJellyfinMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<JellyfinMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized jellyfin media sources");
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize jellyfin media sources: {Error}",
|
||||
error.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(
|
||||
SynchronizeJellyfinLibraries request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Jellyfin libraries for source {MediaSourceId}",
|
||||
request.JellyfinMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Jellyfin libraries for source {MediaSourceId}: {Error}",
|
||||
request.JellyfinMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeAdminUserId(
|
||||
SynchronizeJellyfinAdminUserId request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Jellyfin admin user id for source {MediaSourceId}",
|
||||
request.JellyfinMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Jellyfin admin user id for source {MediaSourceId}: {Error}",
|
||||
request.JellyfinMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeJellyfinLibrary(
|
||||
ISynchronizeJellyfinLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing jellyfin library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize jellyfin library {LibraryId}: {Error}",
|
||||
request.JellyfinLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeSources(
|
||||
SynchronizeJellyfinMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<JellyfinMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized jellyfin media sources");
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize jellyfin media sources: {Error}",
|
||||
error.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(
|
||||
SynchronizeJellyfinLibraries request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Jellyfin libraries for source {MediaSourceId}",
|
||||
request.JellyfinMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Jellyfin libraries for source {MediaSourceId}: {Error}",
|
||||
request.JellyfinMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeAdminUserId(
|
||||
SynchronizeJellyfinAdminUserId request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized Jellyfin admin user id for source {MediaSourceId}",
|
||||
request.JellyfinMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize Jellyfin admin user id for source {MediaSourceId}: {Error}",
|
||||
request.JellyfinMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeJellyfinLibrary(
|
||||
ISynchronizeJellyfinLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing jellyfin library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize jellyfin library {LibraryId}: {Error}",
|
||||
request.JellyfinLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
+126
-138
@@ -1,156 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Plex.Commands;
|
||||
using ErsatzTV.Application.Plex;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class PlexService : BackgroundService
|
||||
{
|
||||
public class PlexService : BackgroundService
|
||||
private readonly ChannelReader<IPlexBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<PlexService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public PlexService(
|
||||
ChannelReader<IPlexBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<PlexService> logger)
|
||||
{
|
||||
private readonly ChannelReader<IPlexBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<PlexService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public PlexService(
|
||||
ChannelReader<IPlexBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<PlexService> logger)
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.PlexSecretsPath))
|
||||
{
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
await File.WriteAllTextAsync(FileSystemLayout.PlexSecretsPath, "{}", cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
_logger.LogInformation(
|
||||
"Plex service started; secrets are at {PlexSecretsPath}",
|
||||
FileSystemLayout.PlexSecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizePlexMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IPlexBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
if (!File.Exists(FileSystemLayout.PlexSecretsPath))
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(FileSystemLayout.PlexSecretsPath, "{}", cancellationToken);
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case TryCompletePlexPinFlow pinRequest:
|
||||
requestTask = CompletePinFlow(pinRequest, cancellationToken);
|
||||
break;
|
||||
case SynchronizePlexMediaSources sourcesRequest:
|
||||
requestTask = SynchronizeSources(sourcesRequest, cancellationToken);
|
||||
break;
|
||||
case SynchronizePlexLibraries synchronizePlexLibrariesRequest:
|
||||
requestTask = SynchronizeLibraries(synchronizePlexLibrariesRequest, cancellationToken);
|
||||
break;
|
||||
case ISynchronizePlexLibraryById synchronizePlexLibraryById:
|
||||
requestTask = SynchronizePlexLibrary(synchronizePlexLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Plex service started; secrets are at {PlexSecretsPath}",
|
||||
FileSystemLayout.PlexSecretsPath);
|
||||
|
||||
// synchronize sources on startup
|
||||
await SynchronizeSources(new SynchronizePlexMediaSources(), cancellationToken);
|
||||
|
||||
await foreach (IPlexBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task requestTask;
|
||||
switch (request)
|
||||
{
|
||||
case TryCompletePlexPinFlow pinRequest:
|
||||
requestTask = CompletePinFlow(pinRequest, cancellationToken);
|
||||
break;
|
||||
case SynchronizePlexMediaSources sourcesRequest:
|
||||
requestTask = SynchronizeSources(sourcesRequest, cancellationToken);
|
||||
break;
|
||||
case SynchronizePlexLibraries synchronizePlexLibrariesRequest:
|
||||
requestTask = SynchronizeLibraries(synchronizePlexLibrariesRequest, cancellationToken);
|
||||
break;
|
||||
case ISynchronizePlexLibraryById synchronizePlexLibraryById:
|
||||
requestTask = SynchronizePlexLibrary(synchronizePlexLibraryById, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
|
||||
}
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process plex background service request");
|
||||
}
|
||||
_logger.LogWarning(ex, "Failed to process plex background service request");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<PlexMediaSource>> SynchronizeSources(
|
||||
SynchronizePlexMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<PlexMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
return result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized plex media sources");
|
||||
}
|
||||
|
||||
return sources;
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize plex media sources: {Error}",
|
||||
error.Value);
|
||||
return new List<PlexMediaSource>();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CompletePinFlow(
|
||||
TryCompletePlexPinFlow request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, bool> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
success => _logger.LogInformation(
|
||||
success ? "Successfully authenticated with plex" : "Plex authentication timeout"),
|
||||
error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(SynchronizePlexLibraries request, CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized plex libraries for source {MediaSourceId}",
|
||||
request.PlexMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize plex libraries for source {MediaSourceId}: {Error}",
|
||||
request.PlexMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizePlexLibrary(
|
||||
ISynchronizePlexLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing plex library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize plex library {LibraryId}: {Error}",
|
||||
request.PlexLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<PlexMediaSource>> SynchronizeSources(
|
||||
SynchronizePlexMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, List<PlexMediaSource>> result = await mediator.Send(request, cancellationToken);
|
||||
return result.Match(
|
||||
sources =>
|
||||
{
|
||||
if (sources.Any())
|
||||
{
|
||||
_logger.LogInformation("Successfully synchronized plex media sources");
|
||||
}
|
||||
|
||||
return sources;
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize plex media sources: {Error}",
|
||||
error.Value);
|
||||
return new List<PlexMediaSource>();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CompletePinFlow(
|
||||
TryCompletePlexPinFlow request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, bool> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
success => _logger.LogInformation(
|
||||
success ? "Successfully authenticated with plex" : "Plex authentication timeout"),
|
||||
error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizeLibraries(SynchronizePlexLibraries request, CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
_ => _logger.LogInformation(
|
||||
"Successfully synchronized plex libraries for source {MediaSourceId}",
|
||||
request.PlexMediaSourceId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize plex libraries for source {MediaSourceId}: {Error}",
|
||||
request.PlexMediaSourceId,
|
||||
error.Value));
|
||||
}
|
||||
|
||||
private async Task SynchronizePlexLibrary(
|
||||
ISynchronizePlexLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||
result.BiIter(
|
||||
name => _logger.LogDebug("Done synchronizing plex library {Name}", name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to synchronize plex library {LibraryId}: {Error}",
|
||||
request.PlexLibraryId,
|
||||
error.Value));
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
|
||||
public class CacheCleanerService : IHostedService
|
||||
{
|
||||
public class CacheCleanerService : IHostedService
|
||||
private readonly ILogger<CacheCleanerService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public CacheCleanerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<CacheCleanerService> logger)
|
||||
{
|
||||
private readonly ILogger<CacheCleanerService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public CacheCleanerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<CacheCleanerService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
if (Directory.Exists(FileSystemLayout.LegacyImageCacheFolder))
|
||||
{
|
||||
_logger.LogInformation("Migrating channel logos from legacy image cache folder");
|
||||
|
||||
List<string> logos = await dbContext.Channels
|
||||
.SelectMany(c => c.Artwork)
|
||||
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||
.Map(a => a.Path)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
|
||||
foreach (string logo in logos)
|
||||
{
|
||||
string legacyPath = Path.Combine(FileSystemLayout.LegacyImageCacheFolder, logo);
|
||||
if (File.Exists(legacyPath))
|
||||
{
|
||||
string subfolder = logo.Substring(0, 2);
|
||||
string newPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, logo);
|
||||
await localFileSystem.CopyFile(legacyPath, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting legacy image cache folder");
|
||||
Directory.Delete(FileSystemLayout.LegacyImageCacheFolder, true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
if (Directory.Exists(FileSystemLayout.LegacyImageCacheFolder))
|
||||
{
|
||||
_logger.LogInformation("Migrating channel logos from legacy image cache folder");
|
||||
|
||||
List<string> logos = await dbContext.Channels
|
||||
.SelectMany(c => c.Artwork)
|
||||
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||
.Map(a => a.Path)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
|
||||
foreach (string logo in logos)
|
||||
{
|
||||
string legacyPath = Path.Combine(FileSystemLayout.LegacyImageCacheFolder, logo);
|
||||
if (File.Exists(legacyPath))
|
||||
{
|
||||
string subfolder = logo.Substring(0, 2);
|
||||
string newPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, logo);
|
||||
await localFileSystem.CopyFile(legacyPath, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting legacy image cache folder");
|
||||
Directory.Delete(FileSystemLayout.LegacyImageCacheFolder, true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,38 +1,32 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
|
||||
public class DatabaseMigratorService : IHostedService
|
||||
{
|
||||
public class DatabaseMigratorService : IHostedService
|
||||
private readonly ILogger<DatabaseMigratorService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public DatabaseMigratorService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<DatabaseMigratorService> logger)
|
||||
{
|
||||
private readonly ILogger<DatabaseMigratorService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public DatabaseMigratorService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<DatabaseMigratorService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Applying database migrations");
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
await DbInitializer.Initialize(dbContext, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Done applying database migrations");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Applying database migrations");
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
await DbInitializer.Initialize(dbContext, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Done applying database migrations");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,63 +1,56 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
|
||||
public class EndpointValidatorService : IHostedService
|
||||
{
|
||||
public class EndpointValidatorService : IHostedService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<EndpointValidatorService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<EndpointValidatorService> _logger;
|
||||
|
||||
public EndpointValidatorService(IConfiguration configuration, ILogger<EndpointValidatorService> logger)
|
||||
public EndpointValidatorService(IConfiguration configuration, ILogger<EndpointValidatorService> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string urls = _configuration.GetValue<string>("Kestrel:Endpoints:Http:Url");
|
||||
if (urls.Split(";").Length > 1)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
throw new NotSupportedException($"Multiple endpoints are not supported: {urls}");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
const string PATTERN = @"http:\/\/(.*):(\d+)";
|
||||
Match match = Regex.Match(urls, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
string urls = _configuration.GetValue<string>("Kestrel:Endpoints:Http:Url");
|
||||
if (urls.Split(";").Length > 1)
|
||||
{
|
||||
throw new NotSupportedException($"Multiple endpoints are not supported: {urls}");
|
||||
}
|
||||
string hostname = match.Groups[1].Value;
|
||||
Settings.ListenPort = int.Parse(match.Groups[2].Value);
|
||||
|
||||
const string PATTERN = @"http:\/\/(.*):(\d+)";
|
||||
Match match = Regex.Match(urls, PATTERN);
|
||||
if (match.Success)
|
||||
// IP address must be 0.0.0.0 or 127.0.0.1
|
||||
if (IPAddress.TryParse(hostname, out IPAddress address))
|
||||
{
|
||||
string hostname = match.Groups[1].Value;
|
||||
Settings.ListenPort = int.Parse(match.Groups[2].Value);
|
||||
|
||||
// IP address must be 0.0.0.0 or 127.0.0.1
|
||||
if (IPAddress.TryParse(hostname, out IPAddress address))
|
||||
if (!address.Equals(IPAddress.Parse("0.0.0.0")) && !IPAddress.IsLoopback(address))
|
||||
{
|
||||
if (!address.Equals(IPAddress.Parse("0.0.0.0")) && !IPAddress.IsLoopback(address))
|
||||
{
|
||||
throw new NotSupportedException($"Endpoint MUST include loopback: {urls}");
|
||||
}
|
||||
throw new NotSupportedException($"Endpoint MUST include loopback: {urls}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Invalid endpoint format: {urls}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Server will listen on port {Port} - try UI at {UI}",
|
||||
Settings.ListenPort,
|
||||
$"http://localhost:{Settings.ListenPort}");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Invalid endpoint format: {urls}");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
_logger.LogInformation(
|
||||
"Server will listen on port {Port} - try UI at {UI}",
|
||||
Settings.ListenPort,
|
||||
$"http://localhost:{Settings.ListenPort}");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,50 +1,43 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
|
||||
public class PlatformSettingsService : IHostedService
|
||||
{
|
||||
public class PlatformSettingsService : IHostedService
|
||||
private readonly ILogger<PlatformSettingsService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public PlatformSettingsService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<PlatformSettingsService> logger)
|
||||
{
|
||||
private readonly ILogger<PlatformSettingsService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public PlatformSettingsService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<PlatformSettingsService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService<IRuntimeInfo>();
|
||||
if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux) &&
|
||||
System.IO.Directory.Exists("/dev/dri"))
|
||||
{
|
||||
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
|
||||
IMemoryCache memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
|
||||
|
||||
var devices = localFileSystem.ListFiles("/dev/dri")
|
||||
.Filter(s => s.StartsWith("/dev/dri/render"))
|
||||
.ToList();
|
||||
|
||||
memoryCache.Set("ffmpeg.render_devices", devices);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService<IRuntimeInfo>();
|
||||
if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux) &&
|
||||
System.IO.Directory.Exists("/dev/dri"))
|
||||
{
|
||||
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
|
||||
IMemoryCache memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
|
||||
|
||||
var devices = localFileSystem.ListFiles("/dev/dri")
|
||||
.Filter(s => s.StartsWith("/dev/dri/render"))
|
||||
.ToList();
|
||||
|
||||
memoryCache.Set("ffmpeg.render_devices", devices);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,43 +1,38 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Core;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
|
||||
public class ResourceExtractorService : IHostedService
|
||||
{
|
||||
public class ResourceExtractorService : IHostedService
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
if (!Directory.Exists(FileSystemLayout.ResourcesCacheFolder))
|
||||
{
|
||||
if (!Directory.Exists(FileSystemLayout.ResourcesCacheFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder);
|
||||
}
|
||||
|
||||
Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly;
|
||||
|
||||
await ExtractResource(assembly, "background.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_1.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_2.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_3.png", cancellationToken);
|
||||
await ExtractResource(assembly, "ErsatzTV.png", cancellationToken);
|
||||
await ExtractResource(assembly, "Roboto-Regular.ttf", cancellationToken);
|
||||
await ExtractResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
|
||||
Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly;
|
||||
|
||||
private async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
|
||||
await ExtractResource(assembly, "background.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_1.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_2.png", cancellationToken);
|
||||
await ExtractResource(assembly, "song_background_3.png", cancellationToken);
|
||||
await ExtractResource(assembly, "ErsatzTV.png", cancellationToken);
|
||||
await ExtractResource(assembly, "Roboto-Regular.ttf", cancellationToken);
|
||||
await ExtractResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||
if (resource != null)
|
||||
{
|
||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||
if (resource != null)
|
||||
{
|
||||
await using FileStream fs = File.Create(
|
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||
await resource.CopyToAsync(fs, cancellationToken);
|
||||
}
|
||||
await using FileStream fs = File.Create(
|
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||
await resource.CopyToAsync(fs, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Maintenance.Commands;
|
||||
using ErsatzTV.Application.MediaCollections.Commands;
|
||||
using ErsatzTV.Application.MediaSources.Commands;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Application.Plex.Commands;
|
||||
using ErsatzTV.Application.Search.Commands;
|
||||
using ErsatzTV.Application.Maintenance;
|
||||
using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Plex;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class SchedulerService : BackgroundService
|
||||
{
|
||||
public class SchedulerService : BackgroundService
|
||||
{
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILogger<SchedulerService> _logger;
|
||||
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _plexWorkerChannel;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILogger<SchedulerService> _logger;
|
||||
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _plexWorkerChannel;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
|
||||
public SchedulerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> plexWorkerChannel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SchedulerService> logger)
|
||||
public SchedulerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> plexWorkerChannel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SchedulerService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_workerChannel = workerChannel;
|
||||
_plexWorkerChannel = plexWorkerChannel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTime firstRun = DateTime.Now;
|
||||
|
||||
// run once immediately at startup
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_workerChannel = workerChannel;
|
||||
_plexWorkerChannel = plexWorkerChannel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
await DoWork(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
DateTime firstRun = DateTime.Now;
|
||||
|
||||
// run once immediately at startup
|
||||
int currentMinutes = DateTime.Now.TimeOfDay.Minutes;
|
||||
int toWait = currentMinutes < 30 ? 30 - currentMinutes : 60 - currentMinutes;
|
||||
_logger.LogDebug("Scheduler sleeping for {Minutes} minutes", toWait);
|
||||
await Task.Delay(TimeSpan.FromMinutes(toWait), cancellationToken);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await DoWork(cancellationToken);
|
||||
}
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
int currentMinutes = DateTime.Now.TimeOfDay.Minutes;
|
||||
int toWait = currentMinutes < 30 ? 30 - currentMinutes : 60 - currentMinutes;
|
||||
_logger.LogDebug("Scheduler sleeping for {Minutes} minutes", toWait);
|
||||
await Task.Delay(TimeSpan.FromMinutes(toWait), cancellationToken);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
var roundedMinute = (int)(Math.Round(DateTime.Now.Minute / 5.0) * 5);
|
||||
if (roundedMinute % 30 == 0)
|
||||
{
|
||||
var roundedMinute = (int)(Math.Round(DateTime.Now.Minute / 5.0) * 5);
|
||||
if (roundedMinute % 30 == 0)
|
||||
{
|
||||
// check for playouts to rebuild every 30 minutes
|
||||
await RebuildPlayouts(cancellationToken);
|
||||
}
|
||||
if (roundedMinute % 60 == 0 && DateTime.Now.Subtract(firstRun) > TimeSpan.FromHours(1))
|
||||
{
|
||||
// do other work every hour (on the hour)
|
||||
await DoWork(cancellationToken);
|
||||
}
|
||||
// check for playouts to rebuild every 30 minutes
|
||||
await RebuildPlayouts(cancellationToken);
|
||||
}
|
||||
if (roundedMinute % 60 == 0 && DateTime.Now.Subtract(firstRun) > TimeSpan.FromHours(1))
|
||||
{
|
||||
// do other work every hour (on the hour)
|
||||
await DoWork(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoWork(CancellationToken cancellationToken)
|
||||
private async Task DoWork(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeleteOrphanedArtwork(cancellationToken);
|
||||
await RebuildSearchIndex(cancellationToken);
|
||||
await BuildPlayouts(cancellationToken);
|
||||
await ScanLocalMediaSources(cancellationToken);
|
||||
await ScanPlexMediaSources(cancellationToken);
|
||||
await MatchTraktLists(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
}
|
||||
await DeleteOrphanedArtwork(cancellationToken);
|
||||
await RebuildSearchIndex(cancellationToken);
|
||||
await BuildPlayouts(cancellationToken);
|
||||
await ScanLocalMediaSources(cancellationToken);
|
||||
await ScanPlexMediaSources(cancellationToken);
|
||||
await MatchTraktLists(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task RebuildPlayouts(CancellationToken cancellationToken)
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<Playout> playouts = await dbContext.Playouts
|
||||
.Filter(p => p.DailyRebuildTime != null)
|
||||
.Include(p => p.Channel)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (Playout playout in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)))
|
||||
{
|
||||
if (DateTime.Now.Subtract(DateTime.Today.Add(playout.DailyRebuildTime ?? TimeSpan.FromDays(7))) <
|
||||
TimeSpan.FromMinutes(5))
|
||||
{
|
||||
await _workerChannel.WriteAsync(new BuildPlayout(playout.Id, true), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
}
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BuildPlayouts(CancellationToken cancellationToken)
|
||||
private async Task RebuildPlayouts(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<Playout> playouts = await dbContext.Playouts
|
||||
.Filter(p => p.DailyRebuildTime != null)
|
||||
.Include(p => p.Channel)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
|
||||
|
||||
foreach (Playout playout in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)))
|
||||
{
|
||||
await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanLocalMediaSources(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<int> localLibraryIds = await dbContext.LocalMediaSources
|
||||
.SelectMany(ms => ms.Libraries)
|
||||
.Map(l => l.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (int libraryId in localLibraryIds)
|
||||
{
|
||||
if (_entityLocker.LockLibrary(libraryId))
|
||||
if (DateTime.Now.Subtract(DateTime.Today.Add(playout.DailyRebuildTime ?? TimeSpan.FromDays(7))) <
|
||||
TimeSpan.FromMinutes(5))
|
||||
{
|
||||
await _workerChannel.WriteAsync(
|
||||
new ScanLocalLibraryIfNeeded(libraryId),
|
||||
cancellationToken);
|
||||
await _workerChannel.WriteAsync(new BuildPlayout(playout.Id, true), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanPlexMediaSources(CancellationToken cancellationToken)
|
||||
catch (Exception ex)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<PlexLibrary> plexLibraries = await dbContext.PlexLibraries
|
||||
.Filter(l => l.ShouldSyncItems)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (PlexLibrary library in plexLibraries)
|
||||
{
|
||||
if (_entityLocker.LockLibrary(library.Id))
|
||||
{
|
||||
await _plexWorkerChannel.WriteAsync(
|
||||
new SynchronizePlexLibraryByIdIfNeeded(library.Id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
}
|
||||
|
||||
private async Task MatchTraktLists(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<TraktList> traktLists = await dbContext.TraktLists
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (traktLists.Any() && _entityLocker.LockTrakt())
|
||||
{
|
||||
TraktList last = traktLists.Last();
|
||||
foreach (TraktList list in traktLists)
|
||||
{
|
||||
await _workerChannel.WriteAsync(
|
||||
new MatchTraktListItems(list.Id, list == last),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) =>
|
||||
_workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
|
||||
|
||||
private ValueTask DeleteOrphanedArtwork(CancellationToken cancellationToken) =>
|
||||
_workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BuildPlayouts(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<Playout> playouts = await dbContext.Playouts
|
||||
.Include(p => p.Channel)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
|
||||
{
|
||||
await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanLocalMediaSources(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<int> localLibraryIds = await dbContext.LocalMediaSources
|
||||
.SelectMany(ms => ms.Libraries)
|
||||
.Map(l => l.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (int libraryId in localLibraryIds)
|
||||
{
|
||||
if (_entityLocker.LockLibrary(libraryId))
|
||||
{
|
||||
await _workerChannel.WriteAsync(
|
||||
new ScanLocalLibraryIfNeeded(libraryId),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanPlexMediaSources(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<PlexLibrary> plexLibraries = await dbContext.PlexLibraries
|
||||
.Filter(l => l.ShouldSyncItems)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (PlexLibrary library in plexLibraries)
|
||||
{
|
||||
if (_entityLocker.LockLibrary(library.Id))
|
||||
{
|
||||
await _plexWorkerChannel.WriteAsync(
|
||||
new SynchronizePlexLibraryByIdIfNeeded(library.Id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MatchTraktLists(CancellationToken cancellationToken)
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
List<TraktList> traktLists = await dbContext.TraktLists
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (traktLists.Any() && _entityLocker.LockTrakt())
|
||||
{
|
||||
TraktList last = traktLists.Last();
|
||||
foreach (TraktList list in traktLists)
|
||||
{
|
||||
await _workerChannel.WriteAsync(
|
||||
new MatchTraktListItems(list.Id, list == last),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) =>
|
||||
_workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
|
||||
|
||||
private ValueTask DeleteOrphanedArtwork(CancellationToken cancellationToken) =>
|
||||
_workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
|
||||
}
|
||||
@@ -1,99 +1,90 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Maintenance.Commands;
|
||||
using ErsatzTV.Application.MediaCollections.Commands;
|
||||
using ErsatzTV.Application.MediaSources.Commands;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Application.Search.Commands;
|
||||
using ErsatzTV.Application.Maintenance;
|
||||
using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Services
|
||||
namespace ErsatzTV.Services;
|
||||
|
||||
public class WorkerService : BackgroundService
|
||||
{
|
||||
public class WorkerService : BackgroundService
|
||||
private readonly ChannelReader<IBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<WorkerService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public WorkerService(
|
||||
ChannelReader<IBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<WorkerService> logger)
|
||||
{
|
||||
private readonly ChannelReader<IBackgroundServiceRequest> _channel;
|
||||
private readonly ILogger<WorkerService> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public WorkerService(
|
||||
ChannelReader<IBackgroundServiceRequest> channel,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILogger<WorkerService> logger)
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Worker service started");
|
||||
|
||||
await foreach (IBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
_channel = channel;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Worker service started");
|
||||
|
||||
await foreach (IBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
switch (request)
|
||||
{
|
||||
case BuildPlayout buildPlayout:
|
||||
Either<BaseError, Unit> buildPlayoutResult = await mediator.Send(
|
||||
buildPlayout,
|
||||
cancellationToken);
|
||||
buildPlayoutResult.BiIter(
|
||||
_ => _logger.LogDebug("Built playout {PlayoutId}", buildPlayout.PlayoutId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to build playout {PlayoutId}: {Error}",
|
||||
buildPlayout.PlayoutId,
|
||||
error.Value));
|
||||
break;
|
||||
case IScanLocalLibrary scanLocalLibrary:
|
||||
Either<BaseError, string> scanResult = await mediator.Send(
|
||||
scanLocalLibrary,
|
||||
cancellationToken);
|
||||
scanResult.BiIter(
|
||||
name => _logger.LogDebug(
|
||||
"Done scanning local library {Library}",
|
||||
name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to scan local library {LibraryId}: {Error}",
|
||||
scanLocalLibrary.LibraryId,
|
||||
error.Value));
|
||||
break;
|
||||
case RebuildSearchIndex rebuildSearchIndex:
|
||||
await mediator.Send(rebuildSearchIndex, cancellationToken);
|
||||
break;
|
||||
case DeleteOrphanedArtwork deleteOrphanedArtwork:
|
||||
_logger.LogInformation("Deleting orphaned artwork from the database");
|
||||
await mediator.Send(deleteOrphanedArtwork, cancellationToken);
|
||||
break;
|
||||
case AddTraktList addTraktList:
|
||||
await mediator.Send(addTraktList, cancellationToken);
|
||||
break;
|
||||
case DeleteTraktList deleteTraktList:
|
||||
await mediator.Send(deleteTraktList, cancellationToken);
|
||||
break;
|
||||
case MatchTraktListItems matchTraktListItems:
|
||||
await mediator.Send(matchTraktListItems, cancellationToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
switch (request)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process background service request");
|
||||
case BuildPlayout buildPlayout:
|
||||
Either<BaseError, Unit> buildPlayoutResult = await mediator.Send(
|
||||
buildPlayout,
|
||||
cancellationToken);
|
||||
buildPlayoutResult.BiIter(
|
||||
_ => _logger.LogDebug("Built playout {PlayoutId}", buildPlayout.PlayoutId),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to build playout {PlayoutId}: {Error}",
|
||||
buildPlayout.PlayoutId,
|
||||
error.Value));
|
||||
break;
|
||||
case IScanLocalLibrary scanLocalLibrary:
|
||||
Either<BaseError, string> scanResult = await mediator.Send(
|
||||
scanLocalLibrary,
|
||||
cancellationToken);
|
||||
scanResult.BiIter(
|
||||
name => _logger.LogDebug(
|
||||
"Done scanning local library {Library}",
|
||||
name),
|
||||
error => _logger.LogWarning(
|
||||
"Unable to scan local library {LibraryId}: {Error}",
|
||||
scanLocalLibrary.LibraryId,
|
||||
error.Value));
|
||||
break;
|
||||
case RebuildSearchIndex rebuildSearchIndex:
|
||||
await mediator.Send(rebuildSearchIndex, cancellationToken);
|
||||
break;
|
||||
case DeleteOrphanedArtwork deleteOrphanedArtwork:
|
||||
_logger.LogInformation("Deleting orphaned artwork from the database");
|
||||
await mediator.Send(deleteOrphanedArtwork, cancellationToken);
|
||||
break;
|
||||
case AddTraktList addTraktList:
|
||||
await mediator.Send(addTraktList, cancellationToken);
|
||||
break;
|
||||
case DeleteTraktList deleteTraktList:
|
||||
await mediator.Send(deleteTraktList, cancellationToken);
|
||||
break;
|
||||
case MatchTraktListItems matchTraktListItems:
|
||||
await mediator.Send(matchTraktListItems, cancellationToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process background service request");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user