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:
Jason Dove
2022-03-03 15:36:07 -06:00
committed by GitHub
parent 748581bf5a
commit 1ab98578ab
1164 changed files with 50127 additions and 53816 deletions
+129 -141
View File
@@ -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));
}
}
+34 -41
View File
@@ -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;
}
+41 -50
View File
@@ -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");
}
}
}
}
}
+129 -141
View File
@@ -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
View File
@@ -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);
}
}
}
}
+157 -166
View File
@@ -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);
}
+75 -84
View File
@@ -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");
}
}
}
}
}