This commit is contained in:
CodeDevMLH
2025-12-15 11:27:06 +01:00
parent c867c892de
commit d88c967023
15 changed files with 215 additions and 130 deletions

View File

@@ -6,10 +6,17 @@ using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.Seasonals.Api;
/// <summary>
/// Controller for serving seasonal resources and configuration.
/// </summary>
[ApiController]
[Route("Seasonals")]
public class SeasonalsController : ControllerBase
{
/// <summary>
/// Gets the current plugin configuration.
/// </summary>
/// <returns>The configuration object.</returns>
[HttpGet("Config")]
[Produces("application/json")]
public ActionResult<object> GetConfig()
@@ -22,11 +29,16 @@ public class SeasonalsController : ControllerBase
};
}
/// <summary>
/// Serves embedded resources.
/// </summary>
/// <param name="path">The path to the resource.</param>
/// <returns>The resource file.</returns>
[HttpGet("Resources/{*path}")]
public ActionResult GetResource(string path)
{
// Sanitize path
if (string.IsNullOrWhiteSpace(path) || path.Contains(".."))
if (string.IsNullOrWhiteSpace(path) || path.Contains("..", StringComparison.Ordinal))
{
return BadRequest();
}
@@ -47,7 +59,7 @@ public class SeasonalsController : ControllerBase
return File(stream, contentType);
}
private string GetContentType(string path)
private static string GetContentType(string path)
{
if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) return "application/javascript";
if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return "text/css";

View File

@@ -37,6 +37,7 @@
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
<div class="fieldDescription" style="margin-top: 1em;">Please reload the page (F5) after saving for changes to take effect.</div>
</div>
</form>
</div>

View File

@@ -13,6 +13,7 @@
<Authors>CodeDevMLH</Authors>
<Company>CodeDevMLH</Company>
<Product>Jellyfin Seasonals Plugin</Product>
<Version>1.0.0.0</Version>
<PackageProjectUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</PackageProjectUrl>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
</PropertyGroup>
@@ -26,12 +27,6 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />

View File

@@ -1,24 +1,35 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using MediaBrowser.Common.Configuration;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Handles the injection of the Seasonals script into the Jellyfin web interface.
/// </summary>
public class ScriptInjector
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger;
private const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>";
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="ScriptInjector"/> class.
/// </summary>
/// <param name="appPaths">The application paths.</param>
/// <param name="logger">The logger.</param>
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
{
_appPaths = appPaths;
_logger = logger;
}
/// <summary>
/// Injects the script tag into index.html if it's not already present.
/// </summary>
public void Inject()
{
try
@@ -38,14 +49,15 @@ public class ScriptInjector
}
var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag))
if (content.Contains(ScriptTag, StringComparison.Ordinal))
{
_logger.LogInformation("Seasonals script already injected.");
return;
}
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}");
if (newContent == content)
// Insert before the closing body tag
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal);
if (string.Equals(newContent, content, StringComparison.Ordinal))
{
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
return;
@@ -60,20 +72,35 @@ public class ScriptInjector
}
}
/// <summary>
/// Removes the script tag from index.html.
/// </summary>
public void Remove()
{
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath)) return;
if (string.IsNullOrEmpty(webPath))
{
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) return;
if (!File.Exists(indexPath))
{
return;
}
var content = File.ReadAllText(indexPath);
if (!content.Contains(ScriptTag)) return;
if (!content.Contains(ScriptTag, StringComparison.Ordinal))
{
return;
}
var newContent = content.Replace(ScriptTag, "").Replace($"{ScriptTag}\n", "");
// Try to remove with newline first, then just the tag to ensure clean removal
var newContent = content.Replace($"{ScriptTag}\n", "", StringComparison.Ordinal)
.Replace(ScriptTag, "", StringComparison.Ordinal);
File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
}
@@ -83,37 +110,14 @@ public class ScriptInjector
}
}
/// <summary>
/// Retrieves the path to the Jellyfin web interface directory.
/// </summary>
/// <returns>The path to the web directory, or null if not found.</returns>
private string? GetWebPath()
{
// Try to find the web path using IApplicationPaths
// Note: The property name might vary depending on the Jellyfin version,
// but usually it's accessible via the configuration or standard paths.
// For this plugin context, we'll try to rely on the standard path structure if IApplicationPaths doesn't expose it directly
// or if we need to infer it.
// In many Jellyfin versions, appPaths.WebPath is the property.
// However, since we are in a plugin, we might need to use reflection if the interface doesn't expose it in the reference assembly,
// or just assume it's there.
// Let's try to use the property if it exists.
// If the compilation fails, we will adjust.
// For now, we assume 'WebPath' is available on IApplicationPaths implementation
// but it might not be on the interface in the nuget package.
// Workaround: Check known locations if property is missing or use reflection.
// Attempt 1: Reflection to get WebPath property (safest if interface varies)
var prop = _appPaths.GetType().GetProperty("WebPath");
if (prop != null)
{
return prop.GetValue(_appPaths) as string;
}
// Attempt 2: Guess based on ProgramDataPath (common in Windows)
// Usually <ProgramData>/jellyfin/web or <InstallDir>/jellyfin-web
// Let's try to look relative to the application executable if possible, but that's hard from a plugin.
return null;
// Use reflection to access WebPath property to ensure compatibility across different Jellyfin versions
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string;
}
}

View File

@@ -9,10 +9,7 @@
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
"dependencies": {
"Jellyfin.Controller": "10.11.0",
"Jellyfin.Model": "10.11.0",
"SerilogAnalyzer": "0.15.0",
"SmartAnalyzers.MultithreadingAnalyzer": "1.1.31",
"StyleCop.Analyzers": "1.2.0-beta.556"
"Jellyfin.Model": "10.11.0"
},
"runtime": {
"Jellyfin.Plugin.Seasonals.dll": {}
@@ -166,14 +163,6 @@
}
},
"Polly.Core/8.6.4": {},
"SerilogAnalyzer/0.15.0": {},
"SmartAnalyzers.MultithreadingAnalyzer/1.1.31": {},
"StyleCop.Analyzers/1.2.0-beta.556": {
"dependencies": {
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
"StyleCop.Analyzers.Unstable/1.2.0.556": {},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
@@ -421,34 +410,6 @@
"path": "polly.core/8.6.4",
"hashPath": "polly.core.8.6.4.nupkg.sha512"
},
"SerilogAnalyzer/0.15.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-sVpwfls4MfNnwIXLSGCgaUnV+c9kgJ8ia6GsyRcpd4Vs3gLogSDtSYBYrre2K2u/PNMo8GgG09RehwVnze70Tw==",
"path": "seriloganalyzer/0.15.0",
"hashPath": "seriloganalyzer.0.15.0.nupkg.sha512"
},
"SmartAnalyzers.MultithreadingAnalyzer/1.1.31": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2f2k7bbhDd132ArglCKpzKoWBcp3uzbIFcb4aosnlqIKlfYKDE2HevBVRNVa+LkWFnjXFFWs47Bo96fu8iS//Q==",
"path": "smartanalyzers.multithreadinganalyzer/1.1.31",
"hashPath": "smartanalyzers.multithreadinganalyzer.1.1.31.nupkg.sha512"
},
"StyleCop.Analyzers/1.2.0-beta.556": {
"type": "package",
"serviceable": true,
"sha512": "sha512-llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==",
"path": "stylecop.analyzers/1.2.0-beta.556",
"hashPath": "stylecop.analyzers.1.2.0-beta.556.nupkg.sha512"
},
"StyleCop.Analyzers.Unstable/1.2.0.556": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==",
"path": "stylecop.analyzers.unstable/1.2.0.556",
"hashPath": "stylecop.analyzers.unstable.1.2.0.556.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,

View File

@@ -4,6 +4,24 @@
<name>Jellyfin.Plugin.Seasonals</name>
</assembly>
<members>
<member name="T:Jellyfin.Plugin.Seasonals.Api.SeasonalsController">
<summary>
Controller for serving seasonal resources and configuration.
</summary>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.Api.SeasonalsController.GetConfig">
<summary>
Gets the current plugin configuration.
</summary>
<returns>The configuration object.</returns>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.Api.SeasonalsController.GetResource(System.String)">
<summary>
Serves embedded resources.
</summary>
<param name="path">The path to the resource.</param>
<returns>The resource file.</returns>
</member>
<member name="T:Jellyfin.Plugin.Seasonals.Configuration.PluginConfiguration">
<summary>
Plugin configuration.
@@ -51,5 +69,33 @@
<member name="M:Jellyfin.Plugin.Seasonals.Plugin.GetPages">
<inheritdoc />
</member>
<member name="T:Jellyfin.Plugin.Seasonals.ScriptInjector">
<summary>
Handles the injection of the Seasonals script into the Jellyfin web interface.
</summary>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.ScriptInjector.#ctor(MediaBrowser.Common.Configuration.IApplicationPaths,Microsoft.Extensions.Logging.ILogger{Jellyfin.Plugin.Seasonals.ScriptInjector})">
<summary>
Initializes a new instance of the <see cref="T:Jellyfin.Plugin.Seasonals.ScriptInjector"/> class.
</summary>
<param name="appPaths">The application paths.</param>
<param name="logger">The logger.</param>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.ScriptInjector.Inject">
<summary>
Injects the script tag into index.html if it's not already present.
</summary>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.ScriptInjector.Remove">
<summary>
Removes the script tag from index.html.
</summary>
</member>
<member name="M:Jellyfin.Plugin.Seasonals.ScriptInjector.GetWebPath">
<summary>
Retrieves the path to the Jellyfin web interface directory.
</summary>
<returns>The path to the web directory, or null if not found.</returns>
</member>
</members>
</doc>