Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d018a8e5ac | |||
| 25c7a56901 | |||
| dfe59ed84a | |||
| 795f42048f |
@@ -17,7 +17,9 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: |
|
||||||
|
9.0.x
|
||||||
|
10.0.x
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release
|
run: dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release
|
||||||
@@ -82,6 +84,7 @@ jobs:
|
|||||||
|
|
||||||
VERSION="${{ github.ref_name }}"
|
VERSION="${{ github.ref_name }}"
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
|
CLEAN_VERSION="${VERSION%%-*}"
|
||||||
|
|
||||||
cat > manifest.json <<EOF
|
cat > manifest.json <<EOF
|
||||||
[
|
[
|
||||||
@@ -95,7 +98,7 @@ jobs:
|
|||||||
"imageUrl": "",
|
"imageUrl": "",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"version": "$VERSION",
|
"version": "$CLEAN_VERSION",
|
||||||
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 12.0)",
|
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 12.0)",
|
||||||
"targetAbi": "12.0.0.0",
|
"targetAbi": "12.0.0.0",
|
||||||
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
|
||||||
@@ -104,7 +107,7 @@ jobs:
|
|||||||
"repositoryUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf12.0.zip"
|
"repositoryUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf12.0.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "$VERSION",
|
"version": "$CLEAN_VERSION",
|
||||||
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 10.11)",
|
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 10.11)",
|
||||||
"targetAbi": "10.11.0.0",
|
"targetAbi": "10.11.0.0",
|
||||||
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ namespace JellyfinPlugin.Api
|
|||||||
|
|
||||||
[HttpGet("CropById")]
|
[HttpGet("CropById")]
|
||||||
[Produces("text/plain")]
|
[Produces("text/plain")]
|
||||||
public ContentResult GetCropById([FromQuery] string itemId)
|
public ContentResult GetCropById([FromQuery] string itemId, [FromQuery] bool ignoreMode = false)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(itemId)) return Content(string.Empty, "text/plain");
|
if (string.IsNullOrEmpty(itemId)) return Content(string.Empty, "text/plain");
|
||||||
|
|
||||||
var mode = Plugin.Instance!.Configuration.PlaybackMode;
|
var mode = Plugin.Instance!.Configuration.PlaybackMode;
|
||||||
if (mode == "Server") return Content(string.Empty, "text/plain");
|
if (mode == "Server" && !ignoreMode) return Content(string.Empty, "text/plain");
|
||||||
|
|
||||||
if (Guid.TryParse(itemId, out Guid id))
|
if (Guid.TryParse(itemId, out Guid id))
|
||||||
{
|
{
|
||||||
@@ -152,14 +152,82 @@ namespace JellyfinPlugin.Api
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkServerCrop(itemId) {
|
||||||
|
window._originalFetch('/BlackBarRemover/CropById?itemId=' + itemId + '&ignoreMode=true')
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(crop => {
|
||||||
|
if (crop) {
|
||||||
|
window._isServerCropping = true;
|
||||||
|
} else {
|
||||||
|
window._isServerCropping = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => { window._isServerCropping = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const playbackInfoObserver = new MutationObserver((mutations) => {
|
||||||
|
if (!window._isServerCropping) return;
|
||||||
|
|
||||||
|
for (let m of mutations) {
|
||||||
|
if (m.type === 'characterData') {
|
||||||
|
if (m.target.nodeValue) {
|
||||||
|
const text = m.target.nodeValue.trim();
|
||||||
|
if (text === ""The video's resolution is not supported"" ||
|
||||||
|
text === ""VideoResolutionNotSupported"" ||
|
||||||
|
text.includes(""rozdzielczość nie jest obsługiwana"") ||
|
||||||
|
text === ""Rozdzielczość wideo nie jest obsługiwana"") {
|
||||||
|
m.target.nodeValue = ""Black Bar Removal (Server Mode)"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (m.type === 'childList') {
|
||||||
|
m.addedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const textContent = node.textContent || """";
|
||||||
|
if (textContent.includes(""The video's resolution is not supported"") ||
|
||||||
|
textContent.includes(""VideoResolutionNotSupported"") ||
|
||||||
|
textContent.includes(""rozdzielczość"")) {
|
||||||
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
|
||||||
|
let textNode;
|
||||||
|
while (textNode = walker.nextNode()) {
|
||||||
|
if (textNode.nodeValue) {
|
||||||
|
const t = textNode.nodeValue.trim();
|
||||||
|
if (t === ""The video's resolution is not supported"" ||
|
||||||
|
t === ""VideoResolutionNotSupported"" ||
|
||||||
|
t.includes(""rozdzielczość nie jest obsługiwana"") ||
|
||||||
|
t === ""Rozdzielczość wideo nie jest obsługiwana"") {
|
||||||
|
textNode.nodeValue = ""Black Bar Removal (Server Mode)"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (node.nodeValue) {
|
||||||
|
const t = node.nodeValue.trim();
|
||||||
|
if (t === ""The video's resolution is not supported"" ||
|
||||||
|
t === ""VideoResolutionNotSupported"" ||
|
||||||
|
t.includes(""rozdzielczość nie jest obsługiwana"") ||
|
||||||
|
t === ""Rozdzielczość wideo nie jest obsługiwana"") {
|
||||||
|
node.nodeValue = ""Black Bar Removal (Server Mode)"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playbackInfoObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video');
|
||||||
if (video && interceptedItemId && interceptedItemId !== currentItemId) {
|
if (video && interceptedItemId && interceptedItemId !== currentItemId) {
|
||||||
currentItemId = interceptedItemId;
|
currentItemId = interceptedItemId;
|
||||||
console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId);
|
console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId);
|
||||||
fetchCrop(currentItemId);
|
fetchCrop(currentItemId);
|
||||||
|
checkServerCrop(currentItemId);
|
||||||
} else if (!video) {
|
} else if (!video) {
|
||||||
currentItemId = null;
|
currentItemId = null;
|
||||||
|
window._isServerCropping = false;
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace JellyfinPlugin
|
||||||
|
{
|
||||||
|
public class PlaybackInfoInterceptorStartupFilter : IStartupFilter
|
||||||
|
{
|
||||||
|
private readonly ILogger<PlaybackInfoInterceptorStartupFilter> _logger;
|
||||||
|
private readonly MediaBrowser.Controller.Library.ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
public PlaybackInfoInterceptorStartupFilter(ILogger<PlaybackInfoInterceptorStartupFilter> logger, MediaBrowser.Controller.Library.ILibraryManager libraryManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||||
|
{
|
||||||
|
return app =>
|
||||||
|
{
|
||||||
|
app.Use(InvokeAsync);
|
||||||
|
next(app);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetCropFromId(string itemId)
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(itemId, out Guid id))
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
if (item != null && item.ProviderIds != null)
|
||||||
|
{
|
||||||
|
if (item.ProviderIds.TryGetValue("BlackBarCrop", out string? cropData))
|
||||||
|
{
|
||||||
|
return cropData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InvokeAsync(HttpContext context, Func<Task> nextMw)
|
||||||
|
{
|
||||||
|
var path = context.Request.Path.Value ?? string.Empty;
|
||||||
|
|
||||||
|
var config = Plugin.Instance?.Configuration;
|
||||||
|
if (config == null || !config.IsEnabled || config.PlaybackMode == "Client")
|
||||||
|
{
|
||||||
|
await nextMw().ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HLS STREAM QUERY REWRITE ---
|
||||||
|
// If the client requests the m3u8 playlist, we simply tell Jellyfin NOT to allow video stream copy.
|
||||||
|
// This elegantly forces transcoding for the BEST supported codec without breaking FFmpeg tags/filters!
|
||||||
|
if (path.EndsWith("master.m3u8", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.EndsWith("main.m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var matchId = Regex.Match(path, @"/Videos/([^/]+)/", RegexOptions.IgnoreCase);
|
||||||
|
if (matchId.Success)
|
||||||
|
{
|
||||||
|
var idStr = matchId.Groups[1].Value;
|
||||||
|
var crop = GetCropFromId(idStr);
|
||||||
|
if (crop != null)
|
||||||
|
{
|
||||||
|
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(context.Request.QueryString.Value);
|
||||||
|
var items = query.SelectMany(x => x.Value, (col, val) => new System.Collections.Generic.KeyValuePair<string, string>(col.Key, val)).ToList();
|
||||||
|
|
||||||
|
var newQueryString = "?";
|
||||||
|
foreach (var kvp in items)
|
||||||
|
{
|
||||||
|
if (!kvp.Key.Equals("allowVideoStreamCopy", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!kvp.Key.Equals("transcodeReasons", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
newQueryString += $"{kvp.Key}={System.Net.WebUtility.UrlEncode(kvp.Value)}&";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Force disable stream copy AND provide a valid reason for the dashboard
|
||||||
|
newQueryString += "allowVideoStreamCopy=false&transcodeReasons=VideoResolutionNotSupported";
|
||||||
|
|
||||||
|
context.Request.QueryString = new QueryString(newQueryString);
|
||||||
|
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepted HLS request for {idStr}. Appended allowVideoStreamCopy=false and transcodeReasons=VideoResolutionNotSupported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(path, @"/Items/([^/]+)/PlaybackInfo", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
await nextMw().ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemId = match.Groups[1].Value;
|
||||||
|
var cropData = GetCropFromId(itemId);
|
||||||
|
|
||||||
|
if (cropData == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"[Black Bar Remover DEBUG] PlaybackInfo request for {itemId}, but no crop found in DB. Skipping interception.");
|
||||||
|
await nextMw().ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepting PlaybackInfo for {itemId}. Found crop in DB: {cropData}. Forcing transcode.");
|
||||||
|
|
||||||
|
// Kestrel middleware do nadpisania odpowiedzi
|
||||||
|
context.Request.Headers.Remove("Accept-Encoding");
|
||||||
|
var originalBody = context.Response.Body;
|
||||||
|
using var buffer = new MemoryStream();
|
||||||
|
context.Response.Body = buffer;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await nextMw().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.Response.Body = originalBody;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Body = originalBody;
|
||||||
|
buffer.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var isJson = context.Response.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||||
|
|
||||||
|
if (!isJson || context.Response.StatusCode != 200)
|
||||||
|
{
|
||||||
|
await buffer.CopyToAsync(originalBody).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json;
|
||||||
|
using (var reader = new StreamReader(buffer, Encoding.UTF8, true, 1024, leaveOpen: true))
|
||||||
|
{
|
||||||
|
json = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var root = JsonNode.Parse(json);
|
||||||
|
var mediaSources = root?["MediaSources"]?.AsArray();
|
||||||
|
if (mediaSources != null)
|
||||||
|
{
|
||||||
|
bool modified = false;
|
||||||
|
foreach (var source in mediaSources)
|
||||||
|
{
|
||||||
|
if (source == null) continue;
|
||||||
|
|
||||||
|
var obj = source.AsObject();
|
||||||
|
|
||||||
|
// Pobieramy flagi niezależnie od wielkości liter (Jellyfin 10.9+ może używać camelCase)
|
||||||
|
var dpNode = obj.FirstOrDefault(x => x.Key.Equals("SupportsDirectPlay", StringComparison.OrdinalIgnoreCase)).Value;
|
||||||
|
var dsNode = obj.FirstOrDefault(x => x.Key.Equals("SupportsDirectStream", StringComparison.OrdinalIgnoreCase)).Value;
|
||||||
|
|
||||||
|
var dp = dpNode?.GetValue<bool>() ?? false;
|
||||||
|
var ds = dsNode?.GetValue<bool>() ?? false;
|
||||||
|
|
||||||
|
if (dp || ds)
|
||||||
|
{
|
||||||
|
// Szukamy oryginalnych kluczy, żeby ich nie zdublować w innym casing-u
|
||||||
|
var dpKey = obj.FirstOrDefault(x => x.Key.Equals("SupportsDirectPlay", StringComparison.OrdinalIgnoreCase)).Key ?? "SupportsDirectPlay";
|
||||||
|
var dsKey = obj.FirstOrDefault(x => x.Key.Equals("SupportsDirectStream", StringComparison.OrdinalIgnoreCase)).Key ?? "SupportsDirectStream";
|
||||||
|
var trKey = obj.FirstOrDefault(x => x.Key.Equals("TranscodingReasons", StringComparison.OrdinalIgnoreCase)).Key ?? "TranscodingReasons";
|
||||||
|
|
||||||
|
obj[dpKey] = false;
|
||||||
|
obj[dsKey] = false;
|
||||||
|
|
||||||
|
var transcodingReasons = obj[trKey]?.GetValue<string>();
|
||||||
|
string customReason = "VideoCodecNotSupported";
|
||||||
|
|
||||||
|
if (transcodingReasons == null)
|
||||||
|
{
|
||||||
|
obj[trKey] = customReason;
|
||||||
|
}
|
||||||
|
else if (!transcodingReasons.Contains(customReason))
|
||||||
|
{
|
||||||
|
obj[trKey] = transcodingReasons + "," + customReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dodatkowo obcinamy wsparcie dla Direct Stream w pod-strumieniach (Jellyfin czasami wymusza z poziomu klienta)
|
||||||
|
var reqKey = obj.FirstOrDefault(x => x.Key.Equals("RequiresOpening", StringComparison.OrdinalIgnoreCase)).Key ?? "RequiresOpening";
|
||||||
|
obj[reqKey] = true;
|
||||||
|
|
||||||
|
var reqCloseKey = obj.FirstOrDefault(x => x.Key.Equals("RequiresClosing", StringComparison.OrdinalIgnoreCase)).Key ?? "RequiresClosing";
|
||||||
|
obj[reqCloseKey] = true;
|
||||||
|
|
||||||
|
modified = true;
|
||||||
|
_logger.LogInformation($"[Black Bar Remover DEBUG] Modified PlaybackInfo JSON: forced {dpKey}/{dsKey} to false for MediaSource ID {obj.FirstOrDefault(x => x.Key.Equals("Id", StringComparison.OrdinalIgnoreCase)).Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified)
|
||||||
|
{
|
||||||
|
json = root!.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
|
||||||
|
_logger.LogInformation($"[Black Bar Remover DEBUG] Successfully rewrote PlaybackInfo JSON to enforce transcoding.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"[Black Bar Remover DEBUG] Error modifying PlaybackInfo JSON: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
context.Response.ContentLength = bytes.Length;
|
||||||
|
context.Response.Headers.Remove("ETag");
|
||||||
|
context.Response.Headers.Remove("Last-Modified");
|
||||||
|
await originalBody.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@ namespace JellyfinPlugin
|
|||||||
{
|
{
|
||||||
public void RegisterServices(IServiceCollection serviceCollection, MediaBrowser.Controller.IServerApplicationHost applicationHost)
|
public void RegisterServices(IServiceCollection serviceCollection, MediaBrowser.Controller.IServerApplicationHost applicationHost)
|
||||||
{
|
{
|
||||||
// Rejestracja serwisów tła
|
// Zarejestrowanie filtru wstrzykującego nasz skrypt kliencki
|
||||||
serviceCollection.AddSingleton<Microsoft.AspNetCore.Hosting.IStartupFilter, ScriptInjectionStartupFilter>();
|
serviceCollection.AddSingleton<Microsoft.AspNetCore.Hosting.IStartupFilter, ScriptInjectionStartupFilter>();
|
||||||
|
|
||||||
|
// Zarejestrowanie filtru wymuszającego transkodowanie dla Server Mode
|
||||||
|
serviceCollection.AddSingleton<Microsoft.AspNetCore.Hosting.IStartupFilter, PlaybackInfoInterceptorStartupFilter>();
|
||||||
serviceCollection.AddHostedService<WrapperGenerator>();
|
serviceCollection.AddHostedService<WrapperGenerator>();
|
||||||
serviceCollection.AddHostedService<LibraryMonitor>();
|
serviceCollection.AddHostedService<LibraryMonitor>();
|
||||||
|
|
||||||
|
|||||||
@@ -77,29 +77,79 @@ namespace JellyfinPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
string script = $@"#!/bin/bash
|
string script = $@"#!/bin/bash
|
||||||
# Zmienna przechowująca ścieżkę do prawdziwego FFmpeg Jellyfina
|
|
||||||
REAL_FFMPEG=""{realFfmpegPath}""
|
REAL_FFMPEG=""{realFfmpegPath}""
|
||||||
|
|
||||||
# Znajdujemy ścieżkę pliku wejściowego (argument po -i)
|
|
||||||
INPUT_FILE=""""
|
INPUT_FILE=""""
|
||||||
for i in ""${{!@}}""; do
|
for (( i=1; i<=$#; i++ )); do
|
||||||
if [[ ""${{@:$i:1}}"" == ""-i"" ]]; then
|
arg=""${{!i}}""
|
||||||
INPUT_FILE=""${{@:$i+1:1}}""
|
if [[ ""$arg"" == ""-i"" ]]; then
|
||||||
|
let next_idx=i+1
|
||||||
|
if [ $next_idx -le $# ]; then
|
||||||
|
INPUT_FILE=""${{!next_idx}}""
|
||||||
|
fi
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Pobieramy crop z API naszej wtyczki
|
|
||||||
if [ -n ""$INPUT_FILE"" ]; then
|
if [ -n ""$INPUT_FILE"" ]; then
|
||||||
CROP_DATA=$(curl -s --max-time 1 -G --data-urlencode ""Path=$INPUT_FILE"" http://127.0.0.1:8096/BlackBarRemover/CropByPath)
|
# Jellyfin przekazuje ścieżkę z prefiksem file: (np. file:""/home/..."").
|
||||||
|
# Shell usuwa cudzysłowy automatycznie – zostaje nam tylko file: do usunięcia.
|
||||||
|
CLEAN_PATH=""${{INPUT_FILE#file:}}""
|
||||||
|
|
||||||
|
CROP_DATA=$(curl -s --max-time 1 -G --data-urlencode ""Path=$CLEAN_PATH"" http://127.0.0.1:8096/BlackBarRemover/CropByPath)
|
||||||
|
|
||||||
if [ -n ""$CROP_DATA"" ] && [ ""$CROP_DATA"" != """" ]; then
|
if [ -n ""$CROP_DATA"" ] && [ ""$CROP_DATA"" != """" ]; then
|
||||||
exec ""$REAL_FFMPEG"" ""$@"" -vf ""crop=$CROP_DATA""
|
# Modyfikujemy argumenty w locie
|
||||||
|
declare -a NEW_ARGS
|
||||||
|
HAS_VF=0
|
||||||
|
IS_COPY=0
|
||||||
|
|
||||||
|
for (( i=1; i<=$#; i++ )); do
|
||||||
|
arg=""${{!i}}""
|
||||||
|
|
||||||
|
if [[ ""$arg"" == ""copy"" ]]; then
|
||||||
|
let prev_idx=i-1
|
||||||
|
prev_arg=""""
|
||||||
|
if [ $prev_idx -gt 0 ]; then
|
||||||
|
prev_arg=""${{!prev_idx}}""
|
||||||
|
fi
|
||||||
|
if [[ ""$prev_arg"" == *codec:v* ]] || [[ ""$prev_arg"" == *-c:v* ]] || [[ ""$prev_arg"" == *-vcodec* ]]; then
|
||||||
|
IS_COPY=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ""$arg"" == ""-vf"" ]]; then
|
||||||
|
HAS_VF=1
|
||||||
|
NEW_ARGS+=(""-vf"")
|
||||||
|
next_arg=""""
|
||||||
|
let next_idx=i+1
|
||||||
|
if [ $next_idx -le $# ]; then
|
||||||
|
next_arg=""${{!next_idx}}""
|
||||||
|
fi
|
||||||
|
NEW_ARGS+=(""crop=$CROP_DATA,$next_arg"")
|
||||||
|
let i++
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_ARGS+=(""$arg"")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Jeżeli Jellyfin nadal upiera się przy copy, nie możemy wstrzyknąć filtra crop (bo zcrashuje FFmpega)
|
||||||
|
if [ $IS_COPY -eq 1 ]; then
|
||||||
|
exec ""$REAL_FFMPEG"" ""$@""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $HAS_VF -eq 0 ]; then
|
||||||
|
NEW_ARGS+=(""-vf"")
|
||||||
|
NEW_ARGS+=(""crop=$CROP_DATA"")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec ""$REAL_FFMPEG"" ""${{NEW_ARGS[@]}}""
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Zwykłe uruchomienie bez crop
|
|
||||||
exec ""$REAL_FFMPEG"" ""$@""
|
exec ""$REAL_FFMPEG"" ""$@""
|
||||||
";
|
";
|
||||||
File.WriteAllText(wrapperPath, script);
|
File.WriteAllText(wrapperPath, script);
|
||||||
|
|||||||
Reference in New Issue
Block a user