diff --git a/scripts/apiHelper.js b/scripts/apiHelper.js index 9a82052..12840d5 100644 --- a/scripts/apiHelper.js +++ b/scripts/apiHelper.js @@ -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 = []; diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index b6ddd7e..1266c25 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -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 }); diff --git a/scripts/homeScreenConfig2.js b/scripts/homeScreenConfig2.js index c602adc..3113d57 100644 --- a/scripts/homeScreenConfig2.js +++ b/scripts/homeScreenConfig2.js @@ -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, diff --git a/scripts/libraryCacheUtils.js b/scripts/libraryCacheUtils.js index 3cc2276..5038fcd 100644 --- a/scripts/libraryCacheUtils.js +++ b/scripts/libraryCacheUtils.js @@ -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) { diff --git a/scripts/moviesCache.js b/scripts/moviesCache.js index ee77fce..f729e28 100644 --- a/scripts/moviesCache.js +++ b/scripts/moviesCache.js @@ -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; diff --git a/scripts/peopleCache.js b/scripts/peopleCache.js index adc9ee7..0f06c28 100644 --- a/scripts/peopleCache.js +++ b/scripts/peopleCache.js @@ -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) { diff --git a/scripts/sectionHelper.js b/scripts/sectionHelper.js index 01387e9..c336282 100644 --- a/scripts/sectionHelper.js +++ b/scripts/sectionHelper.js @@ -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); } diff --git a/scripts/seriesCache.js b/scripts/seriesCache.js index 64aa8e4..4942396 100644 --- a/scripts/seriesCache.js +++ b/scripts/seriesCache.js @@ -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; diff --git a/scripts/studiosCache.js b/scripts/studiosCache.js index c451e70..36e7bb7 100644 --- a/scripts/studiosCache.js +++ b/scripts/studiosCache.js @@ -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 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; }