Files
2026-06-08 21:58:16 +12:00

212 lines
6.2 KiB
JavaScript

function parseJsonBlob(blob) {
if (!blob) return null;
try {
const text = typeof blob === 'string' ? blob : Buffer.from(blob).toString('utf8');
return JSON.parse(text);
} catch {
return null;
}
}
/**
* Emby stores GUIDs in SQLite as 16-byte blobs using Microsoft mixed-endian ordering.
* Components 1-3 are little-endian; components 4-5 are big-endian.
*/
export function blobToEmbyGuid(blob) {
if (!blob) return '';
const b = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
if (b.length !== 16) return Buffer.from(b).toString('hex').toLowerCase();
const out = new Uint8Array([
b[3], b[2], b[1], b[0],
b[5], b[4],
b[7], b[6],
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]
]);
return Buffer.from(out).toString('hex').toLowerCase();
}
function hasTable(db, tableName) {
const row = db
.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?")
.get(tableName);
return !!row?.found;
}
function normalizeSectionUserId(sectionUserId) {
return typeof sectionUserId === 'string' ? sectionUserId.trim().toLowerCase() : '';
}
export function normalizeSectionsForUser(sections, expectedEmbyGuid) {
if (!Array.isArray(sections)) return [];
if (!expectedEmbyGuid) return sections;
return sections.map((section) => ({
...section,
UserId: expectedEmbyGuid
}));
}
function loadUsersTableUsers(db) {
const cols = db.prepare('PRAGMA table_info(Users)').all().map((c) => c.name);
const nameCol = cols.find((c) => /^username$/i.test(c)) || cols.find((c) => /^name$/i.test(c));
const guidCol = cols.find((c) => /^guid$/i.test(c));
const idCol = cols.find((c) => /^id$/i.test(c)) || 'Id';
if (!nameCol) {
throw new Error(`Cannot find a name column in Users table. Columns found: ${cols.join(', ')}`);
}
const selectCols = [idCol, nameCol, guidCol].filter(Boolean).join(', ');
const rows = db.prepare(`SELECT ${selectCols} FROM Users`).all();
return rows.map((row) => {
const rawId = row[idCol] ?? row.Id ?? row.id;
const rawGuid = guidCol ? row[guidCol] : null;
let embyGuid = '';
let guid = '';
if (rawGuid) {
if (rawGuid instanceof Uint8Array || rawGuid instanceof Buffer) {
const buf = Buffer.from(rawGuid);
guid = buf.toString('hex').toUpperCase();
embyGuid = blobToEmbyGuid(rawGuid);
} else if (typeof rawGuid === 'string') {
const clean = rawGuid.replace(/-/g, '').toLowerCase();
embyGuid = clean;
guid = clean.toUpperCase();
}
}
return {
id: rawId,
name: row[nameCol] || `User ${rawId}`,
guid,
embyGuid,
sourceTable: 'Users'
};
});
}
function loadLocalUsers(db) {
const rows = db.prepare('SELECT Id, guid, data FROM LocalUsersv2').all();
return rows.map((row) => {
const parsed = parseJsonBlob(row.data);
const guid = row.guid ? Buffer.from(row.guid).toString('hex').toUpperCase() : '';
const embyGuidFromBlob = blobToEmbyGuid(row.guid);
const embyGuidFromJson = normalizeSectionUserId(parsed?.IdString);
const embyGuid = embyGuidFromJson || embyGuidFromBlob;
return {
id: row.Id,
name: parsed?.Name || `User ${row.Id}`,
guid,
embyGuid,
sourceTable: 'LocalUsersv2',
profile: parsed || null
};
});
}
export function loadCanonicalUsers(db) {
if (hasTable(db, 'LocalUsersv2')) {
return loadLocalUsers(db);
}
if (hasTable(db, 'Users')) {
return loadUsersTableUsers(db);
}
throw new Error('No supported user table found. Expected LocalUsersv2 or Users.');
}
export function loadHomeScreenUsers(db) {
const users = loadCanonicalUsers(db);
const settingsRows = db
.prepare(
`SELECT us.UserId, us.Value
FROM UserSettings us
JOIN UserSettingsKeys usk ON us.UserSettingsKeyId = usk.UserSettingsKeyId
WHERE usk.Name = 'homescreensettings'`
)
.all();
const settingsMap = new Map(settingsRows.map((row) => [String(row.UserId), row.Value]));
const userIds = new Set(users.map((user) => String(user.id)));
let matchedUsers = 0;
let mismatchedUsers = 0;
let normalizedUsers = 0;
let missingSectionUserIds = 0;
const hydratedUsers = users.map((user) => {
const rawValue = settingsMap.get(String(user.id));
let sections = [];
try {
if (rawValue) sections = JSON.parse(rawValue).Sections || [];
} catch {
sections = [];
}
const actualUserIds = [...new Set(sections.map((section) => normalizeSectionUserId(section?.UserId)).filter(Boolean))];
const mismatchedSectionUserIds = user.embyGuid
? actualUserIds.filter((id) => id !== user.embyGuid)
: actualUserIds;
const missingIdsForUser = sections.filter((section) => !normalizeSectionUserId(section?.UserId)).length;
const normalizedSections = normalizeSectionsForUser(sections, user.embyGuid);
const sectionsWereNormalized =
user.embyGuid &&
JSON.stringify(sections) !== JSON.stringify(normalizedSections);
if (mismatchedSectionUserIds.length === 0) matchedUsers++;
else mismatchedUsers++;
if (sectionsWereNormalized) normalizedUsers++;
missingSectionUserIds += missingIdsForUser;
return {
id: user.id,
name: user.name,
guid: user.guid,
embyGuid: user.embyGuid,
sections: normalizedSections,
details: {
sourceTable: user.sourceTable,
lastLoginDate: user.profile?.LastLoginDate || null,
lastActivityDate: user.profile?.LastActivityDate || null,
usesIdForConfigurationPath: user.profile?.UsesIdForConfigurationPath ?? null,
importedCollectionsCount: Array.isArray(user.profile?.ImportedCollections) ? user.profile.ImportedCollections.length : 0
},
match: {
sourceTable: user.sourceTable,
settingsUserId: user.id,
expectedSectionUserId: user.embyGuid,
actualSectionUserIds: actualUserIds,
mismatchedSectionUserIds,
missingSectionUserIds: missingIdsForUser,
ok: mismatchedSectionUserIds.length === 0
}
};
});
const orphanedSettingsUserIds = settingsRows
.map((row) => String(row.UserId))
.filter((userId, index, all) => all.indexOf(userId) === index && !userIds.has(userId));
return {
users: hydratedUsers,
validation: {
userSource: users[0]?.sourceTable || null,
userCount: hydratedUsers.length,
settingsCount: settingsRows.length,
matchedUsers,
mismatchedUsers,
normalizedUsers,
missingSectionUserIds,
orphanedSettingsUserIds
}
};
}
export function loadUserLookup(db) {
return new Map(loadCanonicalUsers(db).map((user) => [String(user.id), user]));
}