From e2c286e42cc24a3bc63b709372d3b8625145f28e Mon Sep 17 00:00:00 2001 From: Karol Labus Date: Thu, 17 Sep 2026 16:08:24 +0200 Subject: [PATCH] Replace on-disk index.html patching with IStartupFilter request-time injection --- JellyfinPlugin/HtmlPatcher.cs | 92 ------------- JellyfinPlugin/PluginServiceRegistrator.cs | 2 +- .../ScriptInjectionStartupFilter.cs | 125 ++++++++++++++++++ 3 files changed, 126 insertions(+), 93 deletions(-) delete mode 100644 JellyfinPlugin/HtmlPatcher.cs create mode 100644 JellyfinPlugin/ScriptInjectionStartupFilter.cs diff --git a/JellyfinPlugin/HtmlPatcher.cs b/JellyfinPlugin/HtmlPatcher.cs deleted file mode 100644 index 7f8a000..0000000 --- a/JellyfinPlugin/HtmlPatcher.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Logging; - -using Microsoft.Extensions.Hosting; - -namespace JellyfinPlugin -{ - public class HtmlPatcher : IHostedService - { - private readonly IApplicationPaths _appPaths; - private readonly ILogger _logger; - - public HtmlPatcher(IApplicationPaths appPaths, ILogger logger) - { - _appPaths = appPaths; - _logger = logger; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - PatchHtml(); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private void PatchHtml() - { - try - { - // Znajdujemy ścieżkę do web UI Jellyfina. - // W większości instalacji linuksowych (np. Debian/Ubuntu) to: - string[] possiblePaths = { - Path.Combine(_appPaths.ProgramDataPath, "web"), - "/usr/share/jellyfin/web", - "/jellyfin/jellyfin-web", // Docker - }; - - string webPath = string.Empty; - foreach (var path in possiblePaths) - { - if (Directory.Exists(path) && File.Exists(Path.Combine(path, "index.html"))) - { - webPath = path; - break; - } - } - - if (string.IsNullOrEmpty(webPath)) - { - _logger.LogWarning("Failed to find Jellyfin Web directory. Manual installation of the client script is required."); - return; - } - - string indexPath = Path.Combine(webPath, "index.html"); - string indexContent = File.ReadAllText(indexPath); - - string scriptTag = ""; - - if (!indexContent.Contains("BlackBarRemover/ClientScript.js")) - { - indexContent = indexContent.Replace("", $" {scriptTag}\n"); - File.WriteAllText(indexPath, indexContent); - _logger.LogInformation("Successfully injected BlackBarRemover script into index.html at {Path}", indexPath); - } - else - { - _logger.LogInformation("BlackBarRemover client script is already injected into index.html"); - } - } - catch (UnauthorizedAccessException) - { - _logger.LogWarning("Permission denied to modify index.html. Change Jellyfin Web file permissions or add the script manually in Custom CSS."); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while injecting the script into index.html."); - } - } - - public void Dispose() - { - } - } -} diff --git a/JellyfinPlugin/PluginServiceRegistrator.cs b/JellyfinPlugin/PluginServiceRegistrator.cs index 099a332..056cde9 100644 --- a/JellyfinPlugin/PluginServiceRegistrator.cs +++ b/JellyfinPlugin/PluginServiceRegistrator.cs @@ -9,7 +9,7 @@ namespace JellyfinPlugin public void RegisterServices(IServiceCollection serviceCollection, MediaBrowser.Controller.IServerApplicationHost applicationHost) { // Rejestracja serwisów tła - serviceCollection.AddHostedService(); + serviceCollection.AddSingleton(); serviceCollection.AddHostedService(); serviceCollection.AddHostedService(); diff --git a/JellyfinPlugin/ScriptInjectionStartupFilter.cs b/JellyfinPlugin/ScriptInjectionStartupFilter.cs new file mode 100644 index 0000000..6e1012f --- /dev/null +++ b/JellyfinPlugin/ScriptInjectionStartupFilter.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace JellyfinPlugin +{ + public class ScriptInjectionStartupFilter : IStartupFilter + { + private readonly ILogger _logger; + private int _loggedOnce; + + public ScriptInjectionStartupFilter(ILogger logger) + { + _logger = logger; + } + + public Action Configure(Action next) + { + return app => + { + app.Use(InvokeAsync); + next(app); + }; + } + + private async Task InvokeAsync(HttpContext context, Func nextMw) + { + if (!IsIndexRequest(context.Request.Path.Value)) + { + await nextMw().ConfigureAwait(false); + return; + } + + if (!HttpMethods.IsGet(context.Request.Method)) + { + await nextMw().ConfigureAwait(false); + return; + } + + var config = Plugin.Instance?.Configuration; + if (config == null || !config.IsEnabled) + { + await nextMw().ConfigureAwait(false); + return; + } + + context.Request.Headers.Remove("Accept-Encoding"); + context.Request.Headers.Remove("Range"); + context.Request.Headers.Remove("If-Range"); + + 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 isHtml = context.Response.StatusCode == 200 + && (context.Response.ContentType?.Contains("text/html", StringComparison.OrdinalIgnoreCase) ?? false); + + if (!isHtml) + { + await buffer.CopyToAsync(originalBody).ConfigureAwait(false); + return; + } + + string html; + using (var reader = new StreamReader(buffer, Encoding.UTF8, true, 1024, leaveOpen: true)) + { + html = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + try + { + var alreadyInjected = html.IndexOf("BlackBarRemover/ClientScript.js", StringComparison.OrdinalIgnoreCase) >= 0; + var bodyClose = html.LastIndexOf("", StringComparison.OrdinalIgnoreCase); + + if (!alreadyInjected && bodyClose >= 0) + { + var tag = ""; + html = html.Substring(0, bodyClose) + tag + "\n" + html.Substring(bodyClose); + + if (System.Threading.Interlocked.Exchange(ref _loggedOnce, 1) == 0) + { + _logger.LogInformation("Black Bar Remover: injected the client script via request-time middleware (IStartupFilter)."); + } + } + } + catch (Exception ex) + { + _logger.LogWarning($"Script injection middleware error (serving original HTML): {ex.Message}"); + } + + var bytes = Encoding.UTF8.GetBytes(html); + context.Response.ContentType = "text/html;charset=utf-8"; + context.Response.ContentLength = bytes.Length; + context.Response.Headers.Remove("ETag"); + context.Response.Headers.Remove("Last-Modified"); + context.Response.Headers.Remove("Accept-Ranges"); + await originalBody.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + } + + private static bool IsIndexRequest(string? path) + { + if (string.IsNullOrEmpty(path)) return false; + return path.EndsWith("/web/index.html", StringComparison.OrdinalIgnoreCase) + || path.EndsWith("/web/", StringComparison.OrdinalIgnoreCase) + || path.Equals("/web", StringComparison.OrdinalIgnoreCase); + } + } +}