Add retroactive scan task for existing libraries
This commit is contained in:
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,8 @@ namespace JellyfinPlugin
|
||||
await _ffmpegSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
string cropData = await DetectCropAsync(path);
|
||||
double limit = Plugin.Instance!.Configuration.CropDetectLimit;
|
||||
string cropData = await CropHelper.DetectCropAsync(path, limit);
|
||||
if (!string.IsNullOrEmpty(cropData))
|
||||
{
|
||||
if (video.ProviderIds == null)
|
||||
@@ -76,7 +77,6 @@ namespace JellyfinPlugin
|
||||
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);
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
_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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Building from Source
|
||||
|
||||
1. Clone this repository.
|
||||
2. Build the plugin using the .NET 10 SDK:
|
||||
```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/`).
|
||||
4. Restart the Jellyfin server.
|
||||
|
||||
### Configuration
|
||||
### Configuration & Initial Scan
|
||||
|
||||
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.
|
||||
@@ -36,6 +34,7 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
|
||||
- `Client`: Uses CSS injection in the web player.
|
||||
- `Server`: Requires setting up the FFmpeg wrapper.
|
||||
- `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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user