Files
KefinTweaks/scripts/userHelper.js
T

377 lines
15 KiB
JavaScript

(function() {
'use strict';
const LOG = (...args) => console.log('[KefinTweaks APIHelper]', ...args);
const WARN = (...args) => console.warn('[KefinTweaks APIHelper]', ...args);
const ERR = (...args) => console.error('[KefinTweaks APIHelper]', ...args);
/**
* Ensure ApiClient is available before using helper methods
*/
function ensureApiClient() {
if (typeof ApiClient === 'undefined' || !ApiClient) {
throw new Error('ApiClient is not available');
}
}
function ensureLoggedIn() {
if (typeof ApiClient === 'undefined' || !ApiClient) {
throw new Error('ApiClient is not available');
}
if (!ApiClient._loggedIn) {
throw new Error('User is not logged in');
}
}
// Poll until ApiClient reports logged in. Singleton in-flight Promise.
// Prefer localusersignedin; fall back to setInterval (no requestIdleCallback).
let loginWaitPromise = null;
function isUserLoggedIn() {
try {
return !!(window.ApiClient && window.ApiClient._loggedIn);
} catch (_) {
return false;
}
}
function getConnectionManager() {
return window.ServerConnections || window.ConnectionManager || null;
}
function getEventsApi() {
return window.Events || window.events || null;
}
async function waitForLogin(maxWaitMs = 60000) {
if (isUserLoggedIn()) {
return true;
}
if (loginWaitPromise) {
return loginWaitPromise;
}
loginWaitPromise = new Promise((resolve) => {
let settled = false;
let intervalId = null;
const startTime = Date.now();
const conn = getConnectionManager();
const eventsApi = getEventsApi();
const cleanup = () => {
if (intervalId != null) {
clearInterval(intervalId);
intervalId = null;
}
if (conn && eventsApi?.off) {
try {
eventsApi.off(conn, 'localusersignedin', onSignedIn);
} catch (_) { /* ignore */ }
}
};
const done = (ok) => {
if (settled) return;
settled = true;
cleanup();
loginWaitPromise = null;
resolve(ok);
};
const onSignedIn = () => {
LOG('waitForLogin: localusersignedin');
done(true);
};
if (conn && eventsApi?.on) {
try {
eventsApi.on(conn, 'localusersignedin', onSignedIn);
} catch (e) {
WARN('waitForLogin: could not subscribe localusersignedin', e);
}
}
intervalId = setInterval(() => {
if (isUserLoggedIn()) {
LOG('waitForLogin: logged in');
done(true);
return;
}
if (Date.now() - startTime >= maxWaitMs) {
WARN('waitForLogin: timed out after', maxWaitMs, 'ms');
done(false);
}
}, 200);
});
return loginWaitPromise;
}
/**
* API Helper functions for Jellyfin operations
*/
const userHelper = {
/**
* Get current-user display preferences.
* @param {{ cacheOnly?: boolean }} [options]
* @returns {Promise<{ promise: Promise<object>, cached: object|null }>}
*/
getUserDisplayPreferences: async function(options = {}) {
const cacheOnly = options.cacheOnly === true;
let cachedDisplayPreferences = null;
// Cache hits must not wait on login (home paint can race signed-in)
if (__userDisplayPreferences) {
cachedDisplayPreferences = __userDisplayPreferences;
} else if (window.LocalStorageCache) {
const cache = new window.LocalStorageCache();
const displayPreferencesData = cache.get('userDisplayPreferences');
if (displayPreferencesData) {
cachedDisplayPreferences = displayPreferencesData;
// Hydrate memory so later cacheOnly calls skip localStorage too
__userDisplayPreferences = displayPreferencesData;
}
}
if (cacheOnly && cachedDisplayPreferences) {
return {
promise: Promise.resolve(cachedDisplayPreferences),
cached: cachedDisplayPreferences
};
}
// Network path: wait for login instead of hard-throwing ensureLoggedIn
ensureApiClient();
const loggedIn = await waitForLogin();
if (!loggedIn) {
if (cachedDisplayPreferences) {
WARN('getUserDisplayPreferences: login wait timed out; returning cache only');
return {
promise: Promise.resolve(cachedDisplayPreferences),
cached: cachedDisplayPreferences
};
}
throw new Error('User is not logged in');
}
// Wrap this in a promise to return with the cached data
const userDisplayPreferencesPromise = new Promise(async (resolve, reject) => {
try {
// Fetch the user display preferences
LOG('Fetching user display preferences');
const response = await fetch(`${ApiClient.serverAddress()}/DisplayPreferences/usersettings?userId=${ApiClient.getCurrentUserId()}&client=emby`, {
headers: {
'Authorization': window.apiHelper.getAuthHeader()
}
});
if (!response.ok) {
reject(new Error(`Failed to fetch display preferences: ${response.status}`));
return;
}
const data = await response.json();
__userDisplayPreferences = data;
// Save the display preferences to localStorage
if (window.LocalStorageCache) {
const cache = new window.LocalStorageCache();
cache.set('userDisplayPreferences', data, ApiClient.getCurrentUserId(), 24 * 60 * 60 * 1000);
}
resolve(data);
} catch (error) {
reject(error);
}
});
return { promise: userDisplayPreferencesPromise, cached: cachedDisplayPreferences };
},
useEpisodeImages: async function() {
try {
ensureApiClient();
ensureLoggedIn();
const { promise, cached } = await this.getUserDisplayPreferences();
const userDisplayPreferences = await promise;
return userDisplayPreferences.CustomPrefs?.useEpisodeImagesInNextUpAndResume === 'true';
} catch (error) {
WARN('Failed to load user display preferences for useEpisodeImages:', error);
return false;
}
},
updateCustomPrefsByKey: async function(key, value) {
const { promise, cached } = await this.getUserDisplayPreferences();
const userDisplayPreferences = await promise;
if (!userDisplayPreferences) {
LOG('No user display preferences found');
return false;
}
userDisplayPreferences.CustomPrefs[key] = value;
return await this.updateDisplayPreferences(userDisplayPreferences);
},
updateDisplayPreferences: async function(prefs) {
ensureApiClient();
const userId = ApiClient.getCurrentUserId();
return await this.updateDisplayPreferencesForUser(userId, prefs, { updateCache: true });
},
/**
* Fetch display preferences for an arbitrary user (admin).
* @param {string} userId
* @returns {Promise<object>}
*/
getUserDisplayPreferencesForUser: async function(userId) {
ensureApiClient();
if (!userId) throw new Error('userId is required');
const response = await fetch(
`${ApiClient.serverAddress()}/DisplayPreferences/usersettings?userId=${encodeURIComponent(userId)}&client=emby`,
{ headers: { 'Authorization': window.apiHelper.getAuthHeader() } }
);
if (!response.ok) {
throw new Error(`Failed to fetch display preferences for ${userId}: ${response.status}`);
}
return response.json();
},
/**
* Persist display preferences for an arbitrary user (admin).
* @param {string} userId
* @param {object} prefs
* @param {{ updateCache?: boolean }} [options]
* @returns {Promise<boolean>}
*/
updateDisplayPreferencesForUser: async function(userId, prefs, options = {}) {
ensureApiClient();
if (!userId) throw new Error('userId is required');
const serverAddress = ApiClient.serverAddress();
const url = `${serverAddress}/DisplayPreferences/usersettings?userId=${encodeURIComponent(userId)}&client=emby`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': window.apiHelper.getAuthHeader()
},
body: JSON.stringify(prefs)
});
if (response.ok && options.updateCache && userId === ApiClient.getCurrentUserId()) {
__userDisplayPreferences = prefs;
if (window.LocalStorageCache) {
try {
const cache = new window.LocalStorageCache();
cache.set('userDisplayPreferences', prefs, userId, 24 * 60 * 60 * 1000);
} catch (_) { /* ignore */ }
}
}
return response.ok;
},
/**
* Parse CustomPrefs.kefinTweaks (JSON string or object) into a plain object.
* @param {object|string|null} customPrefsOrPrefs - CustomPrefs object, full display prefs, or raw kefinTweaks value
* @returns {object}
*/
parseKefinTweaks: function(customPrefsOrPrefs) {
if (!customPrefsOrPrefs) return {};
let raw = customPrefsOrPrefs;
if (customPrefsOrPrefs.CustomPrefs) {
raw = customPrefsOrPrefs.CustomPrefs.kefinTweaks;
} else if (customPrefsOrPrefs.kefinTweaks !== undefined) {
raw = customPrefsOrPrefs.kefinTweaks;
}
if (!raw) return {};
if (typeof raw === 'object') return { ...raw };
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (e) {
WARN('Failed to parse CustomPrefs.kefinTweaks:', e);
return {};
}
},
/**
* Load a single feature blob from CustomPrefs.kefinTweaks.<featureKey>.
* @param {string} featureKey
* @param {*} [defaultValue=null]
* @returns {Promise<*>}
*/
getKefinTweaksFeature: async function(featureKey, defaultValue = null) {
if (!featureKey) return defaultValue;
try {
const { promise } = await this.getUserDisplayPreferences();
const prefs = await promise;
const kefin = this.parseKefinTweaks(prefs);
return Object.prototype.hasOwnProperty.call(kefin, featureKey)
? kefin[featureKey]
: defaultValue;
} catch (e) {
WARN(`getKefinTweaksFeature(${featureKey}) failed:`, e);
return defaultValue;
}
},
/**
* Replace CustomPrefs.kefinTweaks.<featureKey> while preserving sibling keys.
* @param {string} featureKey
* @param {*} value
* @returns {Promise<boolean>}
*/
setKefinTweaksFeature: async function(featureKey, value) {
if (!featureKey) return false;
try {
const { promise } = await this.getUserDisplayPreferences();
const prefs = await promise;
if (!prefs) return false;
if (!prefs.CustomPrefs) prefs.CustomPrefs = {};
const kefin = this.parseKefinTweaks(prefs);
kefin[featureKey] = value;
prefs.CustomPrefs.kefinTweaks = JSON.stringify(kefin);
return await this.updateDisplayPreferences(prefs);
} catch (e) {
ERR(`setKefinTweaksFeature(${featureKey}) failed:`, e);
return false;
}
},
/**
* Update CustomPrefs.kefinTweaks.<featureKey> via object merge or updater function.
* @param {string} featureKey
* @param {object|Function} patchOrFn - Partial object to merge, or (current) => nextValue
* @returns {Promise<boolean>}
*/
updateKefinTweaksFeature: async function(featureKey, patchOrFn) {
if (!featureKey) return false;
try {
const { promise } = await this.getUserDisplayPreferences();
const prefs = await promise;
if (!prefs) return false;
if (!prefs.CustomPrefs) prefs.CustomPrefs = {};
const kefin = this.parseKefinTweaks(prefs);
const current = Object.prototype.hasOwnProperty.call(kefin, featureKey)
? kefin[featureKey]
: null;
let next;
if (typeof patchOrFn === 'function') {
next = patchOrFn(current);
} else if (patchOrFn && typeof patchOrFn === 'object' && !Array.isArray(patchOrFn)
&& current && typeof current === 'object' && !Array.isArray(current)) {
next = { ...current, ...patchOrFn };
} else {
next = patchOrFn;
}
kefin[featureKey] = next;
prefs.CustomPrefs.kefinTweaks = JSON.stringify(kefin);
return await this.updateDisplayPreferences(prefs);
} catch (e) {
ERR(`updateKefinTweaksFeature(${featureKey}) failed:`, e);
return false;
}
},
waitForLogin: waitForLogin
};
// Updated by getUserDisplayPreferences / updateDisplayPreferencesForUser({ updateCache: true })
let __userDisplayPreferences = null;
// Expose userHelper to global window object
window.userHelper = userHelper;
console.log('[KefinTweaks UserHelper] Module loaded and available at window.userHelper');
})();