215 lines
7.7 KiB
C#
215 lines
7.7 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Threading.Channels;
|
|
using CliWrap;
|
|
using ErsatzTV.Application.Search;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Errors;
|
|
using ErsatzTV.Core.Interfaces.Repositories;
|
|
using ErsatzTV.Core.MediaSources;
|
|
using ErsatzTV.Core.Metadata;
|
|
using ErsatzTV.FFmpeg.Runtime;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Newtonsoft.Json;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using Serilog.Formatting.Compact.Reader;
|
|
|
|
namespace ErsatzTV.Application.Libraries;
|
|
|
|
public abstract class CallLibraryScannerHandler<TRequest>
|
|
{
|
|
private readonly int _batchSize = 100;
|
|
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
|
|
private readonly IConfigElementRepository _configElementRepository;
|
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
|
private readonly IMediator _mediator;
|
|
private readonly IRuntimeInfo _runtimeInfo;
|
|
private readonly List<int> _toReindex = [];
|
|
private readonly List<int> _toRemove = [];
|
|
private string _libraryName;
|
|
|
|
protected CallLibraryScannerHandler(
|
|
IDbContextFactory<TvContext> dbContextFactory,
|
|
IConfigElementRepository configElementRepository,
|
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
|
IMediator mediator,
|
|
IRuntimeInfo runtimeInfo)
|
|
{
|
|
_dbContextFactory = dbContextFactory;
|
|
_configElementRepository = configElementRepository;
|
|
_channel = channel;
|
|
_mediator = mediator;
|
|
_runtimeInfo = runtimeInfo;
|
|
}
|
|
|
|
protected async Task<Either<BaseError, string>> PerformScan(
|
|
string scanner,
|
|
List<string> arguments,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using var forcefulCts = new CancellationTokenSource();
|
|
|
|
await using CancellationTokenRegistration link = cancellationToken.Register(
|
|
() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10))
|
|
);
|
|
|
|
CommandResult process = await Cli.Wrap(scanner)
|
|
.WithArguments(arguments)
|
|
.WithValidation(CommandResultValidation.None)
|
|
.WithStandardErrorPipe(PipeTarget.ToDelegate(ProcessLogOutput))
|
|
.WithStandardOutputPipe(PipeTarget.ToDelegate(ProcessProgressOutput))
|
|
.ExecuteAsync(forcefulCts.Token, cancellationToken);
|
|
|
|
if (process.ExitCode != 0)
|
|
{
|
|
return BaseError.New($"ErsatzTV.Scanner exited with code {process.ExitCode}");
|
|
}
|
|
|
|
if (_toReindex.Count > 0)
|
|
{
|
|
await _channel.WriteAsync(new ReindexMediaItems(_toReindex.ToArray()), cancellationToken);
|
|
_toReindex.Clear();
|
|
}
|
|
|
|
if (_toRemove.Count > 0)
|
|
{
|
|
await _channel.WriteAsync(new RemoveMediaItems(_toReindex.ToArray()), cancellationToken);
|
|
_toRemove.Clear();
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
return _libraryName ?? string.Empty;
|
|
}
|
|
|
|
private static void ProcessLogOutput(string s)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(s))
|
|
{
|
|
try
|
|
{
|
|
// make a new log event to force using local time
|
|
// because the compact json writer used by the scanner
|
|
// writes in UTC
|
|
LogEvent logEvent = LogEventReader.ReadFromString(s);
|
|
|
|
ILogger log = Log.Logger;
|
|
if (logEvent.Properties.TryGetValue("SourceContext", out LogEventPropertyValue property))
|
|
{
|
|
log = log.ForContext(
|
|
Constants.SourceContextPropertyName,
|
|
property.ToString().Trim('"'));
|
|
}
|
|
|
|
log.Write(
|
|
new LogEvent(
|
|
logEvent.Timestamp.ToLocalTime(),
|
|
logEvent.Level,
|
|
logEvent.Exception,
|
|
logEvent.MessageTemplate,
|
|
logEvent.Properties.Map(pair => new LogEventProperty(pair.Key, pair.Value))));
|
|
}
|
|
catch
|
|
{
|
|
Console.WriteLine(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ProcessProgressOutput(string s)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(s))
|
|
{
|
|
try
|
|
{
|
|
ScannerProgressUpdate progressUpdate = JsonConvert.DeserializeObject<ScannerProgressUpdate>(s);
|
|
if (progressUpdate != null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(progressUpdate.LibraryName))
|
|
{
|
|
_libraryName = progressUpdate.LibraryName;
|
|
}
|
|
|
|
_toReindex.AddRange(progressUpdate.ItemsToReindex);
|
|
if (_toReindex.Count >= _batchSize)
|
|
{
|
|
await _channel.WriteAsync(new ReindexMediaItems(_toReindex.ToArray()));
|
|
_toReindex.Clear();
|
|
}
|
|
|
|
_toRemove.AddRange(progressUpdate.ItemsToRemove);
|
|
if (_toRemove.Count >= _batchSize)
|
|
{
|
|
await _channel.WriteAsync(new RemoveMediaItems(_toReindex.ToArray()));
|
|
_toRemove.Clear();
|
|
}
|
|
|
|
if (progressUpdate.PercentComplete is not null)
|
|
{
|
|
var progress = new LibraryScanProgress(
|
|
progressUpdate.LibraryId,
|
|
progressUpdate.PercentComplete.Value);
|
|
|
|
await _mediator.Publish(progress);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Logger.Warning(ex, "Unable to process scanner progress update");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected abstract Task<DateTimeOffset> GetLastScan(TvContext dbContext, TRequest request);
|
|
protected abstract bool ScanIsRequired(DateTimeOffset lastScan, int libraryRefreshInterval, TRequest request);
|
|
|
|
protected async Task<Validation<BaseError, string>> Validate(TRequest request)
|
|
{
|
|
int libraryRefreshInterval = await _configElementRepository
|
|
.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
|
.IfNoneAsync(0);
|
|
|
|
libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999);
|
|
|
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
|
|
DateTimeOffset lastScan = await GetLastScan(dbContext, request);
|
|
if (!ScanIsRequired(lastScan, libraryRefreshInterval, request))
|
|
{
|
|
return new ScanIsNotRequired();
|
|
}
|
|
|
|
string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
|
|
? "ErsatzTV.Scanner.exe"
|
|
: "ErsatzTV.Scanner";
|
|
|
|
string processFileName = Environment.ProcessPath ?? string.Empty;
|
|
string processExecutable = Path.GetFileNameWithoutExtension(processFileName);
|
|
string folderName = Path.GetDirectoryName(processFileName);
|
|
if ("dotnet".Equals(processExecutable, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
folderName = AppContext.BaseDirectory;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(folderName))
|
|
{
|
|
string localFileName = Path.Combine(folderName, executable);
|
|
if (File.Exists(localFileName))
|
|
{
|
|
return localFileName;
|
|
}
|
|
}
|
|
|
|
return BaseError.New("Unable to locate ErsatzTV.Scanner executable");
|
|
}
|
|
}
|