Compare commits

...

22 Commits

Author SHA1 Message Date
CodeDevMLH
9663ab78d2 Update manifest.json for release v1.6.1.14 [skip ci] 2026-02-13 02:23:29 +00:00
CodeDevMLH
f633e4273f Bump version to 1.6.1.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:22:39 +01:00
CodeDevMLH
c0895fd8d7 Fix slideshow current slide index update logic 2026-02-13 03:22:21 +01:00
CodeDevMLH
002ccdb08b Enhance label styling and adjust layout in configuration form 2026-02-13 03:11:53 +01:00
CodeDevMLH
7cb03410ee Update manifest.json for release v1.6.1.13 [skip ci] 2026-02-13 02:02:19 +00:00
CodeDevMLH
17c8681e93 Bump version to 1.6.1.13 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:01:29 +01:00
CodeDevMLH
3a4c663c0e Enhance configuration layout and improve seasonal section handling 2026-02-13 03:01:17 +01:00
CodeDevMLH
3385196611 Update manifest.json for release v1.6.1.12 [skip ci] 2026-02-13 01:41:56 +00:00
CodeDevMLH
2538556f7c Bump version to 1.6.1.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 02:41:06 +01:00
CodeDevMLH
550ebed942 Update manifest.json for release v1.6.1.11 [skip ci] 2026-02-13 01:19:07 +00:00
CodeDevMLH
21d55711d4 Enhance configuration layout for season name input and update select appearance
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 02:18:18 +01:00
CodeDevMLH
81a0d375be Update manifest.json for release v1.6.1.11 [skip ci] 2026-02-13 01:12:47 +00:00
CodeDevMLH
23cbc0a85a Bump version to 1.6.1.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 02:11:58 +01:00
CodeDevMLH
2de066cca8 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced 2026-02-13 02:11:46 +01:00
CodeDevMLH
138e50ff15 Enhance configuration layout and add disablePictureInPicture option for video playback 2026-02-13 02:11:20 +01:00
CodeDevMLH
bf72dc08a3 Update manifest.json for release v1.6.1.10 [skip ci] 2026-02-13 00:41:36 +00:00
CodeDevMLH
65a63b4aa0 Bump version to 1.6.1.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 01:40:48 +01:00
CodeDevMLH
a1df756c56 Enhance seasonal section input fields with improved layout and descriptions 2026-02-13 01:40:33 +01:00
CodeDevMLH
f2d383ec61 Refactor video playback logic in SlideshowManager for improved fallback handling 2026-02-13 01:36:26 +01:00
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
5 changed files with 321 additions and 160 deletions

View File

@@ -34,6 +34,7 @@ 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 bool ApplyLimitsToCustomIds { get; set; } = false;

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,9 +102,8 @@
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"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -113,48 +113,63 @@
</label> </label>
<div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div> <div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div>
</div> </div>
<div class="inputContainer"> <div id="customMediaIdsContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Media/Collection/Playlist <div class="inputContainer">
IDs <label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist
(Newline or Comma separated)</label> IDs
<textarea is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" (Newline or Comma separated)</label>
style="width: 100%; height: 150px; font-family: monospace;"></textarea> <textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow. style="width: 100%; height: 150px; font-family: monospace;"></textarea>
You can separate them by new line or comma. <div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as
<br><br> your default content.
<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 You can separate them by new line or comma.
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [Method] DESCRIPTION</code>. <br><br>
<br> <b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
Methods: brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>.
<ul> <br>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li> Methods:
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li> <ul>
</ul> <li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<br> <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>
You can also add a description after the ID using any separator like space, pipe </ul>
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code> You can also add a description after the ID using any separator like space, pipe
<br><br> (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use <br><br>
the pipe (|) separator. <b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use
<br> the pipe (|) separator.
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).</div> <br>
<div class="fieldDescription" id="seasonalMediaIdsDesc" style="display: none;"> <b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
<b>Seasonal Mode Enabled:</b> Define lines with date ranges (Format: DD.MM-DD.MM | </div>
<i>name</i> | <i>IDs</i>).<br> <p>You can find the IDs of your items in the URL of the item page in the web interface.<br>
Example:<br> Example:
<code>20.10-31.10 | Halloween | ID1, ID2 [https://youtu.be/...]</code><br> <code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br>
<code>01.12-26.12 | Christmas | ID3, ID4</code><br> You can also insert a name of a collection or playlist to fetch the IDs of all items in
<i>Only lines matching the current date will be used. If no line matches, it will try to it (will take the first hit.<br><b>Note:</b> there is currently no feedback if the name
fetch the list.txt or use random items.</i> resolution succeeded, you will have to look if the bar displays the correct items).
</p>
</div> </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</span>
</label>
<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 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="material-icons" style="font-size: 24px;">add</i>
<span>Add Season</span>
</button>
</div>
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
@@ -187,7 +202,7 @@
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label> <label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select"> <select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Auto">Auto (Smart)</option> <option value="Auto">Auto (Smart)</option>
<option value="Maximum">Maximum (4K+)</option> <option value="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option> <option value="1080p">1080p</option>
@@ -289,7 +304,7 @@
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting</h2>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortBy">Sort By</label> <label class="selectLabel" for="SortBy">Sort By</label>
<select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select"> <select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Random">Random</option> <option value="Random">Random</option>
<option value="Original">Original (Custom List Order)</option> <option value="Original">Original (Custom List Order)</option>
<option value="PremiereDate">Premiere Date</option> <option value="PremiereDate">Premiere Date</option>
@@ -303,7 +318,7 @@
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortOrder">Sort Order</label> <label class="selectLabel" for="SortOrder">Sort Order</label>
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select"> <select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Ascending">Ascending</option> <option value="Ascending">Ascending</option>
<option value="Descending">Descending</option> <option value="Descending">Descending</option>
</select> </select>
@@ -424,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
@@ -467,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',
@@ -494,6 +528,105 @@
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, index) {
MediaBarEnhancedConfigurationPage.createSectionElement(container, section, index + 1);
});
},
addSeasonalSection: function(page) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
var index = container.children.length + 1;
MediaBarEnhancedConfigurationPage.createSectionElement(container, {
Name: 'New Season',
StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1,
MediaIds: ''
}, index);
},
createSectionElement: function(container, data, index) {
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 emby-select-withcolor ' + cls + '" style="width: auto; display: inline-block; margin-right: 5px; -webkit-appearance: menulist; appearance: menulist;">';
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;
}
var labelText = 'Season list #' + (index || 1);
div.innerHTML =
'<div class="inputContainer" style="margin-bottom: 0.5em;">' +
' <label class="inputLabel" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' +
' <div style="display: flex; align-items: center;">' +
' <div style="flex-grow:1;">' +
' <input is="emby-input" type="text" class="emby-input section-name" value="' + (data.Name || '') + '" />' +
' </div>' +
' <button type="button" class="raised emby-button remove-section" style="background: #a94442; min-width: unset; margin-left: 1em;">Remove</button>' +
' </div>' +
' <div class="fieldDescription">Name of the season</div>' +
'</div>' +
'<div class="inputContainer" style="margin-bottom: 1em;">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' +
' <div style="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>' +
' <div class="fieldDescription">Date range (inclusive) when this content is active.</div>' +
'</div>' +
'<div class="inputContainer">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Media IDs</label>' +
' <textarea is="emby-textarea" class="emby-textarea section-ids" style="width: 100%; height: 80px; font-family: monospace;">' + (data.MediaIds || '') + '</textarea>' +
' <div class="fieldDescription">Comma-separated list of Movie/Series/Collection IDs to show during this season.</div>' +
'</div>';
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.8</Version> <Version>1.6.1.14</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

@@ -1319,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(),
} }
@@ -1333,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 [];
@@ -1783,7 +1783,7 @@ const SlideCreator = {
}, },
'onStateChange': (event) => { 'onStateChange': (event) => {
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}, },
'onError': (event) => { 'onError': (event) => {
@@ -1808,6 +1808,7 @@ const SlideCreator = {
autoplay: false, autoplay: false,
preload: "auto", preload: "auto",
loop: false, loop: false,
disablePictureInPicture: true,
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;" style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;"
}; };
@@ -1841,7 +1842,7 @@ const SlideCreator = {
}); });
backdrop.addEventListener('ended', () => { backdrop.addEventListener('ended', () => {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
}); });
backdrop.addEventListener('error', () => { backdrop.addEventListener('error', () => {
@@ -2314,6 +2315,8 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
STATE.slideshow.currentSlideIndex = index;
// Restore focus for TV mode navigation continuity // Restore focus for TV mode navigation continuity
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (focusSelector) { if (focusSelector) {
@@ -2347,8 +2350,6 @@ const SlideshowManager = {
if (logo) logo.classList.add("animate"); if (logo) logo.classList.add("animate");
} }
STATE.slideshow.currentSlideIndex = index;
if (index === 0 || !previousVisibleSlide) { if (index === 0 || !previousVisibleSlide) {
const dotsContainer = container.querySelector(".dots-container"); const dotsContainer = container.querySelector(".dots-container");
if (dotsContainer) { if (dotsContainer) {
@@ -2703,7 +2704,7 @@ const SlideshowManager = {
videoBackdrop.play().catch(() => { videoBackdrop.play().catch(() => {
setTimeout(() => { setTimeout(() => {
if (videoBackdrop.paused) { if (videoBackdrop.paused && slide.classList.contains('active')) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
videoBackdrop.muted = true; videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err)); videoBackdrop.play().catch(err => console.error("Muted fallback failed", err));
@@ -2730,6 +2731,8 @@ const SlideshowManager = {
} }
setTimeout(() => { setTimeout(() => {
if (!slide.classList.contains('active')) return;
if (player.getPlayerState && if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING && player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) { player.getPlayerState() !== YT.PlayerState.BUFFERING) {
@@ -2740,7 +2743,9 @@ const SlideshowManager = {
}, 1000); }, 1000);
return true; return true;
} else if (player && typeof player.seekTo === 'function') { } else if (player && typeof player.seekTo === 'function') {
player.seekTo(player._startTime || 0); // Fallback if loadVideoById is not available or videoId missing but player object exists
const startTime = player._startTime || 0;
player.seekTo(startTime);
player.playVideo(); player.playVideo();
return true; return true;
} }
@@ -2969,22 +2974,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;
@@ -2993,83 +3072,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;
}, },
/** /**
@@ -3111,7 +3113,7 @@ 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 ${rawId}:`, e); console.warn(`Error resolving item ${rawId}:`, e);
@@ -3129,15 +3131,40 @@ 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 // Apply max items limit to custom IDs if enabled
if (CONFIG.applyLimitsToCustomIds && itemIds.length > CONFIG.maxItems) { if (CONFIG.applyLimitsToCustomIds) {
console.log(`Limiting custom IDs from ${itemIds.length} to ${CONFIG.maxItems}`); let movieCount = 0;
itemIds = itemIds.slice(0, CONFIG.maxItems); 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);
} }
} }

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.8", "version": "1.6.1.14",
"changelog": "- fix tv mode issue\n- refactor video playback management", "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.8/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.14/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "96db191473dc77edead21e8230c1115b", "checksum": "6af6dd96995a90bf91d811467c88da04",
"timestamp": "2026-02-12T23:51:47Z" "timestamp": "2026-02-13T02:23:28Z"
}, },
{ {
"version": "1.6.0.2", "version": "1.6.0.2",