add possible solution for later

This commit is contained in:
CodeDevMLH
2025-12-17 01:13:10 +01:00
parent 9bc6aedf87
commit ca813bacb7
5 changed files with 225 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<JellyfinVersion>10.11.0</JellyfinVersion>
<TargetFramework Condition="$(JellyfinVersion.StartsWith('10.11'))">net9.0</TargetFramework>
<TargetFramework Condition="!$(JellyfinVersion.StartsWith('10.11'))">net8.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Seasonals</RootNamespace>
<Nullable>enable</Nullable>
<!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode> -->
<!-- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> -->
<!-- <GenerateDocumentationFile>true</GenerateDocumentationFile> -->
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.1.0.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" />
<PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
<None Remove="Web\**" />
<EmbeddedResource Include="Web\**" />
<None Include="..\README.md" />
<None Include="..\logo.png" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

50
feat/Plugin.cs Normal file
View File

@@ -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;
/// <summary>
/// The main plugin.
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <inheritdoc />
public override string Name => "Seasonals";
/// <inheritdoc />
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
/// <summary>
/// Gets the current plugin instance.
/// </summary>
public static Plugin? Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return
[
new PluginPageInfo
{
Name = "seasonals",
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
}
];
}
}

View File

@@ -0,0 +1,18 @@
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Registers plugin services.
/// </summary>
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddTransient<IStartupFilter, ScriptInjectionStartupFilter>();
}
}

View File

@@ -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;
/// <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);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Startup filter to register the ScriptInjectionMiddleware.
/// </summary>
public class ScriptInjectionStartupFilter : IStartupFilter
{
/// <inheritdoc />
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<ScriptInjectionMiddleware>();
next(builder);
};
}
}