10 Commits

Author SHA1 Message Date
CodeDevMLH
4433cbb949 feat: Update to version 1.2.0.0 with script injection improvements and fallback support 2025-12-17 16:34:02 +01:00
CodeDevMLH
ca813bacb7 add possible solution for later 2025-12-17 01:13:10 +01:00
CodeDevMLH
9bc6aedf87 fix path 2025-12-17 01:12:58 +01:00
MLH
ddfbf40bf7 manifest-v1.json hinzugefügt 2025-12-16 23:40:40 +01:00
CodeDevMLH
9ce88e19ad fix: Update sourceUrl for version 1.1.1.0 in manifest 2025-12-16 01:59:05 +01:00
CodeDevMLH
da6d868067 feat: Add version 1.1.1.0 with bug fixes and advanced configuration UI 2025-12-16 01:58:44 +01:00
CodeDevMLH
d97f017e32 fix: Update checksum and timestamp for version 1.1.0.0 in manifest 2025-12-16 01:40:56 +01:00
CodeDevMLH
29c6255904 fix: Update imageUrl in manifest to correct logo path 2025-12-16 01:40:23 +01:00
CodeDevMLH
baa1ddde66 fix: Update README for clarity and correct repository links 2025-12-16 01:37:40 +01:00
CodeDevMLH
93f09f42cf fix: Update source URLs in manifest for version downloads 2025-12-16 01:28:10 +01:00
17 changed files with 423 additions and 35 deletions

View File

@@ -12,13 +12,14 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title> <Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.1.0.0</Version> <Version>1.2.0.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" /> <PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" />
<PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" /> <PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,12 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Jellyfin.Plugin.Seasonals.Configuration; using Jellyfin.Plugin.Seasonals.Configuration;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Jellyfin.Plugin.Seasonals; namespace Jellyfin.Plugin.Seasonals;
@@ -28,7 +33,10 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{ {
Instance = this; Instance = this;
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>()); _scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
_scriptInjector.Inject(); if (!_scriptInjector.Inject())
{
TryRegisterFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -42,16 +50,99 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary> /// </summary>
public static Plugin? Instance { get; private set; } public static Plugin? Instance { get; private set; }
/// <summary>
/// Callback method for FileTransformation plugin.
/// </summary>
/// <param name="payload">The payload containing the file contents.</param>
/// <returns>The modified file contents.</returns>
public static string TransformIndexHtml(JObject payload)
{
// CRITICAL: Always return original content if something fails or is null
string? originalContents = payload?["contents"]?.ToString();
if (string.IsNullOrEmpty(originalContents))
{
return originalContents ?? string.Empty;
}
try
{
var html = originalContents;
const string inject = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>\n<body";
if (!html.Contains("seasonals.js", StringComparison.Ordinal))
{
return html.Replace("<body", inject, StringComparison.OrdinalIgnoreCase);
}
return html;
}
catch
{
// On error, return original content to avoid breaking the UI
return originalContents;
}
}
private void TryRegisterFallback(ILogger logger)
{
try
{
// Find the FileTransformation assembly across all load contexts
var assembly = AssemblyLoadContext.All
.SelectMany(x => x.Assemblies)
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
if (assembly == null)
{
logger.LogWarning("FileTransformation plugin not found. Fallback injection skipped.");
return;
}
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (type == null)
{
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
return;
}
var method = type.GetMethod("RegisterTransformation");
if (method == null)
{
logger.LogWarning("RegisterTransformation method not found.");
return;
}
// Create JObject payload directly using Newtonsoft.Json
var payload = new JObject
{
{ "id", Id.ToString() },
{ "fileNamePattern", "index.html" },
{ "callbackAssembly", this.GetType().Assembly.FullName },
{ "callbackClass", this.GetType().FullName },
{ "callbackMethod", nameof(TransformIndexHtml) }
};
// Invoke RegisterTransformation with the JObject payload
method.Invoke(null, new object[] { payload });
logger.LogInformation("Successfully registered fallback transformation via FileTransformation plugin.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to register fallback transformation.");
}
}
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages() public IEnumerable<PluginPageInfo> GetPages()
{ {
return return new[]
[ {
new PluginPageInfo new PluginPageInfo
{ {
Name = Name, Name = Name,
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace) EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
} }
]; };
} }
} }

View File

@@ -13,7 +13,7 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; 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>"; private const string Marker = "</body>";
/// <summary> /// <summary>
@@ -30,7 +30,8 @@ public class ScriptInjector
/// <summary> /// <summary>
/// Injects the script tag into index.html if it's not already present. /// Injects the script tag into index.html if it's not already present.
/// </summary> /// </summary>
public void Inject() /// <returns>True if injection was successful or already present, false otherwise.</returns>
public bool Inject()
{ {
try try
{ {
@@ -38,21 +39,21 @@ public class ScriptInjector
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped."); _logger.LogWarning("Could not find Jellyfin web path. Script injection skipped.");
return; return false;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
_logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath); _logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath);
return; return false;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag, StringComparison.Ordinal)) if (content.Contains(ScriptTag, StringComparison.Ordinal))
{ {
_logger.LogInformation("Seasonals script already injected."); _logger.LogInformation("Seasonals script already injected.");
return; return true;
} }
// Insert before the closing body tag // Insert before the closing body tag
@@ -60,15 +61,22 @@ public class ScriptInjector
if (string.Equals(newContent, content, StringComparison.Ordinal)) if (string.Equals(newContent, content, StringComparison.Ordinal))
{ {
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped."); _logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
return; return false;
} }
File.WriteAllText(indexPath, newContent); File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully injected Seasonals script into index.html."); _logger.LogInformation("Successfully injected Seasonals script into index.html.");
return true;
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Permission denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag);
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error injecting Seasonals script."); _logger.LogError(ex, "Error injecting Seasonals script.");
return false;
} }
} }
@@ -104,6 +112,10 @@ public class ScriptInjector
File.WriteAllText(indexPath, newContent); File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully removed Seasonals script from index.html."); _logger.LogInformation("Successfully removed Seasonals script from index.html.");
} }
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Permission denied when attempting to remove script from index.html.");
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error removing Seasonals script."); _logger.LogError(ex, "Error removing Seasonals script.");

View File

@@ -162,6 +162,7 @@ async function initializeTheme() {
automateThemeSelection = config.automateSeasonSelection; automateThemeSelection = config.automateSeasonSelection;
defaultTheme = config.selectedSeason; defaultTheme = config.selectedSeason;
window.SeasonalsPluginConfig = config; window.SeasonalsPluginConfig = config;
console.log('Seasonals Config loaded:', config);
} else { } else {
console.error('Failed to fetch Seasonals config'); console.error('Failed to fetch Seasonals config');
} }
@@ -170,7 +171,7 @@ async function initializeTheme() {
} }
let currentTheme; let currentTheme;
if (!automateThemeSelection) { if (automateThemeSelection === false) {
currentTheme = defaultTheme; currentTheme = defaultTheme;
} else { } else {
currentTheme = determineCurrentTheme(); currentTheme = determineCurrentTheme();
@@ -178,7 +179,7 @@ async function initializeTheme() {
console.log(`Selected theme: ${currentTheme}`); console.log(`Selected theme: ${currentTheme}`);
if (currentTheme === 'none') { if (!currentTheme || currentTheme === 'none') {
console.log('No theme selected.'); console.log('No theme selected.');
removeSelf(); removeSelf();
return; return;
@@ -200,8 +201,9 @@ async function initializeTheme() {
removeSelf(); removeSelf();
} }
// Ensure DOM is ready before initializing
//document.addEventListener('DOMContentLoaded', initializeTheme); if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', initializeTheme);
} else {
initializeTheme(); initializeTheme();
}); }

View File

@@ -2,7 +2,9 @@
Jellyfin Seasonals is a plugin that adds seasonal themes to your Jellyfin web interface. Depending on the configuration, it automatically selects a theme based on the current date or allows you to manually set a default theme. Jellyfin Seasonals is a plugin that adds seasonal themes to your Jellyfin web interface. Depending on the configuration, it automatically selects a theme based on the current date or allows you to manually set a default theme.
This plugin is based on my manual mod (see the `manual` branch), which builds up on the awesome work of [BobHasNoSoul-jellyfin-mods](https://github.com/BobHasNoSoul/jellyfin-mods). This plugin is based on my manual mod (see the [legacy branch](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy)), which builds up on the awesome work of [BobHasNoSoul-jellyfin-mods](https://github.com/BobHasNoSoul/jellyfin-mods).
![logo](https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/logo.png)
--- ---
@@ -13,7 +15,7 @@ This plugin is based on my manual mod (see the `manual` branch), which builds up
- [Overview](#overview) - [Overview](#overview)
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Usage](#usage)
- [Automatic Selection Dates](#automatic-selection-dates) - [Automatic Theme Selection](#automatic-theme-selection)
- [Build Process](#build-process) - [Build Process](#build-process)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Legacy Manual Installation](#legacy-manual-installation) - [Legacy Manual Installation](#legacy-manual-installation)
@@ -63,18 +65,20 @@ This plugin is based on my manual mod (see the `manual` branch), which builds up
## Installation ## Installation
This plugin is based on Jellyfin Version `10.11.x`
To install this plugin, you will first need to add the repository in Jellyfin. To install this plugin, you will first need to add the repository in Jellyfin.
1. Open your Jellyfin Dashboard. 1. Open your Jellyfin Dashboard.
2. Navigate to **Plugins** > **Repositories**. 2. Navigate to **Plugins** > **Manage Repositories**.
3. Click the **+** sign to add a new repository. 3. Click the **+ New Repository** button to add a new repository.
4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field: 4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field:
```bash ```bash
https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-seasonals/main/manifest.json https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/manifest.json
``` ```
5. Click **Save**. 5. Click **Add**.
6. Go to the **Catalog** tab at the top. 6. Go to the **Available** tab at the top.
7. Under **General**, find the **Seasonals** plugin. 7. Find the **Seasonals** plugin (Under **General**)
8. Click on it and select **Install**. 8. Click on it and select **Install**.
9. **Restart your Jellyfin server.** 9. **Restart your Jellyfin server.**
10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes. 10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes.
@@ -144,7 +148,7 @@ Feel free to contribute to this project by creating pull requests or reporting i
<script src="seasonals/seasonals.js"></script> <script src="seasonals/seasonals.js"></script>
``` ```
2. **Deploy Files** 2. **Deploy Files**
Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/main/seasonals)) inside the Jellyfin web server directory (labeld with "web"). Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy/seasonals)) inside the Jellyfin web server directory (labeld with "web").
3. **Configure Themes** 3. **Configure Themes**
Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below: Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below:

View File

@@ -6,10 +6,11 @@
"compilationOptions": {}, "compilationOptions": {},
"targets": { "targets": {
".NETCoreApp,Version=v9.0": { ".NETCoreApp,Version=v9.0": {
"Jellyfin.Plugin.Seasonals/1.1.0.0": { "Jellyfin.Plugin.Seasonals/1.2.0.0": {
"dependencies": { "dependencies": {
"Jellyfin.Controller": "10.11.0", "Jellyfin.Controller": "10.11.0",
"Jellyfin.Model": "10.11.0" "Jellyfin.Model": "10.11.0",
"Newtonsoft.Json": "13.0.4"
}, },
"runtime": { "runtime": {
"Jellyfin.Plugin.Seasonals.dll": {} "Jellyfin.Plugin.Seasonals.dll": {}
@@ -326,6 +327,14 @@
} }
} }
}, },
"Newtonsoft.Json/13.0.4": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.4.30916"
}
}
},
"Polly/8.6.4": { "Polly/8.6.4": {
"dependencies": { "dependencies": {
"Polly.Core": "8.6.4" "Polly.Core": "8.6.4"
@@ -363,7 +372,7 @@
} }
}, },
"libraries": { "libraries": {
"Jellyfin.Plugin.Seasonals/1.1.0.0": { "Jellyfin.Plugin.Seasonals/1.2.0.0": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
@@ -578,6 +587,13 @@
"path": "nebml/1.1.0.5", "path": "nebml/1.1.0.5",
"hashPath": "nebml.1.1.0.5.nupkg.sha512" "hashPath": "nebml.1.1.0.5.nupkg.sha512"
}, },
"Newtonsoft.Json/13.0.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==",
"path": "newtonsoft.json/13.0.4",
"hashPath": "newtonsoft.json.13.0.4.nupkg.sha512"
},
"Polly/8.6.4": { "Polly/8.6.4": {
"type": "package", "type": "package",
"serviceable": true, "serviceable": true,

View File

@@ -1,7 +1,7 @@
--- ---
name: "Seasonals" name: "Seasonals"
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4" guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
version: "1.1.0.0" version: "1.2.0.0"
targetAbi: "10.11.0.0" targetAbi: "10.11.0.0"
framework: "net9.0" framework: "net9.0"
overview: "Seasonal effects for Jellyfin" overview: "Seasonal effects for Jellyfin"

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

21
manifest-v1.json Normal file
View File

@@ -0,0 +1,21 @@
[
{
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
"name": "Seasonals",
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.",
"overview": "Seasonal effects for Jellyfin",
"owner": "CodeDevMLH",
"category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [
{
"version": "1.0.0.0",
"changelog": "Initial release",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.0.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be6d06a959b3e18e5058a6d8fb6d800c",
"timestamp": "2025-12-15T15:33:15Z"
}
]
}
]

View File

@@ -6,21 +6,37 @@
"overview": "Seasonal effects for Jellyfin", "overview": "Seasonal effects for Jellyfin",
"owner": "CodeDevMLH", "owner": "CodeDevMLH",
"category": "General", "category": "General",
"imageUrl": "https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-seasonals/main/icon.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.2.0.0",
"changelog": "Bug fixing: Fix path, fix injection issue, added File Transformator as fallback if direct injection is blocked due to permissions.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.2.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be2e93364396b6e0e02368d5a7db53bc",
"timestamp": "2025-12-17T15:32:08Z"
},
{
"version": "1.1.1.0",
"changelog": "Bug fixing: Added Advanced Configuration UI for customizing individual seasonal effects.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.1.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "f1d7e2b24ad474904fa0eb1749e855b0",
"timestamp": "2025-12-16T00:56:08Z"
},
{ {
"version": "1.1.0.0", "version": "1.1.0.0",
"changelog": "Added Advanced Configuration UI for customizing individual seasonal effects.", "changelog": "Added Advanced Configuration UI for customizing individual seasonal effects.",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://github.com/CodeDevMLH/jellyfin-plugin-seasonals", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "CHECKSUM_HIER_EINFÜGEN", "checksum": "efc26cf0d09b313c52c089fc43df12ab",
"timestamp": "2025-12-16T00:00:00Z" "timestamp": "2025-12-16T00:22:10Z"
}, },
{ {
"version": "1.0.0.0", "version": "1.0.0.0",
"changelog": "Initial release", "changelog": "Initial release",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/bin/Publish/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.0.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be6d06a959b3e18e5058a6d8fb6d800c", "checksum": "be6d06a959b3e18e5058a6d8fb6d800c",
"timestamp": "2025-12-15T15:33:15Z" "timestamp": "2025-12-15T15:33:15Z"
} }