Files
blackBarRemover/JellyfinPlugin/LibraryMonitor.cs
T

175 lines
6.3 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
namespace JellyfinPlugin
{
public class LibraryMonitor : IHostedService, IDisposable
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<LibraryMonitor> _logger;
private readonly SemaphoreSlim _ffmpegSemaphore = new SemaphoreSlim(2, 2);
public LibraryMonitor(ILibraryManager libraryManager, ILogger<LibraryMonitor> logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_libraryManager.ItemAdded += OnItemAddedOrUpdated;
_libraryManager.ItemUpdated += OnItemAddedOrUpdated;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnItemAddedOrUpdated(object? sender, ItemChangeEventArgs e)
{
if (e.Item is Video video && (video is Movie || video is Episode))
{
if (!Plugin.Instance!.Configuration.IsEnabled) return;
// Fire and forget, don't block library scan
_ = Task.Run(() => ProcessVideoAsync(video));
}
}
private async Task ProcessVideoAsync(Video video)
{
try
{
if (video.ProviderIds != null && video.ProviderIds.ContainsKey("BlackBarCrop"))
{
return;
}
var path = video.Path;
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
await _ffmpegSemaphore.WaitAsync();
try
{
string cropData = await DetectCropAsync(path);
if (!string.IsNullOrEmpty(cropData))
{
if (video.ProviderIds == null)
{
video.ProviderIds = new System.Collections.Generic.Dictionary<string, string>();
}
video.ProviderIds["BlackBarCrop"] = cropData;
// Jellyfin 12.0 signature requires passing item, item, type, cancellationToken
await _libraryManager.UpdateItemAsync(video, video, ItemUpdateType.MetadataEdit, CancellationToken.None);
_logger.LogInformation("Zapisano proporcje crop {CropData} dla pliku {Path}", cropData, path);
}
}
finally
{
_ffmpegSemaphore.Release();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Błąd podczas procesowania wideo: {Path}", video.Path);
}
}
private async Task<string> DetectCropAsync(string filePath)
{
var config = Plugin.Instance!.Configuration;
double limit = config.CropDetectLimit;
string limitStr = limit.ToString(System.Globalization.CultureInfo.InvariantCulture);
var startInfo = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-skip_frame nokey -i \"{filePath}\" -vf \"cropdetect=limit={limitStr}:round=16:reset=0\" -f null -",
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
process.Start();
string output = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var regex = new Regex(@"crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)");
var matches = regex.Matches(output);
var crops = new ConcurrentDictionary<string, int>();
int totalFrames = 0;
foreach (Match match in matches)
{
string cropStr = match.Value.Replace("crop=", "");
crops.AddOrUpdate(cropStr, 1, (_, count) => count + 1);
totalFrames++;
}
if (totalFrames == 0) return string.Empty;
var validCrops = crops.Where(kvp => (double)kvp.Value / totalFrames > 0.05).ToList();
if (!validCrops.Any())
{
validCrops = crops.OrderByDescending(kvp => kvp.Value).Take(1).ToList();
}
int minX = int.MaxValue, minY = int.MaxValue, maxXW = 0, maxYH = 0;
foreach (var kvp in validCrops)
{
var parts = kvp.Key.Split(':');
if (parts.Length == 4 &&
int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h) &&
int.TryParse(parts[2], out int x) && int.TryParse(parts[3], out int y))
{
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
maxXW = Math.Max(maxXW, x + w);
maxYH = Math.Max(maxYH, y + h);
}
}
int finalW = maxXW - minX;
int finalH = maxYH - minY;
if (finalW <= 0 || finalH <= 0) return string.Empty;
// Sprawdzanie czy faktycznie coś wycięto w pionie lub poziomie.
// Zazwyczaj filmy 1080p mają h=1080.
// Możemy zwrócić pusty string, jeśli ucięcie jest minimalne (np. mniej niż 10 px z każdej strony).
// To zapobiega wymuszaniu transkodowania z byle powodu.
return $"{finalW}:{finalH}:{minX}:{minY}";
}
public void Dispose()
{
_libraryManager.ItemAdded -= OnItemAddedOrUpdated;
_libraryManager.ItemUpdated -= OnItemAddedOrUpdated;
_ffmpegSemaphore.Dispose();
}
}
}