389 lines
11 KiB
JavaScript
389 lines
11 KiB
JavaScript
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');
|
||
}
|