export const GENRES = { '2042': 'Action', '1212': 'Sci-Fi', '4910': 'Crime', '62': 'Drama', '82': 'Comedy', '293': 'Animation', '36': 'Documentary', '5024': 'Horror', '4835': 'Romance', '4428': 'Thriller', '5709': 'War', '14124': 'Western', '218': 'Food', '396654': 'Reality', '19618': 'Travel', '17565': 'Mini Series', '2008': 'Mystery', '235': 'Family', '480': 'Fantasy' }; export const SECTION_TYPES = [ { value: 'resume', label: 'Resume / Next Up' }, { value: 'items', label: 'Items (filtered)' }, { value: 'userviews', label: 'Libraries' }, { value: 'boxset', label: 'Box Set' }, { value: 'collections', label: 'Collections' }, { value: 'latestepisodereleases', label: 'Latest episode releases' }, { value: 'latestmoviereleases', label: 'Latest movie releases' }, { value: 'latestmediablock', label: 'Latest media' } ]; export const COLLECTION_TYPES = [ { value: '', label: '(none)' }, { value: 'movies', label: 'Movies' }, { value: 'tvshows', label: 'TV Shows' }, { value: 'boxsets', label: 'Box Sets' } ]; export const ITEM_TYPES = ['Movie', 'Series', 'Episode', 'BoxSet']; export const SORT_OPTIONS = [ { value: '', label: '(none)' }, { value: 'default', label: 'Default (boxset)' }, { value: 'DatePlayed', label: 'Date played' }, { value: 'DateLastContentAdded,SortName', label: 'Date added' }, { value: 'ProductionYear,PremiereDate,SortName', label: 'Release year' }, { value: 'CommunityRating', label: 'Community rating' }, { value: 'CriticRating,SortName', label: 'Critic rating' }, { value: 'DateCreated,SortName', label: 'Date created' }, { value: 'Random', label: 'Random' }, { value: 'SortName', label: 'Name' } ]; export const IMAGE_TYPES = [ { value: '', label: 'Default' }, { value: 'Thumb', label: 'Thumb' }, { value: 'Primary', label: 'Primary / Poster' } ]; export const PAGE_ICONS = { edit: 'edit', sync: 'sync', collections: 'collections', settings: 'settings' }; export const SECTION_ICONS = { resume: 'resume', items: 'items', userviews: 'userviews', boxset: 'boxset', collections: 'collections', latestepisodereleases: 'latestepisodereleases', latestmoviereleases: 'latestmoviereleases', latestmediablock: 'latestmediablock' }; export function genId() { return crypto.randomUUID().replace(/-/g, '').slice(0, 32); } export function createEmptySection(userId) { return { UserId: userId, Name: 'New Section', CustomName: 'New Section', Id: genId(), SectionType: 'items', ImageType: 'Thumb', CollectionType: 'movies', SortBy: 'Random', SortOrder: 'Descending', Monitor: [], ItemTypes: ['Movie'], ExcludedFolders: [], CardSizeOffset: 0, IncludeNextUpInResume: true, Query: { StudioIds: [], TagIds: [], GenreIds: [], CollectionTypes: [], IsPlayed: false } }; } export function createRecentlyWatchedSection(userId, userName = '') { return { UserId: userId, Name: `Recently Watched${userName ? ` - ${userName}` : ''}`, CustomName: `Recently Watched${userName ? ` - ${userName}` : ''}`, Id: genId(), SectionType: 'items', ImageType: 'Thumb', CollectionType: '', SortBy: 'DatePlayed', SortOrder: 'Descending', Monitor: [], ItemTypes: ['Movie', 'Series'], ExcludedFolders: [], CardSizeOffset: 0, IncludeNextUpInResume: true, Query: { StudioIds: [], TagIds: [], GenreIds: [], CollectionTypes: [], IsPlayed: true } }; } export function createBoxSetSection(userId, collectionName, collectionId) { return { UserId: userId, Name: collectionName || 'New Collection', CustomName: collectionName || 'New Collection', Id: genId(), SectionType: 'boxset', ImageType: 'Thumb', ItemTypes: [], SortBy: 'Random', SortOrder: 'Descending', Monitor: [], ExcludedFolders: [], CardSizeOffset: 0, IncludeNextUpInResume: true, ParentItem: { Name: collectionName || 'New Collection', Id: String(collectionId || '') }, ParentId: String(collectionId || '') }; } export function getSectionTypeLabel(type) { const found = SECTION_TYPES.find((t) => t.value === type); return found ? found.label : type; } export function getSectionIconName(type) { return SECTION_ICONS[type] || 'spark'; } export function getGenreNames(genreIds) { if (!genreIds || genreIds.length === 0) return ''; return genreIds.map((id) => GENRES[id] || id).join(', '); } export function extractUserName(sections) { for (const s of sections) { const name = s.CustomName || s.Name || ''; const extracted = extractWatchlistOwnerName(name); if (extracted && extracted !== name) return extracted; } return null; } export function isWatchlistSection(section) { if (!section) return false; const name = `${section.CustomName || ''} ${section.Name || ''}`.toLowerCase(); return !!section.Query?.IsFavorite || name.includes('watchlist') || name.includes('watch list'); } export function isUpNextSection(section) { if (!section) return false; if (section.SectionType === 'resume') return true; const name = `${section.CustomName || ''} ${section.Name || ''}`.trim().toLowerCase(); return name === 'up next' || name === 'next up' || name === 'resume / up next'; } export function isNewToEmbySection(section) { if (!section) return false; const name = `${section.CustomName || ''} ${section.Name || ''}`.toLowerCase(); return name.includes('new to emby'); } export function isRecentlyWatchedSection(section) { if (!section) return false; const name = `${section.CustomName || ''} ${section.Name || ''}`.toLowerCase(); return section.Query?.IsPlayed === true || name.includes('recently watched'); } export function isFixedOrderSection(section) { if (!section) return false; return ( isUpNextSection(section) || isWatchlistSection(section) || isNewToEmbySection(section) || isRecentlyWatchedSection(section) || section.SectionType === 'latestepisodereleases' || section.SectionType === 'latestmoviereleases' || section.SectionType === 'latestmediablock' || section.SectionType === 'userviews' ); } function renameWatchlistLabel(label, targetName) { if (!label || !targetName) return label; if (/^\s*watch\s+list\s*$/i.test(label)) return 'Watch List'; if (/^\s*watchlist\s*$/i.test(label)) return 'Watchlist'; if (/watch\s+list/i.test(label)) { return label.replace(/^.*?watch\s+list/i, `${targetName}'s Watch List`); } if (/watchlist/i.test(label)) { return label.replace(/^.*?watchlist/i, `${targetName}'s Watchlist`); } return label; } function extractWatchlistOwnerName(label) { if (!label) return ''; const normalized = String(label).trim(); if (!/watchlist|watch\s+list/i.test(normalized)) return ''; return normalized .replace(/\bwatchlist\b/i, '') .replace(/\bwatch\s+list\b/i, '') .replace(/['’]s$/i, '') .trim(); } function normalizeNameForCompare(name) { return String(name || '') .toLowerCase() .replace(/['’]/g, '') .replace(/[^a-z0-9]+/g, ''); } function labelsMatchUser(label, targetName) { const owner = extractWatchlistOwnerName(label); if (!owner || !targetName) return false; return normalizeNameForCompare(owner) === normalizeNameForCompare(targetName); } function getPreferredUserName(user) { return user?.embyName || user?.name || ''; } export function getWatchlistLabelsForTarget(sourceSection, targetUser) { const existingTargetWatchlist = targetUser?.sections?.find((section) => isWatchlistSection(section)); const targetName = getPreferredUserName(targetUser); if (existingTargetWatchlist) { const existingName = existingTargetWatchlist.CustomName || existingTargetWatchlist.Name || ''; if (!targetName || labelsMatchUser(existingName, targetName)) { return { Name: existingTargetWatchlist.Name || sourceSection.Name, CustomName: existingTargetWatchlist.CustomName || existingTargetWatchlist.Name || sourceSection.CustomName || sourceSection.Name }; } return { Name: renameWatchlistLabel(existingTargetWatchlist.Name || sourceSection.Name, targetName), CustomName: renameWatchlistLabel( existingTargetWatchlist.CustomName || existingTargetWatchlist.Name || sourceSection.CustomName || sourceSection.Name, targetName ) }; } return { Name: renameWatchlistLabel(sourceSection.Name, targetName), CustomName: renameWatchlistLabel(sourceSection.CustomName, targetName) }; } export function applySectionStandards(sourceSection, targetUser) { const section = JSON.parse(JSON.stringify(sourceSection || {})); if (isUpNextSection(section)) { section.Name = 'Up Next'; section.CustomName = 'Up Next'; return section; } if (isWatchlistSection(section)) { const labels = getWatchlistLabelsForTarget(section, targetUser); if (labels.Name) section.Name = labels.Name; if (labels.CustomName) section.CustomName = labels.CustomName; return section; } if (isNewToEmbySection(section)) { section.Name = 'New to Emby'; section.CustomName = 'New to Emby'; section.SortBy = 'DateLastContentAdded,SortName'; section.SortOrder = 'Descending'; return section; } if (isRecentlyWatchedSection(section)) { const targetName = getPreferredUserName(targetUser); const label = `Recently Watched${targetName ? ` - ${targetName}` : ''}`; section.Name = label; section.CustomName = label; section.SortBy = 'DatePlayed'; section.SortOrder = 'Descending'; return section; } if (!isFixedOrderSection(section) && ['items', 'collections', 'boxset'].includes(section.SectionType)) { section.SortBy = 'Random'; section.SortOrder = 'Descending'; } if (section.SectionType === 'userviews') { section.Name = 'Libraries'; section.CustomName = 'Libraries'; } return section; } /** * Build SQL UPDATE statements from modified user data. * Each user's entire homescreensettings JSON is replaced. */ export function generateSQL(users, originalUsers) { const statements = []; for (const user of users) { if (!user.sections || user.sections.length === 0) continue; const original = originalUsers.find((u) => u.id === user.id); if (!original) continue; const origJSON = JSON.stringify({ Sections: original.sections }); const newJSON = JSON.stringify({ Sections: user.sections }); if (origJSON === newJSON) continue; const escapedValue = newJSON.replace(/'/g, "''"); statements.push( `-- User: ${user.name} (DB ID: ${user.id})`, `UPDATE UserSettings SET Value = '${escapedValue}' WHERE UserId = ${user.id} AND UserSettingsKeyId = (SELECT UserSettingsKeyId FROM UserSettingsKeys WHERE Name = 'homescreensettings');`, '' ); } if (statements.length === 0) { return '-- No changes detected'; } return [ '-- ===========================================', '-- Emby Home Screen Settings Update', `-- Generated: ${new Date().toISOString()}`, '-- ===========================================', '-- IMPORTANT: Stop Emby before running this!', '-- sqlite3 /path/to/users.db < this_file.sql', '-- Then restart Emby.', '-- ===========================================', '', 'BEGIN TRANSACTION;', '', ...statements, 'COMMIT;' ].join('\n'); }