Store Person images in person cache and move client cache args to "dataSourceOptions" instead of queryOptions

This commit is contained in:
HighImKevin
2026-09-28 01:25:00 -04:00
parent 6b51d57f95
commit 5ae1e66b87
9 changed files with 109 additions and 35 deletions
+31 -4
View File
@@ -1069,7 +1069,11 @@
}
if (query.dataSource) {
return { dataSource: query.dataSource, options: queryOptions };
return {
dataSource: query.dataSource,
options: queryOptions,
dataSourceOptions: query.dataSourceOptions || {}
};
}
return this.buildStandardQuery(queryOptions, userId, serverUrl, buildContext);
@@ -1141,15 +1145,38 @@
return `${serverUrl}${path}?${params.toString()}`;
},
/**
* Resolve args for a cache dataSource method.
* Prefers explicit dataSourceOptions; falls back to legacy ItemType/MinCount in queryOptions.
*/
resolveDataSourceMethodArgs: function(dataSourceOptions, queryOptions) {
if (dataSourceOptions && typeof dataSourceOptions === 'object') {
const keys = Object.keys(dataSourceOptions);
if (keys.length) return { ...dataSourceOptions };
}
const qo = queryOptions || {};
const out = {};
if (qo.ItemType != null && qo.ItemType !== '') out.ItemType = qo.ItemType;
else if (qo.itemType != null && qo.itemType !== '') out.ItemType = qo.itemType;
if (qo.MinCount != null && qo.MinCount !== '') out.MinCount = qo.MinCount;
else if (qo.minCount != null && qo.minCount !== '') out.MinCount = qo.minCount;
else if (qo.minimumSeries != null && qo.minimumSeries !== '') out.minimumSeries = qo.minimumSeries;
return out;
},
/**
* Fetch from data source (cache-based)
* @param {string} dataSource - Data source in format "CacheName.methodName"
* @param {Object} options - Query options (sorting, limits, etc.)
* @param {Object} queryOptions - Post-processing options (SortBy, SortOrder, Limit, etc.)
* @param {boolean} [useCache=true]
* @param {Object} [dataSourceOptions] - Args passed to the cache method (e.g. ItemType, MinCount)
* @returns {Object} - Object with data, dataPromise, and isStalePromise
*/
fetchFromDataSource: async function(dataSource, options, useCache = true) {
fetchFromDataSource: async function(dataSource, queryOptions, useCache = true, dataSourceOptions) {
const [cacheName, methodName] = dataSource.split('.');
const cache = window[cacheName];
const options = queryOptions || {};
const methodArgs = this.resolveDataSourceMethodArgs(dataSourceOptions, options);
if (!cache || !cache[methodName]) {
WARN(`Data source not available: ${dataSource}`);
@@ -1161,7 +1188,7 @@
}
try {
let dataPromise = Promise.resolve(cache[methodName](options));
let dataPromise = Promise.resolve(cache[methodName](methodArgs));
dataPromise = dataPromise.then(data => {
if (!Array.isArray(data)) {
data = [];
+12 -2
View File
@@ -1138,7 +1138,12 @@
let queryResult;
if (query.dataSource) {
queryResult = await ApiHelper.fetchFromDataSource(query.dataSource, query.queryOptions || {});
queryResult = await ApiHelper.fetchFromDataSource(
query.dataSource,
query.queryOptions || {},
true,
query.dataSourceOptions
);
} else {
const queryUrl = ApiHelper.buildQueryFromSection(
query,
@@ -1234,7 +1239,12 @@
if (query.dataSource) {
// Handle cache-based data sources
queryResult = await ApiHelper.fetchFromDataSource(query.dataSource, query.queryOptions || {}, false);
queryResult = await ApiHelper.fetchFromDataSource(
query.dataSource,
query.queryOptions || {},
false,
query.dataSourceOptions
);
} else {
// Build and execute query
const queryUrl = ApiHelper.buildQueryFromSection(query, userId, serverUrl, sectionConfig.renderMode === 'Spotlight', { sectionType: sectionConfig.type });
+12 -6
View File
@@ -1365,7 +1365,8 @@
discoveryItemType: 'Movie',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopDirectors',
queryOptions: { ItemType: 'Movie', Limit: 100 }
dataSourceOptions: { ItemType: 'Movie' },
queryOptions: { Limit: 100 }
},
name: 'Directed by {Person}',
enabled: true,
@@ -1392,7 +1393,8 @@
discoveryItemType: 'Movie',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopWriters',
queryOptions: { ItemType: 'Movie', Limit: 100 }
dataSourceOptions: { ItemType: 'Movie' },
queryOptions: { Limit: 100 }
},
name: 'Written by {Person}',
enabled: true,
@@ -1419,7 +1421,8 @@
discoveryItemType: 'Movie',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopActors',
queryOptions: { ItemType: 'Movie', Limit: 100 }
dataSourceOptions: { ItemType: 'Movie' },
queryOptions: { Limit: 100 }
},
name: 'Starring {Person}',
enabled: true,
@@ -1970,7 +1973,8 @@
discoveryItemType: 'Series',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopDirectors',
queryOptions: { ItemType: 'Series', Limit: 100 }
dataSourceOptions: { ItemType: 'Series' },
queryOptions: { Limit: 100 }
},
name: 'Directed by {Person}',
enabled: false,
@@ -1998,7 +2002,8 @@
discoveryItemType: 'Series',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopWriters',
queryOptions: { ItemType: 'Series', Limit: 100 }
dataSourceOptions: { ItemType: 'Series' },
queryOptions: { Limit: 100 }
},
name: 'Written by {Person}',
enabled: false,
@@ -2026,7 +2031,8 @@
discoveryItemType: 'Series',
discoverySourceQuery: {
dataSource: 'PeopleCache.getTopActors',
queryOptions: { ItemType: 'Series', Limit: 100 }
dataSourceOptions: { ItemType: 'Series' },
queryOptions: { Limit: 100 }
},
name: 'Starring {Person}',
enabled: true,
+6 -1
View File
@@ -5,12 +5,17 @@
function stripPerson(person) {
if (!person) return null;
return {
const out = {
Name: person.Name,
Id: person.Id,
Role: person.Role,
Type: person.Type
};
if (person.PrimaryImageTag) out.PrimaryImageTag = person.PrimaryImageTag;
if (person.ImageBlurHashes?.Primary) {
out.ImageBlurHashes = { Primary: person.ImageBlurHashes.Primary };
}
return out;
}
function stripChapter(chapter) {
+4 -4
View File
@@ -7,13 +7,13 @@
const WARN = (...args) => console.warn('[KefinTweaks Movies Cache]', ...args);
const ERR = (...args) => console.error('[KefinTweaks Movies Cache]', ...args);
const CACHE_NAME = 'movies_library';
const PARTIAL_CACHE_NAME = 'movies_library_partial';
const CACHE_NAME = 'movies_library_v2';
const PARTIAL_CACHE_NAME = 'movies_library_partial_v2';
const LAST_FETCH_KEY_PREFIX = 'kefinTweaks_lastDateFetchedMovieCache_';
const IMDB_TOP_250_CACHE_TTL = 24 * 60 * 60 * 1000;
const MOVIE_FIELDS = [
'ProviderIds', 'People', 'Studios', 'Taglines', 'Genres', 'Overview', 'PrimaryImageAspectRatio',
'DateCreated', 'DateLastMediaAdded'
'ImageBlurHashes', 'DateCreated', 'DateLastMediaAdded'
].join(',');
let movies = null;
@@ -116,7 +116,7 @@
}
/**
* Full crawl with partial resume. Commits to movies_library only when complete.
* Full crawl with partial resume. Commits to movies_library_v2 only when complete.
*/
async function fetchAndCacheMovies() {
const cache = window.IndexedDBCache;
+8 -3
View File
@@ -7,7 +7,7 @@
const WARN = (...args) => console.warn('[KefinTweaks PeopleCache]', ...args);
const ERR = (...args) => console.error('[KefinTweaks PeopleCache]', ...args);
const CACHE_NAME = 'library_top_people_v3';
const CACHE_NAME = 'library_top_people_v4';
const DEFAULT_MIN = 10;
const DEFAULT_PER_TYPE_MAX = 100;
const EPISODE_CHUNK_SIZE = 500;
@@ -337,7 +337,7 @@
const params = new URLSearchParams({
IncludeItemTypes: 'Episode',
Recursive: 'true',
Fields: 'People',
Fields: 'People,ImageBlurHashes',
ExcludeLocationTypes: 'Virtual',
EnableTotalRecordCount: 'false',
SortBy: 'DateCreated',
@@ -906,7 +906,12 @@
if (personType === 'Director') list = filterRoleList('Director', itemType);
else if (personType === 'Writer') list = filterRoleList('Writer', itemType);
else list = filterRoleList('Actor', itemType);
return list.slice(0, limit).map((p) => ({ Id: p.Id, Name: p.Name, Type: 'Person' }));
return list.slice(0, limit).map((p) => {
const out = { Id: p.Id, Name: p.Name, Type: 'Person' };
if (p.PrimaryImageTag) out.PrimaryImageTag = p.PrimaryImageTag;
if (p.ImageBlurHashes) out.ImageBlurHashes = p.ImageBlurHashes;
return out;
});
}
async function waitForLoginReady(maxWaitMs = 60000) {
+14 -3
View File
@@ -273,7 +273,12 @@
let queryResult;
if (query.dataSource) {
queryResult = await ApiHelper.fetchFromDataSource(query.dataSource, query.queryOptions || {});
queryResult = await ApiHelper.fetchFromDataSource(
query.dataSource,
query.queryOptions || {},
true,
query.dataSourceOptions
);
} else {
const queryUrl = ApiHelper.buildQueryFromSection(query, userId, serverUrl, sectionConfig.renderMode === 'Spotlight', { sectionType: sectionConfig.type });
@@ -575,7 +580,8 @@
: 'getTopActors';
return {
dataSource: `PeopleCache.${method}`,
queryOptions: { ItemType: itemType, Limit: 100 }
dataSourceOptions: { ItemType: itemType },
queryOptions: { Limit: 100 }
};
}
if (discoveryType === 'Similar' || discoveryType === 'Watchlist') {
@@ -676,7 +682,12 @@
const built = ApiHelper.buildQueryFromSection(sourceQuery, userId, serverUrl, false);
if (typeof built === 'object' && built.dataSource) {
const result = await ApiHelper.fetchFromDataSource(built.dataSource, built.options || {}, false);
const result = await ApiHelper.fetchFromDataSource(
built.dataSource,
built.options || {},
false,
built.dataSourceOptions
);
return unwrapQueryItems(result);
}
+3 -3
View File
@@ -7,12 +7,12 @@
const WARN = (...args) => console.warn('[KefinTweaks Series Cache]', ...args);
const ERR = (...args) => console.error('[KefinTweaks Series Cache]', ...args);
const CACHE_NAME = 'series_library';
const PARTIAL_CACHE_NAME = 'series_library_partial';
const CACHE_NAME = 'series_library_v2';
const PARTIAL_CACHE_NAME = 'series_library_partial_v2';
const LAST_FETCH_KEY_PREFIX = 'kefinTweaks_lastDateFetchedSeriesCache_';
const SERIES_FIELDS = [
'ProviderIds', 'People', 'Studios', 'Taglines', 'Genres', 'Overview',
'PrimaryImageAspectRatio', 'ChildCount', 'RecursiveItemCount'
'PrimaryImageAspectRatio', 'ImageBlurHashes', 'ChildCount', 'RecursiveItemCount'
].join(',');
let series = null;
+19 -9
View File
@@ -83,22 +83,32 @@
/**
* Gets popular TV networks (studios) filtered by minimum series count
* @param {number} minimumSeries - Minimum number of series required (default: 0, returns all)
* @param {number|{ MinCount?: number, minCount?: number, minimumSeries?: number }} minOrOptions
* Minimum series count, or queryOptions bag from fetchFromDataSource
* @returns {Promise<Array>} - Array of studios meeting the minimum threshold
*/
async function getPopularTVNetworks(minimumSeries) {
if (!minimumSeries) {
minimumSeries = window.KefinTweaksConfig?.HOME_SETTINGS?.minimumSeriesForPopularTVNetworks ?? window.KefinHomeConfig2?.HOME_SETTINGS?.minimumSeriesForPopularTVNetworks;
async function getPopularTVNetworks(minOrOptions) {
let minimumSeries;
if (minOrOptions && typeof minOrOptions === 'object') {
minimumSeries = minOrOptions.MinCount ?? minOrOptions.minCount ?? minOrOptions.minimumSeries;
} else {
minimumSeries = minOrOptions;
}
if (minimumSeries == null || minimumSeries === '') {
minimumSeries = window.KefinTweaksConfig?.HOME_SETTINGS?.minimumSeriesForPopularTVNetworks
?? window.KefinHomeConfig2?.HOME_SETTINGS?.minimumSeriesForPopularTVNetworks;
}
const studios = await getSeriesStudios();
if (minimumSeries > 0) {
return studios.filter(studio =>
(studio.SeriesCount || studio.ChildCount || 0) >= minimumSeries
const threshold = Number(minimumSeries);
if (Number.isFinite(threshold) && threshold > 0) {
return studios.filter(studio =>
(studio.SeriesCount || studio.ChildCount || 0) >= threshold
);
}
return studios;
}