Compare commits

..

71 Commits

Author SHA1 Message Date
CodeDevMLH
9bcb2f984a Update manifest.json for release v1.7.1.4 [skip ci] 2026-03-08 15:58:12 +00:00
CodeDevMLH
c23a614f9f Bump version to 1.7.1.4 and update changelog for new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-08 16:57:21 +01:00
CodeDevMLH
3a367cb2be Add ShowPaginationDots configuration option and update related UI elements 2026-03-08 16:57:16 +01:00
CodeDevMLH
2993bfe3f2 del old tmp 2026-03-08 16:56:18 +01:00
CodeDevMLH
3ffa2c262a Update manifest.json for release v1.7.1.3 [skip ci] 2026-03-08 15:20:14 +00:00
CodeDevMLH
dc88110e9c Bump version to 1.7.1.3 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-03-08 16:19:17 +01:00
CodeDevMLH
f9ae62a459 Refactor button-container styles for improved layout and responsiveness 2026-03-08 16:19:07 +01:00
CodeDevMLH
9e2f861213 Update manifest.json for release v1.7.1.2 [skip ci] 2026-03-08 15:08:30 +00:00
CodeDevMLH
4781618a52 Bump version to 1.7.1.2 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-08 16:07:39 +01:00
CodeDevMLH
2bed81c1f8 Refactor button-container styles for improved layout and responsiveness 2026-03-08 16:07:32 +01:00
CodeDevMLH
292fcfc389 Update manifest.json for release v1.7.1.1 [skip ci] 2026-03-08 14:48:17 +00:00
CodeDevMLH
da718a0e57 Bump version to 1.7.1.1 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-03-08 15:47:26 +01:00
CodeDevMLH
95a8907496 test2 2026-03-08 15:47:09 +01:00
CodeDevMLH
0498756529 Update manifest.json for release v1.7.1.0 [skip ci] 2026-03-08 14:32:40 +00:00
CodeDevMLH
f1d92080b2 Bump version to 1.7.1.0 and update changelog for mobile button fix
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-03-08 15:31:52 +01:00
CodeDevMLH
586b57d23e Enhance button layout in button-container for better responsiveness 2026-03-08 15:30:21 +01:00
CodeDevMLH
47b05a82ba Update changelog for version 1.7.0.14: enhance iframe security, fix playback issues on iOS/MacOS, disable animations for TV layout, remove list.txt functionality, and improve logging. [skip ci] 2026-03-06 04:30:04 +01:00
CodeDevMLH
cb45e0cb43 Update manifest.json for release v1.7.0.14 [skip ci] 2026-03-06 03:13:48 +00:00
CodeDevMLH
19246c3a19 Bump version to 1.7.0.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 04:12:58 +01:00
CodeDevMLH
99b4b09306 Disable animations and backdrop filters for TV layout on high pixel density screens 2026-03-06 04:12:52 +01:00
CodeDevMLH
563a1e5484 Update manifest.json for release v1.7.0.13 [skip ci] 2026-03-06 03:10:54 +00:00
CodeDevMLH
788708370d Bump version to 1.7.0.13
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 04:10:04 +01:00
CodeDevMLH
4bb0de11da Remove fetchItemIdsFromList function and related logic for item ID retrieval 2026-03-06 04:09:36 +01:00
CodeDevMLH
d2abfc6b46 Enhance logging in media bar with contextual messages 2026-03-06 04:06:18 +01:00
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
10 changed files with 617 additions and 3954 deletions

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

@@ -18,6 +18,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public int PreloadCount { get; set; } = 3; public int PreloadCount { get; set; } = 3;
public int FadeTransitionDuration { get; set; } = 500; public int FadeTransitionDuration { get; set; } = 500;
public int MaxPaginationDots { get; set; } = 15; public int MaxPaginationDots { get; set; } = 15;
public bool ShowPaginationDots { get; set; } = true;
public bool SlideAnimationEnabled { get; set; } = true; public bool SlideAnimationEnabled { get; set; } = true;
public bool EnableVideoBackdrop { get; set; } = true; public bool EnableVideoBackdrop { get; set; } = true;
public bool UseSponsorBlock { get; set; } = true; public bool UseSponsorBlock { get; set; } = true;

View File

@@ -43,7 +43,8 @@
<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="MediaBarIsEnabled" name="MediaBarIsEnabled" /> <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>
@@ -57,21 +58,25 @@
<div class="fieldDescription">Show trailers as background if available.<br>Adds a <div class="fieldDescription">Show trailers as background if available.<br>Adds a
mute/unmute and pause/play button to control the video in the right top corner.</div> mute/unmute and pause/play button to control the video in the right top corner.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalTrailersContainer"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalTrailersContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers" <input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers"
name="PreferLocalTrailers" /> name="PreferLocalTrailers" />
<span>Prefer Local Trailers</span> <span>Prefer Local Trailers</span>
</label> </label>
<div class="fieldDescription">If enabled, local trailers will be preferred over remote (YouTube) trailers.</div> <div class="fieldDescription">If enabled, local trailers will be preferred over remote
(YouTube) trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalBackdropsContainer"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalBackdropsContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops" <input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops"
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 remote and local 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>
@@ -111,7 +116,8 @@
<span>Enable Custom Media IDs</span> <span>Enable Custom Media IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the slideshow will show the items listed <div class="fieldDescription">If enabled, the slideshow will show the items listed
below as the default content. If the list is empty, random items from your library are used.</div> below as the default content. If the list is empty, random items from your library are
used.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -119,35 +125,47 @@
name="ApplyLimitsToCustomIds" /> name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span> <span>Apply Limits to Custom IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div> <div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content
Limits) will also apply to Custom Media IDs and Collections. By default, custom lists
are not limited.</div>
</div> </div>
<div id="customMediaIdsContainer"> <div id="customMediaIdsContainer">
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label> <label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds"
name="CustomMediaIds"
style="width: 100%; height: 150px; font-family: monospace;"></textarea> style="width: 100%; height: 150px; font-family: monospace;"></textarea>
<div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as your default content. You can separate them by new line or comma. <div class="fieldDescription">Enter the IDs of the items you want to show in the
slideshow as your default content. You can separate them by new line or comma.
<br><br> <br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in <b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>. Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or
<code>ID [JellyfinItemId] DESCRIPTION</code>.
<br> <br>
Methods: Methods:
<ul> <ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li> <li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li> <li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g.
a Theme Video or Backdrop Video) using the native player.</li>
</ul> </ul>
You can also add a description after the ID using any separator like space, pipe (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code> You can also add a description after the ID using any separator like space, pipe (|)
or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br> <br><br>
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use the pipe (|) separator. <b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a
description, you <b>MUST</b> use the pipe (|) separator.
<br> <br>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f). <b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
</div> </div>
<p>You can find the IDs of your items in the URL of the item page in the web interface.<br> <p>You can find the IDs of your items in the URL of the item page in the web
interface.<br>
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
it (will take the first hit.<br><b>Note:</b> There is currently no feedback if the name in it (will take the first hit.<br><b>Note:</b> There is currently no feedback if
resolution succeeded, you will have to look if the bar displays the correct items). the name resolution succeeded, you will have to look if the bar displays the correct
items).
</p> </p>
</div> </div>
</div> </div>
@@ -160,14 +178,18 @@
name="EnableSeasonalContent" /> name="EnableSeasonalContent" />
<span>Enable Seasonal Content</span> <span>Enable Seasonal Content</span>
</label> </label>
<div class="fieldDescription">When enabled, seasonal sections below will override the default list or random selection <div class="fieldDescription">When enabled, seasonal sections below will override the
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> default list or random selection
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;"> <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> <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>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>
<div id="seasonalSectionsList"></div> <div id="seasonalSectionsList"></div>
@@ -189,8 +211,9 @@
name="SlideAnimationEnabled" /> name="SlideAnimationEnabled" />
<span>Enable Slide Animations</span> <span>Enable Slide Animations</span>
</label> </label>
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is <div class="fieldDescription">Enable the zooming-in effect on background images when a new
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div> slide is shown (does not affect trailer backdrops). Attention: This may cause
performance issues on weaker client hardware.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -199,7 +222,8 @@
<span>Enable Client-Side Settings</span> <span>Enable Client-Side Settings</span>
</label> </label>
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to <div class="fieldDescription">If enabled, users will see a media bar icon in the header to
override settings (like disabling the bar or trailer backdrops) locally on their device.</div> override settings (like disabling the bar or trailer backdrops) locally on their device.
</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -207,7 +231,8 @@
name="RandomizeThemeVideos" /> name="RandomizeThemeVideos" />
<span>Randomize Backdrop Video</span> <span>Randomize Backdrop Video</span>
</label> </label>
<div class="fieldDescription">If enabled, a random video from the backdrops/theme videos will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random video from the backdrops/theme videos
will be selected instead of the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -215,18 +240,22 @@
name="RandomizeLocalTrailers" /> name="RandomizeLocalTrailers" />
<span>Randomize Local Trailer</span> <span>Randomize Local Trailer</span>
</label> </label>
<div class="fieldDescription">If enabled, a random local trailer will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random local trailer will be selected instead of
the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" /> <input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
<span>Use SponsorBlock</span> <span>Use SponsorBlock</span>
</label> </label>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is available).</div> <div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is
available).</div>
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label> <label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Auto">Auto (Smart)</option> <option value="Auto">Auto (Smart)</option>
<option value="Maximum">Maximum (4K+)</option> <option value="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option> <option value="1080p">1080p</option>
@@ -241,7 +270,9 @@
<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>
@@ -328,7 +359,9 @@
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting</h2>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortBy">Sort By</label> <label class="selectLabel" for="SortBy">Sort By</label>
<select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortBy" name="SortBy"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Random">Random</option> <option value="Random">Random</option>
<option value="Original">Original (Custom List Order)</option> <option value="Original">Original (Custom List Order)</option>
<option value="PremiereDate">Premiere Date</option> <option value="PremiereDate">Premiere Date</option>
@@ -342,14 +375,17 @@
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortOrder">Sort Order</label> <label class="selectLabel" for="SortOrder">Sort Order</label>
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortOrder" name="SortOrder"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Ascending">Ascending</option> <option value="Ascending">Ascending</option>
<option value="Descending">Descending</option> <option value="Descending">Descending</option>
</select> </select>
<div class="fieldDescription">Sort items in Ascending or Descending order.</div> <div class="fieldDescription">Sort items in Ascending or Descending order.</div>
</div> </div>
<div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;"> <div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;">
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original' preserves Custom List order. <b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original'
preserves Custom List order.
</div> </div>
<h2 class="sectionTitle">Content Limits</h2> <h2 class="sectionTitle">Content Limits</h2>
@@ -377,6 +413,15 @@
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" /> <input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of slides to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" />
<span>Show Pagination Dots</span>
</label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div>
</div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
Dots</label> Dots</label>
@@ -397,7 +442,8 @@
name="IncludeWatchedContent" /> name="IncludeWatchedContent" />
<span>Include Watched Content</span> <span>Include Watched Content</span>
</label> </label>
<div class="fieldDescription">If enabled, watched content will be included in the random selection results.</div> <div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div> </div>
</div> </div>
@@ -459,7 +505,7 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -563,7 +609,7 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {

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.6.2</Version> <Version>1.7.1.4</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

@@ -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

@@ -768,7 +768,22 @@
.button-container { .button-container {
top: calc(50% + 25vh); top: calc(50% + 25vh);
left: 50%; left: 50%;
transform: translateX(-50%) scale(0.95); transform: translateX(-50%);
width: max-content;
max-width: 98vw;
flex-wrap: nowrap;
justify-content: center;
gap: 15px;
}
.button-container button {
margin: 0 !important;
min-width: 0 !important;
}
.button-container .detail-button,
.button-container .favorite-button {
flex-shrink: 0;
} }
.logo { .logo {
@@ -805,30 +820,6 @@
.trailer-button { .trailer-button {
padding: 8px 14px; padding: 8px 14px;
} }
.misc-info {
flex-wrap: wrap;
justify-content: center;
gap: 0px 10px;
}
.runTime {
width: 100%;
justify-content: center;
margin-top: 0.5vh;
}
.misc-info .separator-icon:last-of-type {
display: none;
}
.genre {
top: calc(50% + 16vh);
}
.button-container {
top: calc(50% + 27vh);
}
} }
} }
@@ -1000,3 +991,19 @@
.layout-tv .backdrop-container{ .layout-tv .backdrop-container{
top: -5%; top: -5%;
} }
@media screen and (min-width: 960px) and (-webkit-device-pixel-ratio: 1) {
.layout-tv .backdrop.animate {
animation: none !important;
}
.layout-tv .logo.animate {
animation: none !important;
}
.layout-tv .slide-counter,
.layout-tv .dots-container {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}

View File

@@ -1,4 +1,4 @@
/* /*
* Jellyfin Slideshow by M0RPH3US v4.0.1 * Jellyfin Slideshow by M0RPH3US v4.0.1
* Modified by CodeDevMLH * Modified by CodeDevMLH
* *
@@ -38,6 +38,7 @@ const CONFIG = {
preloadCount: 3, preloadCount: 3,
fadeTransitionDuration: 500, fadeTransitionDuration: 500,
maxPaginationDots: 15, maxPaginationDots: 15,
showPaginationDots: true,
slideAnimationEnabled: true, slideAnimationEnabled: true,
enableVideoBackdrop: true, enableVideoBackdrop: true,
useSponsorBlock: true, useSponsorBlock: true,
@@ -124,7 +125,7 @@ const processNextRequest = () => {
}) })
.then(callback) .then(callback)
.catch((error) => { .catch((error) => {
console.error("Error in throttled request:", error); console.error("🎬 Media Bar:", "Error in throttled request:", error);
}) })
.finally(() => { .finally(() => {
setTimeout(processNextRequest, 100); setTimeout(processNextRequest, 100);
@@ -158,18 +159,26 @@ const isUserLoggedIn = () => {
window.ApiClient._serverInfo.AccessToken window.ApiClient._serverInfo.AccessToken
); );
} catch (error) { } catch (error) {
console.error("Error checking login status:", error); console.error("🎬 Media Bar:", "Error checking login status:", error);
return false; return false;
} }
}; };
/**
* 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
*/ */
const initJellyfinData = (callback) => { const initJellyfinData = (callback) => {
if (!window.ApiClient) { if (!window.ApiClient) {
console.warn("⏳ window.ApiClient is not available yet. Retrying..."); console.warn("🎬 Media Bar:", "⏳ window.ApiClient is not available yet. Retrying...");
setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval); setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
return; return;
} }
@@ -190,7 +199,7 @@ const initJellyfinData = (callback) => {
callback(); callback();
} }
} catch (error) { } catch (error) {
console.error("Error initializing Jellyfin data:", error); console.error("🎬 Media Bar:", "Error initializing Jellyfin data:", error);
setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval); setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
} }
}; };
@@ -202,9 +211,9 @@ const initLocalization = async () => {
try { try {
const locale = await LocalizationUtils.getCurrentLocale(); const locale = await LocalizationUtils.getCurrentLocale();
await LocalizationUtils.loadTranslations(locale); await LocalizationUtils.loadTranslations(locale);
console.log("✅ Localization initialized"); console.log("🎬 Media Bar:", "✅ Localization initialized");
} catch (error) { } catch (error) {
console.error("Error initializing localization:", error); console.error("🎬 Media Bar:", "Error initializing localization:", error);
} }
}; };
@@ -324,7 +333,7 @@ const initLoadingScreen = () => {
* Resets the slideshow state completely * Resets the slideshow state completely
*/ */
const resetSlideshowState = () => { const resetSlideshowState = () => {
console.log("🔄 Resetting slideshow state..."); console.log("🎬 Media Bar:", "🔄 Resetting slideshow state...");
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
@@ -378,14 +387,14 @@ const startLoginStatusWatcher = () => {
if (isLoggedIn !== wasLoggedIn) { if (isLoggedIn !== wasLoggedIn) {
if (isLoggedIn) { if (isLoggedIn) {
console.log("👤 User logged in. Initializing slideshow..."); console.log("🎬 Media Bar:", "👤 User logged in. Initializing slideshow...");
if (!STATE.slideshow.hasInitialized) { if (!STATE.slideshow.hasInitialized) {
waitForApiClientAndInitialize(); waitForApiClientAndInitialize();
} else { } else {
console.log("🔄 Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "🔄 Slideshow already initialized, skipping");
} }
} else { } else {
console.log("👋 User logged out. Stopping slideshow..."); console.log("🎬 Media Bar:", "👋 User logged out. Stopping slideshow...");
resetSlideshowState(); resetSlideshowState();
} }
wasLoggedIn = isLoggedIn; wasLoggedIn = isLoggedIn;
@@ -403,7 +412,7 @@ const waitForApiClientAndInitialize = () => {
window.slideshowCheckInterval = setInterval(() => { window.slideshowCheckInterval = setInterval(() => {
if (!window.ApiClient) { if (!window.ApiClient) {
console.log("⏳ ApiClient not available yet. Waiting..."); console.log("🎬 Media Bar:", "⏳ ApiClient not available yet. Waiting...");
return; return;
} }
@@ -413,23 +422,23 @@ const waitForApiClientAndInitialize = () => {
window.ApiClient._serverInfo && window.ApiClient._serverInfo &&
window.ApiClient._serverInfo.AccessToken window.ApiClient._serverInfo.AccessToken
) { ) {
console.log( console.log("🎬 Media Bar:",
"🔓 User is fully logged in. Starting slideshow initialization..." "🔓 User is fully logged in. Starting slideshow initialization..."
); );
clearInterval(window.slideshowCheckInterval); clearInterval(window.slideshowCheckInterval);
if (!STATE.slideshow.hasInitialized) { if (!STATE.slideshow.hasInitialized) {
initJellyfinData(async () => { initJellyfinData(async () => {
console.log("✅ Jellyfin API client initialized successfully"); console.log("🎬 Media Bar:", "✅ Jellyfin API client initialized successfully");
await initLocalization(); await initLocalization();
await fetchPluginConfig(); await fetchPluginConfig();
slidesInit(); slidesInit();
}); });
} else { } else {
console.log("🔄 Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "🔄 Slideshow already initialized, skipping");
} }
} else { } else {
console.log( console.log("🎬 Media Bar:",
"🔒 Authentication incomplete. Waiting for complete login..." "🔒 Authentication incomplete. Waiting for complete login..."
); );
} }
@@ -460,11 +469,11 @@ const fetchPluginConfig = async () => {
// Sync to LocalStorage for next load // Sync to LocalStorage for next load
localStorage.setItem('mediaBarEnhanced-enableLoadingScreen', CONFIG.enableLoadingScreen); localStorage.setItem('mediaBarEnhanced-enableLoadingScreen', CONFIG.enableLoadingScreen);
console.log("✅ MediaBarEnhanced config loaded", CONFIG); console.log("🎬 Media Bar:", "✅ MediaBarEnhanced config loaded", CONFIG);
} }
} }
} catch (e) { } catch (e) {
console.error("Failed to load MediaBarEnhanced config", e); console.error("🎬 Media Bar:", "Failed to load MediaBarEnhanced config", e);
} }
}; };
@@ -730,20 +739,25 @@ const SlideUtils = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Invalid URL for modal:", url); console.warn("🎬 Media Bar:", "Invalid URL for modal:", url);
} }
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,
@@ -751,7 +765,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
} }
}); });
@@ -809,7 +822,7 @@ const LocalizationUtils = {
locale = localStorage.getItem("language").toLowerCase(); locale = localStorage.getItem("language").toLowerCase();
} }
} catch (e) { } catch (e) {
console.warn("Could not access localStorage for language:", e); console.warn("🎬 Media Bar:", "Could not access localStorage for language:", e);
} }
if (!locale) { if (!locale) {
@@ -835,7 +848,7 @@ const LocalizationUtils = {
} }
} }
} catch (error) { } catch (error) {
console.warn("Could not fetch user audio language preference:", error); console.warn("🎬 Media Bar:", "Could not fetch user audio language preference:", error);
} }
} }
@@ -855,7 +868,7 @@ const LocalizationUtils = {
} }
} }
} catch (error) { } catch (error) {
console.warn("Could not fetch server metadata language preference:", error); console.warn("🎬 Media Bar:", "Could not fetch server metadata language preference:", error);
} }
} }
@@ -900,7 +913,7 @@ const LocalizationUtils = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Error checking performance entries:", e); console.warn("🎬 Media Bar:", "Error checking performance entries:", e);
} }
} }
@@ -965,7 +978,7 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error('Failed to parse JSON from standard extraction.'); console.error("🎬 Media Bar:", 'Failed to parse JSON from standard extraction.');
// Try alternative extraction below // Try alternative extraction below
} }
@@ -977,7 +990,7 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error('Failed to parse JSON from direct extraction.'); console.error("🎬 Media Bar:", 'Failed to parse JSON from direct extraction.');
// Try direct extraction // Try direct extraction
} }
} }
@@ -991,11 +1004,11 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error("Failed to parse JSON from chunk:", e); console.error("🎬 Media Bar:", "Failed to parse JSON from chunk:", e);
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading translations:", error); console.error("🎬 Media Bar:", "Error loading translations:", error);
} finally { } finally {
delete this.isLoading[locale]; delete this.isLoading[locale];
} }
@@ -1059,38 +1072,11 @@ const ApiUtils = {
return itemData; return itemData;
} catch (error) { } catch (error) {
console.error(`Error fetching details for item ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching details for item ${itemId}:`, error);
return null; return null;
} }
}, },
/**
* Fetch item IDs from the list file
* @returns {Promise<Array>} Array of item IDs
*/
// MARK: LIST FILE
async fetchItemIdsFromList() {
try {
const listFileName = `${STATE.jellyfinData.serverAddress}/web/avatars/list.txt?userId=${STATE.jellyfinData.userId}`;
const response = await fetch(listFileName);
if (!response.ok) {
console.warn("list.txt not found or inaccessible. Using random items.");
return [];
}
const text = await response.text();
return text
.split("\n")
.map((id) => id.trim())
.filter((id) => id)
.slice(1);
} catch (error) {
console.error("Error fetching list.txt:", error);
return [];
}
},
/** /**
* Fetches random items from the server * Fetches random items from the server
* @returns {Promise<Array>} Array of item objects * @returns {Promise<Array>} Array of item objects
@@ -1101,7 +1087,7 @@ const ApiUtils = {
!STATE.jellyfinData.accessToken || !STATE.jellyfinData.accessToken ||
STATE.jellyfinData.accessToken === "Not Found" STATE.jellyfinData.accessToken === "Not Found"
) { ) {
console.warn("Access token not available. Delaying API request..."); console.warn("🎬 Media Bar:", "Access token not available. Delaying API request...");
return []; return [];
} }
@@ -1109,11 +1095,11 @@ const ApiUtils = {
!STATE.jellyfinData.serverAddress || !STATE.jellyfinData.serverAddress ||
STATE.jellyfinData.serverAddress === "Not Found" STATE.jellyfinData.serverAddress === "Not Found"
) { ) {
console.warn("Server address not available. Delaying API request..."); console.warn("🎬 Media Bar:", "Server address not available. Delaying API request...");
return []; return [];
} }
console.log("Fetching random items from server..."); console.log("🎬 Media Bar:", "Fetching random items from server...");
let sortParams = `sortBy=${CONFIG.sortBy}`; let sortParams = `sortBy=${CONFIG.sortBy}`;
@@ -1134,7 +1120,7 @@ const ApiUtils = {
); );
if (!response.ok) { if (!response.ok) {
console.error( console.error("🎬 Media Bar:",
`Failed to fetch items: ${response.status} ${response.statusText}` `Failed to fetch items: ${response.status} ${response.statusText}`
); );
return []; return [];
@@ -1143,13 +1129,13 @@ const ApiUtils = {
const data = await response.json(); const data = await response.json();
const items = data.Items || []; const items = data.Items || [];
console.log( console.log("🎬 Media Bar:",
`Successfully fetched ${items.length} random items from server` `Successfully fetched ${items.length} random items from server`
); );
return items.map((item) => item.Id); return items.map((item) => item.Id);
} catch (error) { } catch (error) {
console.error("Error fetching item IDs:", error); console.error("🎬 Media Bar:", "Error fetching item IDs:", error);
return []; return [];
} }
}, },
@@ -1173,7 +1159,7 @@ const ApiUtils = {
try { try {
const sessionId = await this.getSessionId(); const sessionId = await this.getSessionId();
if (!sessionId) { if (!sessionId) {
console.error("Session ID not found."); console.error("🎬 Media Bar:", "Session ID not found.");
return false; return false;
} }
@@ -1189,10 +1175,10 @@ const ApiUtils = {
); );
} }
console.log("Play command sent successfully to session:", sessionId); console.log("🎬 Media Bar:", "Play command sent successfully to session:", sessionId);
return true; return true;
} catch (error) { } catch (error) {
console.error("Error sending play command:", error); console.error("🎬 Media Bar:", "Error sending play command:", error);
return false; return false;
} }
}, },
@@ -1218,7 +1204,7 @@ const ApiUtils = {
const sessions = await response.json(); const sessions = await response.json();
if (!sessions || sessions.length === 0) { if (!sessions || sessions.length === 0) {
console.warn( console.warn("🎬 Media Bar:",
"No sessions found for deviceId:", "No sessions found for deviceId:",
STATE.jellyfinData.deviceId STATE.jellyfinData.deviceId
); );
@@ -1227,7 +1213,7 @@ const ApiUtils = {
return sessions[0].Id; return sessions[0].Id;
} catch (error) { } catch (error) {
console.error("Error fetching session data:", error); console.error("🎬 Media Bar:", "Error fetching session data:", error);
return null; return null;
} }
}, },
@@ -1255,7 +1241,7 @@ const ApiUtils = {
} }
button.classList.toggle("favorited", !isFavorite); button.classList.toggle("favorited", !isFavorite);
} catch (error) { } catch (error) {
console.error("Error toggling favorite:", error); console.error("🎬 Media Bar:", "Error toggling favorite:", error);
} }
}, },
@@ -1297,7 +1283,7 @@ const ApiUtils = {
this._sponsorBlockCache[videoId] = result; this._sponsorBlockCache[videoId] = result;
return result; return result;
} catch (error) { } catch (error) {
console.warn('Error fetching SponsorBlock data:', error); console.warn("🎬 Media Bar:", 'Error fetching SponsorBlock data:', error);
return { intro: null, outro: null }; return { intro: null, outro: null };
} }
}, },
@@ -1317,7 +1303,7 @@ const ApiUtils = {
); );
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to search for '${name}'`); console.warn("🎬 Media Bar:", `Failed to search for '${name}'`);
return null; return null;
} }
@@ -1327,7 +1313,7 @@ const ApiUtils = {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error searching for '${name}':`, error); console.error("🎬 Media Bar:", `Error searching for '${name}':`, error);
return null; return null;
} }
}, },
@@ -1347,16 +1333,16 @@ const ApiUtils = {
); );
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to fetch collection items for ${collectionId}`); console.warn("🎬 Media Bar:", `Failed to fetch collection items for ${collectionId}`);
return []; return [];
} }
const data = await response.json(); const data = await response.json();
const items = data.Items || []; const items = data.Items || [];
console.log(`Resolved collection ${collectionId} to ${items.length} items`); console.log("🎬 Media Bar:", `Resolved collection ${collectionId} to ${items.length} items`);
return items.map(i => ({ Id: i.Id, Type: i.Type })); return items.map(i => ({ Id: i.Id, Type: i.Type }));
} catch (error) { } catch (error) {
console.error(`Error fetching collection items for ${collectionId}:`, error); console.error("🎬 Media Bar:", `Error fetching collection items for ${collectionId}:`, error);
return []; return [];
} }
}, },
@@ -1386,7 +1372,7 @@ const ApiUtils = {
if (CONFIG.randomizeLocalTrailers && trailers.length > 1) { if (CONFIG.randomizeLocalTrailers && trailers.length > 1) {
const randomIndex = Math.floor(Math.random() * trailers.length); const randomIndex = Math.floor(Math.random() * trailers.length);
trailer = trailers[randomIndex]; trailer = trailers[randomIndex];
console.log(`Using random local trailer (${randomIndex + 1}/${trailers.length}) for ${itemId}: ${trailer.Name}`); console.log("🎬 Media Bar:", `Using random local trailer (${randomIndex + 1}/${trailers.length}) for ${itemId}: ${trailer.Name}`);
} else { } else {
trailer = trailers[0]; trailer = trailers[0];
} }
@@ -1395,12 +1381,12 @@ 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;
} catch (error) { } catch (error) {
console.error(`Error fetching local trailer for ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching local trailer for ${itemId}:`, error);
return null; return null;
} }
}, },
@@ -1426,21 +1412,21 @@ const ApiUtils = {
if (CONFIG.randomizeThemeVideos && items.length > 1) { if (CONFIG.randomizeThemeVideos && items.length > 1) {
const randomIndex = Math.floor(Math.random() * items.length); const randomIndex = Math.floor(Math.random() * items.length);
video = items[randomIndex]; video = items[randomIndex];
console.log(`Found Theme Video (Random ${randomIndex + 1}/${items.length}) via ThemeVideos endpoint: ${video.Name} (${video.Id})`); console.log("🎬 Media Bar:", `Found Theme Video (Random ${randomIndex + 1}/${items.length}) via ThemeVideos endpoint: ${video.Name} (${video.Id})`);
} else { } else {
video = items[0]; video = items[0];
console.log(`Found Theme Video (First) via ThemeVideos endpoint: ${video.Name} (${video.Id})`); console.log("🎬 Media Bar:", `Found Theme Video (First) via ThemeVideos endpoint: ${video.Name} (${video.Id})`);
} }
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`
}; };
} }
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error fetching theme videos for ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching theme videos for ${itemId}:`, error);
return null; return null;
} }
} }
@@ -1624,7 +1610,7 @@ const SlideCreator = {
*/ */
createSlideElement(item, title) { createSlideElement(item, title) {
if (!item || !item.Id) { if (!item || !item.Id) {
console.error("Invalid item data:", item); console.error("🎬 Media Bar:", "Invalid item data:", item);
return null; return null;
} }
@@ -1656,27 +1642,27 @@ const SlideCreator = {
if (guidMatch) { if (guidMatch) {
const videoId = guidMatch[1]; const videoId = guidMatch[1];
console.log(`Using custom local video ID for ${itemId}: ${videoId}`); console.log("🎬 Media Bar:", `Using custom local video ID for ${itemId}: ${videoId}`);
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.)
trailerUrl = customValue; trailerUrl = customValue;
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `Using custom trailer URL for ${itemId}: ${trailerUrl}`);
} }
} }
// 1b. Check Theme Video if preferred (Local Backdrop) // 1b. Check Theme Video if preferred (Local Backdrop)
else if (CONFIG.preferLocalBackdrops && item.themeVideoUrl) { else if (CONFIG.preferLocalBackdrops && item.themeVideoUrl) {
trailerUrl = item.themeVideoUrl; trailerUrl = item.themeVideoUrl;
console.log(`Using theme video (local backdrop) for ${itemId}: ${trailerUrl.url || trailerUrl}`); console.log("🎬 Media Bar:", `Using theme video (local backdrop) for ${itemId}: ${trailerUrl.url || trailerUrl}`);
} }
// 1c. Check Local Trailer if preferred // 1c. Check Local Trailer if preferred
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl; trailerUrl = item.localTrailerUrl;
console.log(`Using local trailer for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `Using local trailer for ${itemId}: ${trailerUrl}`);
} }
// 1d. Fallback to Remote Trailer // 1d. Fallback to Remote Trailer
else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) { else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
@@ -1685,10 +1671,10 @@ const SlideCreator = {
// 1e. Final Fallback to Local Trailer (even if not preferred) // 1e. Final Fallback to Local Trailer (even if not preferred)
else if (item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl; trailerUrl = item.localTrailerUrl;
console.log(`Using local trailer fallback for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `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);
@@ -1715,10 +1701,15 @@ const SlideCreator = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Invalid trailer URL:", trailerUrl); console.warn("🎬 Media Bar:", "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";
@@ -1729,12 +1720,17 @@ const SlideCreator = {
style: "opacity: 0; transition: opacity 1.2s ease-in-out;" // Start interrupted/transparent style: "opacity: 0; transition: opacity 1.2s ease-in-out;" // Start interrupted/transparent
}); });
const ytPlayerDiv = SlideUtils.createElement("div", { // Create an iframe upfront
const ytPlayerIframe = SlideUtils.createElement("iframe", {
id: `youtube-player-${itemId}`, id: `youtube-player-${itemId}`,
style: "width: 100%; height: 100%;" 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(ytPlayerDiv); videoBackdrop.appendChild(ytPlayerIframe);
// Initialize YouTube Player // Initialize YouTube Player
SlideUtils.loadYouTubeIframeAPI().then(() => { SlideUtils.loadYouTubeIframeAPI().then(() => {
@@ -1751,7 +1747,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
}; };
@@ -1773,17 +1768,14 @@ const SlideCreator = {
// Apply SponsorBlock start/end times // Apply SponsorBlock start/end times
if (segments.intro) { if (segments.intro) {
playerVars.start = Math.ceil(segments.intro[1]); playerVars.start = Math.ceil(segments.intro[1]);
console.info(`SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`); console.info("🎬 Media Bar:", `SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`);
} }
if (segments.outro) { if (segments.outro) {
playerVars.end = Math.floor(segments.outro[0]); playerVars.end = Math.floor(segments.outro[0]);
console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`); console.info("🎬 Media Bar:", `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) => {
@@ -1818,16 +1810,16 @@ const SlideCreator = {
// Re-check conditions before processing fallback // Re-check conditions before processing fallback
const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) { if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) {
console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`); console.log("🎬 Media Bar:", `Navigation detected during autoplay check for ${itemId}, stopping video.`);
try { try {
event.target.stopVideo(); event.target.stopVideo();
} catch (e) { console.warn("Error stopping video in timeout:", e); } } catch (e) { console.warn("🎬 Media Bar:", "Error stopping video in timeout:", e); }
return; return;
} }
if (event.target.getPlayerState() !== YT.PlayerState.PLAYING && if (event.target.getPlayerState() !== YT.PlayerState.PLAYING &&
event.target.getPlayerState() !== YT.PlayerState.BUFFERING) { event.target.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); console.warn("🎬 Media Bar:", `Autoplay blocked for ${itemId}, attempting muted fallback`);
event.target.mute(); event.target.mute();
event.target.playVideo(); event.target.playVideo();
} }
@@ -1858,7 +1850,7 @@ const SlideCreator = {
} }
}, },
'onError': (event) => { 'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`); console.warn("🎬 Media Bar:", `YouTube player error ${event.data} for video ${videoId}`);
// Fallback to next slide on error // Fallback to next slide on error
if (CONFIG.waitForTrailerToEnd) { if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -1870,7 +1862,7 @@ 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 videoSrc = (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl);
@@ -1892,7 +1884,7 @@ const SlideCreator = {
videoBackdrop.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("🎬 Media Bar:", `Local video ${itemId} started playing but slide is not active, pausing.`);
event.target.pause(); event.target.pause();
event.target.currentTime = 0; event.target.currentTime = 0;
return; return;
@@ -1914,7 +1906,7 @@ const SlideCreator = {
}); });
videoBackdrop.addEventListener('error', (event) => { videoBackdrop.addEventListener('error', (event) => {
console.warn(`Local video error for item ${itemId}`); console.warn("🎬 Media Bar:", `Local video error for item ${itemId}`);
const slide = event.target.closest('.slide'); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -2277,7 +2269,7 @@ const SlideCreator = {
return slideElement; return slideElement;
} catch (error) { } catch (error) {
console.error("Error creating slide for item:", error, itemId); console.error("🎬 Media Bar:", "Error creating slide for item:", error, itemId);
return null; return null;
} }
}, },
@@ -2289,6 +2281,8 @@ const SlideCreator = {
const SlideshowManager = { const SlideshowManager = {
createPaginationDots() { createPaginationDots() {
if (!CONFIG.showPaginationDots) return;
let dotsContainer = document.querySelector(".dots-container"); let dotsContainer = document.querySelector(".dots-container");
if (!dotsContainer) { if (!dotsContainer) {
dotsContainer = document.createElement("div"); dotsContainer = document.createElement("div");
@@ -2376,7 +2370,7 @@ const SlideshowManager = {
this.upgradeSlideImageQuality(currentSlide); this.upgradeSlideImageQuality(currentSlide);
if (!currentSlide) { if (!currentSlide) {
console.error(`Failed to create slide for item ${currentItemId}`); console.error("🎬 Media Bar:", `Failed to create slide for item ${currentItemId}`);
STATE.slideshow.isTransitioning = false; STATE.slideshow.isTransitioning = false;
setTimeout(() => this.nextSlide(), 500); setTimeout(() => this.nextSlide(), 500);
return; return;
@@ -2468,9 +2462,9 @@ const SlideshowManager = {
// Check if it actually started playing after a short delay (handling autoplay blocks) // Check if it actually started playing after a short delay (handling autoplay blocks)
setTimeout(() => { setTimeout(() => {
if (videoBackdrop.paused && currentSlide.classList.contains('active')) { if (videoBackdrop.paused && currentSlide.classList.contains('active')) {
console.warn(`Autoplay blocked for ${currentItemId}, attempting muted fallback`); console.warn("🎬 Media Bar:", `Autoplay blocked for ${currentItemId}, attempting muted fallback`);
videoBackdrop.muted = true; videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err)); videoBackdrop.play().catch(err => console.error("🎬 Media Bar:", "Muted fallback failed", err));
} }
}, 1000); }, 1000);
}); });
@@ -2497,7 +2491,7 @@ const SlideshowManager = {
if (player.getPlayerState && if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING && player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) { player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted..."); console.log("🎬 Media Bar:", "YouTube loadVideoById didn't start playback, retrying muted...");
player.mute(); player.mute();
player.playVideo(); player.playVideo();
} }
@@ -2556,13 +2550,13 @@ const SlideshowManager = {
this.pruneSlideCache(); this.pruneSlideCache();
} catch (error) { } catch (error) {
console.error("Error updating current slide:", error); console.error("🎬 Media Bar:", "Error updating current slide:", error);
} finally { } finally {
setTimeout(() => { setTimeout(() => {
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");
@@ -2603,7 +2597,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
@@ -2698,7 +2694,7 @@ const SlideshowManager = {
delete STATE.slideshow.createdSlides[itemId]; delete STATE.slideshow.createdSlides[itemId];
prunedAny = true; prunedAny = true;
console.log(`Pruned slide ${itemId} at distance ${distance} from view`); console.log("🎬 Media Bar:", `Pruned slide ${itemId} at distance ${distance} from view`);
} }
}); });
@@ -2745,7 +2741,7 @@ const SlideshowManager = {
} }
video.play().catch(error => { video.play().catch(error => {
console.warn("Unmuted play blocked, reverting to muted..."); console.warn("🎬 Media Bar:", "Unmuted play blocked, reverting to muted...");
STATE.slideshow.isMuted = true; STATE.slideshow.isMuted = true;
video.muted = true; video.muted = true;
video.play(); video.play();
@@ -2766,7 +2762,7 @@ const SlideshowManager = {
setTimeout(() => { setTimeout(() => {
const state = player.getPlayerState(); const state = player.getPlayerState();
if (state === 2) { if (state === 2) {
console.log("Video was paused after unmute..."); console.log("🎬 Media Bar:", "Video was paused after unmute...");
STATE.slideshow.isMuted = true; STATE.slideshow.isMuted = true;
player.mute(); player.mute();
player.playVideo(); player.playVideo();
@@ -2857,7 +2853,7 @@ const SlideshowManager = {
player.pauseVideo(); player.pauseVideo();
} }
} catch (e) { } catch (e) {
console.warn("Error pausing/stopping YouTube player:", e); console.warn("🎬 Media Bar:", "Error pausing/stopping YouTube player:", e);
} }
}); });
} }
@@ -2877,7 +2873,7 @@ const SlideshowManager = {
video.removeAttribute('src'); video.removeAttribute('src');
video.load(); video.load();
} catch (e) { } catch (e) {
console.warn("Error stopping HTML5 video:", e); console.warn("🎬 Media Bar:", "Error stopping HTML5 video:", e);
} }
}); });
} }
@@ -2918,7 +2914,7 @@ const SlideshowManager = {
} }
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("🎬 Media Bar:", "Error resuming HTML5 video:", e));
} }
}, },
@@ -3120,14 +3116,14 @@ const SlideshowManager = {
} }
if (isInRange) { if (isInRange) {
console.log(`Seasonal match found: ${section.Name}`); console.log("🎬 Media Bar:", `Seasonal match found: ${section.Name}`);
idsString = section.MediaIds; idsString = section.MediaIds;
usingSeasonal = true; usingSeasonal = true;
break; // Use first matching season break; // Use first matching season
} }
} }
} catch (e) { } catch (e) {
console.error("Error parsing seasonal sections in JS:", e); console.error("🎬 Media Bar:", "Error parsing seasonal sections in JS:", e);
} }
} }
@@ -3185,14 +3181,14 @@ const SlideshowManager = {
if (guidMatch) { if (guidMatch) {
id = guidMatch[1]; id = guidMatch[1];
} else { } else {
console.log(`Input '${rawId}' is not a GUID, searching for Collection/Playlist by name...`); console.log("🎬 Media Bar:", `Input '${rawId}' is not a GUID, searching for Collection/Playlist by name...`);
const resolvedId = await ApiUtils.findCollectionOrPlaylistByName(rawId); const resolvedId = await ApiUtils.findCollectionOrPlaylistByName(rawId);
if (resolvedId) { if (resolvedId) {
console.log(`Resolved name '${rawId}' to ID: ${resolvedId}`); console.log("🎬 Media Bar:", `Resolved name '${rawId}' to ID: ${resolvedId}`);
id = resolvedId; id = resolvedId;
} else { } else {
console.warn(`Could not find Collection or Playlist with name: '${rawId}'`); console.warn("🎬 Media Bar:", `Could not find Collection or Playlist with name: '${rawId}'`);
continue; // Skip if resolution failed continue; // Skip if resolution failed
} }
} }
@@ -3200,14 +3196,14 @@ const SlideshowManager = {
const item = await ApiUtils.fetchItemDetails(id); const item = await ApiUtils.fetchItemDetails(id);
if (item && (item.Type === 'BoxSet' || item.Type === 'Playlist')) { if (item && (item.Type === 'BoxSet' || item.Type === 'Playlist')) {
console.log(`Found Collection/Playlist: ${id} (${item.Type}), fetching children...`); console.log("🎬 Media Bar:", `Found Collection/Playlist: ${id} (${item.Type}), fetching children...`);
const children = await ApiUtils.fetchCollectionItems(id); const children = await ApiUtils.fetchCollectionItems(id);
finalIds.push(...children); finalIds.push(...children);
} else if (item) { } else if (item) {
finalIds.push({ Id: item.Id, Type: item.Type }); finalIds.push({ Id: item.Id, Type: item.Type });
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${rawId}:`, e); console.warn("🎬 Media Bar:", `Error resolving item ${rawId}:`, e);
} }
} }
return finalIds; return finalIds;
@@ -3223,7 +3219,7 @@ const SlideshowManager = {
// 1. Try Custom Media/Collection IDs from Config & seasonal content // 1. Try Custom Media/Collection IDs from Config & seasonal content
if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) { if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) {
console.log("Using Custom Media IDs from configuration"); console.log("🎬 Media Bar:", "Using Custom Media IDs from configuration");
const rawIds = this.parseCustomIds(); const rawIds = this.parseCustomIds();
const resolvedItems = await this.resolveCollectionsAndItems(rawIds); const resolvedItems = await this.resolveCollectionsAndItems(rawIds);
@@ -3253,20 +3249,15 @@ const SlideshowManager = {
} }
} }
itemIds = keptItems.map(i => i.Id); itemIds = keptItems.map(i => i.Id);
console.log(`Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`); console.log("🎬 Media Bar:", `Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`);
} else { } else {
itemIds = resolvedItems.map(i => i.Id); itemIds = resolvedItems.map(i => i.Id);
} }
} }
// 2. Try Avatar List (list.txt) // 2. Fallback to server query (Random)
if (itemIds.length === 0) { if (itemIds.length === 0) {
itemIds = await ApiUtils.fetchItemIdsFromList(); console.log("🎬 Media Bar:", "No custom list found, fetching random items from server...");
}
// 3. Fallback to server query (Random)
if (itemIds.length === 0) {
console.log("No custom list found, fetching random items from server...");
itemIds = await ApiUtils.fetchItemIdsFromServer(); itemIds = await ApiUtils.fetchItemIdsFromServer();
if (CONFIG.sortBy === 'Random') { if (CONFIG.sortBy === 'Random') {
@@ -3278,7 +3269,7 @@ const SlideshowManager = {
itemIds = SlideUtils.shuffleArray(itemIds); itemIds = SlideUtils.shuffleArray(itemIds);
} else if (CONFIG.sortBy !== 'Original') { } else if (CONFIG.sortBy !== 'Original') {
// Client-side sort required... // Client-side sort required...
console.log(`Sorting ${itemIds.length} custom items by ${CONFIG.sortBy} ${CONFIG.sortOrder}`); console.log("🎬 Media Bar:", `Sorting ${itemIds.length} custom items by ${CONFIG.sortBy} ${CONFIG.sortOrder}`);
const itemsWithDetails = []; const itemsWithDetails = [];
for (const id of itemIds) { for (const id of itemIds) {
const item = await ApiUtils.fetchItemDetails(id); const item = await ApiUtils.fetchItemDetails(id);
@@ -3320,7 +3311,7 @@ const SlideshowManager = {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading slideshow data:", error); console.error("🎬 Media Bar:", "Error loading slideshow data:", error);
} finally { } finally {
STATE.slideshow.isLoading = false; STATE.slideshow.isLoading = false;
} }
@@ -3466,7 +3457,7 @@ const MediaBarEnhancedSettingsManager = {
this.initialized = true; this.initialized = true;
this.injectSettingsIcon(); this.injectSettingsIcon();
console.log("MediaBarEnhanced: Client-Side Settings Manager initialized."); console.log("🎬 Media Bar:", "Client-Side Settings Manager initialized.");
}, },
getSetting(key, defaultValue) { getSetting(key, defaultValue) {
@@ -3640,7 +3631,7 @@ const initPageVisibilityHandler = () => {
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos"); console.log("🎬 Media Bar:", "Tab inactive - pausing slideshow and videos");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying; wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
@@ -3658,7 +3649,7 @@ const initPageVisibilityHandler = () => {
player.pauseVideo(); player.pauseVideo();
STATE.slideshow.isVideoPlaying = false; STATE.slideshow.isVideoPlaying = false;
} catch (e) { } catch (e) {
console.warn("Error pausing video on tab hide:", e); console.warn("🎬 Media Bar:", "Error pausing video on tab hide:", e);
} }
} else if (player.tagName === 'VIDEO') { // HTML5 Video } else if (player.tagName === 'VIDEO') { // HTML5 Video
player.pause(); player.pause();
@@ -3667,7 +3658,7 @@ const initPageVisibilityHandler = () => {
} }
} }
} else { } else {
console.log("Tab active - resuming slideshow"); console.log("🎬 Media Bar:", "Tab active - resuming slideshow");
if (!STATE.slideshow.isPaused) { if (!STATE.slideshow.isPaused) {
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
@@ -3680,16 +3671,16 @@ const initPageVisibilityHandler = () => {
player.playVideo(); player.playVideo();
STATE.slideshow.isVideoPlaying = true; STATE.slideshow.isVideoPlaying = true;
} catch (e) { } catch (e) {
console.warn("Error resuming video on tab show:", e); console.warn("🎬 Media Bar:", "Error resuming video on tab show:", e);
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start(); STATE.slideshow.slideInterval.start();
} }
} }
} else if (player.tagName === 'VIDEO') { // HTML5 Video } else if (player.tagName === 'VIDEO') { // HTML5 Video
try { try {
player.play().catch(e => console.warn("Error resuming HTML5 video:", e)); player.play().catch(e => console.warn("🎬 Media Bar:", "Error resuming HTML5 video:", e));
STATE.slideshow.isVideoPlaying = true; STATE.slideshow.isVideoPlaying = true;
} catch(e) { console.warn(e); } } catch(e) { console.warn("🎬 Media Bar:", e); }
} }
} else { } else {
// No video was playing, just restart interval // No video was playing, just restart interval
@@ -3715,15 +3706,15 @@ const initPageVisibilityHandler = () => {
*/ */
const slidesInit = async () => { const slidesInit = async () => {
if (STATE.slideshow.hasInitialized) { if (STATE.slideshow.hasInitialized) {
console.log("⚠️ Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "⚠️ Slideshow already initialized, skipping");
return; return;
} }
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("🎬 Media Bar:", "Disabled by client-side setting.");
const homeSections = document.querySelector('.homeSectionsContainer'); const homeSections = document.querySelector('.homeSectionsContainer');
if (homeSections) { if (homeSections) {
homeSections.style.top = '0'; homeSections.style.top = '0';
@@ -3813,7 +3804,7 @@ const slidesInit = async () => {
const lazyLoadObserver = initLazyLoading(); const lazyLoadObserver = initLazyLoading();
try { try {
console.log("🌟 Initializing Enhanced Jellyfin Slideshow"); console.log("🎬 Media Bar:", "🌟 Initializing Enhanced Jellyfin Slideshow");
initArrowNavigation(); initArrowNavigation();
@@ -3827,9 +3818,9 @@ const slidesInit = async () => {
VisibilityObserver.init(); VisibilityObserver.init();
console.log("✅ Enhanced Jellyfin Slideshow initialized successfully"); console.log("🎬 Media Bar:", "✅ Enhanced Jellyfin Slideshow initialized successfully");
} catch (error) { } catch (error) {
console.error("Error initializing slideshow:", error); console.error("🎬 Media Bar:", "Error initializing slideshow:", error);
STATE.slideshow.hasInitialized = false; STATE.slideshow.hasInitialized = false;
} }
}; };

View File

@@ -9,12 +9,28 @@
"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.6.6.2", "version": "1.7.1.4",
"changelog": "- feat: add option to disable pagination dots/counter\n- fix button issue on mobile when using ElegantFin Theme",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "8e7da86aadc1bea0f28c4d5b49109663",
"timestamp": "2026-03-08T15:58:12Z"
},
{
"version": "1.7.0.14",
"changelog": "- Switched to YouTube no-cookie host and referrer policy for iframe security\n- fix playback issues on iOS/MacOS \n- Disable animations and backdrop filters for TV layout\n- removed list.txt functionality to clean up, use the web ui instead\n- Enhance logging with contextual messages, in order to be able to clearly assign logs to this plugin",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.0.14/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "07875c74aab766657be3b8033be6d53f",
"timestamp": "2026-03-06T03:13:48Z"
},
{
"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)", "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", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.2/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "5672ad96720adf049e780c85174a88bc", "checksum": "2c55cf9687e44b04a0824997e2980dc9",
"timestamp": "2026-02-19T02:36:41Z" "timestamp": "2026-02-19T17:21:40Z"
}, },
{ {
"version": "1.6.5.2", "version": "1.6.5.2",

View File

@@ -1,462 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Media Bar Enhanced Configuration</title>
</head>
<body>
<div id="MediaBarEnhancedConfigurationPage" data-role="page" class="page type-interior pluginConfigurationPage"
data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-textarea">
<div data-role="content">
<div class="content-primary">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">Media Bar Enhanced</h2>
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
target="_blank" href="https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced">
<i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>${Help}</span>
</a>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<div style="margin-bottom: 1.5em;">
<button class="jellyfin-tab-button active" onclick="showTab('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;">
<h3>General Settings</h3>
</button>
<button class="jellyfin-tab-button" onclick="showTab('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;">
<h3>Custom Content</h3>
</button>
<button class="jellyfin-tab-button" onclick="showTab('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;">
<h3>Advanced Settings</h3>
</button>
</div>
<form id="mediaBarEnhancedConfigForm">
<!-- BASIC TAB -->
<div id="basic" class="tab-content">
<h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IsEnabled" name="IsEnabled" />
<span>Enable Media Bar Enhanced Plugin</span>
</label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableVideoBackdrop"
name="EnableVideoBackdrop" />
<span>Enable Trailer Backdrops</span>
</label>
<div class="fieldDescription">Show trailers as background if available.<br>Adds a
mute/unmute and pause/play button to control the video in the right top corner.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" />
<span>Wait For Trailer To End</span>
</label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" />
<span>Enable Trailer On Mobile</span>
</label>
<div class="fieldDescription">Allow video playback on mobile devices.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ShowTrailerButton"
name="ShowTrailerButton" />
<span>Show Trailer Button</span>
</label>
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if
trailer is not set as backdrop or if no trailer is available.</div>
</div>
</div>
<!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableCustomMediaIds"
name="EnableCustomMediaIds" />
<span>Enable Custom Media IDs</span>
</label>
<div class="fieldDescription">If enabled, the slideshow will try to show the items listed
below. If the list is empty, default behavior (random items) is used. Disable this
to temporarily ignore your custom list without deleting it.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent"
name="EnableSeasonalContent" />
<span>Enable Seasonal Content Mode</span>
</label>
<div class="fieldDescription">Enable this to define time-based lists in the field below.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Media/Collection/Playlist
IDs
(Newline or Comma separated)</label>
<textarea is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
style="width: 100%; height: 150px; font-family: monospace;"></textarea>
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
You can separate them by new line or comma.
<br><br>
<b>Manual Trailer Override:</b> You can specify a YouTube URL for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [https://youtu.be/...] DESCRIPTION</code>
<br><br>
You can also add a description after the ID using any separator like space, pipe
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br>
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use
the pipe (|) separator.
<br>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).</div>
<div class="fieldDescription" id="seasonalMediaIdsDesc" style="display: none;">
<b>Seasonal Mode Enabled:</b> Define lines with date ranges (Format: DD.MM-DD.MM |
<i>name</i> | <i>IDs</i>).<br>
Example:<br>
<code>20.10-31.10 | Halloween | ID1, ID2 [https://youtu.be/...]</code><br>
<code>01.12-26.12 | Christmas | ID3, ID4</code><br>
<i>Only lines matching the current date will be used. If no line matches, it will try to
use random items.</i>
</div>
<p>You can find the IDs of your items in the URL of the item page in the web interface.<br>
Example:
<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
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.).
</p>
</div>
</div>
<!-- ADVANCED TAB -->
<div id="advanced" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Features</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="SlideAnimationEnabled"
name="SlideAnimationEnabled" />
<span>Enable Slide Animations</span>
</label>
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableClientSideSettings"
name="EnableClientSideSettings" />
<span>Enable Client-Side Settings</span>
</label>
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to
override settings (like disabling the bar or trailer backdrops) locally on their device.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
<span>Use SponsorBlock</span>
</label>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers.</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
class="emby-select-withcolor emby-select">
<option value="Auto">Auto (Smart)</option>
<option value="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option>
<option value="720p">720p</option>
</select>
<div class="fieldDescription">"Auto" selects Maximum if screen width > 1920px, otherwise
1080p.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="StartMuted" name="StartMuted" />
<span>Start Muted</span>
</label>
<div class="fieldDescription">Start trailer video playback muted. (Known issue: In the
Android/IOS app, backdrop trailers are always muted.)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="FullWidthVideo" name="FullWidthVideo" />
<span>Full Width Video</span>
</label>
<div class="fieldDescription">Stretch video to full width. Very nice on desktops, on mobile
devices only the middle of the video is visible.<br>Disable to get the full aspect ratio
on
mobile devices. (looks bad on desktops)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableLoadingScreen"
name="EnableLoadingScreen" />
<span>Enable Loading Screen</span>
</label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" />
<span>Always Show Arrows</span>
</label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead
of only on hover.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableKeyboardControls"
name="EnableKeyboardControls" />
<span>Enable Keyboard Controls</span>
</label>
<div class="fieldDescription">Enable keyboard shortcuts (Arrows left/right (change slide),
Space (pause), M (mute/unmute)) for
the slideshow.</div>
</div>
<h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ShuffleInterval">Shuffle Interval
(ms)</label>
<input is="emby-input" type="number" id="ShuffleInterval" name="ShuffleInterval" />
<div class="fieldDescription">Time in milliseconds between changing slides.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RetryInterval">Retry Interval
(ms)</label>
<input is="emby-input" type="number" id="RetryInterval" name="RetryInterval" />
<div class="fieldDescription">Time in milliseconds to wait before retrying failed
operations.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="LoadingCheckInterval">Loading Check
Interval (ms)</label>
<input is="emby-input" type="number" id="LoadingCheckInterval"
name="LoadingCheckInterval" />
<div class="fieldDescription">Frequency of checking wether the login screen or home screen
has loaded (in milliseconds).</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="FadeTransitionDuration">Fade
Transition Duration (ms)</label>
<input is="emby-input" type="number" id="FadeTransitionDuration"
name="FadeTransitionDuration" />
<div class="fieldDescription">Duration in milliseconds of the transition between slides.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MinSwipeDistance">Min Swipe Distance
(px)</label>
<input is="emby-input" type="number" id="MinSwipeDistance" name="MinSwipeDistance" />
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div>
</div>
<h2 class="sectionTitle">Content Limits</h2>
<p>Leave a setting blank to use the default value.</p>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxItems">Total Max Items</label>
<input is="emby-input" type="number" id="MaxItems" name="MaxItems" />
<div class="fieldDescription">Maximum total items to fetch (for all content types combined).
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxMovies">Max Movies</label>
<input is="emby-input" type="number" id="MaxMovies" name="MaxMovies" />
<div class="fieldDescription">Maximum movies to include in slideshow (for random selection).
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxTvShows">Max TV Shows</label>
<input is="emby-input" type="number" id="MaxTvShows" name="MaxTvShows" />
<div class="fieldDescription">Maximum TV shows to include in slideshow (for random
selection).</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of images to preload.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
Dots</label>
<input is="emby-input" type="number" id="MaxPaginationDots" name="MaxPaginationDots" />
<div class="fieldDescription">Maximum number of dots to show in navigation. If the number
will be exceeded, the dots will "turn" into a counter style (e.g. 1/100). Set to 0 to
enable counter style directly.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPlotLength">Max Plot
Length</label>
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div>
</div>
</div>
<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>
All changes require a page refresh (ctrl + r or F5) after saving for changes to take effect.
<br />
If old settings persist, please force clear browser cache.
</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel"
onclick="history.back();">
<span>${ButtonCancel}</span>
</button>
</div>
</form>
</div>
</div>
<script>
function showTab(tabId, btn) {
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
document.getElementById(tabId).style.display = 'block';
document.querySelectorAll('.jellyfin-tab-button').forEach(b => {
b.classList.remove('active');
b.style.color = '#ccc';
b.style.borderBottom = '2px solid transparent';
});
if (btn) {
btn.classList.add('active');
btn.style.color = '#fff';
btn.style.borderBottom = '2px solid #00a4dc';
}
}
var MediaBarEnhancedConfigurationPage = {
pluginId: 'd7e11d57-819b-4bdd-a88d-53c5f5560225',
loadConfiguration: function (page) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) {
var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings'
];
keys.forEach(function (key) {
var el = page.querySelector('#' + key);
if (el) {
if (el.type === 'checkbox') {
el.checked = config[key];
} else {
el.value = config[key];
}
}
});
// Handle Seasonal UI logic
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
var normalDesc = page.querySelector('#customMediaIdsDesc');
var seasonalDesc = page.querySelector('#seasonalMediaIdsDesc');
function updateDesc() {
if (seasonalCheckbox && seasonalCheckbox.checked) {
if (normalDesc) normalDesc.style.display = 'none';
if (seasonalDesc) seasonalDesc.style.display = 'block';
} else {
if (normalDesc) normalDesc.style.display = 'block';
if (seasonalDesc) seasonalDesc.style.display = 'none';
}
}
if (seasonalCheckbox) {
seasonalCheckbox.addEventListener('change', updateDesc);
updateDesc();
}
Dashboard.hideLoadingMsg();
});
},
saveConfiguration: function (page) {
Dashboard.showLoadingMsg();
var config = {};
var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings'
];
keys.forEach(function (key) {
var el = page.querySelector('#' + key);
if (el) {
if (el.type === 'checkbox') {
config[key] = el.checked;
} else {
config[key] = (el.type === 'number') ? parseInt(el.value, 10) : el.value;
}
}
});
ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
}
};
document.querySelector('#MediaBarEnhancedConfigurationPage').addEventListener('pageshow', function () {
MediaBarEnhancedConfigurationPage.loadConfiguration(this);
});
document.querySelector('#mediaBarEnhancedConfigForm').addEventListener('submit', function (e) {
e.preventDefault();
MediaBarEnhancedConfigurationPage.saveConfiguration(document.querySelector('#MediaBarEnhancedConfigurationPage'));
return false;
});
</script>
<style>
.jellyfin-tab-button.active {
color: #fff !important;
border-bottom: 2px solid #00a4dc !important;
}
</style>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff