Add retroactive scan task for existing libraries

This commit is contained in:
2026-09-17 14:20:14 +02:00
parent bb7af50879
commit 904aada05d
4 changed files with 175 additions and 77 deletions
+77
View File
@@ -0,0 +1,77 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace JellyfinPlugin
{
public static class CropHelper
{
public static async Task<string> DetectCropAsync(string filePath, double limit)
{
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;
return $"{finalW}:{finalH}:{minX}:{minY}";
}
}
}
+2 -74
View File
@@ -68,7 +68,8 @@ namespace JellyfinPlugin
await _ffmpegSemaphore.WaitAsync(); await _ffmpegSemaphore.WaitAsync();
try try
{ {
string cropData = await DetectCropAsync(path); double limit = Plugin.Instance!.Configuration.CropDetectLimit;
string cropData = await CropHelper.DetectCropAsync(path, limit);
if (!string.IsNullOrEmpty(cropData)) if (!string.IsNullOrEmpty(cropData))
{ {
if (video.ProviderIds == null) if (video.ProviderIds == null)
@@ -76,7 +77,6 @@ namespace JellyfinPlugin
video.ProviderIds = new System.Collections.Generic.Dictionary<string, string>(); video.ProviderIds = new System.Collections.Generic.Dictionary<string, string>();
} }
video.ProviderIds["BlackBarCrop"] = cropData; video.ProviderIds["BlackBarCrop"] = cropData;
// Jellyfin 12.0 signature requires passing item, item, type, cancellationToken
await _libraryManager.UpdateItemAsync(video, video, ItemUpdateType.MetadataEdit, CancellationToken.None); await _libraryManager.UpdateItemAsync(video, video, ItemUpdateType.MetadataEdit, CancellationToken.None);
_logger.LogInformation("Zapisano proporcje crop {CropData} dla pliku {Path}", cropData, path); _logger.LogInformation("Zapisano proporcje crop {CropData} dla pliku {Path}", cropData, path);
} }
@@ -92,78 +92,6 @@ namespace JellyfinPlugin
} }
} }
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() public void Dispose()
{ {
_libraryManager.ItemAdded -= OnItemAddedOrUpdated; _libraryManager.ItemAdded -= OnItemAddedOrUpdated;
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace JellyfinPlugin.Tasks
{
public class LibraryRetroactiveScanTask : IScheduledTask
{
private readonly ILogger<LibraryRetroactiveScanTask> _logger;
private readonly ILibraryManager _libraryManager;
public LibraryRetroactiveScanTask(ILogger<LibraryRetroactiveScanTask> logger, ILibraryManager libraryManager)
{
_logger = logger;
_libraryManager = libraryManager;
}
public string Name => "Skanuj brakujące czarne pasy (Black Bar Remover)";
public string Key => "BlackBarRetroactiveScanTask";
public string Description => "Skanuje istniejącą bibliotekę, pomijając już przeanalizowane pliki. Użyj tego po zainstalowaniu wtyczki.";
public string Category => "Library";
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var config = Plugin.Instance!.Configuration;
if (!config.IsEnabled)
{
_logger.LogInformation("Wtyczka wyłączona. Pomijanie.");
return;
}
var items = _libraryManager.RootFolder.GetRecursiveChildren()
.Where(x => x is Movie || x is Episode)
.ToList();
int total = items.Count;
int processed = 0;
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
if (item.ProviderIds != null && item.ProviderIds.ContainsKey("BlackBarCrop"))
{
processed++;
progress.Report((processed / (double)total) * 100);
continue;
}
var path = item.Path;
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
try
{
string cropData = await CropHelper.DetectCropAsync(path, config.CropDetectLimit);
if (item.ProviderIds == null)
{
item.ProviderIds = new Dictionary<string, string>();
}
// Zawsze dodajemy flagę, nawet jak puste, żeby nie skanować drugi raz tego samego pliku
// (puste oznacza że nie ma czarnych pasów do ucięcia)
item.ProviderIds["BlackBarCrop"] = cropData ?? string.Empty;
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, cancellationToken);
if (!string.IsNullOrEmpty(cropData))
_logger.LogInformation("Znaleziono pasy {Crop} w {Path}", cropData, path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Błąd skanowania pliku {Path}", path);
}
}
processed++;
progress.Report((processed / (double)total) * 100);
}
}
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return Array.Empty<TaskTriggerInfo>();
}
}
}
+2 -3
View File
@@ -18,8 +18,6 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
## Installation ## Installation
### Method 1: Building from Source
1. Clone this repository. 1. Clone this repository.
2. Build the plugin using the .NET 10 SDK: 2. Build the plugin using the .NET 10 SDK:
```bash ```bash
@@ -28,7 +26,7 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
3. Copy the compiled DLLs from `JellyfinPlugin/bin/Release/net10.0/` into a new folder named `BlackBarRemover` inside your Jellyfin plugins directory (e.g., `/var/lib/jellyfin/plugins/BlackBarRemover/`). 3. Copy the compiled DLLs from `JellyfinPlugin/bin/Release/net10.0/` into a new folder named `BlackBarRemover` inside your Jellyfin plugins directory (e.g., `/var/lib/jellyfin/plugins/BlackBarRemover/`).
4. Restart the Jellyfin server. 4. Restart the Jellyfin server.
### Configuration ### Configuration & Initial Scan
1. Go to **Dashboard** -> **Plugins** -> **Black Bar Remover**. 1. Go to **Dashboard** -> **Plugins** -> **Black Bar Remover**.
2. Set your **Crop Detect Limit** (Default is 0.15). If your web-dl rips have noisy black bars that are not being cropped, increase this value to 0.20 or 0.25. 2. Set your **Crop Detect Limit** (Default is 0.15). If your web-dl rips have noisy black bars that are not being cropped, increase this value to 0.20 or 0.25.
@@ -36,6 +34,7 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
- `Client`: Uses CSS injection in the web player. - `Client`: Uses CSS injection in the web player.
- `Server`: Requires setting up the FFmpeg wrapper. - `Server`: Requires setting up the FFmpeg wrapper.
- `Both`: Attempts both methods depending on the client. - `Both`: Attempts both methods depending on the client.
4. **Important for existing libraries**: The plugin automatically scans *new* files when they are added to Jellyfin. For your existing library, go to **Dashboard -> Scheduled Tasks** and run the **"Skanuj brakujące czarne pasy (Black Bar Remover)"** task manually. It will scan all existing movies and episodes in the background.
### Activating the Server-Side Wrapper (Optional) ### Activating the Server-Side Wrapper (Optional)