Homelabtoolkit v2
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user