Replace on-disk index.html patching with IStartupFilter request-time injection
This commit is contained in:
@@ -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<HtmlPatcher> _logger;
|
|
||||||
|
|
||||||
public HtmlPatcher(IApplicationPaths appPaths, ILogger<HtmlPatcher> 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 = "<script src=\"/BlackBarRemover/ClientScript.js\"></script>";
|
|
||||||
|
|
||||||
if (!indexContent.Contains("BlackBarRemover/ClientScript.js"))
|
|
||||||
{
|
|
||||||
indexContent = indexContent.Replace("</body>", $" {scriptTag}\n</body>");
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ 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
|
// Rejestracja serwisów tła
|
||||||
serviceCollection.AddHostedService<HtmlPatcher>();
|
serviceCollection.AddSingleton<Microsoft.AspNetCore.Hosting.IStartupFilter, ScriptInjectionStartupFilter>();
|
||||||
serviceCollection.AddHostedService<WrapperGenerator>();
|
serviceCollection.AddHostedService<WrapperGenerator>();
|
||||||
serviceCollection.AddHostedService<LibraryMonitor>();
|
serviceCollection.AddHostedService<LibraryMonitor>();
|
||||||
|
|
||||||
|
|||||||
@@ -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<ScriptInjectionStartupFilter> _logger;
|
||||||
|
private int _loggedOnce;
|
||||||
|
|
||||||
|
public ScriptInjectionStartupFilter(ILogger<ScriptInjectionStartupFilter> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||||
|
{
|
||||||
|
return app =>
|
||||||
|
{
|
||||||
|
app.Use(InvokeAsync);
|
||||||
|
next(app);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InvokeAsync(HttpContext context, Func<Task> 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("</body>", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!alreadyInjected && bodyClose >= 0)
|
||||||
|
{
|
||||||
|
var tag = "<script src=\"/BlackBarRemover/ClientScript.js\"></script>";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user