Compare commits

...

31 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
6 changed files with 610 additions and 366 deletions

View File

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

View File

@@ -94,6 +94,7 @@
<!-- CUSTOM CONTENT TAB --> <!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;"> <div id="custom" class="tab-content" style="display:none;">
<!-- Default Custom Media IDs -->
<h2 class="sectionTitle">Custom Media IDs</h2> <h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -101,61 +102,74 @@
name="EnableCustomMediaIds" /> name="EnableCustomMediaIds" />
<span>Enable Custom Media IDs</span> <span>Enable Custom Media IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the slideshow will try to show the items listed <div class="fieldDescription">If enabled, the slideshow will show the items listed
below. If the list is empty, default behavior (random items) is used. Disable this below as the default content. If the list is empty, random items from your library are used.</div>
to temporarily ignore your custom list without deleting it.</div>
</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"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent" <input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent"
name="EnableSeasonalContent" /> name="EnableSeasonalContent" />
<span>Enable Seasonal Content Mode</span> <span>Enable Seasonal Content</span>
</label> </label>
<div class="fieldDescription">Enable this to define time-based lists in the field below. <div class="fieldDescription">When enabled, seasonal sections below will override the default list
</div> 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>
<div class="inputContainer"> <div id="seasonalContentContainer" style="display: none;">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Media/Collection/Playlist <div id="seasonalSectionsList"></div>
IDs <button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button"
(Newline or Comma separated)</label> style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
<textarea is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" <i class="md-icon">add</i>
style="width: 100%; height: 150px; font-family: monospace;"></textarea> <span>Add Season</span>
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow. </button>
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 [Method] 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>
<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> </div>
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
@@ -337,7 +351,7 @@
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label> <label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
<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 images to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
@@ -411,7 +425,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers' 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -425,24 +439,38 @@
} }
}); });
// Handle Seasonal UI logic // Render Seasonal Sections
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent'); try {
var normalDesc = page.querySelector('#customMediaIdsDesc'); var sections = JSON.parse(config.SeasonalSections || "[]");
var seasonalDesc = page.querySelector('#seasonalMediaIdsDesc'); MediaBarEnhancedConfigurationPage.renderSeasonalSections(page, sections);
} catch (e) {
console.error("Error parsing SeasonalSections", e);
}
function updateDesc() { // Handle Seasonal UI visibility
if (seasonalCheckbox && seasonalCheckbox.checked) { var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
if (normalDesc) normalDesc.style.display = 'none'; var seasonalContainer = page.querySelector('#seasonalContentContainer');
if (seasonalDesc) seasonalDesc.style.display = 'block';
} else { function updateSeasonalVisibility() {
if (normalDesc) normalDesc.style.display = 'block'; if (seasonalContainer) {
if (seasonalDesc) seasonalDesc.style.display = 'none'; seasonalContainer.style.display = seasonalCheckbox && seasonalCheckbox.checked ? 'block' : 'none';
} }
} }
if (seasonalCheckbox) { if (seasonalCheckbox) {
seasonalCheckbox.addEventListener('change', updateDesc); seasonalCheckbox.addEventListener('change', updateSeasonalVisibility);
updateDesc(); 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 // Handle Prefer Local Trailers visibility
@@ -468,6 +496,11 @@
saveConfiguration: function (page) { saveConfiguration: function (page) {
Dashboard.showLoadingMsg(); 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 config = {};
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
@@ -478,7 +511,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers' 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -495,6 +528,89 @@
ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) { ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(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> --> <!-- <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.1.0</Version> <Version>1.6.1.9</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

@@ -175,6 +175,7 @@
overflow: hidden; overflow: hidden;
margin: 0 auto; margin: 0 auto;
pointer-events: auto; pointer-events: auto;
outline: none;
} }
#slides-container[style*="display: none"], #slides-container[style*="display: none"],
@@ -996,14 +997,6 @@
margin: 0; margin: 0;
} }
.layout-tv .video-backdrop { .layout-tv .backdrop-container{
top: -5%; top: -5%;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
} }

View File

@@ -57,6 +57,8 @@ const CONFIG = {
enableClientSideSettings: false, enableClientSideSettings: false,
sortBy: "Random", sortBy: "Random",
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false,
seasonalSections: "[]",
}; };
// State management // State management
@@ -611,7 +613,8 @@ const SlideUtils = {
if (!container) { if (!container) {
container = this.createElement("div", { container = this.createElement("div", {
id: "slides-container", id: "slides-container",
className: "noautofocus" className: "noautofocus",
tabIndex: "-1"
}); });
document.body.appendChild(container); document.body.appendChild(container);
} }
@@ -1316,7 +1319,7 @@ const ApiUtils = {
async fetchCollectionItems(collectionId) { async fetchCollectionItems(collectionId) {
try { try {
const response = await fetch( 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(), headers: this.getAuthHeaders(),
} }
@@ -1330,7 +1333,7 @@ const ApiUtils = {
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(`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) { } catch (error) {
console.error(`Error fetching collection items for ${collectionId}:`, error); console.error(`Error fetching collection items for ${collectionId}:`, error);
return []; return [];
@@ -1432,16 +1435,19 @@ const VisibilityObserver = {
// If a full screen video player is active, hide slideshow and stop playback // If a full screen video player is active, hide slideshow and stop playback
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) { if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) {
const container = document.getElementById("slides-container"); if (this._lastVisibleState !== 'player-active') {
if (container) { this._lastVisibleState = 'player-active';
container.style.display = "none"; const container = document.getElementById("slides-container");
container.style.visibility = "hidden"; if (container) {
container.style.pointerEvents = "none"; 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; return;
} }
@@ -1456,20 +1462,27 @@ const VisibilityObserver = {
activeTab && activeTab &&
activeTab.getAttribute("data-index") === "0"; activeTab.getAttribute("data-index") === "0";
container.style.display = isVisible ? "block" : "none"; const newState = isVisible ? 'visible' : 'hidden';
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none"; // 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 (isVisible) {
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) { if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
STATE.slideshow.slideInterval.start(); STATE.slideshow.slideInterval.start();
SlideshowManager.resumeActivePlayback(); 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 * Initializes visibility observer
*/ */
init() { init() {
// MARK: Mark
const observer = new MutationObserver(() => this.updateVisibility()); 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 }); observer.observe(document.body, { childList: true, subtree: true });
document.body.addEventListener("click", () => this.updateVisibility()); document.body.addEventListener("click", () => this.updateVisibility());
@@ -1696,6 +1715,13 @@ const SlideCreator = {
playerVars: playerVars, playerVars: playerVars,
events: { events: {
'onReady': (event) => { '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 // Store start/end time and videoId for later use
event.target._startTime = playerVars.start || 0; event.target._startTime = playerVars.start || 0;
event.target._endTime = playerVars.end || undefined; event.target._endTime = playerVars.end || undefined;
@@ -1712,11 +1738,18 @@ const SlideCreator = {
event.target.setPlaybackQuality(quality); 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 slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) { 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(); event.target.playVideo();
// 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)
@@ -1750,14 +1783,11 @@ const SlideCreator = {
}, },
'onStateChange': (event) => { 'onStateChange': (event) => {
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
if (CONFIG.waitForTrailerToEnd) { SlideshowManager.nextSlide();
SlideshowManager.nextSlide();
} else {
event.target.playVideo(); // Loop if not waiting for end if trailer is shorter than slide duration
}
} }
}, },
'onError': () => { 'onError': (event) => {
console.warn(`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();
@@ -1793,16 +1823,25 @@ const SlideCreator = {
STATE.slideshow.videoPlayers[itemId] = backdrop; 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) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
}); });
backdrop.addEventListener('ended', () => { backdrop.addEventListener('ended', () => {
if (CONFIG.waitForTrailerToEnd) { SlideshowManager.nextSlide();
SlideshowManager.nextSlide();
}
}); });
backdrop.addEventListener('error', () => { backdrop.addEventListener('error', () => {
@@ -2275,100 +2314,27 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
if (focusSelector) { // Restore focus for TV mode navigation continuity
requestAnimationFrame(() => {
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector); const target = currentSlide.querySelector(focusSelector);
if (target) { if (target) {
requestAnimationFrame(() => { target.focus();
target.focus(); return;
});
} }
} }
// Always ensure container has focus in TV mode to keep keyboard navigation working
// Manage Video Playback: Stop others, Play current const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
// 1. Pause all other YouTube players document.body.classList.contains('layout-tv');
if (STATE.slideshow.videoPlayers) { if (isTvMode) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { container.focus({ preventScroll: true });
if (id !== currentItemId) {
const p = STATE.slideshow.videoPlayers[id];
if (p && typeof p.pauseVideo === 'function') {
p.pauseVideo();
}
}
});
}
// 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();
} }
}); });
// 3. Play and Reset current video // Manage Video Playback: Stop others, Play current
const videoBackdrop = currentSlide.querySelector('.video-backdrop'); this.pauseOtherVideos(currentItemId);
this.playCurrentVideo(currentSlide, currentItemId);
// 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();
}
}
}
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
@@ -2377,7 +2343,8 @@ const SlideshowManager = {
if (backdrop && !backdrop.classList.contains("video-backdrop")) { if (backdrop && !backdrop.classList.contains("video-backdrop")) {
backdrop.classList.add("animate"); 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; STATE.slideshow.currentSlideIndex = index;
@@ -2460,18 +2427,20 @@ const SlideshowManager = {
*/ */
async preloadAdjacentSlides(currentIndex) { async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems; 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; // Preload next slides
const itemId = STATE.slideshow.itemIds[nextIndex]; 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); // Preload previous slides
for (let i = 1; i <= preloadCount; i++) {
if (preloadCount > 1) { const prevIndex = (currentIndex - i + totalItems) % totalItems;
const prevIndex = (currentIndex - 1 + totalItems) % totalItems; const prevItemId = STATE.slideshow.itemIds[prevIndex];
const prevItemId = STATE.slideshow.itemIds[prevIndex]; SlideCreator.createSlideForItemId(prevItemId);
SlideCreator.createSlideForItemId(prevItemId);
} }
}, },
@@ -2500,12 +2469,20 @@ const SlideshowManager = {
pruneSlideCache() { pruneSlideCache() {
const currentIndex = STATE.slideshow.currentSlideIndex; const currentIndex = STATE.slideshow.currentSlideIndex;
const keepRange = 5; const keepRange = 5;
let prunedAny = false;
Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => { Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => {
const index = STATE.slideshow.itemIds.indexOf(itemId); const index = STATE.slideshow.itemIds.indexOf(itemId);
if (index === -1) return; 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) { if (distance > keepRange) {
// Destroy video player if exists // Destroy video player if exists
if (STATE.slideshow.videoPlayers[itemId]) { if (STATE.slideshow.videoPlayers[itemId]) {
@@ -2524,10 +2501,27 @@ const SlideshowManager = {
if (slide) slide.remove(); if (slide) slide.remove();
delete STATE.slideshow.createdSlides[itemId]; delete STATE.slideshow.createdSlides[itemId];
prunedAny = true;
console.log(`Pruned slide ${itemId} at distance ${distance} from view`); 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() { toggleMute() {
@@ -2646,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) * Stops all video playback (YouTube and HTML5)
* Used when navigating away from the home screen * Used when navigating away from the home screen
@@ -2675,14 +2778,16 @@ const SlideshowManager = {
}); });
} }
// 2. Pause all HTML5 videos // 2. Stop and mute all HTML5 videos
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.querySelectorAll('video').forEach(video => { container.querySelectorAll('video').forEach(video => {
try { try {
video.pause(); video.pause();
video.muted = true;
video.currentTime = 0;
} catch (e) { } catch (e) {
console.warn("Error pausing HTML5 video:", e); console.warn("Error stopping HTML5 video:", e);
} }
}); });
} }
@@ -2700,18 +2805,24 @@ const SlideshowManager = {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
if (!currentSlide) return; if (!currentSlide) return;
// 1. Try YouTube Player // YouTube player: just resume, don't reload
const ytPlayer = STATE.slideshow.videoPlayers[currentItemId]; const ytPlayer = STATE.slideshow.videoPlayers?.[currentItemId];
if (ytPlayer && typeof ytPlayer.playVideo === 'function') { 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(); ytPlayer.playVideo();
return;
} }
// 2. Try HTML5 Video // HTML5 video: just resume, don't reset currentTime
const html5Video = currentSlide.querySelector('video'); const html5Video = currentSlide.querySelector('video.video-backdrop');
if (html5Video) { if (html5Video) {
if (STATE.slideshow.isMuted) { html5Video.muted = STATE.slideshow.isMuted;
html5Video.muted = true; if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
}
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
} }
}, },
@@ -2786,7 +2897,7 @@ const SlideshowManager = {
// Determine if we should handle navigation keys (Arrows, Space, M) // Determine if we should handle navigation keys (Arrows, Space, M)
// TV Mode: Strict focus required (must be on slideshow) // TV Mode: Strict focus required (must be on slideshow)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused) // 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) // Check for Input Fields (always ignore typing)
const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable); const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
@@ -2859,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 * @returns {string[]} Array of media IDs
*/ */
parseCustomIds() { parseCustomIds() {
if (!CONFIG.enableSeasonalContent) { let idsString = CONFIG.customMediaIds;
return CONFIG.customMediaIds let usingSeasonal = false;
.split(/[\n,]/).map((line) => {
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(/\[(.*?)\]/); const urlMatch = line.match(/\[(.*?)\]/);
let id = line; let id = line;
if (urlMatch) { if (urlMatch) {
const url = urlMatch[1]; const url = urlMatch[1];
// Remove the [url] part from the ID string for parsing
id = line.replace(/\[.*?\]/, '').trim(); id = line.replace(/\[.*?\]/, '').trim();
// Attempt to extract GUID if present
const guidMatch = id.match(/([0-9a-f]{32})/i); const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) { if (guidMatch) {
id = guidMatch[1]; id = guidMatch[1];
} else { } else {
// Fallback: split by pipe if used
id = id.split('|')[0].trim(); id = id.split('|')[0].trim();
} }
STATE.slideshow.customTrailerUrls[id] = url; STATE.slideshow.customTrailerUrls[id] = url;
@@ -2883,83 +3068,6 @@ const SlideshowManager = {
}) })
.map((id) => id.trim()) .map((id) => id.trim())
.filter((id) => id); .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;
}, },
/** /**
@@ -3001,10 +3109,10 @@ const SlideshowManager = {
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); finalIds.push({ Id: item.Id, Type: item.Type });
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${id}:`, e); console.warn(`Error resolving item ${rawId}:`, e);
} }
} }
return finalIds; return finalIds;
@@ -3019,10 +3127,41 @@ const SlideshowManager = {
let itemIds = []; let itemIds = [];
// 1. Try Custom Media/Collection IDs from Config & seasonal content // 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"); console.log("Using Custom Media IDs from configuration");
const rawIds = this.parseCustomIds(); 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) // 2. Try Avatar List (list.txt)
@@ -3402,75 +3541,69 @@ const MediaBarEnhancedSettingsManager = {
* Initialize page visibility handling to pause when tab is inactive * Initialize page visibility handling to pause when tab is inactive
*/ */
const initPageVisibilityHandler = () => { const initPageVisibilityHandler = () => {
let wasVideoPlayingBeforeHide = false;
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') ||
document.querySelector('.youtubePlayerContainer:not(.hide)');
if (document.hidden) { if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos"); // Stop slide timer
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
// Pause active video if playing if (isVideoPlayerOpen) {
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; // Jellyfin video is playing --> full stop to free all resources
if (currentItemId) { console.log("Tab inactive and Jellyfin player active - stopping all playback");
// YouTube SlideshowManager.stopAllPlayback();
if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { } else {
const player = STATE.slideshow.videoPlayers[currentItemId]; // Simple tab switch: stop all others, pause only the current
if (typeof player.pauseVideo === "function") { console.log("Tab inactive. Pausing current video, stopping others");
try { const currentItemId = STATE.slideshow.itemIds?.[STATE.slideshow.currentSlideIndex];
player.pauseVideo();
STATE.slideshow.isVideoPlaying = false; // Stop all players except the current one
} catch (e) { if (STATE.slideshow.videoPlayers) {
console.warn("Error pausing video on tab hide:", e); 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 { } else {
console.log("Tab active - resuming slideshow"); console.log("Tab active. Resuming slideshow");
if (!STATE.slideshow.isPaused) { const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home";
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) {
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { SlideshowManager.resumeActivePlayback();
const player = STATE.slideshow.videoPlayers[currentItemId];
if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) {
// YouTube STATE.slideshow.slideInterval.start();
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();
}
}
} }
wasVideoPlayingBeforeHide = false;
} }
} }
}); });

View File

@@ -9,12 +9,12 @@
"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.1.0", "version": "1.6.1.9",
"changelog": "- fix tv mode issue", "changelog": "- fix tv mode issue\n- refactor video playback management",
"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.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.9/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "646e58f7723dab118e767a57d62004c9", "checksum": "e6e3b72fe679d90137617d8a717592b8",
"timestamp": "2026-02-11T17:24:36Z" "timestamp": "2026-02-13T00:16:39Z"
}, },
{ {
"version": "1.6.0.2", "version": "1.6.0.2",