Files
Jellyfin-Seasonals-Plugin/feat/ScriptInjectionMiddleware.cs
2025-12-17 01:13:10 +01:00

103 lines
3.7 KiB
C#

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Middleware to inject the Seasonals script into the Jellyfin web interface.
/// </summary>
public class ScriptInjectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ScriptInjectionMiddleware> _logger;
private const string ScriptTag = "<script src=\"Seasonals/Resources/seasonals.js\"></script>";
private const string Marker = "</body>";
/// <summary>
/// Initializes a new instance of the <see cref="ScriptInjectionMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public ScriptInjectionMiddleware(RequestDelegate next, ILogger<ScriptInjectionMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A task representing the asynchronous operation.</returns>
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);
}
}