Compare commits

...

10 Commits

Author SHA1 Message Date
karol 0cfaa50805 fix: update manifest on master branch via Gitea API
Build and Release Plugin / release (push) Failing after 33s
2026-09-17 21:17:44 +02:00
karol fea812d3a3 fix: update manifest via Gitea API in release workflow
Build and Release Plugin / release (push) Failing after 32s
2026-09-17 21:15:28 +02:00
karol fe6e20a68a fix: use authenticated git push URL in release workflow
Build and Release Plugin / release (push) Failing after 33s
2026-09-17 21:12:40 +02:00
karol b7693b606e fix: replace release assets cleanly in Gitea Action
Build and Release Plugin / release (push) Failing after 34s
2026-09-17 21:09:12 +02:00
karol 13f9fd642c fix: make Gitea release step idempotent in workflow
Build and Release Plugin / release (push) Successful in 34s
2026-09-17 21:05:29 +02:00
karol 322cfd9acd fix: fix server mode crop application, request rewrite, and prevent infinite library scan loop
Build and Release Plugin / release (push) Failing after 1m7s
2026-09-17 20:51:54 +02:00
karol 648930aa21 fix: remove RequiresOpening/RequiresClosing from PlaybackInfo rewrite to prevent LiveStream errors in Jellyfin 12
Build and Release Plugin / release (push) Successful in 35s
2026-09-17 20:07:27 +02:00
karol cb16bf0074 fix(server-mode): handle quote stripping in wrapper and empty crop checks
Build and Release Plugin / release (push) Successful in 31s
2026-09-17 19:54:01 +02:00
karol f6ed790801 fix: resolve ffmpeg path dynamically, handle empty crop check, and strip quotes in wrapper
Build and Release Plugin / release (push) Successful in 37s
2026-09-17 19:26:25 +02:00
karol d5e11a52ed fix(ci): point sourceUrl to zip archive download link in manifest.json
Build and Release Plugin / release (push) Successful in 31s
2026-09-17 18:29:04 +02:00
8 changed files with 163 additions and 180 deletions
+44 -21
View File
@@ -54,18 +54,30 @@ jobs:
TAG="${{ github.ref_name }}"
IS_PRERELEASE="false"
if [[ "$TAG" == *"beta"* ]] || [[ "$TAG" == *"alpha"* ]] || [[ "$TAG" == v0.* ]]; then
if [[ "$TAG" =~ beta|alpha|^v0\. ]]; then
IS_PRERELEASE="true"
fi
# Create Release
RELEASE_RES=$(curl -s -X POST "$API_URL/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\", \"name\":\"Release $TAG\", \"body\":\"Automated release\", \"draft\":false, \"prerelease\":$IS_PRERELEASE}")
# Check if release exists
EXISTING_REL=$(curl -s -H "Authorization: token $TOKEN" "$API_URL/repos/$REPO/releases/tags/$TAG")
REL_ID=$(echo "$EXISTING_REL" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
REL_ID=$(echo "$RELEASE_RES" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
if [ -z "$REL_ID" ] || [ "$REL_ID" == "null" ]; then
RELEASE_RES=$(curl -s -X POST "$API_URL/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\", \"name\":\"Release $TAG\", \"body\":\"Automated release\", \"draft\":false, \"prerelease\":$IS_PRERELEASE}")
REL_ID=$(echo "$RELEASE_RES" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
fi
# Delete existing assets for this release if present
ASSETS=$(curl -s -H "Authorization: token $TOKEN" "$API_URL/repos/$REPO/releases/$REL_ID/assets")
echo "$ASSETS" | grep -o '"id":[0-9]*' | cut -d':' -f2 | while read -r asset_id; do
if [ -n "$asset_id" ]; then
curl -s -X DELETE -H "Authorization: token $TOKEN" "$API_URL/repos/$REPO/releases/$REL_ID/assets/$asset_id"
fi
done
# Upload Assets
curl -s -X POST "$API_URL/repos/$REPO/releases/$REL_ID/assets" \
-H "Authorization: token $TOKEN" \
@@ -75,12 +87,11 @@ jobs:
-H "Authorization: token $TOKEN" \
-F "attachment=@BlackBarRemover_${TAG}_jf12.0.zip"
- name: Update manifest.json
- name: Update manifest.json via API
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
git checkout -B manifest
API_URL="${{ github.api_url }}"
REPO="${{ github.repository }}"
TOKEN="${{ github.token }}"
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
@@ -101,25 +112,37 @@ jobs:
"version": "$CLEAN_VERSION",
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 12.0)",
"targetAbi": "12.0.0.0",
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf12.0.zip",
"checksum": "${{ steps.checksum.outputs.md5_10 }}",
"timestamp": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",
"repositoryUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf12.0.zip"
"timestamp": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
},
{
"version": "$CLEAN_VERSION",
"changelog": "Automated Release ${{ github.ref_name }} (Jellyfin 10.11)",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover",
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf10.11.zip",
"checksum": "${{ steps.checksum.outputs.md5_9 }}",
"timestamp": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",
"repositoryUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/${{ github.ref_name }}/BlackBarRemover_${{ github.ref_name }}_jf10.11.zip"
"timestamp": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
}
]
}
]
EOF
git add manifest.json
git commit -m "Update manifest for ${{ github.ref_name }}"
git push -f origin manifest
B64_CONTENT=$(base64 -w0 manifest.json)
# Update on master branch
EXISTING_MANIFEST=$(curl -s -H "Authorization: token $TOKEN" "$API_URL/repos/$REPO/contents/manifest.json?ref=master")
FILE_SHA=$(echo "$EXISTING_MANIFEST" | grep -o '"sha":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$FILE_SHA" ]; then
curl -s -X PUT "$API_URL/repos/$REPO/contents/manifest.json" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"content\":\"$B64_CONTENT\", \"message\":\"Update manifest.json for ${{ github.ref_name }}\", \"sha\":\"$FILE_SHA\", \"branch\":\"master\"}"
else
curl -s -X PUT "$API_URL/repos/$REPO/contents/manifest.json" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"content\":\"$B64_CONTENT\", \"message\":\"Create manifest.json for ${{ github.ref_name }}\", \"branch\":\"master\"}"
fi
+1
View File
@@ -25,6 +25,7 @@ namespace JellyfinPlugin.Api
public ContentResult GetCropByPath([FromQuery] string path)
{
if (string.IsNullOrEmpty(path)) return Content(string.Empty, "text/plain");
path = path.Trim('"').Trim('\'');
var mode = Plugin.Instance!.Configuration.PlaybackMode;
if (mode == "Client") return Content(string.Empty, "text/plain");
+8 -1
View File
@@ -9,6 +9,13 @@ namespace JellyfinPlugin
{
public static class CropHelper
{
private static string GetFFmpegPath()
{
if (File.Exists("/usr/lib/jellyfin-ffmpeg/ffmpeg")) return "/usr/lib/jellyfin-ffmpeg/ffmpeg";
if (File.Exists("/usr/bin/ffmpeg")) return "/usr/bin/ffmpeg";
return "ffmpeg";
}
public static async Task<string> DetectCropAsync(string filePath, double limit)
{
// FFmpeg w nowych wersjach przyjmuje limit w formie ułamka, np. 24/255 to ok 0.094
@@ -17,7 +24,7 @@ namespace JellyfinPlugin
var startInfo = new ProcessStartInfo
{
FileName = "ffmpeg",
FileName = GetFFmpegPath(),
// KLUCZOWE: reset=1, żeby oceniał każdą klatkę niezależnie!
Arguments = $"-skip_frame nokey -i \"{filePath}\" -vf \"cropdetect=limit={limitStr}:round=16:reset=1\" -f null -",
RedirectStandardError = true,
+17 -1
View File
@@ -23,6 +23,8 @@ namespace JellyfinPlugin
private readonly ILogger<LibraryMonitor> _logger;
private readonly SemaphoreSlim _ffmpegSemaphore = new SemaphoreSlim(2, 2);
private readonly ConcurrentDictionary<Guid, byte> _processedItems = new ConcurrentDictionary<Guid, byte>();
public LibraryMonitor(ILibraryManager libraryManager, ILogger<LibraryMonitor> logger)
{
_libraryManager = libraryManager;
@@ -48,6 +50,16 @@ namespace JellyfinPlugin
{
if (!Plugin.Instance!.Configuration.IsEnabled) return;
if (_processedItems.ContainsKey(video.Id)) return;
if (video.ProviderIds != null &&
video.ProviderIds.TryGetValue("BlackBarCrop", out string? existingCrop) &&
!string.IsNullOrEmpty(existingCrop))
{
_processedItems.TryAdd(video.Id, 0);
return;
}
// Fire and forget, don't block library scan
_ = Task.Run(() => ProcessVideoAsync(video));
}
@@ -57,7 +69,11 @@ namespace JellyfinPlugin
{
try
{
if (video.ProviderIds != null && video.ProviderIds.ContainsKey("BlackBarCrop"))
if (!_processedItems.TryAdd(video.Id, 0)) return;
if (video.ProviderIds != null &&
video.ProviderIds.TryGetValue("BlackBarCrop", out string? existingCrop) &&
!string.IsNullOrEmpty(existingCrop))
{
return;
}
@@ -63,8 +63,8 @@ namespace JellyfinPlugin
// --- 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))
if (path.Contains("/Videos/", StringComparison.OrdinalIgnoreCase) &&
(path.Contains(".m3u8", StringComparison.OrdinalIgnoreCase) || path.Contains("/stream", StringComparison.OrdinalIgnoreCase)))
{
var matchId = Regex.Match(path, @"/Videos/([^/]+)/", RegexOptions.IgnoreCase);
if (matchId.Success)
@@ -112,113 +112,40 @@ namespace JellyfinPlugin
return;
}
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepting PlaybackInfo for {itemId}. Found crop in DB: {cropData}. Forcing transcode.");
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepting PlaybackInfo request for {itemId}. Found crop in DB: {cropData}. Disabling DirectPlayProfiles in incoming request.");
// 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;
// Modify incoming request body to clear DirectPlayProfiles
context.Request.EnableBuffering();
string reqJson = string.Empty;
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true))
{
reqJson = await reader.ReadToEndAsync().ConfigureAwait(false);
}
JsonNode rootNode;
try
{
await nextMw().ConfigureAwait(false);
rootNode = string.IsNullOrWhiteSpace(reqJson) ? new JsonObject() : (JsonNode.Parse(reqJson) ?? new JsonObject());
}
catch
{
context.Response.Body = originalBody;
throw;
rootNode = new JsonObject();
}
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)
if (rootNode["DeviceProfile"] is not JsonObject devProfile)
{
await buffer.CopyToAsync(originalBody).ConfigureAwait(false);
return;
devProfile = new JsonObject();
rootNode["DeviceProfile"] = devProfile;
}
string json;
using (var reader = new StreamReader(buffer, Encoding.UTF8, true, 1024, leaveOpen: true))
{
json = await reader.ReadToEndAsync().ConfigureAwait(false);
}
devProfile["DirectPlayProfiles"] = new JsonArray();
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;
var newReqBytes = Encoding.UTF8.GetBytes(rootNode.ToJsonString());
var reqMs = new MemoryStream(newReqBytes);
context.Request.Body = reqMs;
context.Request.ContentLength = newReqBytes.Length;
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);
await nextMw().ConfigureAwait(false);
}
}
}
+8 -10
View File
@@ -58,18 +58,16 @@ namespace JellyfinPlugin.Tasks
{
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))
{
if (item.ProviderIds == null)
{
item.ProviderIds = new Dictionary<string, string>();
}
item.ProviderIds["BlackBarCrop"] = cropData;
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, cancellationToken);
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
}
}
catch (Exception ex)
{
@@ -48,7 +48,9 @@ namespace JellyfinPlugin.Tasks
{
cancellationToken.ThrowIfCancellationRequested();
if (item.ProviderIds != null && item.ProviderIds.ContainsKey("BlackBarCrop"))
if (item.ProviderIds != null &&
item.ProviderIds.TryGetValue("BlackBarCrop", out string? existingCrop) &&
!string.IsNullOrEmpty(existingCrop))
{
processed++;
progress.Report((processed / (double)total) * 100);
@@ -62,18 +64,16 @@ namespace JellyfinPlugin.Tasks
{
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))
{
if (item.ProviderIds == null)
{
item.ProviderIds = new Dictionary<string, string>();
}
item.ProviderIds["BlackBarCrop"] = cropData;
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, cancellationToken);
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
}
}
catch (Exception ex)
{
+52 -41
View File
@@ -44,37 +44,34 @@ namespace JellyfinPlugin
return;
}
// Pobieramy obecną konfigurację kodowania
var encodingConfig = _configManager.GetConfiguration("encoding");
if (encodingConfig == null) return;
// Przygotowujemy zamianę pliku ffmpeg bezpośrednio w /usr/lib/jellyfin-ffmpeg/ffmpeg
string defaultFfmpeg = "/usr/lib/jellyfin-ffmpeg/ffmpeg";
string origFfmpeg = "/usr/lib/jellyfin-ffmpeg/ffmpeg.original";
string realFfmpegPath = origFfmpeg;
// Używamy dynamic, aby ominąć potrzebę rzutowania na konkretny, wewnętrzny typ Jellyfin (EncodingOptions)
dynamic dynConfig = encodingConfig;
string currentFfmpegPath = dynConfig.EncoderAppPath ?? "/usr/lib/jellyfin-ffmpeg/ffmpeg";
if (!File.Exists(origFfmpeg))
{
if (File.Exists(defaultFfmpeg))
{
try
{
File.Move(defaultFfmpeg, origFfmpeg);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Nie udało się przenieść {DefaultFfmpeg} do {OrigFfmpeg}", defaultFfmpeg, origFfmpeg);
realFfmpegPath = defaultFfmpeg;
}
}
else if (File.Exists("/usr/bin/ffmpeg"))
{
realFfmpegPath = "/usr/bin/ffmpeg";
}
}
string wrapperDir = Path.Combine(_appPaths.PluginsPath, "BlackBarRemover");
Directory.CreateDirectory(wrapperDir);
string wrapperPath = Path.Combine(wrapperDir, "ffmpeg-wrapper.sh");
// Jeśli obecna ścieżka to już nasz wrapper, nie robimy pętli w nieskończoność.
// Znajdujemy poprzednio zapisaną prawdziwą ścieżkę z pliku.
string realFfmpegPath = currentFfmpegPath;
if (currentFfmpegPath == wrapperPath)
{
if (File.Exists(wrapperPath))
{
var lines = File.ReadAllLines(wrapperPath);
foreach (var line in lines)
{
if (line.StartsWith("REAL_FFMPEG="))
{
realFfmpegPath = line.Split('=')[1].Trim('"');
break;
}
}
}
if (realFfmpegPath == wrapperPath) realFfmpegPath = "/usr/lib/jellyfin-ffmpeg/ffmpeg";
}
string wrapperPath = Path.Combine(wrapperDir, "ffmpeg");
string script = $@"#!/bin/bash
REAL_FFMPEG=""{realFfmpegPath}""
@@ -92,9 +89,7 @@ for (( i=1; i<=$#; i++ )); do
done
if [ -n ""$INPUT_FILE"" ]; then
# 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:}}""
CLEAN_PATH=$(echo ""$INPUT_FILE"" | tr -d '""' | sed 's/^file://')
CROP_DATA=$(curl -s --max-time 1 -G --data-urlencode ""Path=$CLEAN_PATH"" http://127.0.0.1:8096/BlackBarRemover/CropByPath)
@@ -126,7 +121,7 @@ if [ -n ""$INPUT_FILE"" ]; then
if [ $next_idx -le $# ]; then
next_arg=""${{!next_idx}}""
fi
NEW_ARGS+=(""crop=$CROP_DATA,$next_arg"")
NEW_ARGS+=(""$next_arg,crop=$CROP_DATA"")
let i++
continue
fi
@@ -153,21 +148,37 @@ fi
exec ""$REAL_FFMPEG"" ""$@""
";
File.WriteAllText(wrapperPath, script);
if (Environment.OSVersion.Platform == PlatformID.Unix)
if (File.Exists(origFfmpeg) || defaultFfmpeg != origFfmpeg)
{
System.Diagnostics.Process.Start("chmod", $"+x \"{wrapperPath}\"");
try
{
File.WriteAllText(defaultFfmpeg, script);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Nie udało się zapisać skryptu wrappera do {DefaultFfmpeg}", defaultFfmpeg);
}
}
_logger.LogInformation("Wygenerowano skrypt FFmpeg Wrapper w lokalizacji: {Path}", wrapperPath);
// Teraz automagicznie podmieniamy ścieżkę w Jellyfin!
if (dynConfig.EncoderAppPath != wrapperPath)
if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD() || OperatingSystem.IsMacOS())
{
dynConfig.EncoderAppPath = wrapperPath;
_configManager.SaveConfiguration("encoding", encodingConfig);
_logger.LogInformation("Automatycznie zmieniono ścieżkę FFmpeg w konfiguracji Jellyfin na wrapper wtyczki.");
string[] pathsToChmod = new[] { wrapperPath, defaultFfmpeg };
foreach (var path in pathsToChmod)
{
if (!File.Exists(path)) continue;
try
{
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
catch
{
var proc = System.Diagnostics.Process.Start("chmod", $"+x \"{path}\"");
proc?.WaitForExit();
}
}
}
_logger.LogInformation("Wygenerowano skrypt FFmpeg Wrapper w lokalizacji: {Path} oraz {DefaultFfmpeg}", wrapperPath, defaultFfmpeg);
}
catch (Exception ex)
{