diff --git a/scripts/configuration.js b/scripts/configuration.js
index 26c52b1..a2a4b40 100644
--- a/scripts/configuration.js
+++ b/scripts/configuration.js
@@ -27,6 +27,66 @@
{ key: 'writtenByWriterRecentlyWatched', label: 'Written by [Writer] because you recently watched [Movie]', defaultName: 'Written by [Writer] because you recently watched [Movie]' }
];
+ const SUPPORTED_CUSTOM_SECTION_PARAMS = {
+ // Booleans
+ hasThemeSong: { label: 'Has Theme Song', type: 'boolean' },
+ hasThemeVideo: { label: 'Has Theme Video', type: 'boolean' },
+ hasSubtitles: { label: 'Has Subtitles', type: 'boolean' },
+ hasSpecialFeature: { label: 'Has Special Feature', type: 'boolean' },
+ hasTrailer: { label: 'Has Trailer', type: 'boolean' },
+ hasParentalRating: { label: 'Has Parental Rating', type: 'boolean' },
+ isHd: { label: 'Is HD', type: 'boolean' },
+ is4K: { label: 'Is 4K', type: 'boolean' },
+ isMissing: { label: 'Is Missing', type: 'boolean' },
+ isUnaired: { label: 'Is Unaired', type: 'boolean' },
+ hasOverview: { label: 'Has Overview', type: 'boolean' },
+ isFavorite: { label: 'Is Favorite', type: 'boolean' },
+ isPlayed: { label: 'Is Played', type: 'boolean' },
+ hasOfficialRating: { label: 'Has Official Rating', type: 'boolean' },
+ recursive: { label: 'Recursive', type: 'boolean', default: true },
+ is3D: { label: 'Is 3D', type: 'boolean' },
+ isSeries: { label: 'Is Series (Live TV)', type: 'boolean' },
+
+ // Strings
+ maxOfficialRating: { label: 'Max Official Rating', type: 'string', hint: 'e.g., PG-13, TV-MA' },
+ minOfficialRating: { label: 'Min Official Rating', type: 'string', hint: 'e.g., PG, TV-14' },
+ person: { label: 'Person Name', type: 'string' },
+ nameStartsWith: { label: 'Name Starts With', type: 'string' },
+ nameStartsWithOrGreater: { label: 'Name Starts With Or Greater', type: 'string' },
+ nameLessThan: { label: 'Name Less Than', type: 'string' },
+
+ // Numbers
+ indexNumber: { label: 'Index Number', type: 'number' },
+ parentIndexNumber: { label: 'Parent Index Number', type: 'number' },
+ minCommunityRating: { label: 'Min Community Rating', type: 'number' },
+ minCriticRating: { label: 'Min Critic Rating', type: 'number' },
+ limit: { label: 'Limit', type: 'number' },
+
+ // Dates (ISO 8601)
+ minPremiereDate: { label: 'Min Premiere Date', type: 'date', hint: 'YYYY-MM-DD' },
+ maxPremiereDate: { label: 'Max Premiere Date', type: 'date', hint: 'YYYY-MM-DD' },
+ minDateLastSaved: { label: 'Min Date Last Saved', type: 'date', hint: 'YYYY-MM-DD' },
+ minDateLastSavedForUser: { label: 'Min Date Last Saved For User', type: 'date', hint: 'YYYY-MM-DD' },
+
+ // Arrays (Comma-separated strings in UI)
+ locationTypes: { label: 'Location Types', type: 'array', hint: 'FileSystem, Remote, Virtual, Offline' },
+ excludeLocationTypes: { label: 'Exclude Location Types', type: 'array', hint: 'FileSystem, Remote, Virtual, Offline' },
+ excludeItemIds: { label: 'Exclude Item IDs', type: 'array', hint: 'Comma-separated GUIDs' },
+ excludeItemTypes: { label: 'Exclude Item Types', type: 'array', hint: 'Movie, Series, Episode, etc.' },
+ filters: { label: 'Filters', type: 'array', hint: 'IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes' },
+ imageTypes: { label: 'Image Types', type: 'array', hint: 'Primary, Backdrop, Thumb, Logo, etc.' },
+ officialRatings: { label: 'Official Ratings', type: 'array', hint: 'PG, PG-13, R, etc.' },
+ years: { label: 'Years', type: 'array', hint: 'Comma-separated years' },
+ personTypes: { label: 'Person Types', type: 'array', hint: 'Actor, Director, Writer, etc.' },
+ studios: { label: 'Studios', type: 'array', hint: 'Pipe-delimited names' },
+ artists: { label: 'Artists', type: 'array', hint: 'Pipe-delimited names' },
+ albums: { label: 'Albums', type: 'array', hint: 'Pipe-delimited names' },
+ seriesStatus: { label: 'Series Status', type: 'array', hint: 'Continuing, Ended, Unreleased' },
+ personIds: { label: 'Person IDs', type: 'array', hint: 'Comma-separated GUIDs' },
+ studioIds: { label: 'Studio IDs', type: 'array', hint: 'Comma-separated GUIDs' },
+ ids: { label: 'Item IDs', type: 'array', hint: 'Comma-separated GUIDs' }
+ };
+
// Check if user is admin
async function isAdmin() {
try {
@@ -919,12 +979,15 @@
// Helper function to build Include Item Types badge field
function buildIncludeItemTypesField(prefix, sectionIndex, sanitizedPrefix, currentTypes, sourceType) {
- // Available item types (excluding Collections and Playlists unless source type is Parent)
- const availableTypes = ['Movie', 'Series', 'Season', 'Episode', 'MusicArtist', 'MusicAlbum', 'Audio', 'MusicVideo', 'Book'];
- const parentOnlyTypes = ['BoxSet', 'Playlist']; // Only available when source type is Parent
+ // All available item types
+ const allAvailableTypes = [
+ 'AggregateFolder', 'Audio', 'AudioBook', 'BasePluginFolder', 'Book', 'BoxSet', 'Channel',
+ 'ChannelFolderItem', 'CollectionFolder', 'Episode', 'Folder', 'Genre', 'ManualPlaylistsFolder',
+ 'Movie', 'LiveTvChannel', 'LiveTvProgram', 'MusicAlbum', 'MusicArtist', 'MusicGenre', 'MusicVideo',
+ 'Person', 'Photo', 'PhotoAlbum', 'Playlist', 'PlaylistsFolder', 'Program', 'Recording', 'Season',
+ 'Series', 'Studio', 'Trailer', 'TvChannel', 'TvProgram', 'UserRootFolder', 'UserView', 'Video', 'Year'
+ ];
- // All available types for this source type
- const allAvailableTypes = sourceType === 'Parent' ? [...availableTypes, ...parentOnlyTypes] : availableTypes;
const selectionLabel = currentTypes.length ? `${currentTypes.length} selected` : 'Select item types';
// Current types as badges
@@ -974,14 +1037,91 @@
`;
}
+ // Helper function to build Additional Options Controls (Dropdown + Add Button)
+ function buildAdditionalOptionsControls(prefix, sectionIndex, sanitizedPrefix) {
+ // Build dropdown options from supported params
+ const dropdownOptions = Object.entries(SUPPORTED_CUSTOM_SECTION_PARAMS)
+ .sort((a, b) => a[1].label.localeCompare(b[1].label))
+ .map(([key, meta]) => ``)
+ .join('');
+
+ return `
+
+
Additional Options
+
+
+
+
+
+ `;
+ }
+
+ // Helper function to build Additional Options List
+ function buildAdditionalOptionsList(prefix, sectionIndex, sanitizedPrefix, currentOptions) {
+ const optionsHtml = (currentOptions || []).map((option, index) => {
+ const meta = SUPPORTED_CUSTOM_SECTION_PARAMS[option.key] || { label: option.key, type: 'string' };
+ const inputId = `${sanitizedPrefix}_additionalOption_${index}`;
+
+ let inputField = '';
+ if (meta.type === 'boolean') {
+ const isChecked = option.value === true || option.value === 'true';
+ inputField = buildJellyfinCheckbox(inputId, isChecked, meta.label, {
+ 'data-key': option.key,
+ 'class': 'additional-option-value',
+ 'data-type': 'boolean'
+ });
+ } else {
+ inputField = `
+
+ `;
+ }
+
+ return `
+
+ ${inputField}
+
+
+ `;
+ }).join('');
+
+ return `
+
+ `;
+ }
+
// Helper function to build Jellyfin-style checkbox
function buildJellyfinCheckbox(id, checked, label, dataAttributes = {}) {
- const dataAttrs = Object.entries(dataAttributes).map(([key, value]) => `${key}="${value}"`).join(' ');
+ let className = 'emby-checkbox';
+ const otherAttrs = {};
+
+ for (const [key, value] of Object.entries(dataAttributes)) {
+ if (key === 'class') {
+ className += ' ' + value;
+ } else {
+ otherAttrs[key] = value;
+ }
+ }
+
+ const dataAttrs = Object.entries(otherAttrs).map(([key, value]) => `${key}="${value}"`).join(' ');
const checkedAttr = checked ? 'checked' : '';
return `
${buildIncludeItemTypesField(prefix, sectionIndex, sanitizedSectionPrefix, includeItemTypes, type)}
+
+
+ ${buildAdditionalOptionsControls(prefix, sectionIndex, sanitizedSectionPrefix)}
+
+
+ ${buildAdditionalOptionsList(prefix, sectionIndex, sanitizedSectionPrefix, additionalQueryOptions)}
`;
}
@@ -1294,7 +1441,8 @@
nameLabel = 'Section Name',
nameInputAttributes = {},
includeCardFormat = true,
- includeItemLimit = true
+ includeItemLimit = true,
+ includeIsPlayed = false
} = options;
const nameValue = config.name ?? defaultName ?? '';
@@ -1303,6 +1451,12 @@
const sortOrderDirection = config.sortOrderDirection ?? 'Ascending';
const cardFormat = config.cardFormat ?? 'Poster';
const order = config.order ?? 100;
+
+ // IsPlayed logic
+ // If isPlayed is undefined or null, it means the filter is disabled.
+ // If it's true/false, the filter is enabled and set to that value.
+ const isPlayedFilterEnabled = config.isPlayed !== undefined && config.isPlayed !== null;
+ const isPlayedValue = config.isPlayed === true; // true if played, false if unplayed
let html = '';
@@ -1343,10 +1497,29 @@
@@ -1384,6 +1557,22 @@
`;
}
+ if (includeIsPlayed) {
+ html += `
+
+
+ ${buildJellyfinCheckbox(`${prefix}_isPlayed_enabled`, isPlayedFilterEnabled, 'Filter by Played Status')}
+
+
+
+
+
+
`;
+ }
+
return html;
}
@@ -1479,7 +1668,7 @@
${buildJellyfinCheckbox('homeScreen_recentlyReleased_movies_enabled', (recentlyReleased.movies || {}).enabled !== false, 'Enabled')}
- ${createSectionConfiguration('homeScreen_recentlyReleased_movies', recentlyReleased.movies || {}, { includeName: true, defaultName: 'Recently Released Movies', includeSortOrder: false, includeOrder: true })}
+ ${createSectionConfiguration('homeScreen_recentlyReleased_movies', recentlyReleased.movies || {}, { includeName: true, defaultName: 'Recently Released Movies', includeSortOrder: false, includeOrder: true, includeIsPlayed: true })}
@@ -1496,7 +1685,7 @@
${buildJellyfinCheckbox('homeScreen_recentlyReleased_episodes_enabled', (recentlyReleased.episodes || {}).enabled !== false, 'Enabled')}
- ${createSectionConfiguration('homeScreen_recentlyReleased_episodes', recentlyReleased.episodes || {}, { includeName: true, defaultName: 'Recently Aired Episodes', includeSortOrder: false, includeOrder: true })}
+ ${createSectionConfiguration('homeScreen_recentlyReleased_episodes', recentlyReleased.episodes || {}, { includeName: true, defaultName: 'Recently Aired Episodes', includeSortOrder: false, includeOrder: true, includeIsPlayed: true })}
@@ -1515,7 +1704,7 @@
- ${createSectionConfiguration('homeScreen_trending', trending, { includeName: true, defaultName: 'Trending' })}
+ ${createSectionConfiguration('homeScreen_trending', trending, { includeName: true, defaultName: 'Trending', includeIsPlayed: true })}
@@ -1581,7 +1770,7 @@
- ${createSectionConfiguration('homeScreen_imdbTop250', imdbTop250, { includeName: true, defaultName: 'IMDb Top 250' })}
+ ${createSectionConfiguration('homeScreen_imdbTop250', imdbTop250, { includeName: true, defaultName: 'IMDb Top 250', includeIsPlayed: true })}
@@ -1658,7 +1847,11 @@
0.0 = never, 1.0 = always
- ${createSectionConfiguration('homeScreen_discovery', discovery, { includeName: false, includeOrder: false, includeCardFormat: false, includeSortOrder: false, includeItemLimit: false })}
+
+ ${buildJellyfinCheckbox('homeScreen_discovery_renderSpotlightAboveMatching', discovery.renderSpotlightAboveMatching === true, 'Spotlight Grouping')}
+
Render Spotlight discovery sections alongside the corresponding discovery section (e.g. Action Spotlight above Action Movies).
+
+ ${createSectionConfiguration('homeScreen_discovery', discovery, { includeName: false, includeOrder: false, includeCardFormat: false, includeSortOrder: false, includeItemLimit: false })}
@@ -1668,7 +1861,7 @@
${DISCOVERY_SECTION_DEFINITIONS.map(section => {
const sectionConfig = normalizedDiscoverySections[section.key];
const prefix = `homeScreen_discovery_sectionTypes_${section.key}`;
- const configHtml = createSectionConfiguration(prefix, sectionConfig, { includeName: true, includeOrder: true, includeSortOrder: false, defaultName: section.defaultName });
+ const configHtml = createSectionConfiguration(prefix, sectionConfig, { includeName: true, includeOrder: true, includeSortOrder: false, defaultName: section.defaultName, includeIsPlayed: true });
const extraFields = section.extras?.minimumItems !== undefined
? `
@@ -1965,6 +2158,17 @@
setupToggleVisibility('#homeScreen_watchAgain_enabled', '#homeScreen_watchAgain_container');
setupToggleVisibility('#homeScreen_discovery_enabled', '#homeScreen_discovery_container');
+ // Discovery Sections IsPlayed Toggles
+ ['recentlyReleased_movies', 'recentlyReleased_episodes', 'trending', 'popularTVNetworks', 'upcoming', 'imdbTop250'].forEach(key => {
+ setupToggleVisibility(`#homeScreen_${key}_isPlayed_enabled`, `#homeScreen_${key}_isPlayed_options`);
+ });
+
+ // Dynamic Discovery Sections IsPlayed Toggles
+ DISCOVERY_SECTION_DEFINITIONS.forEach(section => {
+ const prefix = `homeScreen_discovery_sectionTypes_${section.key}`;
+ setupToggleVisibility(`#${prefix}_isPlayed_enabled`, `#${prefix}_isPlayed_options`);
+ });
+
// Setup event listeners for section management
// Add section to seasonal season
modalInstance.dialogContent.addEventListener('click', (e) => {
@@ -2356,6 +2560,76 @@
enabledSpan.textContent = enabledCheckbox.checked ? 'Enabled' : 'Disabled';
});
}
+
+ // Setup additional options listeners
+ const additionalOptionsControls = sectionItem.querySelector(`.${prefix}_section_additionalOptions_controls[data-section-index="${sectionIndex}"]`);
+ const additionalOptionsListContainer = sectionItem.querySelector(`.${prefix}_section_additionalOptions_list_container[data-section-index="${sectionIndex}"]`);
+
+ if (additionalOptionsControls) {
+ const addBtn = additionalOptionsControls.querySelector('.add-additional-option');
+ const select = additionalOptionsControls.querySelector('.additional-option-select');
+ const list = additionalOptionsListContainer ? additionalOptionsListContainer.querySelector('.additional-options-list') : null;
+
+ if (addBtn && select && list) {
+ addBtn.addEventListener('click', () => {
+ const key = select.value;
+ if (!key) return;
+
+ const meta = SUPPORTED_CUSTOM_SECTION_PARAMS[key];
+ if (!meta) return;
+
+ const index = list.children.length;
+ const sanitizedPrefix = `${prefix}_section_${sanitizedIndex}`;
+ const inputId = `${sanitizedPrefix}_additionalOption_${Date.now()}_${index}`;
+
+ let inputField = '';
+ if (meta.type === 'boolean') {
+ inputField = buildJellyfinCheckbox(inputId, meta.default === true, meta.label, {
+ 'data-key': key,
+ 'class': 'additional-option-value',
+ 'data-type': 'boolean'
+ });
+ } else {
+ inputField = `
+
+ `;
+ }
+
+ const row = document.createElement('div');
+ row.className = 'additional-option-row';
+ row.style.cssText = 'display: flex; align-items: flex-end; gap: 0.5em; margin-top: 0.5em; padding: 0.5em; background: rgba(255,255,255,0.05); border-radius: 4px;';
+ row.innerHTML = `
+ ${inputField}
+
+ `;
+
+ // Add delete listener for new row
+ row.querySelector('.remove-additional-option').addEventListener('click', () => {
+ row.remove();
+ });
+
+ list.appendChild(row);
+
+ // Reset select
+ select.value = '';
+ });
+ }
+ }
+
+ // Add listeners for existing delete buttons
+ if (additionalOptionsListContainer) {
+ additionalOptionsListContainer.querySelectorAll('.remove-additional-option').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const row = e.target.closest('.additional-option-row');
+ if (row) row.remove();
+ });
+ });
+ }
}
function deleteSection(prefix, sectionIndex, isSeasonalNested) {
@@ -4351,6 +4625,28 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
if (includeItemTypes.length === 0 && type !== 'Parent') {
includeItemTypes = ['Movie'];
}
+
+ const additionalQueryOptions = [];
+ const additionalOptionsContainer = sectionItem.querySelector(`.${prefix}_section_additionalOptions_list_container[data-section-index="${sectionIndex}"]`);
+ if (additionalOptionsContainer) {
+ const optionRows = additionalOptionsContainer.querySelectorAll('.additional-options-list .additional-option-row');
+ optionRows.forEach(row => {
+ const input = row.querySelector('.additional-option-value');
+ if (input) {
+ const key = input.getAttribute('data-key');
+ const type = input.getAttribute('data-type');
+ let value;
+ if (type === 'boolean') {
+ value = input.checked;
+ } else {
+ value = input.value;
+ }
+ if (key) {
+ additionalQueryOptions.push({ key, value });
+ }
+ }
+ });
+ }
return {
id: sectionIdAttr,
@@ -4366,7 +4662,8 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
spotlight: spotlight,
discoveryEnabled: discoveryEnabled,
searchTerm: searchTerm,
- includeItemTypes: includeItemTypes
+ includeItemTypes: includeItemTypes,
+ additionalQueryOptions: additionalQueryOptions
};
}
@@ -4377,8 +4674,6 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
}
const userId = apiClient.getCurrentUserId();
- const serverAddress = apiClient.serverAddress();
- const token = apiClient.accessToken();
const type = sectionConfig.type || 'Genre';
const searchTerm = sectionConfig.searchTerm?.trim() || '';
const includeItemTypes = Array.isArray(sectionConfig.includeItemTypes) && sectionConfig.includeItemTypes.length > 0
@@ -4389,8 +4684,20 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
const itemLimit = parseInt(sectionConfig.itemLimit, 10) || 16;
const sources = (sectionConfig.source || '').split(',').map(s => s.trim()).filter(Boolean);
const fields = 'PrimaryImageAspectRatio,DateCreated,Overview,ProductionYear,ImageTags,BackdropImageTags,ParentBackdropImageTags,ParentThumbImageTag,SeriesPrimaryImageTag';
- const headers = { 'Authorization': `MediaBrowser Token="${token}"` };
+ // Parse additional query options
+ const additionalOptions = {};
+ if (Array.isArray(sectionConfig.additionalQueryOptions)) {
+ sectionConfig.additionalQueryOptions.forEach(option => {
+ if (option && option.key) {
+ let value = option.value;
+ if (value === 'true' || value === true) value = true;
+ else if (value === 'false' || value === false) value = false;
+ additionalOptions[option.key] = value;
+ }
+ });
+ }
+
const dedupeAndLimit = (items = []) => {
let working = Array.isArray(items) ? [...items] : [];
if (sortOrder === 'Random') {
@@ -4409,42 +4716,42 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
return limited;
};
- const fetchJson = async (url) => {
- const response = await fetch(url, { headers });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- return response.json();
- };
-
try {
+ const queryBase = {
+ IncludeItemTypes: includeItemTypes,
+ Recursive: true,
+ Fields: fields,
+ SortBy: sortOrder,
+ SortOrder: sortOrderDirection,
+ ...additionalOptions
+ };
+
if (type === 'Genre' || type === 'Tag') {
if (sources.length === 0) {
return [];
}
const paramName = type === 'Genre' ? 'Genres' : 'Tags';
- const valueParam = sources.map(s => encodeURIComponent(s)).join('|');
- const url = `${serverAddress}/Items?userId=${userId}&${paramName}=${valueParam}` +
- `&IncludeItemTypes=${encodeURIComponent(includeItemTypes)}` +
- `&Recursive=true&Fields=${encodeURIComponent(fields)}` +
- `&Limit=${itemLimit}&SortBy=${encodeURIComponent(sortOrder)}&SortOrder=${encodeURIComponent(sortOrderDirection)}` +
- (searchTerm ? `&SearchTerm=${encodeURIComponent(searchTerm)}` : '');
- const data = await fetchJson(url);
- return dedupeAndLimit(data.Items || []);
+ // Genres use pipe delimiter usually, tags comma?
+ // homeScreen.js uses pipe for Genres, comma for Tags.
+ // apiClient handles array/string? usually string.
+ const delimiter = type === 'Genre' ? '|' : ',';
+ const valueParam = sources.join(delimiter);
+
+ const params = {
+ ...queryBase,
+ [paramName]: valueParam,
+ Limit: itemLimit
+ };
+ if (searchTerm) params.SearchTerm = searchTerm;
+
+ const response = await apiClient.getItems(userId, params);
+ return dedupeAndLimit(response.Items || []);
}
if (type === 'Collection' || type === 'Playlist' || type === 'Parent') {
const existingIds = new Set();
let collected = [];
- const queryBase = {
- IncludeItemTypes: includeItemTypes,
- Recursive: true,
- Fields: fields,
- SortBy: sortOrder,
- SortOrder: sortOrderDirection
- };
-
const runQuery = async (params) => {
const response = await apiClient.getItems(userId, params);
if (response?.Items?.length) {
@@ -4562,6 +4869,17 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
config.order = parseInt(document.getElementById(`${prefix}_order`)?.value || '100', 10);
}
+ // Check for IsPlayed configuration
+ const isPlayedEnabledCheckbox = document.getElementById(`${prefix}_isPlayed_enabled`);
+ if (isPlayedEnabledCheckbox) {
+ if (isPlayedEnabledCheckbox.checked) {
+ const isPlayedValueSelect = document.getElementById(`${prefix}_isPlayed_value`);
+ config.isPlayed = isPlayedValueSelect?.value === 'true';
+ } else {
+ config.isPlayed = null;
+ }
+ }
+
return config;
}
@@ -4683,6 +5001,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
minPeopleAppearances: parseInt(document.getElementById('homeScreen_discovery_minPeopleAppearances')?.value || '10'),
minGenreMovieCount: parseInt(document.getElementById('homeScreen_discovery_minGenreMovieCount')?.value || '50'),
spotlightDiscoveryChance: parseFloat(document.getElementById('homeScreen_discovery_spotlightDiscoveryChance')?.value || '0.5'),
+ renderSpotlightAboveMatching: document.getElementById('homeScreen_discovery_renderSpotlightAboveMatching')?.checked === true,
defaultItemLimit: parseInt(document.getElementById('homeScreen_discovery_itemLimit')?.value || '16'),
defaultSortOrder: document.getElementById('homeScreen_discovery_sortOrder')?.value || 'Random',
defaultCardFormat: document.getElementById('homeScreen_discovery_cardFormat')?.value || 'Poster',
diff --git a/scripts/homeScreen.js b/scripts/homeScreen.js
index 4f284e2..0142c79 100644
--- a/scripts/homeScreen.js
+++ b/scripts/homeScreen.js
@@ -695,6 +695,17 @@
const discoverySectionSettingsCache = {};
+ /**
+ * Helper to get IsPlayed filter string from config
+ * @param {Object} config - Section configuration object
+ * @returns {string|null} - 'IsPlayed', 'IsUnplayed', or null
+ */
+ function getIsPlayedFilter(config) {
+ if (config && config.isPlayed === true) return 'IsPlayed';
+ if (config && config.isPlayed === false) return 'IsUnplayed';
+ return null;
+ }
+
let collectionsData = null;
async function getCollectionsData() {
if (collectionsData !== null) {
@@ -795,14 +806,25 @@
function applyDiscoverySectionOrdering(items, sectionConfig) {
if (!Array.isArray(items)) return [];
+
+ // Filter by IsPlayed if configured
+ let filteredItems = items;
+ const isPlayedFilter = sectionConfig?.isPlayed;
+
+ if (isPlayedFilter === true) {
+ filteredItems = items.filter(item => item.UserData && item.UserData.Played);
+ } else if (isPlayedFilter === false) {
+ filteredItems = items.filter(item => !item.UserData || !item.UserData.Played);
+ }
+
const sortOrder = sectionConfig?.sortOrder || discoveryConfig.defaultSortOrder || defaultSortOrder;
const sortDirection = sectionConfig?.sortOrderDirection || 'Ascending';
- let sortedItems = items;
+ let sortedItems = filteredItems;
if (sortOrder === 'Random') {
- sortedItems = [...items].sort(() => Math.random() - 0.5);
+ sortedItems = [...filteredItems].sort(() => Math.random() - 0.5);
} else if (window.cardBuilder && typeof window.cardBuilder.sortItems === 'function') {
- sortedItems = window.cardBuilder.sortItems(items, sortOrder, sortDirection);
+ sortedItems = window.cardBuilder.sortItems(filteredItems, sortOrder, sortDirection);
}
const limit = parseDiscoveryNumber(
@@ -867,74 +889,6 @@
/************ Helpers ************/
- /**
- * Fetches item data from Jellyfin API to determine type
- * @param {string} itemId - The item ID to fetch
- * @returns {Promise