103 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|