mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 07:42:47 +01:00
345 lines
13 KiB
JavaScript
345 lines
13 KiB
JavaScript
// KefinTweaks People Cache Manager
|
|
// Handles fetching, processing, and caching of top actors, directors, and writers
|
|
(function() {
|
|
'use strict';
|
|
|
|
const LOG = (...args) => console.log('[KefinTweaks PeopleCache]', ...args);
|
|
const WARN = (...args) => console.warn('[KefinTweaks PeopleCache]', ...args);
|
|
const ERR = (...args) => console.error('[KefinTweaks PeopleCache]', ...args);
|
|
|
|
// Configuration
|
|
const MIN_PEOPLE_APPEARANCES = 2; // Minimum number of items a person must appear in to be tracked
|
|
|
|
// State
|
|
let moviesTopPeople = null;
|
|
let isComplete = false;
|
|
/** @type {Promise<*>|null} Shared so concurrent callers await the same paginated fetch */
|
|
let fetchPromise = null;
|
|
/** @type {Promise<void>|null} Shared so concurrent callers await the same build */
|
|
let initPromise = null;
|
|
|
|
/**
|
|
* Extracts necessary item data for caching (similar to watchlist format)
|
|
*/
|
|
function extractItemDataForCache(item) {
|
|
return {
|
|
Id: item.Id,
|
|
Type: item.Type,
|
|
Name: item.Name,
|
|
SeriesId: item.SeriesId,
|
|
SeriesName: item.SeriesName,
|
|
IndexNumber: item.IndexNumber,
|
|
ParentIndexNumber: item.ParentIndexNumber,
|
|
PremiereDate: item.PremiereDate,
|
|
ProductionYear: item.ProductionYear,
|
|
UserData: {
|
|
Played: item.UserData?.Played || false,
|
|
IsFavorite: item.UserData?.IsFavorite || false
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Processes people data from movies array and returns RAW data (all people, unfiltered)
|
|
*/
|
|
function processPeopleDataRaw(movies, existingPeopleMap = null) {
|
|
try {
|
|
const peopleMap = existingPeopleMap ? new Map(existingPeopleMap) : new Map();
|
|
|
|
movies.forEach(movie => {
|
|
// Extract item data for caching
|
|
const itemData = extractItemDataForCache(movie);
|
|
|
|
if (movie.People) {
|
|
movie.People.forEach(person => {
|
|
const key = person.Id;
|
|
if (!peopleMap.has(key)) {
|
|
peopleMap.set(key, {
|
|
...person,
|
|
directorCount: 0,
|
|
writerCount: 0,
|
|
actorCount: 0,
|
|
directorItems: [],
|
|
writerItems: [],
|
|
actorItems: []
|
|
});
|
|
}
|
|
|
|
const personData = peopleMap.get(key);
|
|
if (person.Type === 'Director') {
|
|
personData.directorCount++;
|
|
if (!personData.directorItems.some(item => item.Id === movie.Id)) {
|
|
personData.directorItems.push(itemData);
|
|
}
|
|
} else if (person.Type === 'Writer') {
|
|
personData.writerCount++;
|
|
if (!personData.writerItems.some(item => item.Id === movie.Id)) {
|
|
personData.writerItems.push(itemData);
|
|
}
|
|
} else if (person.Type === 'Actor') {
|
|
personData.actorCount++;
|
|
if (!personData.actorItems.some(item => item.Id === movie.Id)) {
|
|
personData.actorItems.push(itemData);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return peopleMap;
|
|
} catch (err) {
|
|
ERR('Failed to process people data:', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filters and sorts raw people data
|
|
*/
|
|
function filterPeopleData(peopleMap) {
|
|
try {
|
|
if (!peopleMap) return null;
|
|
|
|
const allPeople = Array.from(peopleMap.values());
|
|
|
|
// Get threshold from config or default
|
|
const minAppearances = (window.KefinTweaksConfig &&
|
|
window.KefinTweaksConfig.homeScreen &&
|
|
window.KefinTweaksConfig.homeScreen.discovery &&
|
|
window.KefinTweaksConfig.homeScreen.discovery.minPeopleAppearances)
|
|
|| MIN_PEOPLE_APPEARANCES;
|
|
|
|
const topDirectors = allPeople
|
|
.filter(person => person.directorCount >= minAppearances)
|
|
.sort((a, b) => b.directorCount - a.directorCount)
|
|
.map(person => ({
|
|
...person,
|
|
count: person.directorCount,
|
|
items: person.directorItems || []
|
|
}));
|
|
|
|
const topWriters = allPeople
|
|
.filter(person => person.writerCount >= minAppearances)
|
|
.sort((a, b) => b.writerCount - a.writerCount)
|
|
.map(person => ({
|
|
...person,
|
|
count: person.writerCount,
|
|
items: person.writerItems || []
|
|
}));
|
|
|
|
const topActors = allPeople
|
|
.filter(person => person.actorCount >= minAppearances)
|
|
.sort((a, b) => b.actorCount - a.actorCount)
|
|
.map(person => ({
|
|
...person,
|
|
count: person.actorCount,
|
|
items: person.actorItems || []
|
|
}));
|
|
|
|
return {
|
|
actors: topActors,
|
|
directors: topDirectors,
|
|
writers: topWriters
|
|
};
|
|
} catch (err) {
|
|
ERR('Failed to filter people data:', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches and processes top people data from all movies using pagination
|
|
*/
|
|
async function fetchTopPeople() {
|
|
if (fetchPromise) {
|
|
return fetchPromise;
|
|
}
|
|
|
|
fetchPromise = (async () => {
|
|
try {
|
|
LOG('Starting paginated fetch of movies for people data processing...');
|
|
const apiClient = window.ApiClient;
|
|
const serverUrl = apiClient.serverAddress();
|
|
const userId = apiClient.getCurrentUserId();
|
|
const indexedDBCache = window.IndexedDBCache;
|
|
|
|
let startIndex = 0;
|
|
const limit = 500;
|
|
let hasMoreData = true;
|
|
let peopleMap = null;
|
|
|
|
// Load existing raw people data if available (for resuming)
|
|
const existingRawData = await indexedDBCache.get('movies_top_people_raw', userId);
|
|
if (existingRawData && !existingRawData.isComplete) {
|
|
const moviesProcessed = existingRawData.moviesProcessedCount;
|
|
if (moviesProcessed > 0) {
|
|
startIndex = moviesProcessed;
|
|
peopleMap = new Map(existingRawData.peopleData || []);
|
|
LOG(`Resuming from existing data: ${moviesProcessed} movies already processed`);
|
|
}
|
|
}
|
|
|
|
while (hasMoreData) {
|
|
const url = `${serverUrl}/Items?IncludeItemTypes=Movie&Recursive=true&Fields=People,UserData&Limit=${limit}&StartIndex=${startIndex}`;
|
|
|
|
LOG(`Fetching movies ${startIndex} to ${startIndex + limit - 1}...`);
|
|
|
|
const data = await window.apiHelper.getQuery(url);
|
|
const movies = (data && data.Items) || [];
|
|
|
|
if (movies.length === 0) {
|
|
hasMoreData = false;
|
|
isComplete = true;
|
|
} else {
|
|
peopleMap = processPeopleDataRaw(movies, peopleMap);
|
|
if (!peopleMap) {
|
|
throw new Error('processPeopleDataRaw failed');
|
|
}
|
|
startIndex += movies.length;
|
|
|
|
// Update global state with partial results for immediate use
|
|
const filteredData = filterPeopleData(peopleMap);
|
|
if (filteredData) {
|
|
moviesTopPeople = filteredData;
|
|
}
|
|
|
|
// Checkpoint raw data (non-fatal if IndexedDB rejects large payloads)
|
|
const rawDataToCache = {
|
|
isComplete: movies.length < limit,
|
|
moviesProcessedCount: startIndex,
|
|
peopleData: Array.from(peopleMap.entries())
|
|
};
|
|
try {
|
|
await indexedDBCache.set('movies_top_people_raw', rawDataToCache, userId, 7 * 24 * 60 * 60 * 1000);
|
|
} catch (checkpointErr) {
|
|
WARN('Failed to checkpoint raw people data (continuing):', checkpointErr);
|
|
}
|
|
|
|
if (movies.length < limit) {
|
|
hasMoreData = false;
|
|
isComplete = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const finalPeopleData = filterPeopleData(peopleMap) || {
|
|
actors: [],
|
|
directors: [],
|
|
writers: []
|
|
};
|
|
finalPeopleData.isComplete = true;
|
|
moviesTopPeople = finalPeopleData;
|
|
isComplete = true;
|
|
|
|
try {
|
|
await indexedDBCache.set('movies_top_people', finalPeopleData, userId, 7 * 24 * 60 * 60 * 1000);
|
|
await indexedDBCache.clear('movies_top_people_raw', userId);
|
|
} catch (saveErr) {
|
|
WARN('Failed to persist complete people cache (in-memory still available):', saveErr);
|
|
}
|
|
|
|
LOG(peopleMap ? 'People cache complete and saved' : 'People cache complete (empty library)');
|
|
return finalPeopleData;
|
|
} catch (err) {
|
|
ERR('Failed to fetch top people data:', err);
|
|
return null;
|
|
} finally {
|
|
fetchPromise = null;
|
|
}
|
|
})();
|
|
|
|
return fetchPromise;
|
|
}
|
|
|
|
/**
|
|
* Initializes the people cache (awaits build; concurrent callers share one promise)
|
|
*/
|
|
async function initialize() {
|
|
if (moviesTopPeople !== null && isComplete) {
|
|
return;
|
|
}
|
|
if (initPromise) {
|
|
return initPromise;
|
|
}
|
|
|
|
initPromise = (async () => {
|
|
try {
|
|
if (moviesTopPeople !== null && isComplete) {
|
|
return;
|
|
}
|
|
|
|
const indexedDBCache = window.IndexedDBCache;
|
|
const userId = window.ApiClient.getCurrentUserId();
|
|
|
|
// Check for valid complete filtered data
|
|
const validComplete = await indexedDBCache.isCacheValid('movies_top_people', userId);
|
|
if (validComplete) {
|
|
const cachedData = await indexedDBCache.get('movies_top_people', userId);
|
|
if (cachedData && cachedData.isComplete) {
|
|
moviesTopPeople = cachedData;
|
|
isComplete = true;
|
|
LOG('Loaded complete top people data from IndexedDB');
|
|
return;
|
|
}
|
|
}
|
|
await fetchTopPeople();
|
|
} finally {
|
|
initPromise = null;
|
|
}
|
|
})();
|
|
|
|
return initPromise;
|
|
}
|
|
|
|
async function getTopPeople() {
|
|
LOG('Getting top people data');
|
|
try {
|
|
if (!moviesTopPeople) {
|
|
await initialize();
|
|
}
|
|
return moviesTopPeople;
|
|
} catch (err) {
|
|
ERR('Failed to get top people data:', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getTopActors() {
|
|
if (!moviesTopPeople) {
|
|
await initialize();
|
|
}
|
|
return moviesTopPeople?.actors ?? [];
|
|
}
|
|
|
|
async function getTopDirectors() {
|
|
if (!moviesTopPeople) {
|
|
await initialize();
|
|
}
|
|
return moviesTopPeople?.directors ?? [];
|
|
}
|
|
|
|
async function getTopWriters() {
|
|
if (!moviesTopPeople) {
|
|
await initialize();
|
|
}
|
|
return moviesTopPeople?.writers ?? [];
|
|
}
|
|
|
|
function isCacheComplete() {
|
|
return isComplete;
|
|
}
|
|
|
|
// Expose API
|
|
window.PeopleCache = {
|
|
init: initialize,
|
|
getTopPeople: getTopPeople,
|
|
getTopActors: getTopActors,
|
|
getTopDirectors: getTopDirectors,
|
|
getTopWriters: getTopWriters,
|
|
isComplete: isCacheComplete
|
|
};
|
|
|
|
LOG('Module loaded');
|
|
|
|
})();
|