Compare commits

..

39 Commits

Author SHA1 Message Date
CodeDevMLH
b85f52d8d3 Update manifest.json for release v1.6.1.9 [skip ci] 2026-02-13 00:16:40 +00:00
CodeDevMLH
ad18acb011 Bump version to 1.6.1.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-13 01:15:47 +01:00
CodeDevMLH
2ae147ac01 Add seasonal content support and enhance custom media ID handling 2026-02-13 01:15:33 +01:00
CodeDevMLH
9896044988 Update manifest.json for release v1.6.1.8 [skip ci] 2026-02-12 23:51:47 +00:00
CodeDevMLH
93e91e2e60 Bump version to 1.6.1.8 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-13 00:50:52 +01:00
CodeDevMLH
b613b028d0 Add configuration options for custom ID limits 2026-02-13 00:50:34 +01:00
CodeDevMLH
9906784845 Enhance visibility change handling to manage video playback more effectively when tab visibility changes 2026-02-12 21:35:29 +01:00
CodeDevMLH
009a3c4720 Update manifest.json for release v1.6.1.7 [skip ci] 2026-02-12 15:57:17 +00:00
CodeDevMLH
595056230a Bump version to 1.6.1.7 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 59s
2026-02-12 16:54:36 +01:00
CodeDevMLH
b18060dfd7 Refactor visibility observer initialization and remove debounce logic 2026-02-12 16:54:20 +01:00
CodeDevMLH
ebb2af9d24 Update manifest.json for release v1.6.1.6 [skip ci] 2026-02-12 02:03:10 +00:00
CodeDevMLH
743af20b8e Bump version to 1.6.1.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-12 03:02:17 +01:00
CodeDevMLH
9844b186d7 Enhance focus management in TV mode by delaying focus call after iframe removal 2026-02-12 03:02:10 +01:00
CodeDevMLH
104b76aa41 Update manifest.json for release v1.6.1.5 [skip ci] 2026-02-12 01:51:53 +00:00
CodeDevMLH
7493c8fa93 Bump version to 1.6.1.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-12 02:51:04 +01:00
CodeDevMLH
77c03157a1 Enhance slide pruning logic to restore focus in TV mode after pruning slides 2026-02-12 02:50:50 +01:00
CodeDevMLH
a7929e1ff6 Update manifest.json for release v1.6.1.4 [skip ci] 2026-02-12 01:29:37 +00:00
CodeDevMLH
c78e07de62 Bump version to 1.6.1.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-12 02:28:47 +01:00
CodeDevMLH
a90f805ea8 Refactor visibility management in slideshow and video playback; optimize state updates and debounce observer for improved performance. 2026-02-12 02:28:32 +01:00
CodeDevMLH
ccba1857e1 Update manifest.json for release v1.6.1.3 [skip ci] 2026-02-12 00:43:53 +00:00
CodeDevMLH
ff56c9370b Bump version to 1.6.1.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-12 01:43:05 +01:00
CodeDevMLH
162600c43f Enhance TV mode support by preventing iframe focus stealing and ensuring container focus continuity; refactor video playback management for improved clarity and functionality. 2026-02-12 01:42:22 +01:00
CodeDevMLH
a21549af47 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 23:50:41 +00:00
CodeDevMLH
1b319ade40 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-12 00:49:49 +01:00
CodeDevMLH
75757d67e7 Adjust backdrop container positioning for TV layout 2026-02-11 20:15:17 +01:00
CodeDevMLH
92fc8d72f7 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 19:13:25 +00:00
CodeDevMLH
cfe9dec550 Bump version to 1.6.1.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-11 20:12:30 +01:00
CodeDevMLH
1bef573aaf Enhance video playback handling and improve slide preloading logic 2026-02-11 20:12:16 +01:00
CodeDevMLH
29a365b690 Update manifest.json for release v1.6.1.1 [skip ci] 2026-02-11 18:57:36 +00:00
CodeDevMLH
0ee0a65309 Bump version to 1.6.1.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-11 19:56:42 +01:00
CodeDevMLH
152d22b709 Refine video playback logic in slideshow and update preload description 2026-02-11 19:56:20 +01:00
CodeDevMLH
216dddad94 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-11 17:24:36 +00:00
CodeDevMLH
a6de148ca1 Bump version to 1.6.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-11 18:23:42 +01:00
CodeDevMLH
1f9378d74d Enhance video backdrop styling for TV layout and improve focus management in slideshow 2026-02-11 18:22:44 +01:00
CodeDevMLH
cc025779dc Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 22:07:34 +00:00
CodeDevMLH
3d10fd59b5 Enhance manual trailer/video override instructions to support Jellyfin Item IDs and clarify usage
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-10 23:06:40 +01:00
CodeDevMLH
bf261eba96 Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 21:44:36 +00:00
CodeDevMLH
e3116c30cf Bump version to 1.6.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-10 22:43:47 +01:00
CodeDevMLH
bb39c91d32 Enhance custom trailer URL handling to support Jellyfin Item IDs and standard URLs 2026-02-10 22:43:40 +01:00
6 changed files with 651 additions and 347 deletions

View File

@@ -34,8 +34,10 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto";
public bool EnableSeasonalContent { get; set; } = false;
public string SeasonalSections { get; set; } = "[]";
public bool IsEnabled { get; set; } = true;
public bool EnableClientSideSettings { get; set; } = false;
public bool ApplyLimitsToCustomIds { get; set; } = false;
public string SortBy { get; set; } = "Random";
public string SortOrder { get; set; } = "Ascending";
}

View File

@@ -94,6 +94,7 @@
<!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;">
<!-- Default Custom Media IDs -->
<h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -101,55 +102,74 @@
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 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>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ApplyLimitsToCustomIds"
name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span>
</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>
<div id="customMediaIdsContainer">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default 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">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>
<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
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>.
<br>
Methods:
<ul>
<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>
</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>
<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>
<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>
<!-- Seasonal Content -->
<h2 class="sectionTitle" style="margin-top: 2em;">Seasonal Content</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent"
name="EnableSeasonalContent" />
<span>Enable Seasonal Content Mode</span>
<span>Enable Seasonal Content</span>
</label>
<div class="fieldDescription">Enable this to define time-based lists in the field below.
</div>
<div class="fieldDescription">When enabled, seasonal sections below will override the default list
during their active date ranges. If no season matches the current date, the default Custom Media IDs above are used as fallback.</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
fetch the list.txt or 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 id="seasonalContentContainer" style="display: none;">
<div id="seasonalSectionsList"></div>
<button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button"
style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
<i class="md-icon">add</i>
<span>Add Season</span>
</button>
</div>
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div>
<!-- ADVANCED TAB -->
@@ -331,7 +351,7 @@
<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 class="fieldDescription">Number of slides to preload.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
@@ -405,7 +425,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers'
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
];
keys.forEach(function (key) {
@@ -419,24 +439,38 @@
}
});
// Handle Seasonal UI logic
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
var normalDesc = page.querySelector('#customMediaIdsDesc');
var seasonalDesc = page.querySelector('#seasonalMediaIdsDesc');
// Render Seasonal Sections
try {
var sections = JSON.parse(config.SeasonalSections || "[]");
MediaBarEnhancedConfigurationPage.renderSeasonalSections(page, sections);
} catch (e) {
console.error("Error parsing SeasonalSections", e);
}
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';
// Handle Seasonal UI visibility
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
var seasonalContainer = page.querySelector('#seasonalContentContainer');
function updateSeasonalVisibility() {
if (seasonalContainer) {
seasonalContainer.style.display = seasonalCheckbox && seasonalCheckbox.checked ? 'block' : 'none';
}
}
if (seasonalCheckbox) {
seasonalCheckbox.addEventListener('change', updateDesc);
updateDesc();
seasonalCheckbox.addEventListener('change', updateSeasonalVisibility);
updateSeasonalVisibility();
}
// Add Season Button
var addSeasonBtn = page.querySelector('#addSeasonBtn');
if (addSeasonBtn) {
// Remove existing listeners to avoid duplicates if re-attached
var newBtn = addSeasonBtn.cloneNode(true);
addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn);
newBtn.addEventListener('click', function() {
MediaBarEnhancedConfigurationPage.addSeasonalSection(page);
});
}
// Handle Prefer Local Trailers visibility
@@ -462,6 +496,11 @@
saveConfiguration: function (page) {
Dashboard.showLoadingMsg();
var sections = MediaBarEnhancedConfigurationPage.getSeasonalSectionsFromUI(page);
var sectionsJson = JSON.stringify(sections);
var seasonalInput = page.querySelector('#SeasonalSections');
if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {};
var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
@@ -472,7 +511,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers'
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
];
keys.forEach(function (key) {
@@ -489,6 +528,89 @@
ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
},
renderSeasonalSections: function(page, sections) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
container.innerHTML = '';
sections.forEach(function(section) {
MediaBarEnhancedConfigurationPage.createSectionElement(container, section);
});
},
addSeasonalSection: function(page) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
MediaBarEnhancedConfigurationPage.createSectionElement(container, {
Name: 'New Season',
StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1,
MediaIds: ''
});
},
createSectionElement: function(container, data) {
var div = document.createElement('div');
div.className = 'seasonal-section';
div.style.cssText = 'background: rgba(0,0,0,0.2); padding: 1em; margin-bottom: 1em; border-radius: 4px; border: 1px solid rgba(255,255,255,0.1);';
var days = [];
for(var i=1; i<=31; i++) days.push(i);
var months = [
{v:1, n:'Jan'}, {v:2, n:'Feb'}, {v:3, n:'Mar'}, {v:4, n:'Apr'},
{v:5, n:'May'}, {v:6, n:'Jun'}, {v:7, n:'Jul'}, {v:8, n:'Aug'},
{v:9, n:'Sep'}, {v:10, n:'Oct'}, {v:11, n:'Nov'}, {v:12, n:'Dec'}
];
function mkSelect(val, opts, cls) {
var h = '<select class="emby-select ' + cls + '" style="width: auto; display: inline-block; margin-right: 5px;">';
opts.forEach(function(o) {
var v = o.v || o;
var n = o.n || o;
h += '<option value="'+v+'" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
});
h += '</select>';
return h;
}
div.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em;">
<input is="emby-input" type="text" class="emby-input section-name" value="${data.Name}" placeholder="Season Name" style="flex: 1; margin-right: 1em; font-weight: bold;">
<button type="button" class="raised emby-button remove-section" style="background: #a94442;">Remove</button>
</div>
<div style="margin-bottom: 0.5em; display: flex; align-items: center; flex-wrap: wrap; gap: 0.5em;">
<span>From:</span>
${mkSelect(data.StartDay, days, 'start-day')}
${mkSelect(data.StartMonth, months, 'start-month')}
<span style="margin-left: 1em;">To:</span>
${mkSelect(data.EndDay, days, 'end-day')}
${mkSelect(data.EndMonth, months, 'end-month')}
</div>
<textarea is="emby-textarea" class="emby-textarea section-ids" style="width: 100%; height: 80px; font-family: monospace;" placeholder="Media IDs...">${data.MediaIds || ''}</textarea>
`;
div.querySelector('.remove-section').addEventListener('click', function() {
div.remove();
});
container.appendChild(div);
},
getSeasonalSectionsFromUI: function(page) {
var sections = [];
var els = page.querySelectorAll('.seasonal-section');
els.forEach(function(el) {
sections.push({
Name: el.querySelector('.section-name').value,
StartDay: parseInt(el.querySelector('.start-day').value),
StartMonth: parseInt(el.querySelector('.start-month').value),
EndDay: parseInt(el.querySelector('.end-day').value),
EndMonth: parseInt(el.querySelector('.end-month').value),
MediaIds: el.querySelector('.section-ids').value
});
});
return sections;
}
};

View File

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

View File

@@ -175,6 +175,7 @@
overflow: hidden;
margin: 0 auto;
pointer-events: auto;
outline: none;
}
#slides-container[style*="display: none"],
@@ -995,3 +996,7 @@
.dots-container .slide-counter {
margin: 0;
}
.layout-tv .backdrop-container{
top: -5%;
}

View File

@@ -57,6 +57,8 @@ const CONFIG = {
enableClientSideSettings: false,
sortBy: "Random",
sortOrder: "Ascending",
applyLimitsToCustomIds: false,
seasonalSections: "[]",
};
// State management
@@ -611,7 +613,8 @@ const SlideUtils = {
if (!container) {
container = this.createElement("div", {
id: "slides-container",
className: "noautofocus"
className: "noautofocus",
tabIndex: "-1"
});
document.body.appendChild(container);
}
@@ -1316,7 +1319,7 @@ const ApiUtils = {
async fetchCollectionItems(collectionId) {
try {
const response = await fetch(
`${STATE.jellyfinData.serverAddress}/Items?ParentId=${collectionId}&Recursive=true&IncludeItemTypes=Movie,Series&fields=Id&userId=${STATE.jellyfinData.userId}`,
`${STATE.jellyfinData.serverAddress}/Items?ParentId=${collectionId}&Recursive=true&IncludeItemTypes=Movie,Series&fields=Id,Type&userId=${STATE.jellyfinData.userId}`,
{
headers: this.getAuthHeaders(),
}
@@ -1330,7 +1333,7 @@ const ApiUtils = {
const data = await response.json();
const items = data.Items || [];
console.log(`Resolved collection ${collectionId} to ${items.length} items`);
return items.map(i => i.Id);
return items.map(i => ({ Id: i.Id, Type: i.Type }));
} catch (error) {
console.error(`Error fetching collection items for ${collectionId}:`, error);
return [];
@@ -1432,16 +1435,19 @@ const VisibilityObserver = {
// If a full screen video player is active, hide slideshow and stop playback
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) {
const container = document.getElementById("slides-container");
if (container) {
container.style.display = "none";
container.style.visibility = "hidden";
container.style.pointerEvents = "none";
if (this._lastVisibleState !== 'player-active') {
this._lastVisibleState = 'player-active';
const container = document.getElementById("slides-container");
if (container) {
container.style.display = "none";
container.style.visibility = "hidden";
container.style.pointerEvents = "none";
}
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
}
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
return;
}
@@ -1456,20 +1462,27 @@ const VisibilityObserver = {
activeTab &&
activeTab.getAttribute("data-index") === "0";
container.style.display = isVisible ? "block" : "none";
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none";
const newState = isVisible ? 'visible' : 'hidden';
// Only update DOM and trigger actions when state actually changes
if (this._lastVisibleState !== newState) {
this._lastVisibleState = newState;
container.style.display = isVisible ? "block" : "none";
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none";
if (isVisible) {
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
STATE.slideshow.slideInterval.start();
SlideshowManager.resumeActivePlayback();
if (isVisible) {
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
STATE.slideshow.slideInterval.start();
SlideshowManager.resumeActivePlayback();
}
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
}
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
}
},
@@ -1477,7 +1490,13 @@ const VisibilityObserver = {
* Initializes visibility observer
*/
init() {
// MARK: Mark
const observer = new MutationObserver(() => this.updateVisibility());
// let debounceTimer = null;
// const observer = new MutationObserver(() => {
// if (debounceTimer) clearTimeout(debounceTimer);
// debounceTimer = setTimeout(() => this.updateVisibility(), 250);
// });
observer.observe(document.body, { childList: true, subtree: true });
document.body.addEventListener("click", () => this.updateVisibility());
@@ -1576,8 +1595,24 @@ const SlideCreator = {
// 1a. Custom URL override
if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) {
trailerUrl = STATE.slideshow.customTrailerUrls[itemId];
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
const customValue = STATE.slideshow.customTrailerUrls[itemId];
// Check if the custom value is a Jellyfin Item ID (GUID)
const guidMatch = customValue.match(/^([0-9a-f]{32})$/i);
if (guidMatch) {
const videoId = guidMatch[1];
console.log(`Using custom local video ID for ${itemId}: ${videoId}`);
trailerUrl = {
id: videoId,
url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?Static=true&api_key=${STATE.jellyfinData.accessToken}`
};
} else {
// Assume it's a standard URL (YouTube, etc.)
trailerUrl = customValue;
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
}
}
// 1b. Check Local Trailer if preferred
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
@@ -1680,6 +1715,13 @@ const SlideCreator = {
playerVars: playerVars,
events: {
'onReady': (event) => {
// Prevent iframe from stealing focus (critical for TV mode)
const iframe = event.target.getIframe();
if (iframe) {
iframe.setAttribute('tabindex', '-1');
iframe.setAttribute('inert', '');
}
// Store start/end time and videoId for later use
event.target._startTime = playerVars.start || 0;
event.target._endTime = playerVars.end || undefined;
@@ -1696,11 +1738,18 @@ const SlideCreator = {
event.target.setPlaybackQuality(quality);
}
// Only play if this is the active slide
// Only play if this is the active slide AND the slideshow is visible
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (currentItemId !== itemId) {
console.log(`Slide ${itemId} is no longer active (current: ${currentItemId}), aborting playback.`);
event.target.mute(); // Mute just in case
return;
}
event.target.playVideo();
// Check if it actually started playing after a short delay (handling autoplay blocks)
@@ -1734,14 +1783,11 @@ const SlideCreator = {
},
'onStateChange': (event) => {
if (event.data === YT.PlayerState.ENDED) {
if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide();
} else {
event.target.playVideo(); // Loop if not waiting for end if trailer is shorter than slide duration
}
SlideshowManager.nextSlide();
}
},
'onError': () => {
'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`);
// Fallback to next slide on error
if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide();
@@ -1757,7 +1803,6 @@ const SlideCreator = {
isVideo = true;
const videoAttributes = {
className: "backdrop video-backdrop",
className: "backdrop video-backdrop",
src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl),
autoplay: false,
@@ -1778,16 +1823,25 @@ const SlideCreator = {
STATE.slideshow.videoPlayers[itemId] = backdrop;
backdrop.addEventListener('play', () => {
backdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (!slide || !slide.classList.contains('active') || currentItemId !== itemId) {
console.log(`Local video ${itemId} started playing but is not active, pausing.`);
event.target.pause();
event.target.currentTime = 0;
return;
}
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
});
backdrop.addEventListener('ended', () => {
if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide();
}
SlideshowManager.nextSlide();
});
backdrop.addEventListener('error', () => {
@@ -2222,6 +2276,16 @@ const SlideshowManager = {
let previousVisibleSlide;
try {
const container = SlideUtils.getOrCreateSlidesContainer();
const activeElement = document.activeElement;
let focusSelector = null;
if (container.contains(activeElement)) {
if (activeElement.classList.contains('play-button')) focusSelector = '.play-button';
else if (activeElement.classList.contains('detail-button')) focusSelector = '.detail-button';
else if (activeElement.classList.contains('favorite-button')) focusSelector = '.favorite-button';
else if (activeElement.classList.contains('trailer-button')) focusSelector = '.trailer-button';
}
const totalItems = STATE.slideshow.totalItems;
index = Math.max(0, Math.min(index, totalItems - 1));
@@ -2249,92 +2313,28 @@ const SlideshowManager = {
}
currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current
// 1. Pause all other YouTube players
if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== currentItemId) {
const p = STATE.slideshow.videoPlayers[id];
if (p && typeof p.pauseVideo === 'function') {
p.pauseVideo();
}
// Restore focus for TV mode navigation continuity
requestAnimationFrame(() => {
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector);
if (target) {
target.focus();
return;
}
});
}
// 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => {
if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) {
video.pause();
}
// Always ensure container has focus in TV mode to keep keyboard navigation working
const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
document.body.classList.contains('layout-tv');
if (isTvMode) {
container.focus({ preventScroll: true });
}
});
// 3. Play and Reset current video
const videoBackdrop = currentSlide.querySelector('.video-backdrop');
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
const hasVideo = !!videoBackdrop;
muteButton.style.display = hasVideo ? 'block' : 'none';
}
if (videoBackdrop) {
if (videoBackdrop.tagName === 'VIDEO') {
videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) {
videoBackdrop.volume = 0.4;
}
videoBackdrop.play().catch(e => {
// Check if it actually started playing after a short delay (handling autoplay blocks)
setTimeout(() => {
if (videoBackdrop.paused) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err));
}
}, 1000);
});
} else if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
// Use loadVideoById to enforce start and end times
player.loadVideoById({
videoId: player._videoId,
startSeconds: player._startTime || 0,
endSeconds: player._endTime
});
if (STATE.slideshow.isMuted) {
player.mute();
} else {
player.unMute();
player.setVolume(40);
}
// Check if playback successfully started, otherwise fallback to muted
setTimeout(() => {
if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted...");
player.mute();
player.playVideo();
}
}, 1000);
} else if (player && typeof player.seekTo === 'function') {
// Fallback if loadVideoById is not available or videoId missing
const startTime = player._startTime || 0;
player.seekTo(startTime);
player.playVideo();
}
}
}
// Manage Video Playback: Stop others, Play current
this.pauseOtherVideos(currentItemId);
this.playCurrentVideo(currentSlide, currentItemId);
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
@@ -2343,7 +2343,8 @@ const SlideshowManager = {
if (backdrop && !backdrop.classList.contains("video-backdrop")) {
backdrop.classList.add("animate");
}
currentSlide.querySelector(".logo").classList.add("animate");
const logo = currentSlide.querySelector(".logo");
if (logo) logo.classList.add("animate");
}
STATE.slideshow.currentSlideIndex = index;
@@ -2426,18 +2427,20 @@ const SlideshowManager = {
*/
async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems;
const preloadCount = CONFIG.preloadCount;
const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5);
const nextIndex = (currentIndex + 1) % totalItems;
const itemId = STATE.slideshow.itemIds[nextIndex];
// Preload next slides
for (let i = 1; i <= preloadCount; i++) {
const nextIndex = (currentIndex + i) % totalItems;
const itemId = STATE.slideshow.itemIds[nextIndex];
SlideCreator.createSlideForItemId(itemId);
}
await SlideCreator.createSlideForItemId(itemId);
if (preloadCount > 1) {
const prevIndex = (currentIndex - 1 + totalItems) % totalItems;
const prevItemId = STATE.slideshow.itemIds[prevIndex];
SlideCreator.createSlideForItemId(prevItemId);
// Preload previous slides
for (let i = 1; i <= preloadCount; i++) {
const prevIndex = (currentIndex - i + totalItems) % totalItems;
const prevItemId = STATE.slideshow.itemIds[prevIndex];
SlideCreator.createSlideForItemId(prevItemId);
}
},
@@ -2466,12 +2469,20 @@ const SlideshowManager = {
pruneSlideCache() {
const currentIndex = STATE.slideshow.currentSlideIndex;
const keepRange = 5;
let prunedAny = false;
Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => {
const index = STATE.slideshow.itemIds.indexOf(itemId);
if (index === -1) return;
const distance = Math.abs(index - currentIndex);
const totalItems = STATE.slideshow.itemIds.length;
// Calculate wrapped distance
let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
distance = Math.min(distance, totalItems - distance);
}
if (distance > keepRange) {
// Destroy video player if exists
if (STATE.slideshow.videoPlayers[itemId]) {
@@ -2490,10 +2501,27 @@ const SlideshowManager = {
if (slide) slide.remove();
delete STATE.slideshow.createdSlides[itemId];
prunedAny = true;
console.log(`Pruned slide ${itemId} at distance ${distance} from view`);
}
});
// After pruning, restore focus to container in TV mode
if (prunedAny) {
const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
document.body.classList.contains('layout-tv');
if (isTvMode) {
// Use setTimeout to execute AFTER Jellyfin's focus manager processes the iframe removal
setTimeout(() => {
const container = document.getElementById("slides-container");
if (container && container.style.display !== 'none') {
container.focus({ preventScroll: true });
}
}, 0);
}
}
},
toggleMute() {
@@ -2612,6 +2640,115 @@ const SlideshowManager = {
}
},
/**
* Pauses all video players except the one with the given item ID
* @param {string} excludeItemId - Item ID to exclude from pausing
*/
pauseOtherVideos(excludeItemId) {
// Pause YouTube players
if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== excludeItemId) {
const p = STATE.slideshow.videoPlayers[id];
if (p) {
try {
if (typeof p.pauseVideo === 'function') {
p.pauseVideo();
if (typeof p.mute === 'function') {
p.mute();
}
}
else if (p.tagName === 'VIDEO') {
p.pause();
p.muted = true;
}
} catch (e) { console.warn("Error pausing player", id, e); }
}
}
});
}
// Pause HTML5 videos
document.querySelectorAll('video').forEach(video => {
const slideParent = video.closest('.slide');
if (slideParent && slideParent.dataset.itemId !== excludeItemId) {
try {
video.pause();
video.muted = true;
} catch (e) {}
}
});
},
/**
* Plays the video backdrop on the given slide and updates mute button visibility
* @param {Element} slide - The slide DOM element
* @param {string} itemId - The item ID of the slide
* @returns {boolean} Whether a video was found and playback attempted
*/
playCurrentVideo(slide, itemId) {
const videoBackdrop = slide.querySelector('.video-backdrop');
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
muteButton.style.display = videoBackdrop ? 'block' : 'none';
}
if (!videoBackdrop) return false;
if (videoBackdrop.tagName === 'VIDEO') {
videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) videoBackdrop.volume = 0.4;
videoBackdrop.play().catch(() => {
setTimeout(() => {
if (videoBackdrop.paused && slide.classList.contains('active')) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err));
}
}, 1000);
});
return true;
}
// YouTube player
const player = STATE.slideshow.videoPlayers?.[itemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
player.loadVideoById({
videoId: player._videoId,
startSeconds: player._startTime || 0,
endSeconds: player._endTime
});
// Explicitly call playVideo to ensure it starts
player.playVideo();
if (STATE.slideshow.isMuted) {
player.mute();
} else {
player.unMute();
player.setVolume(40);
}
setTimeout(() => {
if (!slide.classList.contains('active')) return;
if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted...");
player.mute();
player.playVideo();
}
}, 1000);
return true;
}
return false;
},
/**
* Stops all video playback (YouTube and HTML5)
* Used when navigating away from the home screen
@@ -2641,14 +2778,16 @@ const SlideshowManager = {
});
}
// 2. Pause all HTML5 videos
// 2. Stop and mute all HTML5 videos
const container = document.getElementById("slides-container");
if (container) {
container.querySelectorAll('video').forEach(video => {
try {
video.pause();
video.muted = true;
video.currentTime = 0;
} catch (e) {
console.warn("Error pausing HTML5 video:", e);
console.warn("Error stopping HTML5 video:", e);
}
});
}
@@ -2666,18 +2805,24 @@ const SlideshowManager = {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
if (!currentSlide) return;
// 1. Try YouTube Player
const ytPlayer = STATE.slideshow.videoPlayers[currentItemId];
// YouTube player: just resume, don't reload
const ytPlayer = STATE.slideshow.videoPlayers?.[currentItemId];
if (ytPlayer && typeof ytPlayer.playVideo === 'function') {
if (STATE.slideshow.isMuted) {
if (typeof ytPlayer.mute === 'function') ytPlayer.mute();
} else {
if (typeof ytPlayer.unMute === 'function') ytPlayer.unMute();
if (typeof ytPlayer.setVolume === 'function') ytPlayer.setVolume(40);
}
ytPlayer.playVideo();
return;
}
// 2. Try HTML5 Video
const html5Video = currentSlide.querySelector('video');
// HTML5 video: just resume, don't reset currentTime
const html5Video = currentSlide.querySelector('video.video-backdrop');
if (html5Video) {
if (STATE.slideshow.isMuted) {
html5Video.muted = true;
}
html5Video.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
}
},
@@ -2752,7 +2897,7 @@ const SlideshowManager = {
// Determine if we should handle navigation keys (Arrows, Space, M)
// TV Mode: Strict focus required (must be on slideshow)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
const canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused);
let canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused);
// Check for Input Fields (always ignore typing)
const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
@@ -2825,22 +2970,96 @@ const SlideshowManager = {
},
/**
* Parses custom media IDs, handling seasonal content if enabled
* Parses custom media IDs, handling seasonal content if enabled.
* If Seasonal Content is enabled:
* - Check if any defined season matches the current date.
* - If match: Return IDs from that season.
* - If NO match: Fall back to Default Custom IDs.
* If Custom Media IDs are enabled (and no seasonal match):
* - Return Default Custom IDs.
* If no Custom Media IDs are enabled:
* - Return empty array (triggering random fallback).
* @returns {string[]} Array of media IDs
*/
parseCustomIds() {
if (!CONFIG.enableSeasonalContent) {
return CONFIG.customMediaIds
.split(/[\n,]/).map((line) => {
let idsString = CONFIG.customMediaIds;
let usingSeasonal = false;
if (CONFIG.enableSeasonalContent) {
try {
const sections = JSON.parse(CONFIG.seasonalSections || "[]");
const currentDate = new Date();
const currentMonth = currentDate.getMonth() + 1; // 1-12
const currentDay = currentDate.getDate(); // 1-31
for (const section of sections) {
const startDay = parseInt(section.StartDay);
const startMonth = parseInt(section.StartMonth);
const endDay = parseInt(section.EndDay);
const endMonth = parseInt(section.EndMonth);
let isInRange = false;
if (startMonth === endMonth) {
if (currentMonth === startMonth && currentDay >= startDay && currentDay <= endDay) {
isInRange = true;
}
} else if (startMonth < endMonth) {
// Normal range
if (
(currentMonth > startMonth && currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
} else {
// Wrap around year
if (
(currentMonth > startMonth || currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
}
if (isInRange) {
console.log(`Seasonal match found: ${section.Name}`);
idsString = section.MediaIds;
usingSeasonal = true;
break; // Use first matching season
}
}
} catch (e) {
console.error("Error parsing seasonal sections in JS:", e);
}
}
// If NOT using seasonal content (disabled or no match),
// Custom IDs are disabled, return empty to skip to random
if (!usingSeasonal && !CONFIG.enableCustomMediaIds) {
return [];
}
// Parse the resulting string (either seasonal or default)
if (!idsString) return [];
return idsString
.split(/[\n,]/)
.map((line) => {
const urlMatch = line.match(/\[(.*?)\]/);
let id = line;
if (urlMatch) {
const url = urlMatch[1];
// Remove the [url] part from the ID string for parsing
id = line.replace(/\[.*?\]/, '').trim();
// Attempt to extract GUID if present
const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) {
id = guidMatch[1];
} else {
// Fallback: split by pipe if used
id = id.split('|')[0].trim();
}
STATE.slideshow.customTrailerUrls[id] = url;
@@ -2849,83 +3068,6 @@ const SlideshowManager = {
})
.map((id) => id.trim())
.filter((id) => id);
} else {
return this.parseSeasonalIds();
}
},
/**
* Parses custom media IDs, handling seasonal content if enabled
* @returns {string[]} Array of media IDs
*/
parseSeasonalIds() {
console.log("Using Seasonal Content Mode");
const lines = CONFIG.customMediaIds.split('\n');
const currentDate = new Date();
const currentMonth = currentDate.getMonth() + 1; // 1-12
const currentDay = currentDate.getDate(); // 1-31
const rawIds = [];
for (const line of lines) {
const match = line.match(/^\s*(\d{1,2})\.(\d{1,2})-(\d{1,2})\.(\d{1,2})\s*\|.*\|(.*)$/) ||
line.match(/^\s*(\d{1,2})\.(\d{1,2})-(\d{1,2})\.(\d{1,2})\s*\|(.*)$/);
if (match) {
const startDay = parseInt(match[1]);
const startMonth = parseInt(match[2]);
const endDay = parseInt(match[3]);
const endMonth = parseInt(match[4]);
const idsPart = match[5];
let isInRange = false;
if (startMonth === endMonth) {
if (currentMonth === startMonth && currentDay >= startDay && currentDay <= endDay) {
isInRange = true;
}
} else if (startMonth < endMonth) {
// Normal range spanning months (e.g. 15.06 - 15.08)
if (
(currentMonth > startMonth && currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
} else {
// Wrap around year (e.g. 01.12 - 15.01)
if (
(currentMonth > startMonth || currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
}
if (isInRange) {
console.log(`Seasonal match found: ${line}`);
const ids = idsPart.split(/[,]/).map(line => {
const urlMatch = line.match(/\[(.*?)\]/);
let id = line;
if (urlMatch) {
const url = urlMatch[1];
id = line.replace(/\[.*?\]/, '').trim();
const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) {
id = guidMatch[1];
} else {
id = id.split('|')[0].trim();
}
STATE.slideshow.customTrailerUrls[id] = url;
}
return id.trim();
}).filter(id => id);
rawIds.push(...ids);
}
}
}
return rawIds;
},
/**
@@ -2967,10 +3109,10 @@ const SlideshowManager = {
const children = await ApiUtils.fetchCollectionItems(id);
finalIds.push(...children);
} else if (item) {
finalIds.push(id);
finalIds.push({ Id: item.Id, Type: item.Type });
}
} catch (e) {
console.warn(`Error resolving item ${id}:`, e);
console.warn(`Error resolving item ${rawId}:`, e);
}
}
return finalIds;
@@ -2985,10 +3127,41 @@ const SlideshowManager = {
let itemIds = [];
// 1. Try Custom Media/Collection IDs from Config & seasonal content
if (CONFIG.enableCustomMediaIds && CONFIG.customMediaIds) {
if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) {
console.log("Using Custom Media IDs from configuration");
const rawIds = this.parseCustomIds();
itemIds = await this.resolveCollectionsAndItems(rawIds);
const resolvedItems = await this.resolveCollectionsAndItems(rawIds);
// Apply max items limit to custom IDs if enabled
if (CONFIG.applyLimitsToCustomIds) {
let movieCount = 0;
let showCount = 0;
let keptItems = [];
for (const item of resolvedItems) {
if (keptItems.length >= CONFIG.maxItems) break;
if (item.Type === 'Movie') {
if (movieCount < CONFIG.maxMovies) {
movieCount++;
keptItems.push(item);
}
} else if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') {
// Count Seasons/Episodes as TV Shows
if (showCount < CONFIG.maxTvShows) {
showCount++;
keptItems.push(item);
}
} else {
// Other types: count towards total only
keptItems.push(item);
}
}
itemIds = keptItems.map(i => i.Id);
console.log(`Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`);
} else {
itemIds = resolvedItems.map(i => i.Id);
}
}
// 2. Try Avatar List (list.txt)
@@ -3368,75 +3541,69 @@ const MediaBarEnhancedSettingsManager = {
* Initialize page visibility handling to pause when tab is inactive
*/
const initPageVisibilityHandler = () => {
let wasVideoPlayingBeforeHide = false;
document.addEventListener("visibilitychange", () => {
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') ||
document.querySelector('.youtubePlayerContainer:not(.hide)');
if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
// Stop slide timer
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
// Pause active video if playing
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (currentItemId) {
// YouTube
if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (typeof player.pauseVideo === "function") {
try {
player.pauseVideo();
STATE.slideshow.isVideoPlaying = false;
} catch (e) {
console.warn("Error pausing video on tab hide:", e);
if (isVideoPlayerOpen) {
// Jellyfin video is playing --> full stop to free all resources
console.log("Tab inactive and Jellyfin player active - stopping all playback");
SlideshowManager.stopAllPlayback();
} else {
// Simple tab switch: stop all others, pause only the current
console.log("Tab inactive. Pausing current video, stopping others");
const currentItemId = STATE.slideshow.itemIds?.[STATE.slideshow.currentSlideIndex];
// Stop all players except the current one
if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id === currentItemId) return;
const p = STATE.slideshow.videoPlayers[id];
if (p) {
try {
if (typeof p.stopVideo === 'function') {
p.stopVideo();
if (typeof p.clearVideo === 'function') p.clearVideo();
} else if (p.tagName === 'VIDEO') {
p.pause();
p.muted = true;
p.currentTime = 0;
}
} catch (e) { console.warn("Error stopping background player", id, e); }
}
} else if (player.tagName === 'VIDEO') { // HTML5 Video
player.pause();
STATE.slideshow.isVideoPlaying = false;
});
}
// Pause only the current video
if (currentItemId) {
const player = STATE.slideshow.videoPlayers?.[currentItemId];
if (player) {
try {
if (typeof player.pauseVideo === 'function') {
player.pauseVideo();
} else if (player.tagName === 'VIDEO') {
player.pause();
}
} catch (e) { console.warn("Error pausing video on tab hide:", e); }
}
}
}
} else {
console.log("Tab active - resuming slideshow");
if (!STATE.slideshow.isPaused) {
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
// YouTube
if (typeof player.playVideo === "function") {
try {
player.playVideo();
STATE.slideshow.isVideoPlaying = true;
} catch (e) {
console.warn("Error resuming video on tab show:", e);
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start();
}
}
} else if (player.tagName === 'VIDEO') { // HTML5 Video
try {
player.play().catch(e => console.warn("Error resuming HTML5 video:", e));
STATE.slideshow.isVideoPlaying = true;
} catch(e) { console.warn(e); }
}
} else {
// No video was playing, just restart interval
const activeSlide = document.querySelector('.slide.active');
const hasVideo = activeSlide && activeSlide.querySelector('.video-backdrop');
if (CONFIG.waitForTrailerToEnd && hasVideo) {
// Don't restart interval if waiting for trailer
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start();
}
}
console.log("Tab active. Resuming slideshow");
const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home";
if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) {
SlideshowManager.resumeActivePlayback();
if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) {
STATE.slideshow.slideInterval.start();
}
wasVideoPlayingBeforeHide = false;
}
}
});

View File

@@ -9,12 +9,20 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [
{
"version": "1.6.0.1",
"version": "1.6.1.9",
"changelog": "- fix tv mode issue\n- refactor video playback management",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.9/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "e6e3b72fe679d90137617d8a717592b8",
"timestamp": "2026-02-13T00:16:39Z"
},
{
"version": "1.6.0.2",
"changelog": "- add local trailer support on trailer button\nfix: iOS/MacOS playback issue?",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "ec876a05d68fe2720781c483ebcd1e9e",
"timestamp": "2026-02-10T21:27:24Z"
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "cdd0208f8cc4f4b04f50e7138e508370",
"timestamp": "2026-02-10T22:07:33Z"
},
{
"version": "1.5.1.3",