Add seasonal content support and enhance custom media ID handling

This commit is contained in:
CodeDevMLH
2026-02-13 01:15:33 +01:00
parent 9896044988
commit 2ae147ac01
3 changed files with 293 additions and 152 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 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="md-icon">add</i>
<span>Add Season</span>
</button>
</div>
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
@@ -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,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

@@ -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) => {
@@ -1841,7 +1841,7 @@ const SlideCreator = {
}); });
backdrop.addEventListener('ended', () => { backdrop.addEventListener('ended', () => {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
}); });
backdrop.addEventListener('error', () => { backdrop.addEventListener('error', () => {
@@ -2703,7 +2703,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));
@@ -2721,6 +2721,9 @@ const SlideshowManager = {
startSeconds: player._startTime || 0, startSeconds: player._startTime || 0,
endSeconds: player._endTime endSeconds: player._endTime
}); });
// Explicitly call playVideo to ensure it starts
player.playVideo();
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
player.mute(); player.mute();
@@ -2730,6 +2733,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) {
@@ -2739,10 +2744,6 @@ const SlideshowManager = {
} }
}, 1000); }, 1000);
return true; return true;
} else if (player && typeof player.seekTo === 'function') {
player.seekTo(player._startTime || 0);
player.playVideo();
return true;
} }
return false; return false;
@@ -2969,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;
@@ -2993,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;
}, },
/** /**
@@ -3111,7 +3109,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 +3127,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);
} }
} }