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;
}
// 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);
var newContent = content.Replace(ScriptTag, "").Replace($"{ScriptTag}\n", "");
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>

59
RELEASE_GUIDE.md Normal file
View File

@@ -0,0 +1,59 @@
# Release & Update Guide
Diese Anleitung beschreibt die Schritte, die notwendig sind, um eine neue Version des **Seasonals** Plugins zu veröffentlichen.
## 1. Version erhöhen
Bevor du baust, musst du die Versionsnummer in den folgenden Dateien aktualisieren (z.B. von `1.0.0.0` auf `1.0.1.0`):
1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`**
Suche nach `<Version>...</Version>` und ändere die Nummer.
2. **`build.yaml`**
Ändere den Wert bei `version: "..."`.
3. **`manifest.json`**
Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab).
* `version`: Deine neue Nummer.
* `changelog`: Was hat sich geändert?
* `timestamp`: Das aktuelle Datum (wird später aktualisiert).
* `checksum`: (wird nach dem Build aktualisiert).
## 2. Plugin bauen und packen
Führe den folgenden Befehl im Terminal (PowerShell) im Hauptverzeichnis aus. Dieser Befehl baut das Projekt, erstellt das ZIP-Archiv und berechnet direkt die Checksumme (Hash).
```powershell
dotnet publish Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish; Compress-Archive -Path bin/Publish/* -DestinationPath bin/Publish/Jellyfin.Plugin.Seasonals.zip -Force; $hash = (Get-FileHash -Algorithm MD5 bin/Publish/Jellyfin.Plugin.Seasonals.zip).Hash.ToLower(); $time = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); Write-Output "`n----------------------------------------"; Write-Output "NEUE CHECKSUMME (MD5): $hash"; Write-Output "ZEITSTEMPEL: $time"; Write-Output "----------------------------------------`n"
```
## 3. Manifest aktualisieren
Nachdem der Befehl durchgelaufen ist, siehst du am Ende eine Ausgabe wie:
```text
----------------------------------------
NEUE CHECKSUMME (MD5): ef8654666ffeae9695e660944f644ad3
ZEITSTEMPEL: 2025-12-15T12:34:56Z
----------------------------------------
```
1. Kopiere die **Checksumme**.
2. Öffne `manifest.json`.
3. Füge die Checksumme bei deinem Versionseintrag unter `"checksum"` ein.
4. Kopiere den **Zeitstempel** und füge ihn unter `"timestamp"` ein.
5. Stelle sicher, dass die `sourceUrl` korrekt auf dein Repository zeigt.
## 4. Veröffentlichen
1. Lade die Datei `bin/Publish/Jellyfin.Plugin.Seasonals.zip` irgendwo hoch (z.B. GitHub Releases).
2. Stelle sicher, dass die `sourceUrl` im `manifest.json` auf diesen Download zeigt (oder auf das Repo, je nachdem wie Jellyfin das handhabt - meistens ist `sourceUrl` der Link zum ZIP).
* *Hinweis:* Wenn du das Plugin über ein Repo hostest, muss die URL im Manifest direkt auf die ZIP-Datei zeigen.
## Zusammenfassung der Dateien
| Datei | Zweck | Änderung nötig? |
| :--- | :--- | :--- |
| `Jellyfin.Plugin.Seasonals.csproj` | Definiert die DLL-Version | **Ja** |
| `build.yaml` | Build-Konfiguration | **Ja** |
| `manifest.json` | Plugin-Liste für Jellyfin | **Ja** (Version, Hash, Zeit) |

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>

View File

@@ -13,8 +13,8 @@
"changelog": "Initial release",
"targetAbi": "10.9.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/bin/Publish/Jellyfin.Plugin.Seasonals.zip",
"checksum": "f7e8c352385768c118d1577d09dcff7e",
"timestamp": "2025-12-14T22:50:50Z"
"checksum": "87990236bd9337eda22757423d896e22",
"timestamp": "2025-12-14T23:50:27Z"
}
]
}