175 lines
6.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|