Compare commits

...

81 Commits

Author SHA1 Message Date
CodeDevMLH
8ba14d4df0 Update manifest.json for release v1.7.0.12 [skip ci] 2026-03-06 02:26:17 +00:00
CodeDevMLH
538138fcf3 Bump version to 1.7.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 03:25:23 +01:00
CodeDevMLH
08efb11d95 Remove picture-in-picture support from YouTube player integration 2026-03-06 03:25:08 +01:00
CodeDevMLH
de62c794f9 Update manifest.json for release v1.7.0.11 [skip ci] 2026-03-06 02:19:18 +00:00
CodeDevMLH
fc2491a4ef Bump version to 1.7.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 03:18:26 +01:00
CodeDevMLH
03276d7722 Enhance YouTube player integration with fullscreen and picture-in-picture support 2026-03-06 03:18:13 +01:00
CodeDevMLH
8676160e7b Update manifest.json for release v1.7.0.10 [skip ci] 2026-03-06 02:02:52 +00:00
CodeDevMLH
c562560bc0 Bump version to 1.7.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 03:02:01 +01:00
CodeDevMLH
98474d4ff6 Refactor YouTube player integration to use iframe for improved performance and security 2026-03-06 03:01:34 +01:00
CodeDevMLH
14c0eb43ed Update manifest.json for release v1.7.0.9 [skip ci] 2026-03-06 01:24:40 +00:00
CodeDevMLH
c4cbeda2b8 Bump version to 1.7.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 02:23:50 +01:00
CodeDevMLH
53ad568be4 Fix active slide detection logic in SlideCreator for improved video playback handling 2026-03-06 02:23:13 +01:00
CodeDevMLH
fba64bd0f6 Update manifest.json for release v1.7.0.8 [skip ci] 2026-03-06 01:17:19 +00:00
CodeDevMLH
3da16c4c5c Bump version to 1.7.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 02:16:28 +01:00
CodeDevMLH
c7cd7be3ee Add low-power device detection and adjust video playback settings 2026-03-06 02:16:09 +01:00
CodeDevMLH
6d90523eef Update manifest.json for release v1.7.0.7 [skip ci] 2026-03-06 00:45:38 +00:00
CodeDevMLH
2a3e8057a1 Bump version to 1.7.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 01:44:24 +01:00
CodeDevMLH
42026b0ee8 test revert 2026-03-06 01:44:04 +01:00
CodeDevMLH
64dbc3cfd3 Update manifest.json for release v1.7.0.6 [skip ci] 2026-03-06 00:25:04 +00:00
CodeDevMLH
c998266dd7 Bump version to 1.7.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-03-06 01:24:11 +01:00
CodeDevMLH
9b941e5a77 test again 2026-03-06 01:23:49 +01:00
CodeDevMLH
1d70d7166d Update manifest.json for release v1.7.0.5 [skip ci] 2026-03-05 23:59:06 +00:00
CodeDevMLH
5331f0faf1 Bump version to 1.7.0.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-06 00:58:15 +01:00
CodeDevMLH
0508188705 test nochmal
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-03-06 00:57:58 +01:00
CodeDevMLH
cc861f4263 Update manifest.json for release v1.7.0.4 [skip ci] 2026-03-05 23:35:23 +00:00
CodeDevMLH
10e6cdc4a2 Bump version to 1.7.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 00:34:32 +01:00
CodeDevMLH
a8c7faab6b Add Safari support for YouTube video playback using plain iframe embed 2026-03-06 00:34:19 +01:00
CodeDevMLH
6df390fa18 Update manifest.json for release v1.7.0.3 [skip ci] 2026-03-05 23:23:07 +00:00
CodeDevMLH
d0c3d7ee4d Bump version to 1.7.0.3 (test 3)
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 00:22:14 +01:00
CodeDevMLH
bc621aacdf test 2026-03-06 00:21:54 +01:00
CodeDevMLH
73eb30d671 Update manifest.json for release v1.7.0.2 [skip ci] 2026-03-05 22:44:56 +00:00
CodeDevMLH
2cfbec95c9 Bump version to 1.7.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-05 23:44:05 +01:00
CodeDevMLH
08fc29cba3 Improve video backdrop handling: optimize autoplay logic for fresh loads and enhance Safari compatibility with loadedmetadata event 2026-03-05 23:43:52 +01:00
CodeDevMLH
0d6b835486 Update manifest.json for release v1.7.0.1 [skip ci] 2026-03-05 21:43:51 +00:00
CodeDevMLH
bf620e447f Bump version to 1.7.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-03-05 22:42:58 +01:00
CodeDevMLH
3117d627dd Enhance video playback handling: enable autoplay, adjust mute settings for Safari compatibility, and improve navigation checks during autoplay. 2026-03-05 22:42:40 +01:00
CodeDevMLH
71402f7e86 Update manifest.json for release v1.7.0.0 [skip ci] 2026-03-05 01:05:37 +00:00
CodeDevMLH
cce202b88d Bump version to 1.7.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-03-05 02:04:43 +01:00
CodeDevMLH
1d334e4d95 Add YouTube no-cookie host and referrer policy for iframe security 2026-03-05 02:00:54 +01:00
CodeDevMLH
142063ce63 Update configPage.html to add warning about autoplay failure when disabling start muted option 2026-03-05 02:00:19 +01:00
CodeDevMLH
1a0050ae1a Update manifest.json for release v1.6.6.4 [skip ci] 2026-02-19 17:21:41 +00:00
CodeDevMLH
46ebfdbafc Bump version to 1.6.6.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-19 18:20:41 +01:00
CodeDevMLH
14d2bb957b Remove server configuration check for plugin enablement 2026-02-19 18:20:25 +01:00
CodeDevMLH
7a0c1e4488 Enhance script injection by removing legacy tags and improving logging 2026-02-19 18:20:17 +01:00
CodeDevMLH
ec0e686e00 Update manifest.json for release v1.6.6.3 [skip ci] 2026-02-19 15:50:04 +00:00
CodeDevMLH
54395896b3 Bump version to 1.6.6.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-19 16:49:08 +01:00
CodeDevMLH
8b2fe59f5a Add server configuration check to disable plugin if necessary 2026-02-19 16:49:00 +01:00
CodeDevMLH
a44bf7ebf4 Update manifest.json for release v1.6.6.2 [skip ci] 2026-02-19 02:36:42 +00:00
CodeDevMLH
1f273906bf Bump version to 1.6.6.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-19 03:35:50 +01:00
CodeDevMLH
0534d0458e Add delay before stopping other video players during slideshow transitions 2026-02-19 03:35:38 +01:00
CodeDevMLH
8b0d6f137d Update manifest.json for release v1.6.6.1 [skip ci] 2026-02-19 02:25:34 +00:00
CodeDevMLH
2208b86a47 Bump version to 1.6.6.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-19 03:24:42 +01:00
CodeDevMLH
5a1048687c Enhance video backdrop handling with opacity transitions for smoother playback 2026-02-19 03:24:27 +01:00
CodeDevMLH
d3f6641158 Update manifest.json for release v1.6.6.0 [skip ci] 2026-02-19 01:01:21 +00:00
CodeDevMLH
c214a620e4 Bump version to 1.6.6.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-19 01:52:31 +01:00
CodeDevMLH
f0c9462878 Implement manual mapping for MediaBarIsEnabled and enhance video backdrop handling 2026-02-19 01:52:17 +01:00
CodeDevMLH
e12a5b56a2 Update manifest.json for release v1.6.5.2 [skip ci] 2026-02-16 23:57:58 +00:00
CodeDevMLH
51ff0f2623 Bump version to 1.6.5.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-17 00:57:07 +01:00
CodeDevMLH
2c907debc8 Enhance video handling: release HTTP connections and manage lazy loading for trailers 2026-02-17 00:56:47 +01:00
CodeDevMLH
7b30f8c9e9 typos [skip ci] 2026-02-16 23:06:45 +01:00
CodeDevMLH
3a90605112 Update manifest.json for release v1.6.5.1 [skip ci] 2026-02-16 21:49:25 +00:00
CodeDevMLH
5772d670ff Bump version to 1.6.5.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-16 22:48:33 +01:00
CodeDevMLH
e558594c52 Enhance seasonal section layout and adjust input field width in configuration 2026-02-16 22:48:14 +01:00
CodeDevMLH
343436ac56 Update manifest.json for release v1.6.5.0 [skip ci] 2026-02-16 18:38:41 +00:00
CodeDevMLH
6075e20a11 Update version to 1.6.5.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-16 19:37:46 +01:00
CodeDevMLH
8b7def809b Refactor configuration tabs and enhance seasonal section functionality 2026-02-16 19:36:46 +01:00
CodeDevMLH
43950eac60 Add release existence check to automation workflow 2026-02-16 00:06:15 +01:00
CodeDevMLH
c09f265b26 Update manifest.json for release v1.6.4.1 [skip ci] 2026-02-15 22:56:18 +00:00
CodeDevMLH
379c370b4a ..
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-15 23:55:28 +01:00
CodeDevMLH
b567307003 Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-15 22:53:31 +00:00
CodeDevMLH
ff9ea9eff0 Bump version to 1.6.4.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 23:52:41 +01:00
CodeDevMLH
9427e3e535 Remove autoplay and loop attributes from video backdrop configuration 2026-02-15 23:52:34 +01:00
CodeDevMLH
19318a916d Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-15 22:39:48 +00:00
CodeDevMLH
5d85284df8 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-15 23:38:58 +01:00
CodeDevMLH
2382f850b6 Bump version to 1.6.4.0 2026-02-15 23:38:56 +01:00
CodeDevMLH
22041293f6 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 22:38:19 +00:00
CodeDevMLH
5595158f9d Update field description for Prefer Local Backdrops setting
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-15 23:25:01 +01:00
CodeDevMLH
39f85e0c9b fix backdrop issue? 2026-02-15 23:24:56 +01:00
CodeDevMLH
18a9980a0a Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:21:08 +00:00
CodeDevMLH
deb426833d Bump version to 1.6.3.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 02:19:47 +01:00
CodeDevMLH
bf4b6da0f0 fix path issue on subpath installations 2026-02-15 02:19:37 +01:00
8 changed files with 631 additions and 107 deletions

View File

@@ -51,7 +51,31 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
SERVER_URL="https://git.mahom03-spacecloud.de"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip - name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
# Inject version from manifest into the build # Inject version from manifest into the build
@@ -71,6 +95,7 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV
- name: Update manifest.json - name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
@@ -90,12 +115,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json - name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]" commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json file_pattern: manifest.json
- name: Create Release - name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
server_url: "https://git.mahom03-spacecloud.de" server_url: "https://git.mahom03-spacecloud.de"
@@ -109,6 +136,7 @@ jobs:
# Update Message in Remote Repository # Update Message in Remote Repository
- name: Checkout Central Manifest Repo - name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -116,6 +144,7 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }} token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest - name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
cd central-manifest cd central-manifest
@@ -171,6 +200,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest - name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: | run: |
cd central-manifest cd central-manifest
git config user.name "CodeDevMLH" git config user.name "CodeDevMLH"

View File

@@ -52,7 +52,32 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip - name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
# Inject version from manifest into the build # Inject version from manifest into the build
@@ -72,6 +97,7 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV
- name: Update manifest.json - name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
@@ -91,13 +117,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json - name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]" commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json file_pattern: manifest.json
- name: Generate Commit Log - name: Generate Commit Log
if: success() if: success() && steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
echo "Generating commit log since last tag..." echo "Generating commit log since last tag..."
@@ -131,6 +158,7 @@ jobs:
cat release_body.txt cat release_body.txt
- name: Create Release - name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: "v${{ env.VERSION }}" tag_name: "v${{ env.VERSION }}"
@@ -145,6 +173,7 @@ jobs:
# Update Message in Remote Repository # Update Message in Remote Repository
- name: Checkout Central Manifest Repo - name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -152,6 +181,7 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }} token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest - name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
cd central-manifest cd central-manifest
@@ -207,6 +237,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest - name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: | run: |
cd central-manifest cd central-manifest
git config user.name "CodeDevMLH" git config user.name "CodeDevMLH"

246
Injector_new.cs Normal file
View File

@@ -0,0 +1,246 @@
using System;
using System.Reflection;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using MediaBrowser.Common.Configuration;
using Jellyfin.Plugin.MediaBarEnhanced.Helpers;
namespace Jellyfin.Plugin.MediaBarEnhanced
{
/// <summary>
/// Handles the injection of the MediaBarEnhanced script into the Jellyfin web interface.
/// </summary>
public class ScriptInjector
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
public const string CssTag = "<link rel=\"stylesheet\" href=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
public const string ScriptMarker = "</body>";
public const string CssMarker = "</head>";
/// <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
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
RegisterFileTransformation();
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
RegisterFileTransformation();
return;
}
var content = File.ReadAllText(indexPath);
var injectedJS = false;
var injectedCSS = false;
if (!content.Contains(ScriptTag))
{
var index = content.IndexOf(ScriptMarker, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
content = content.Insert(index, ScriptTag + Environment.NewLine);
injectedJS = true;
}
}
if (!content.Contains(CssTag))
{
var index = content.IndexOf(CssMarker, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
content = content.Insert(index, CssTag + Environment.NewLine);
injectedCSS = true;
}
}
if (injectedJS && injectedCSS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script injected into index.html.");
} else if (injectedJS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected.");
}
else if (injectedCSS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected.");
}
else
{
_logger.LogInformation("MediaBarEnhanced script and CSS already present in index.html. Or could not be injected.");
}
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
RegisterFileTransformation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error injecting MediaBarEnhanced resources. Attempting fallback.");
RegisterFileTransformation();
}
}
/// <summary>
/// Removes the script tag from index.html.
/// </summary>
public void Remove()
{
UnregisterFileTransformation();
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
return;
}
var content = File.ReadAllText(indexPath);
var modified = false;
if (content.Contains(ScriptTag))
{
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
modified = true;
}
if (content.Contains(CssTag))
{
content = content.Replace(CssTag + Environment.NewLine, "").Replace(CssTag, "");
modified = true;
}
if (modified)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script removed from index.html.");
} else
{
_logger.LogInformation("MediaBarEnhanced script not found in index.html. No removal necessary.");
}
}
catch (UnauthorizedAccessException uaEx)
{
_logger.LogError(uaEx, "Unauthorized access when trying to remove MediaBarEnhanced script. Check file permissions.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing MediaBarEnhanced script.");
}
}
private string? GetWebPath()
{
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string;
}
private void RegisterFileTransformation()
{
_logger.LogInformation("MediaBarEnhanced Fallback. Registering file transformations.");
List<JObject> payloads = new List<JObject>();
{
JObject payload = new JObject();
payload.Add("id", "0dfac9d7-d898-4944-900b-1c1837707279");
payload.Add("fileNamePattern", "index.html");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
payloads.Add(payload);
}
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
foreach (JObject payload in payloads)
{
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
}
_logger.LogInformation("File transformations registered successfully.");
}
else
{
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
}
}
else
{
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback failed.");
}
}
private void UnregisterFileTransformation()
{
try
{
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
Guid id = Guid.Parse("0dfac9d7-d898-4944-900b-1c1837707279");
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
_logger.LogInformation("File transformation unregistered successfully.");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
}
}
}
}

View File

@@ -22,15 +22,15 @@
<hr style="max-width: 800px; margin: 1em 0;"> <hr style="max-width: 800px; margin: 1em 0;">
<div style="margin-bottom: 1.5em;"> <div style="margin-bottom: 1.5em;">
<button class="jellyfin-tab-button active" onclick="showTab('basic', this)" <button class="jellyfin-tab-button active" onclick="showTab('media-bar-enhanced-basic', this)"
style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;"> style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;">
<h3>General Settings</h3> <h3>General Settings</h3>
</button> </button>
<button class="jellyfin-tab-button" onclick="showTab('custom', this)" <button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-custom', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;"> style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Custom Content</h3> <h3>Custom Content</h3>
</button> </button>
<button class="jellyfin-tab-button" onclick="showTab('advanced', this)" <button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-advanced', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;"> style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Advanced Settings</h3> <h3>Advanced Settings</h3>
</button> </button>
@@ -39,11 +39,11 @@
<form id="mediaBarEnhancedConfigForm"> <form id="mediaBarEnhancedConfigForm">
<!-- BASIC TAB --> <!-- BASIC TAB -->
<div id="basic" class="tab-content"> <div id="media-bar-enhanced-basic" class="tab-content">
<h2 class="sectionTitle">Main Plugin Settings</h2> <h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="IsEnabled" name="IsEnabled" /> <input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled" name="MediaBarIsEnabled" />
<span>Enable Media Bar Enhanced Plugin</span> <span>Enable Media Bar Enhanced Plugin</span>
</label> </label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div> <div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
@@ -71,7 +71,7 @@
name="PreferLocalBackdrops" /> name="PreferLocalBackdrops" />
<span>Prefer Local Backdrops / Theme Videos</span> <span>Prefer Local Backdrops / Theme Videos</span>
</label> </label>
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over trailers.</div> <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -101,7 +101,7 @@
</div> </div>
<!-- CUSTOM CONTENT TAB --> <!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;"> <div id="media-bar-enhanced-custom" class="tab-content" style="display:none;">
<!-- Default Custom Media IDs --> <!-- Default Custom Media IDs -->
<h2 class="sectionTitle">Custom Media IDs</h2> <h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
@@ -146,7 +146,7 @@
Example: Example:
<code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br> <code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br>
You can also insert a name of a collection or playlist to fetch the IDs of all items in You can also insert a name of a collection or playlist to fetch the IDs of all items in
it (will take the first hit.<br><b>Note:</b> there is currently no feedback if the name it (will take the first hit.<br><b>Note:</b> There is currently no feedback if the name
resolution succeeded, you will have to look if the bar displays the correct items). resolution succeeded, you will have to look if the bar displays the correct items).
</p> </p>
</div> </div>
@@ -164,6 +164,12 @@
during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div> during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>Define seasonal rules to automatically select a selection of items based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
</div>
<div id="seasonalSectionsList"></div> <div id="seasonalSectionsList"></div>
<button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button" <button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button"
style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;"> style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
@@ -175,7 +181,7 @@
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
<div id="advanced" class="tab-content" style="display:none;"> <div id="media-bar-enhanced-advanced" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Features</h2> <h2 class="sectionTitle">Features</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -235,7 +241,7 @@
<span>Start Muted</span> <span>Start Muted</span>
</label> </label>
<div class="fieldDescription">Start trailer video playback muted. (Known issue: In the <div class="fieldDescription">Start trailer video playback muted. (Known issue: In the
Android/IOS app, backdrop trailers are always muted.)</div> Android/IOS app, backdrop trailers are always muted.)<br><b style="color:#ffcc00">Warning:</b> Disabling this may cause autoplay to fail on certain browsers due to strict autoplay policies.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -443,7 +449,7 @@
ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) { ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) {
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
@@ -456,6 +462,12 @@
'IncludeWatchedContent' 'IncludeWatchedContent'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
mediaBarEnabledCheckbox.checked = config.IsEnabled;
}
keys.forEach(function (key) { keys.forEach(function (key) {
var el = page.querySelector('#' + key); var el = page.querySelector('#' + key);
if (el) { if (el) {
@@ -533,8 +545,15 @@
if (seasonalInput) seasonalInput.value = sectionsJson; if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {}; var config = {};
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
config.IsEnabled = mediaBarEnabledCheckbox.checked;
}
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
@@ -612,14 +631,18 @@
div.innerHTML = div.innerHTML =
'<div class="inputContainer" style="margin-bottom: 0.5em;">' + '<div class="inputContainer" style="margin-bottom: 0.5em;">' +
' <label class="inputLabel" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' + ' <div style="display: flex; align-items: center; justify-content: space-between;">' +
' <div style="display: flex; align-items: center;">' + ' <label class="inputLabel section-title" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' +
' <div style="flex-grow:1;">' + ' <div style="display: flex; gap: 0.5em;">' +
' <input is="emby-input" type="text" class="emby-input section-name" value="' + (data.Name || '') + '" />' + ' <button type="button" is="paper-icon-button-light" class="btn-move-up" title="Move Up"><i class="material-icons">arrow_upward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-move-down" title="Move Down"><i class="material-icons">arrow_downward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-remove" title="Remove" style="color: #a94442;"><i class="material-icons">delete</i></button>' +
' </div>' + ' </div>' +
' <button type="button" class="raised emby-button remove-section" style="background: #a94442; min-width: unset; margin-left: 1em;">Remove</button>' +
' </div>' + ' </div>' +
' <div class="inputContainer">' +
' <input is="emby-input" type="text" class="emby-input section-name" style="width: 60%;" value="' + (data.Name || '') + '" />' +
' <div class="fieldDescription">Name of the season</div>' + ' <div class="fieldDescription">Name of the season</div>' +
' </div>' +
'</div>' + '</div>' +
'<div class="inputContainer" style="margin-bottom: 1em;">' + '<div class="inputContainer" style="margin-bottom: 1em;">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' + ' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' +
@@ -639,13 +662,38 @@
' <div class="fieldDescription">Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.<br>Same options available as for the default media IDs.</div>' + ' <div class="fieldDescription">Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.<br>Same options available as for the default media IDs.</div>' +
'</div>'; '</div>';
div.querySelector('.remove-section').addEventListener('click', function() { div.querySelector('.btn-remove').addEventListener('click', function() {
div.remove(); div.remove();
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
});
div.querySelector('.btn-move-up').addEventListener('click', function() {
if (div.previousElementSibling) {
container.insertBefore(div, div.previousElementSibling);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
});
div.querySelector('.btn-move-down').addEventListener('click', function() {
if (div.nextElementSibling) {
container.insertBefore(div.nextElementSibling, div);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
}); });
container.appendChild(div); container.appendChild(div);
}, },
updateSectionTitles: function(container) {
var sections = container.querySelectorAll('.seasonal-section');
sections.forEach(function(section, index) {
var title = section.querySelector('.section-title');
if (title) {
title.innerText = 'Season list #' + (index + 1);
}
});
},
getSeasonalSectionsFromUI: function(page) { getSeasonalSectionsFromUI: function(page) {
var sections = []; var sections = [];
var els = page.querySelectorAll('.seasonal-section'); var els = page.querySelectorAll('.seasonal-section');

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title> <Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.6.2.3</Version> <Version>1.7.0.12</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
</PropertyGroup> </PropertyGroup>

View File

@@ -18,8 +18,8 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>"; public const string ScriptTag = "<script src=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
public const string CssTag = "<link rel=\"stylesheet\" href=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />"; public const string CssTag = "<link rel=\"stylesheet\" href=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
public const string ScriptMarker = "</body>"; public const string ScriptMarker = "</body>";
public const string CssMarker = "</head>"; public const string CssMarker = "</head>";
@@ -60,6 +60,11 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
var injectedJS = false; var injectedJS = false;
var injectedCSS = false; var injectedCSS = false;
var modified = false;
// Cleanup legacy tags first to avoid duplicates or conflicts
content = RemoveLegacyTags(content, ref modified);
if (!content.Contains(ScriptTag)) if (!content.Contains(ScriptTag))
{ {
@@ -81,21 +86,28 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
} }
} }
if (injectedJS || injectedCSS || modified)
{
File.WriteAllText(indexPath, content);
if (injectedJS && injectedCSS) if (injectedJS && injectedCSS)
{ {
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script injected into index.html."); _logger.LogInformation("MediaBarEnhanced script injected into index.html.");
} else if (injectedJS) }
else if (injectedJS)
{ {
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected."); _logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected.");
} }
else if (injectedCSS) else if (injectedCSS)
{ {
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected."); _logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected.");
} }
else else
{
_logger.LogInformation("MediaBarEnhanced script and CSS already present. Legacy tags removed if found.");
}
}
else
{ {
_logger.LogInformation("MediaBarEnhanced script and CSS already present in index.html. Or could not be injected."); _logger.LogInformation("MediaBarEnhanced script and CSS already present in index.html. Or could not be injected.");
} }
@@ -148,6 +160,9 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
modified = true; modified = true;
} }
// Remove legacy tags
content = RemoveLegacyTags(content, ref modified);
if (modified) if (modified)
{ {
File.WriteAllText(indexPath, content); File.WriteAllText(indexPath, content);
@@ -242,5 +257,33 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered."); _logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
} }
} }
/// <summary>
/// Removes legacy script and css tags from the content.
/// </summary>
/// <param name="content">The file content.</param>
/// <param name="modified">Ref bool to track if changes were made.</param>
/// <returns>The modified content.</returns>
private string RemoveLegacyTags(string content, ref bool modified)
{
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
const string LegacyScriptTag = "<script src=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
const string LegacyCssTag = "<link rel=\"stylesheet\" href=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
if (content.Contains(LegacyScriptTag))
{
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
modified = true;
_logger.LogInformation("Legacy MediaBarEnhanced script tag removed.");
}
if (content.Contains(LegacyCssTag))
{
content = content.Replace(LegacyCssTag + Environment.NewLine, "").Replace(LegacyCssTag, "");
modified = true;
_logger.LogInformation("Legacy MediaBarEnhanced CSS tag removed.");
}
return content;
}
} }
} }

View File

@@ -63,6 +63,7 @@ const CONFIG = {
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false, applyLimitsToCustomIds: false,
seasonalSections: "[]", seasonalSections: "[]",
isEnabled: true,
}; };
// State management // State management
@@ -162,6 +163,14 @@ const isUserLoggedIn = () => {
} }
}; };
/**
* Detects if the current device is a low-power device (Smart TVs, etc.)
* @returns {boolean} True if running on a low-power device
*/
const isLowPowerDevice = () => {
return /webOS|LG Browser|SMART-TV|SmartTV|Tizen|Viera|NetCast|Roku|VIDAA/i.test(navigator.userAgent);
};
/** /**
* Initializes Jellyfin data from ApiClient * Initializes Jellyfin data from ApiClient
* @param {Function} callback - Function to call once data is initialized * @param {Function} callback - Function to call once data is initialized
@@ -437,7 +446,7 @@ const waitForApiClientAndInitialize = () => {
const fetchPluginConfig = async () => { const fetchPluginConfig = async () => {
try { try {
const response = await fetch('/MediaBarEnhanced/Config'); const response = await fetch('../MediaBarEnhanced/Config');
if (response.ok) { if (response.ok) {
const pluginConfig = await response.json(); const pluginConfig = await response.json();
if (pluginConfig) { if (pluginConfig) {
@@ -733,16 +742,21 @@ const SlideUtils = {
} }
if (isYoutube && videoId) { if (isYoutube && videoId) {
const playerDiv = this.createElement('div', { id: 'modal-yt-player' }); const ytIframe = this.createElement('iframe', {
contentContainer.appendChild(playerDiv); id: 'modal-yt-player',
src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`,
allow: 'autoplay; encrypted-media',
style: 'width: 100%; height: 100%; border: none;',
referrerpolicy: 'strict-origin-when-cross-origin',
allowfullscreen: 'true'
});
contentContainer.appendChild(ytIframe);
overlay.append(closeButton, contentContainer); overlay.append(closeButton, contentContainer);
document.body.appendChild(overlay); document.body.appendChild(overlay);
this.loadYouTubeIframeAPI().then(() => { this.loadYouTubeIframeAPI().then(() => {
new YT.Player('modal-yt-player', { new YT.Player(ytIframe, {
height: '100%',
width: '100%',
videoId: videoId,
playerVars: { playerVars: {
autoplay: 1, autoplay: 1,
controls: 1, controls: 1,
@@ -750,7 +764,6 @@ const SlideUtils = {
rel: 0, rel: 0,
playsinline: 1, playsinline: 1,
origin: window.location.origin, origin: window.location.origin,
widget_referrer: window.location.href,
enablejsapi: 1 enablejsapi: 1
} }
}); });
@@ -1394,7 +1407,7 @@ const ApiUtils = {
return { return {
id: trailer.Id, id: trailer.Id,
url: `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}` url: `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}&static=true`
}; };
} }
return null; return null;
@@ -1433,7 +1446,7 @@ const ApiUtils = {
return { return {
id: video.Id, id: video.Id,
url: `${STATE.jellyfinData.serverAddress}/Videos/${video.Id}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}` url: `${STATE.jellyfinData.serverAddress}/Videos/${video.Id}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}&static=true`
}; };
} }
} }
@@ -1638,6 +1651,7 @@ const SlideCreator = {
"data-item-id": itemId, "data-item-id": itemId,
}); });
let videoBackdrop;
let backdrop; let backdrop;
let isVideo = false; let isVideo = false;
let trailerUrl = null; let trailerUrl = null;
@@ -1658,7 +1672,7 @@ const SlideCreator = {
trailerUrl = { trailerUrl = {
id: videoId, id: videoId,
url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}` url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}&static=true`
}; };
} else { } else {
// Assume it's a standard URL (YouTube, etc.) // Assume it's a standard URL (YouTube, etc.)
@@ -1686,7 +1700,7 @@ const SlideCreator = {
console.log(`Using local trailer fallback for ${itemId}: ${trailerUrl}`); console.log(`Using local trailer fallback for ${itemId}: ${trailerUrl}`);
} }
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Client Setting Overrides // Client Setting Overrides
const enableVideo = MediaBarEnhancedSettingsManager.getSetting('videoBackdrops', CONFIG.enableVideoBackdrop); const enableVideo = MediaBarEnhancedSettingsManager.getSetting('videoBackdrops', CONFIG.enableVideoBackdrop);
@@ -1716,16 +1730,34 @@ const SlideCreator = {
console.warn("Invalid trailer URL:", trailerUrl); console.warn("Invalid trailer URL:", trailerUrl);
} }
if (isYoutube && videoId) { const isLowPower = isLowPowerDevice();
const itemIndex = STATE.slideshow.itemIds ? STATE.slideshow.itemIds.indexOf(itemId) : -1;
const isActiveSlide = itemIndex !== -1 && itemIndex === STATE.slideshow.currentSlideIndex;
const shouldCreateVideo = !isLowPower || isActiveSlide;
if (isYoutube && videoId && shouldCreateVideo) {
isVideo = true; isVideo = true;
// Create container for YouTube API // Create container for YouTube API
const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default"; const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default";
backdrop = SlideUtils.createElement("div", { // Create a wrapper for opacity transition
videoBackdrop = SlideUtils.createElement("div", {
className: `backdrop video-backdrop ${videoClass}`, className: `backdrop video-backdrop ${videoClass}`,
id: `youtube-player-${itemId}` style: "opacity: 0; transition: opacity 1.2s ease-in-out;" // Start interrupted/transparent
}); });
// Create an iframe upfront
const ytPlayerIframe = SlideUtils.createElement("iframe", {
id: `youtube-player-${itemId}`,
src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`,
style: "width: 100%; height: 100%; border: none;",
allow: "autoplay; encrypted-media",
referrerpolicy: "strict-origin-when-cross-origin",
allowfullscreen: "true"
});
videoBackdrop.appendChild(ytPlayerIframe);
// Initialize YouTube Player // Initialize YouTube Player
SlideUtils.loadYouTubeIframeAPI().then(() => { SlideUtils.loadYouTubeIframeAPI().then(() => {
// Fetch SponsorBlock data // Fetch SponsorBlock data
@@ -1741,7 +1773,6 @@ const SlideCreator = {
loop: 0, loop: 0,
playsinline: 1, playsinline: 1,
origin: window.location.origin, origin: window.location.origin,
widget_referrer: window.location.href,
enablejsapi: 1 enablejsapi: 1
}; };
@@ -1770,10 +1801,7 @@ const SlideCreator = {
console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`); console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`);
} }
STATE.slideshow.videoPlayers[itemId] = new YT.Player(`youtube-player-${itemId}`, { STATE.slideshow.videoPlayers[itemId] = new YT.Player(ytPlayerIframe, {
height: '100%',
width: '100%',
videoId: videoId,
playerVars: playerVars, playerVars: playerVars,
events: { events: {
'onReady': (event) => { 'onReady': (event) => {
@@ -1782,6 +1810,9 @@ const SlideCreator = {
event.target._endTime = playerVars.end || undefined; event.target._endTime = playerVars.end || undefined;
event.target._videoId = videoId; event.target._videoId = videoId;
// Store reference to wrapper for fading
event.target._wrapperDiv = videoBackdrop;
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
event.target.mute(); event.target.mute();
} else { } else {
@@ -1830,14 +1861,17 @@ const SlideCreator = {
} }
}, },
'onStateChange': (event) => { 'onStateChange': (event) => {
// Fade in when playing
if (event.data === YT.PlayerState.PLAYING) {
if (event.target._wrapperDiv) {
event.target._wrapperDiv.style.opacity = "1";
}
}
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} else {
event.target.playVideo(); // Loop if trailer is shorter than slide duration
}
} }
} }
}, },
@@ -1854,27 +1888,26 @@ const SlideCreator = {
}); });
// 2. Check for local video trailers in MediaSources if yt is not available // 2. Check for local video trailers in MediaSources if yt is not available
} else if (!isYoutube) { } else if (!isYoutube && shouldCreateVideo) {
isVideo = true; isVideo = true;
const videoSrc = (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl);
const videoAttributes = { const videoAttributes = {
className: "backdrop video-backdrop", className: "backdrop video-backdrop",
src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl), preload: "none",
autoplay: false,
preload: "auto",
loop: false,
disablePictureInPicture: true, disablePictureInPicture: true,
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;" "data-src": videoSrc,
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none; opacity: 0; transition: opacity 1.2s ease-in-out;"
}; };
videoAttributes.muted = ""; videoAttributes.muted = "";
backdrop = SlideUtils.createElement("video", videoAttributes); videoBackdrop = SlideUtils.createElement("video", videoAttributes);
backdrop.volume = 0.4; videoBackdrop.volume = 0.4;
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = videoBackdrop;
backdrop.addEventListener('play', (event) => { videoBackdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (!slide || !slide.classList.contains('active')) { if (!slide || !slide.classList.contains('active')) {
console.log(`Local video ${itemId} started playing but slide is not active, pausing.`); console.log(`Local video ${itemId} started playing but slide is not active, pausing.`);
@@ -1882,34 +1915,44 @@ const SlideCreator = {
event.target.currentTime = 0; event.target.currentTime = 0;
return; return;
} }
// Fade in
event.target.style.opacity = "1";
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
}); });
backdrop.addEventListener('ended', () => { videoBackdrop.addEventListener('ended', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}); });
backdrop.addEventListener('error', () => { videoBackdrop.addEventListener('error', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); console.warn(`Local video error for item ${itemId}`);
if (CONFIG.waitForTrailerToEnd && slide && slide.classList.contains('active')) { const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}); });
} }
} }
if (!isVideo) { // Always create a static backdrop image (to show while video loads or if no video)
backdrop = SlideUtils.createElement("img", { backdrop = SlideUtils.createElement("img", {
className: "backdrop high-quality", className: "backdrop high-quality",
src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60), src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60),
alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'), alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'),
loading: "eager", loading: "eager",
}); });
// If video, static backdrop should be strictly a background (no animation)
if (isVideo) {
backdrop.style.animation = "none";
backdrop.style.transition = "none";
} }
const backdropOverlay = SlideUtils.createElement("div", { const backdropOverlay = SlideUtils.createElement("div", {
@@ -1919,8 +1962,14 @@ const SlideCreator = {
const backdropContainer = SlideUtils.createElement("div", { const backdropContainer = SlideUtils.createElement("div", {
className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""), className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""),
}); });
backdropContainer.append(backdrop, backdropOverlay); backdropContainer.append(backdrop, backdropOverlay);
// If video exists, append on top of static backdrop
if (isVideo && videoBackdrop) {
backdropContainer.appendChild(videoBackdrop);
}
const logo = SlideUtils.createElement("img", { const logo = SlideUtils.createElement("img", {
className: "logo high-quality", className: "logo high-quality",
src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40), src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40),
@@ -2361,7 +2410,8 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
// 1. Stop all other YouTube players and local video elements // 1. Stop all other YouTube players and local video elements, release connections
setTimeout(() => {
if (STATE.slideshow.videoPlayers) { if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== currentItemId) { if (id !== currentItemId) {
@@ -2371,15 +2421,22 @@ const SlideshowManager = {
if (typeof p.pauseVideo === 'function') { if (typeof p.pauseVideo === 'function') {
p.pauseVideo(); p.pauseVideo();
} }
// HTML5 <video> element (local trailers) // HTML5 <video> element (local trailers), release HTTP connection
if (p instanceof HTMLVideoElement) { if (p instanceof HTMLVideoElement) {
p.pause(); p.pause();
p.muted = true; p.muted = true;
p.currentTime = 0; p.currentTime = 0;
// Save src to data-src and release the HTTP streaming connection
if (p.src && !p.getAttribute('data-src')) {
p.setAttribute('data-src', p.src);
}
p.removeAttribute('src');
p.load();
} }
} }
}); });
} }
}, CONFIG.fadeTransitionDuration);
// 2. Pause all other HTML5 videos e.g. local trailers // 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => { document.querySelectorAll('video').forEach(video => {
@@ -2412,6 +2469,12 @@ const SlideshowManager = {
if (videoBackdrop) { if (videoBackdrop) {
if (videoBackdrop.tagName === 'VIDEO') { if (videoBackdrop.tagName === 'VIDEO') {
// Restore src from data-src if it was deactivated to release connections
const lazySrc = videoBackdrop.getAttribute('data-src');
if (lazySrc && !videoBackdrop.src) {
videoBackdrop.src = lazySrc;
}
videoBackdrop.currentTime = 0; videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted; videoBackdrop.muted = STATE.slideshow.isMuted;
@@ -2517,7 +2580,7 @@ const SlideshowManager = {
STATE.slideshow.isTransitioning = false; STATE.slideshow.isTransitioning = false;
if (previousVisibleSlide) { if (previousVisibleSlide) {
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled) && !isLowPowerDevice();
if (enableAnimations) { if (enableAnimations) {
const prevBackdrop = previousVisibleSlide.querySelector(".backdrop"); const prevBackdrop = previousVisibleSlide.querySelector(".backdrop");
const prevLogo = previousVisibleSlide.querySelector(".logo"); const prevLogo = previousVisibleSlide.querySelector(".logo");
@@ -2558,7 +2621,9 @@ const SlideshowManager = {
*/ */
async preloadAdjacentSlides(currentIndex) { async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems; const totalItems = STATE.slideshow.totalItems;
const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5); let preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5);
if (isLowPowerDevice()) preloadCount = 1; // Strict limit for TVs
const preloadedIds = new Set(); const preloadedIds = new Set();
// Preload next slides // Preload next slides
@@ -2610,7 +2675,7 @@ const SlideshowManager = {
*/ */
pruneSlideCache() { pruneSlideCache() {
const currentIndex = STATE.slideshow.currentSlideIndex; const currentIndex = STATE.slideshow.currentSlideIndex;
const keepRange = 5; const keepRange = CONFIG.preloadCount + 1;
let prunedAny = false; let prunedAny = false;
Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => { Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => {
@@ -2628,7 +2693,17 @@ const SlideshowManager = {
if (STATE.slideshow.videoPlayers[itemId]) { if (STATE.slideshow.videoPlayers[itemId]) {
const player = STATE.slideshow.videoPlayers[itemId]; const player = STATE.slideshow.videoPlayers[itemId];
if (typeof player.destroy === 'function') { if (typeof player.destroy === 'function') {
// YouTube player
player.destroy(); player.destroy();
} else if (player instanceof HTMLVideoElement) {
// HTML5 video, release HTTP streaming connection
player.pause();
// Save src to data-src and release the HTTP streaming connection
if (player.src && !player.getAttribute('data-src')) {
player.setAttribute('data-src', player.src);
}
player.removeAttribute('src');
player.load();
} }
delete STATE.slideshow.videoPlayers[itemId]; delete STATE.slideshow.videoPlayers[itemId];
} }
@@ -2807,7 +2882,7 @@ const SlideshowManager = {
}); });
} }
// 2. Stop and mute all HTML5 videos // 2. Stop and mute all HTML5 videos, release connections
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.querySelectorAll('video').forEach(video => { container.querySelectorAll('video').forEach(video => {
@@ -2815,6 +2890,12 @@ const SlideshowManager = {
video.pause(); video.pause();
video.muted = true; video.muted = true;
video.currentTime = 0; video.currentTime = 0;
// Save src and release HTTP streaming connection
if (video.src && !video.getAttribute('data-src')) {
video.setAttribute('data-src', video.src);
}
video.removeAttribute('src');
video.load();
} catch (e) { } catch (e) {
console.warn("Error stopping HTML5 video:", e); console.warn("Error stopping HTML5 video:", e);
} }
@@ -2847,9 +2928,14 @@ const SlideshowManager = {
return; return;
} }
// HTML5 video: just resume, don't reset currentTime // HTML5 video: restore src if needed, then resume
const html5Video = currentSlide.querySelector('video.video-backdrop'); const html5Video = currentSlide.querySelector('video.video-backdrop');
if (html5Video) { if (html5Video) {
// Restore src from data-src if it was cleared to release connections
const lazySrc = html5Video.getAttribute('data-src');
if (lazySrc && !html5Video.src) {
html5Video.src = lazySrc;
}
html5Video.muted = STATE.slideshow.isMuted; html5Video.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) html5Video.volume = 0.4; if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
@@ -3655,8 +3741,8 @@ const slidesInit = async () => {
if (CONFIG.enableClientSideSettings) { if (CONFIG.enableClientSideSettings) {
MediaBarEnhancedSettingsManager.init(); MediaBarEnhancedSettingsManager.init();
const isEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true); const isClientSideEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true);
if (!isEnabled) { if (!isClientSideEnabled) {
console.log("MediaBarEnhanced: Disabled by client-side setting."); console.log("MediaBarEnhanced: Disabled by client-side setting.");
const homeSections = document.querySelector('.homeSectionsContainer'); const homeSections = document.querySelector('.homeSectionsContainer');
if (homeSections) { if (homeSections) {

View File

@@ -8,6 +8,46 @@
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.7.0.12",
"changelog": "- Add YouTube no-cookie host and referrer policy for iframe security to fix playback issues on iOS/MacOS",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.0.12/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "70589d1f3bd64896594d5312b49d974c",
"timestamp": "2026-03-06T02:26:16Z"
},
{
"version": "1.6.6.4",
"changelog": "- feat: add static backdrop also for video backdrops\n- fix: renaming issue of settings (avoiding conflict with other plugins)",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "2c55cf9687e44b04a0824997e2980dc9",
"timestamp": "2026-02-19T17:21:40Z"
},
{
"version": "1.6.5.2",
"changelog": "- refactored seasonal UI settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.5.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "552cb3376c77ede5a0664ced56bf7d1e",
"timestamp": "2026-02-16T23:57:57Z"
},
{
"version": "1.6.4.1",
"changelog": "- fix slide transition when using local/backdrop videos",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.4.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "a9c5a863427de84639eca082483936da",
"timestamp": "2026-02-15T22:56:17Z"
},
{
"version": "1.6.3.1",
"changelog": "- fix path issue on subpath installations",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.3.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "6a952445bfb80ba4603017358e48da91",
"timestamp": "2026-02-15T22:38:19Z"
},
{ {
"version": "1.6.2.3", "version": "1.6.2.3",
"changelog": "- feat: add options for local backdrops (theme videos) support and randomization features for local trailer/backdrops if more than one is available\n- feat: add option to include watched content in the random selection", "changelog": "- feat: add options for local backdrops (theme videos) support and randomization features for local trailer/backdrops if more than one is available\n- feat: add option to include watched content in the random selection",