// KefinTweaks Home Screen Configuration UI
// Modular configuration modal for managing home screen sections
// Uses the new HomeScreenSection format with queries array
(function() {
'use strict';
const LOG = (...args) => console.log('[KefinTweaks HomeScreen Config]', ...args);
const WARN = (...args) => console.warn('[KefinTweaks HomeScreen Config]', ...args);
const ERR = (...args) => console.error('[KefinTweaks HomeScreen Config]', ...args);
// Modal IDs
const MAIN_MODAL_ID = 'kefin-homescreen-config-main';
const SECTION_EDITOR_MODAL_ID = 'kefin-homescreen-section-editor';
const DISCOVERY_EDITOR_MODAL_ID = 'kefin-homescreen-discovery-editor';
// Supported additional query options (matching configuration.js)
const SUPPORTED_QUERY_OPTIONS = {
Ids: { label: 'Item IDs', type: 'array', hint: 'Comma-separated GUIDs' },
ParentId: { label: 'Parent ID', type: 'string', hint: 'GUID of parent item' },
ExcludeItemIds: { label: 'Exclude Item IDs', type: 'array', hint: 'Comma-separated GUIDs' },
GenreIds: { label: 'Genre IDs', type: 'array', hint: 'Comma-separated GUIDs' },
PersonIds: { label: 'Person IDs', type: 'array', hint: 'Comma-separated GUIDs' },
PersonTypes: { label: 'Person Types', type: 'array', hint: 'Actor, Director, Writer, etc.' },
ExcludePersonTypes: { label: 'Exclude Person Types', type: 'array', hint: 'Actor, Director, Writer, etc.' },
StudioIds: { label: 'Studio IDs', type: 'array', hint: 'Comma-separated GUIDs' },
Fields: { label: 'Fields', type: 'array', hint: 'Comma-separated field names' },
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' },
IndexNumber: { label: 'Index Number', type: 'number', hint: 'Episode/Season index' },
ParentIndexNumber: { label: 'Parent Index Number', type: 'number', hint: 'Season index' },
MinCommunityRating: { label: 'Min Community Rating', type: 'number', hint: '0-10' },
MinCriticRating: { label: 'Min Critic Rating', type: 'number', hint: '0-10' },
IsFavorite: { label: 'Is Favorite', type: 'boolean' },
IsPlayed: { label: 'Is Played', type: 'boolean' },
IsUnplayed: { label: 'Is Unplayed', type: 'boolean' },
IsMissing: { label: 'Is Missing', type: 'boolean' },
IsUnaired: { label: 'Is Unaired', type: 'boolean' },
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' },
HasOverview: { label: 'Has Overview', type: 'boolean' },
HasOfficialRating: { label: 'Has Official Rating', type: 'boolean' },
Recursive: { label: 'Recursive', type: 'boolean', default: true },
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' },
NameStartsWith: { label: 'Name Starts With', type: 'string' },
NameStartsWithOrGreater: { label: 'Name Starts With Or Greater', type: 'string' },
NameLessThan: { label: 'Name Less Than', type: 'string' },
LocationTypes: { label: 'Location Types', type: 'array', hint: 'FileSystem, Remote, Virtual, Offline' },
ExcludeLocationTypes: { label: 'Exclude Location Types', type: 'array', hint: 'FileSystem, Remote, Virtual, Offline' },
ExcludeItemTypes: { label: 'Exclude Item Types', type: 'array', hint: 'Movie, Series, Episode, etc.' },
ImageTypes: { label: 'Image Types', type: 'array', hint: 'Primary, Backdrop, Thumb, Logo, etc.' },
OfficialRatings: { label: 'Official Ratings', type: 'array', hint: 'PG, PG-13, R, 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' },
Years: { label: 'Years', type: 'array', hint: 'Comma-separated years' },
EnableResumable: { label: 'Enable Resumable', type: 'boolean' },
EnableUserData: { label: 'Enable User Data', type: 'boolean' }
};
const CARD_FORMATS = ['Poster', 'Thumb', 'Backdrop', 'Square', 'Random', 'Button', 'Banner', 'Logo', 'Clear Art', 'Disc'];
const SORT_ORDERS = ['Default', 'Random', 'Name', 'SortName', 'DateCreated', 'PremiereDate', 'CommunityRating', 'CriticRating', 'DatePlayed', 'SortName', 'PlayCount', 'PlayedPercentage', 'StartDate', 'Runtime', 'ProductionYear', 'IsPlayed', 'IsUnplayed', 'ParentIndexNumber', 'IndexNumber', 'IsFolder', 'SimilarityScore', 'SearchScore', 'DateLastContentAdded', 'SeriesDatePlayed', 'ChildCount'];
const SORT_ORDER_DIRECTIONS = ['Ascending', 'Descending'];
const RENDER_MODE_OPTIONS = [{ value: 'Normal', label: 'Normal' }, { value: 'Spotlight', label: 'Spotlight' }, { value: 'Random', label: 'Random' }];
const SLIDE_STATE_OPTIONS = [
{ value: 'none', label: 'None' },
{ value: 'dots', label: 'Dots' },
{ value: 'numeric', label: 'Numeric' }
];
const SPOTLIGHT_LAYOUT_OPTIONS = [
{ value: 'Border', label: 'Border' },
{ value: 'Borderless', label: 'Borderless' }
];
const SPOTLIGHT_SIZE_OPTIONS = [
{ value: 'normal', label: 'Normal' },
{ value: 'large', label: 'Large' },
{ value: 'full', label: 'Full' }
];
const SPOTLIGHT_TILE_COUNT_OPTIONS = [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' }
];
const SPOTLIGHT_ENTRANCE_OPTIONS = [
{ value: 'fadeIn', label: 'Fade in' },
{ value: 'fadeInUp', label: 'Fade in up' },
{ value: 'fadeInDown', label: 'Fade in down' }
];
const SPOTLIGHT_SLIDE_OPTIONS = [
{ value: 'kenBurnsZoomIn', label: 'Ken Burns zoom in' },
{ value: 'kenBurnsZoomOut', label: 'Ken Burns zoom out' },
{ value: 'kenBurnsZoomInFullscreen', label: 'Ken Burns zoom in (fullscreen)' },
{ value: 'kenBurnsZoomOutFullscreen', label: 'Ken Burns zoom out (fullscreen)' },
{ value: 'kenBurnsPanRight', label: 'Ken Burns pan right' },
{ value: 'kenBurnsPanLeft', label: 'Ken Burns pan left' },
{ value: 'kenBurnsPanUp', label: 'Ken Burns pan up' },
{ value: 'kenBurnsDiagonal', label: 'Ken Burns diagonal' },
{ value: 'fadeInScale', label: 'Fade in scale' },
{ value: 'parallaxFloat', label: 'Parallax float' },
{ value: 'depthPulse', label: 'Depth pulse' },
{ value: 'slowRotate', label: 'Slow rotate' },
{ value: 'breathe', label: 'Breathe' },
{ value: 'heatHaze', label: 'Heat haze' },
{ value: 'colorWash', label: 'Color wash' },
{ value: 'vignetteIn', label: 'Vignette in' }
];
// Current config state
let currentConfig = null;
let mainModalInstance = null;
let currentActiveTab = 'settings'; // Track the currently active tab
let currentGlobalSettingsSubTab = 'general'; // Track active sub-tab in Global Settings (general, spotlight, discovery, cache)
/**
* Show toast notification
*/
function showToast(message, duration = '3') {
if (window.KefinTweaksToaster && window.KefinTweaksToaster.toast) {
window.KefinTweaksToaster.toast(message, duration);
} else {
// Fallback to alert if toaster not available
alert(message);
}
}
/**
* Flatten section groups into a flat array of sections
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @returns {Array} Flat array of all sections
*/
function flattenSectionGroups(groups) {
if (!Array.isArray(groups)) return [];
const flattened = [];
groups.forEach(group => {
if (group && Array.isArray(group.sections)) {
flattened.push(...group.sections);
}
});
return flattened;
}
/**
* Get all sections from groups with metadata about which group they belong to
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @returns {Array} Array of objects with section and groupName
*/
function getAllSectionsFromGroups(groups) {
if (!Array.isArray(groups)) return [];
const result = [];
groups.forEach(group => {
if (group && Array.isArray(group.sections)) {
group.sections.forEach(section => {
result.push({
section: section,
groupName: group.name || 'Unnamed Group',
groupAuthor: group.author,
groupDescription: group.description
});
});
}
});
return result;
}
/**
* Render section groups with group-level and section-level checkboxes
* Used by import and export modals to display sections within their groups
* @param {HTMLElement} container - Container element to render into
* @param {Array} sectionGroups - Array of section group objects { name, author?, description?, sections: [] }
* @param {Object} options - Options object
* @param {string} options.checkboxClass - CSS class for section checkboxes (e.g., 'export-custom-check', 'import-custom-check')
* @param {string} options.groupCheckboxClass - CSS class for group checkboxes (optional, defaults to checkboxClass + '-group')
*/
function renderSectionGroupsCheckboxList(container, sectionGroups, options = {}) {
if (!Array.isArray(sectionGroups) || sectionGroups.length === 0) {
container.innerHTML = '
No section groups found.
';
return;
}
const checkboxClass = options.checkboxClass || 'section-check';
const groupCheckboxClass = options.groupCheckboxClass || checkboxClass + '-group';
let html = '';
sectionGroups.forEach((group, groupIndex) => {
if (!group || !Array.isArray(group.sections) || group.sections.length === 0) {
return;
}
const groupName = group.name || 'Unnamed Group';
// Group checkbox row
html += `
`;
});
html += '
';
container.innerHTML = html;
// Wire up group checkboxes to toggle all section checkboxes in that group
container.querySelectorAll(`.${groupCheckboxClass}`).forEach(groupCheckbox => {
groupCheckbox.addEventListener('change', (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const isChecked = e.target.checked;
container.querySelectorAll(`.${checkboxClass}[data-group-index="${groupIndex}"]`).forEach(sectionCheckbox => {
sectionCheckbox.checked = isChecked;
});
});
});
}
/**
* Find a section by ID in groups
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @param {string} sectionId - Section ID to find
* @returns {Object|null} Object with section, group, and groupIndex, or null if not found
*/
function findSectionInGroups(groups, sectionId) {
if (!Array.isArray(groups)) return null;
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
const group = groups[groupIndex];
if (group && Array.isArray(group.sections)) {
const sectionIndex = group.sections.findIndex(s => s.id === sectionId);
if (sectionIndex !== -1) {
return {
section: group.sections[sectionIndex],
group: group,
groupIndex: groupIndex,
sectionIndex: sectionIndex
};
}
}
}
return null;
}
/**
* Find a section by ID across all section groups (HOME, SEASONAL, DISCOVERY, CUSTOM)
* @param {Object} config - Configuration object
* @param {string} sectionId - Section ID to find
* @returns {Object|null} Object with section, group, groupIndex, sectionIndex, and groupType, or null if not found
*/
function findSectionInAllGroups(config, sectionId) {
if (!config) return null;
// Check HOME_SECTION_GROUPS
let found = findSectionInGroups(config.HOME_SECTION_GROUPS || [], sectionId);
if (found) {
found.groupType = 'HOME_SECTION_GROUPS';
return found;
}
// Check SEASONAL_SECTION_GROUPS
found = findSectionInGroups(config.SEASONAL_SECTION_GROUPS || [], sectionId);
if (found) {
found.groupType = 'SEASONAL_SECTION_GROUPS';
return found;
}
// Check DISCOVERY_SECTION_GROUPS
found = findSectionInGroups(config.DISCOVERY_SECTION_GROUPS || [], sectionId);
if (found) {
found.groupType = 'DISCOVERY_SECTION_GROUPS';
return found;
}
// Check CUSTOM_SECTION_GROUPS
found = findSectionInGroups(config.CUSTOM_SECTION_GROUPS || [], sectionId);
if (found) {
found.groupType = 'CUSTOM_SECTION_GROUPS';
return found;
}
return null;
}
/**
* Update a section in groups
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @param {string} sectionId - Section ID to update
* @param {Object} updatedSection - Updated section data
* @returns {boolean} True if section was found and updated
*/
function updateSectionInGroups(groups, sectionId, updatedSection) {
const found = findSectionInGroups(groups, sectionId);
if (found) {
found.group.sections[found.sectionIndex] = { ...found.section, ...updatedSection };
return true;
}
return false;
}
/**
* Add a section to a group
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @param {string} groupName - Name of the group to add to
* @param {Object} section - Section to add
* @param {string} author - Optional author for custom groups
*/
function addSectionToGroup(groups, groupName, section, author) {
if (!Array.isArray(groups)) return;
// Find existing group
let targetGroup = groups.find(g => {
if (author) {
return g.name === groupName && g.author === author;
}
return g.name === groupName && !g.author;
});
// Create group if it doesn't exist
if (!targetGroup) {
targetGroup = {
name: groupName,
sections: []
};
if (author) {
targetGroup.author = author;
}
groups.push(targetGroup);
}
// Add section
if (!targetGroup.sections) {
targetGroup.sections = [];
}
targetGroup.sections.push(section);
// Sort sections by order
targetGroup.sections.sort((a, b) => (a.order || 0) - (b.order || 0));
}
/**
* Remove a section from groups
* @param {Array} groups - Array of HomeScreenSectionGroup objects
* @param {string} sectionId - Section ID to remove
* @returns {boolean} True if section was found and removed
*/
function removeSectionFromGroups(groups, sectionId) {
if (!Array.isArray(groups)) return false;
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
const group = groups[groupIndex];
if (group && Array.isArray(group.sections)) {
const sectionIndex = group.sections.findIndex(s => s.id === sectionId);
if (sectionIndex !== -1) {
group.sections.splice(sectionIndex, 1);
// Remove empty groups (except default groups)
if (group.sections.length === 0 && !group.author) {
groups.splice(groupIndex, 1);
}
return true;
}
}
}
return false;
}
/**
* Verify and sync library-based section groups (Recently Added, Popular Genres, etc.) with actual Jellyfin libraries
* @param {Object} config - Current configuration object
* @returns {Object} Updated configuration with synced library-based sections
*/
async function verifyLibrarySectionsConfig(config) {
try {
if (!window.ApiClient || !window.ApiClient._loggedIn) {
WARN('User not logged in, skipping library sections verification');
return config;
}
if (!window.dataHelper || !window.dataHelper.getLibraries) {
WARN('dataHelper.getLibraries not available, skipping library sections verification');
return config;
}
const libraries = await window.dataHelper.getLibraries();
if (!libraries || libraries.length === 0) {
LOG('No libraries found, skipping library sections verification');
return config;
}
// Filter out boxsets and playlists; all templates apply to this list
const filteredLibraries = libraries.filter(l => l.CollectionType && l.CollectionType !== 'boxsets' && l.CollectionType !== 'playlists');
const currentLibraryIds = new Set(filteredLibraries.map(l => l.Id));
const libraryTemplates = [
{
idPrefix: 'recently-added-',
groupId: 'home-recently-added',
groupName: 'Recently Added',
sectionTemplate: {
enabled: false,
order: 61,
cardFormat: 'Poster',
buildSection: function (library) {
let viewMoreUrl = null;
if (library.CollectionType === 'movies') {
viewMoreUrl = `#/movies.html?topParentId=${library.Id}&collectionType=movies&tab=1`;
} else if (library.CollectionType === 'tvshows') {
viewMoreUrl = `#/tv.html?topParentId=${library.Id}&collectionType=tvshows&tab=1`;
}
return {
id: `recently-added-${library.Id}`,
name: `Recently Added ${library.Name}`,
viewMoreUrl: viewMoreUrl,
queries: [{
path: '/Items/Latest',
queryOptions: {
ParentId: library.Id,
SortBy: 'DateCreated',
SortOrder: 'Descending',
Limit: 16
}
}]
};
}
}
},
{
idPrefix: 'popular-genres-',
groupId: 'home-popular-genres',
groupName: 'Popular Genres',
sectionTemplate: {
enabled: false,
order: 81,
cardFormat: 'Thumb',
ttl: 604800000,
buildSection: function (library) {
const ct = (library.CollectionType || '').toLowerCase();
let includeItemTypes = ['Movie', 'Series', 'Video', 'MusicArtist', 'Book', 'AudioBook'];
if (ct === 'music') includeItemTypes = ['MusicArtist'];
else if (ct === 'tvshows') includeItemTypes = ['Series'];
else if (ct === 'movies') includeItemTypes = ['Movie'];
else if (ct === 'books') includeItemTypes = ['Book', 'AudioBook'];
else if (ct === 'homevideos') includeItemTypes = ['Video'];
return {
id: `popular-genres-${library.Id}`,
name: `Popular ${library.Name} Genres`,
queries: [{
path: '/Genres',
ParentId: library.Id,
queryOptions: {
ParentId: library.Id,
IncludeItemTypes: includeItemTypes,
SortBy: 'ChildCount',
SortOrder: 'Descending',
Limit: 20
}
}]
};
}
}
}
];
const groups = config.HOME_SECTION_GROUPS || [];
let updatedGroups = groups.map(g => ({ ...g, sections: Array.isArray(g.sections) ? [...g.sections] : [] }));
let hasChanges = false;
for (const template of libraryTemplates) {
let group = updatedGroups.find(g => g.id === template.groupId);
if (!group) {
group = {
id: template.groupId,
name: template.groupName,
sections: []
};
updatedGroups.push(group);
hasChanges = true;
}
const groupSections = group.sections || [];
const templateSectionIds = groupSections.filter(s => s.id && s.id.startsWith(template.idPrefix)).map(s => s.id);
const existingLibraryIdsForTemplate = new Set(templateSectionIds.map(id => id.replace(template.idPrefix, '')));
const sectionsToRemove = groupSections.filter(s => {
if (!s.id || !s.id.startsWith(template.idPrefix)) return false;
const libId = s.id.replace(template.idPrefix, '');
return !currentLibraryIds.has(libId);
});
const sectionsToRemoveIds = new Set(sectionsToRemove.map(s => s.id));
const librariesToAdd = filteredLibraries.filter(l => !existingLibraryIdsForTemplate.has(l.Id));
if (sectionsToRemoveIds.size > 0 || librariesToAdd.length > 0) hasChanges = true;
group.sections = groupSections.filter(s => !sectionsToRemoveIds.has(s.id));
librariesToAdd.forEach(library => {
const base = template.sectionTemplate.buildSection(library);
const newSection = {
...template.sectionTemplate,
...base,
id: base.id,
name: base.name,
queries: base.queries
};
delete newSection.buildSection;
group.sections.push(newSection);
});
}
if (!hasChanges) {
LOG('Library-based sections are in sync with libraries');
return config;
}
config.HOME_SECTION_GROUPS = updatedGroups;
LOG('Library-based sections synced successfully');
return config;
} catch (error) {
ERR('Error verifying library sections config:', error);
return config;
}
}
/**
* Load configuration from JS Injector and merge with defaults
*/
function loadConfig() {
try {
// Get existing config from window.KefinTweaksConfig
const existingConfig = (window.KefinTweaksConfig && window.KefinTweaksConfig.homeScreenConfig) || {};
// Get defaults from homeScreenConfig2.js
const defaults = window.KefinHomeConfig2;
if (!defaults) {
throw new Error('KefinHomeConfig2 not found. Ensure homeScreenConfig2.js is loaded.');
}
// Merge configs - now using 4 groups
const mergedConfig = {
HOME_SECTION_GROUPS: mergeGroupDefaults(
defaults.HOME_SECTION_GROUPS || [],
existingConfig.HOME_SECTION_GROUPS || [],
'home'
),
SEASONAL_SECTION_GROUPS: mergeGroupDefaults(
defaults.SEASONAL_SECTION_GROUPS || [],
existingConfig.SEASONAL_SECTION_GROUPS || [],
'seasonal'
),
DISCOVERY_SECTION_GROUPS: mergeGroupDefaults(
defaults.DISCOVERY_SECTION_GROUPS || [],
existingConfig.DISCOVERY_SECTION_GROUPS || [],
'discovery'
),
CUSTOM_SECTION_GROUPS: mergeGroupDefaults(
defaults.CUSTOM_SECTION_GROUPS || [],
existingConfig.CUSTOM_SECTION_GROUPS || [],
'custom'
),
DISCOVERY_SETTINGS: { ...defaults.DISCOVERY_SETTINGS, ...(existingConfig.DISCOVERY_SETTINGS || {}) },
SEASONAL_THEME_SETTINGS: { ...defaults.SEASONAL_THEME_SETTINGS, ...(existingConfig.SEASONAL_THEME_SETTINGS || {}) },
CACHE: { ...defaults.CACHE, ...(existingConfig.CACHE || {}) },
SPOTLIGHT_SETTINGS: { ...defaults.SPOTLIGHT_SETTINGS, ...(existingConfig.SPOTLIGHT_SETTINGS || {}) },
HOME_SETTINGS: { ...defaults.HOME_SETTINGS, ...(existingConfig.HOME_SETTINGS || {}) },
MERGE_NEXT_UP: existingConfig.MERGE_NEXT_UP ?? defaults.MERGE_NEXT_UP ?? false
};
// Add ENABLED_NORMAL_SECTIONS and ENABLED_DISCOVERY_SECTIONS
// ENABLED_NORMAL_SECTIONS is all enabled sections from HOME_SECTION_GROUPS, SEASONAL_SECTION_GROUPS, and CUSTOM_SECTION_GROUPS that aren't discovery sections
// If they have a start/end date the current date must fall within that date range
// ENABLED_DISCOVERY_SECTIONS is all enabled sections from DISCOVERY_SECTION_GROUPS and CUSTOM_SECTION_GROUPS that are discovery sections
// If they have a start/end date the current date must fall within that date range
const isInSeasonalPeriod = (startDate, endDate) => {
const currentDate = new Date();
const start = new Date(startDate);
const end = new Date(endDate);
// Ensure the years are the same as the current date year
start.setFullYear(currentDate.getFullYear());
end.setFullYear(currentDate.getFullYear());
return currentDate >= start && currentDate <= end;
};
const enabledHomeSections = flattenSectionGroups(mergedConfig.HOME_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
const enabledSeasonalSections = flattenSectionGroups(mergedConfig.SEASONAL_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && isInSeasonalPeriod(s.startDate, s.endDate));
const enabledCustomSections = flattenSectionGroups(mergedConfig.CUSTOM_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
const enabledDiscoverySections = flattenSectionGroups(mergedConfig.DISCOVERY_SECTION_GROUPS).filter(s => s.enabled === true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
const enabledCustomDiscoverySections = flattenSectionGroups(mergedConfig.CUSTOM_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled === true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
mergedConfig.ENABLED_NORMAL_SECTIONS = [...enabledHomeSections, ...enabledSeasonalSections, ...enabledCustomSections];
mergedConfig.ENABLED_DISCOVERY_SECTIONS = [...enabledDiscoverySections, ...enabledCustomDiscoverySections];
// Verify and sync recently-added library sections
//await verifyLibrarySectionsConfig(mergedConfig);
currentConfig = mergedConfig;
return mergedConfig;
} catch (error) {
ERR('Error loading config:', error);
throw error;
}
}
/**
* Get merged home screen config (exported for use by other scripts)
* @returns {Promise