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');
|
|||
|
|
}
|