From ca813bacb725ec31f511036dca4f0be0142c0eed Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Wed, 17 Dec 2025 01:13:10 +0100 Subject: [PATCH] add possible solution for later --- feat/Jellyfin.Plugin.Seasonals.csproj | 34 +++++++++ feat/Plugin.cs | 50 +++++++++++++ feat/PluginServiceRegistrator.cs | 18 +++++ feat/ScriptInjectionMiddleware.cs | 102 ++++++++++++++++++++++++++ feat/ScriptInjectionStartupFilter.cs | 21 ++++++ 5 files changed, 225 insertions(+) create mode 100644 feat/Jellyfin.Plugin.Seasonals.csproj create mode 100644 feat/Plugin.cs create mode 100644 feat/PluginServiceRegistrator.cs create mode 100644 feat/ScriptInjectionMiddleware.cs create mode 100644 feat/ScriptInjectionStartupFilter.cs diff --git a/feat/Jellyfin.Plugin.Seasonals.csproj b/feat/Jellyfin.Plugin.Seasonals.csproj new file mode 100644 index 0000000..a42bea4 --- /dev/null +++ b/feat/Jellyfin.Plugin.Seasonals.csproj @@ -0,0 +1,34 @@ + + + + 10.11.0 + net9.0 + net8.0 + Jellyfin.Plugin.Seasonals + enable + + + + + Jellyfin Seasonals Plugin + CodeDevMLH + 1.1.0.0 + https://github.com/CodeDevMLH/jellyfin-plugin-seasonals + + + + + + + + + + + + + + + + + + diff --git a/feat/Plugin.cs b/feat/Plugin.cs new file mode 100644 index 0000000..a4b513d --- /dev/null +++ b/feat/Plugin.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Plugin.Seasonals.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace Jellyfin.Plugin.Seasonals; + +/// +/// The main plugin. +/// +public class Plugin : BasePlugin, IHasWebPages +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + /// + public override string Name => "Seasonals"; + + /// + public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"); + + /// + /// Gets the current plugin instance. + /// + public static Plugin? Instance { get; private set; } + + /// + public IEnumerable GetPages() + { + return + [ + new PluginPageInfo + { + Name = "seasonals", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html" + } + ]; + } +} diff --git a/feat/PluginServiceRegistrator.cs b/feat/PluginServiceRegistrator.cs new file mode 100644 index 0000000..f3363b0 --- /dev/null +++ b/feat/PluginServiceRegistrator.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Plugin.Seasonals; + +/// +/// Registers plugin services. +/// +public class PluginServiceRegistrator : IPluginServiceRegistrator +{ + /// + public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) + { + serviceCollection.AddTransient(); + } +} diff --git a/feat/ScriptInjectionMiddleware.cs b/feat/ScriptInjectionMiddleware.cs new file mode 100644 index 0000000..9f61642 --- /dev/null +++ b/feat/ScriptInjectionMiddleware.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Seasonals; + +/// +/// Middleware to inject the Seasonals script into the Jellyfin web interface. +/// +public class ScriptInjectionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private const string ScriptTag = ""; + private const string Marker = ""; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + /// The logger. + public ScriptInjectionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + /// + /// Invokes the middleware. + /// + /// The HTTP context. + /// A task representing the asynchronous operation. + public async Task InvokeAsync(HttpContext context) + { + if (IsIndexRequest(context.Request.Path)) + { + var originalBodyStream = context.Response.Body; + using var responseBody = new MemoryStream(); + context.Response.Body = responseBody; + + try + { + await _next(context); + + context.Response.Body = originalBodyStream; + responseBody.Seek(0, SeekOrigin.Begin); + + if (context.Response.StatusCode == 200 && + context.Response.ContentType != null && + context.Response.ContentType.Contains("text/html", StringComparison.OrdinalIgnoreCase)) + { + using var reader = new StreamReader(responseBody); + var content = await reader.ReadToEndAsync(); + + if (!content.Contains(ScriptTag, StringComparison.Ordinal) && content.Contains(Marker, StringComparison.Ordinal)) + { + var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal); + var bytes = Encoding.UTF8.GetBytes(newContent); + + context.Response.ContentLength = bytes.Length; + await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); + return; + } + } + + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBodyStream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error injecting Seasonals script via middleware."); + // Ensure we try to write back the original response if something failed + if (responseBody.Length > 0 && context.Response.Body == originalBodyStream) + { + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBodyStream); + } + } + } + else + { + await _next(context); + } + } + + private static bool IsIndexRequest(PathString path) + { + if (!path.HasValue) + { + return false; + } + + var p = path.Value; + // Check for root, index.html, or web/index.html + return p.Equals("/", StringComparison.OrdinalIgnoreCase) || + p.Equals("/index.html", StringComparison.OrdinalIgnoreCase) || + p.EndsWith("/web/index.html", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/feat/ScriptInjectionStartupFilter.cs b/feat/ScriptInjectionStartupFilter.cs new file mode 100644 index 0000000..fc2e7c5 --- /dev/null +++ b/feat/ScriptInjectionStartupFilter.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Jellyfin.Plugin.Seasonals; + +/// +/// Startup filter to register the ScriptInjectionMiddleware. +/// +public class ScriptInjectionStartupFilter : IStartupFilter +{ + /// + public Action Configure(Action next) + { + return builder => + { + builder.UseMiddleware(); + next(builder); + }; + } +}