mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Pray for me
This commit is contained in:
1 parent
bd45be2fd6
commit
1dbb3b8518
98 files changed
+47471
-10558
No files matched your search
+124
-188
@@ -1,7 +1,4 @@
|
||||
// KefinTweaks Item Details Collections
|
||||
// Adds related collections to item details pages
|
||||
// Requires: indexedDBCache.js, utils.js, cardBuilder.js modules to be loaded before this script
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
@@ -11,20 +8,90 @@
|
||||
|
||||
const COLLECTIONS_CHILD_COUNT_KEY = `kefinTweaks_${ApiClient.getCurrentUserId()}_allCollectionsChildCount`;
|
||||
const CACHE_NAME = 'collections';
|
||||
const COLLECTION_FETCH_CHUNK_SIZE = 8;
|
||||
|
||||
const state = {
|
||||
allCollections: null,
|
||||
populateInFlight: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Yields execution to allow the UI to repaint and respond to user inputs.
|
||||
* Prefers the modern scheduler.yield API when available.
|
||||
*/
|
||||
function yieldToMain() {
|
||||
if ('scheduler' in window && typeof window.scheduler.yield === 'function') {
|
||||
return window.scheduler.yield();
|
||||
}
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
// Populate collections cache
|
||||
async function populateCollectionsCache(invalidate = false) {
|
||||
// Check if user is logged in and poll for 5 seconds if not
|
||||
if (ApiClient._loggedIn === false) {
|
||||
return;
|
||||
function buildCacheEntries(results) {
|
||||
const cacheData = [];
|
||||
for (const { collection: coll, items } of results) {
|
||||
const itemIds = (items?.Items || []).map((item) => item.Id);
|
||||
if (itemIds.length === 0) continue;
|
||||
cacheData.push({
|
||||
CollectionItem: {
|
||||
Id: coll.Id,
|
||||
Type: coll.Type,
|
||||
Name: coll.Name,
|
||||
ParentIndexNumber: coll.ParentIndexNumber,
|
||||
IndexNumber: coll.IndexNumber,
|
||||
ImageTags: coll.ImageTags,
|
||||
BackdropImageTags: coll.BackdropImageTags,
|
||||
ProductionYear: coll.ProductionYear,
|
||||
PremiereDate: coll.PremiereDate
|
||||
},
|
||||
ItemIds: itemIds
|
||||
});
|
||||
}
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
async function fetchCollectionChildren(coll) {
|
||||
try {
|
||||
const items = await ApiClient.getItems(ApiClient.getCurrentUserId(), {
|
||||
ParentId: coll.Id
|
||||
});
|
||||
return { collection: coll, items };
|
||||
} catch (err) {
|
||||
WARN(`Failed to fetch items for collection ${coll.Id}`, err);
|
||||
return { collection: coll, items: { Items: [] } };
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCollectionChildrenBatched(collections) {
|
||||
const results = [];
|
||||
const items = collections || [];
|
||||
|
||||
for (let i = 0; i < items.length; i += COLLECTION_FETCH_CHUNK_SIZE) {
|
||||
const chunk = items.slice(i, i + COLLECTION_FETCH_CHUNK_SIZE);
|
||||
const chunkResults = await Promise.all(chunk.map(fetchCollectionChildren));
|
||||
results.push(...chunkResults);
|
||||
|
||||
// Yield back to the browser to paint frames & maintain high FPS
|
||||
if (i + COLLECTION_FETCH_CHUNK_SIZE < items.length) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function writeCollectionsCache(cache, collections) {
|
||||
const startTime = performance.now();
|
||||
const collectionResults = await fetchCollectionChildrenBatched(collections);
|
||||
const cacheData = buildCacheEntries(collectionResults);
|
||||
await cache.set(CACHE_NAME, cacheData);
|
||||
const duration = performance.now() - startTime;
|
||||
LOG(`Collections cache populated: ${cacheData.length} collections in ${duration.toFixed(2)}ms`);
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
async function populateCollectionsCache(invalidate = false) {
|
||||
if (ApiClient._loggedIn === false) return;
|
||||
|
||||
const cache = window.IndexedDBCache;
|
||||
|
||||
if (!cache) {
|
||||
WARN('IndexedDBCache instance not available');
|
||||
return;
|
||||
@@ -38,9 +105,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
const allCollectionsChildCount = state.allCollections.Items.map(collection => collection.ChildCount).reduce((a, b) => a + b, 0);
|
||||
const allCollectionsChildCount = (state.allCollections.Items || [])
|
||||
.reduce((sum, coll) => sum + (coll.ChildCount || 0), 0);
|
||||
|
||||
// Get saved Collections Child Count
|
||||
const cachedAllCollectionsChildCount = Number(localStorage.getItem(COLLECTIONS_CHILD_COUNT_KEY));
|
||||
|
||||
if (allCollectionsChildCount !== cachedAllCollectionsChildCount) {
|
||||
@@ -49,173 +116,73 @@
|
||||
LOG('Collections Child Count has changed, invalidating cache');
|
||||
}
|
||||
|
||||
// Check if cache is valid
|
||||
if (!invalidate && await cache.isCacheValid(CACHE_NAME)) {
|
||||
LOG('Collections cache is still valid');
|
||||
return;
|
||||
}
|
||||
|
||||
LOG('Populating collections cache...');
|
||||
|
||||
try {
|
||||
const currentCacheData = await cache.get(CACHE_NAME);
|
||||
const hasExistingCache = Array.isArray(currentCacheData) && currentCacheData.length > 0;
|
||||
|
||||
// Fetch all collection items in parallel
|
||||
const collectionPromises = state.allCollections.Items.map(coll =>
|
||||
ApiClient.getItems(ApiClient.getCurrentUserId(), {
|
||||
ParentId: coll.Id,
|
||||
}).then(items => ({
|
||||
collection: coll,
|
||||
items: items
|
||||
}))
|
||||
);
|
||||
|
||||
// Check if cache already contains data
|
||||
// If so, process the cache refresh in parallel and return the currently cached data instead of waiting
|
||||
// If no cache exists, wait for all fetches to complete
|
||||
const currentCacheData = await cache.get(CACHE_NAME);
|
||||
|
||||
if (currentCacheData && currentCacheData.length > 0) {
|
||||
// Refreshing the cache Collections cache in the background
|
||||
LOG('Refreshing collections cache in the background');
|
||||
Promise.all(collectionPromises).then(async (results) => {
|
||||
LOG('Collections cache refreshed');
|
||||
const cacheData = [];
|
||||
for (const { collection: coll, items } of results) {
|
||||
const collectionItemData = {
|
||||
Id: coll.Id,
|
||||
Type: coll.Type,
|
||||
Name: coll.Name,
|
||||
ParentIndexNumber: coll.ParentIndexNumber,
|
||||
IndexNumber: coll.IndexNumber,
|
||||
ImageTags: coll.ImageTags,
|
||||
BackdropImageTags: coll.BackdropImageTags,
|
||||
ProductionYear: coll.ProductionYear,
|
||||
PremiereDate: coll.PremiereDate
|
||||
};
|
||||
const itemIds = items.Items.map(item => item.Id);
|
||||
if (itemIds.length > 0) {
|
||||
cacheData.push({
|
||||
CollectionItem: collectionItemData,
|
||||
ItemIds: itemIds
|
||||
});
|
||||
}
|
||||
}
|
||||
await cache.set(CACHE_NAME, cacheData);
|
||||
LOG(`Collections cache populated: ${cacheData.length} collections`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup performance timer
|
||||
const startTime = performance.now();
|
||||
|
||||
// Wait for all fetches to complete
|
||||
const collectionResults = await Promise.all(collectionPromises);
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
LOG(`Collections cache populated in ${duration.toFixed(2)}ms`);
|
||||
|
||||
const cacheData = [];
|
||||
|
||||
// Process all results
|
||||
for (const { collection: coll, items } of collectionResults) {
|
||||
// Store collection item data needed for renderCards
|
||||
const collectionItemData = {
|
||||
Id: coll.Id,
|
||||
Type: coll.Type,
|
||||
Name: coll.Name,
|
||||
ParentIndexNumber: coll.ParentIndexNumber,
|
||||
IndexNumber: coll.IndexNumber,
|
||||
ImageTags: coll.ImageTags,
|
||||
BackdropImageTags: coll.BackdropImageTags,
|
||||
ProductionYear: coll.ProductionYear,
|
||||
PremiereDate: coll.PremiereDate
|
||||
};
|
||||
|
||||
// Store item IDs in this collection for lookup
|
||||
const itemIds = items.Items.map(item => item.Id);
|
||||
|
||||
if (itemIds.length > 0) {
|
||||
cacheData.push({
|
||||
CollectionItem: collectionItemData,
|
||||
ItemIds: itemIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await cache.set(CACHE_NAME, cacheData);
|
||||
LOG(`Collections cache populated: ${cacheData.length} collections`);
|
||||
} catch (error) {
|
||||
ERR('Error populating collections cache:', error);
|
||||
if (state.populateInFlight) {
|
||||
if (hasExistingCache) return;
|
||||
return state.populateInFlight;
|
||||
}
|
||||
|
||||
LOG(hasExistingCache
|
||||
? 'Refreshing collections cache in background'
|
||||
: 'Populating collections cache (First run)...');
|
||||
|
||||
const work = writeCollectionsCache(cache, state.allCollections.Items)
|
||||
.catch((error) => {
|
||||
ERR('Error populating collections cache:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
state.populateInFlight = null;
|
||||
});
|
||||
|
||||
state.populateInFlight = work;
|
||||
|
||||
if (hasExistingCache) return;
|
||||
await work; // Wait on initial load so caller can render right after
|
||||
}
|
||||
|
||||
// Get parent collections for an item ID (returns collection items that can be passed to renderCards)
|
||||
async function getParentCollections(itemId) {
|
||||
const cache = window.IndexedDBCache;
|
||||
|
||||
if (!cache) {
|
||||
WARN('IndexedDBCache instance not available');
|
||||
return [];
|
||||
}
|
||||
if (!cache) return [];
|
||||
|
||||
const cacheData = await cache.get(CACHE_NAME);
|
||||
if (!cacheData || !Array.isArray(cacheData)) return [];
|
||||
|
||||
if (!cacheData || !Array.isArray(cacheData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find collections that contain this item and return the collection item objects
|
||||
return cacheData
|
||||
.filter(entry => entry.ItemIds && entry.ItemIds.includes(itemId))
|
||||
.map(entry => entry.CollectionItem);
|
||||
}
|
||||
|
||||
// Render collections section on details page
|
||||
async function renderCollectionsSection(item) {
|
||||
if (!item || !item.Id) {
|
||||
return;
|
||||
}
|
||||
if (!item || !item.Id) return;
|
||||
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!activePage) {
|
||||
return;
|
||||
}
|
||||
if (!activePage) return;
|
||||
|
||||
if (activePage.dataset.collectionsChecked === 'true') {
|
||||
return;
|
||||
}
|
||||
// Prevent redundant runs on the same page element
|
||||
if (activePage.dataset.collectionsChecked === 'true') return;
|
||||
|
||||
activePage.dataset.collectionsChecked = 'true';
|
||||
|
||||
// Check if section already rendered
|
||||
const existingSection = activePage.querySelector('.collections-section');
|
||||
if (existingSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parent collections (returns collection item objects)
|
||||
const parentCollectionItems = await getParentCollections(item.Id);
|
||||
if (!parentCollectionItems || parentCollectionItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!parentCollectionItems || parentCollectionItems.length === 0) return;
|
||||
|
||||
// Check for cardBuilder
|
||||
if (!window.cardBuilder || !window.cardBuilder.renderCards) {
|
||||
WARN('cardBuilder.renderCards not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find detailPageContent
|
||||
const detailPageContent = activePage.querySelector('.detailPageContent');
|
||||
if (!detailPageContent) {
|
||||
WARN('detailPageContent not found');
|
||||
return;
|
||||
}
|
||||
if (!detailPageContent) return;
|
||||
|
||||
// Ensure we don't render duplicate sections
|
||||
if (activePage.querySelector('.collections-section')) return;
|
||||
|
||||
try {
|
||||
// Render collections section using renderCards with cached item data
|
||||
const collectionsSection = window.cardBuilder.renderCards(
|
||||
parentCollectionItems,
|
||||
'Included In',
|
||||
@@ -224,21 +191,15 @@
|
||||
null
|
||||
);
|
||||
|
||||
// Mark as collections section
|
||||
collectionsSection.classList.add('collections-section');
|
||||
collectionsSection.setAttribute('data-collections-section', 'true');
|
||||
|
||||
const scrollerContainer = collectionsSection.querySelector('.emby-scroller');
|
||||
if (scrollerContainer) {
|
||||
scrollerContainer.classList.add('no-padding');
|
||||
}
|
||||
if (scrollerContainer) scrollerContainer.classList.add('no-padding');
|
||||
|
||||
const sectionTitleContainer = collectionsSection.querySelector('.sectionTitleContainer');
|
||||
if (sectionTitleContainer) {
|
||||
sectionTitleContainer.classList.add('no-padding');
|
||||
}
|
||||
if (sectionTitleContainer) sectionTitleContainer.classList.add('no-padding');
|
||||
|
||||
// Add to detailPageContent
|
||||
const similarSection = detailPageContent.querySelector('#similarCollapsible');
|
||||
if (similarSection) {
|
||||
similarSection.before(collectionsSection);
|
||||
@@ -246,13 +207,13 @@
|
||||
detailPageContent.appendChild(collectionsSection);
|
||||
}
|
||||
|
||||
activePage.dataset.collectionsChecked = 'true';
|
||||
LOG(`Rendered ${parentCollectionItems.length} collection(s) for item ${item.Id}`);
|
||||
} catch (error) {
|
||||
ERR('Error rendering collections section:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize onViewPage hook
|
||||
function initializeCollectionsHook() {
|
||||
if (!window.KefinTweaksUtils || !window.KefinTweaksUtils.onViewPage) {
|
||||
WARN('KefinTweaksUtils.onViewPage not available, retrying in 1 second');
|
||||
@@ -264,32 +225,23 @@
|
||||
|
||||
window.KefinTweaksUtils.onViewPage(
|
||||
async (view, element, hash, itemPromise) => {
|
||||
if (ApiClient._loggedIn === false) {
|
||||
return;
|
||||
}
|
||||
if (ApiClient._loggedIn === false) return;
|
||||
|
||||
// Only handle details pages
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!activePage) return;
|
||||
|
||||
// Await the item promise to get the actual item data
|
||||
const item = await itemPromise;
|
||||
const supportedTypes = ['Movie', 'Series', 'Season', 'Episode', 'MusicArtist', 'MusicAlbum', 'Audio', 'Book', 'AudioBook', 'MusicVideo'];
|
||||
|
||||
const supportedTypes = ['Movie', 'Series', 'Season', 'Episode', 'MusicArtist', 'MusicAlbum', 'Audio', 'Book', 'AudioBook', 'MusicVideo' ];
|
||||
|
||||
if (!supportedTypes.includes(item.Type)) {
|
||||
LOG(`Item type ${item.Type} not supported for collections, skipping...`);
|
||||
return;
|
||||
}
|
||||
if (!supportedTypes.includes(item?.Type)) return;
|
||||
|
||||
// 1. Populate/Update cache (waits on first load, runs in background on updates)
|
||||
await populateCollectionsCache();
|
||||
|
||||
// Small delay to ensure details DOM is ready
|
||||
setTimeout(async () => {
|
||||
if (item && item.Id) {
|
||||
await renderCollectionsSection(item);
|
||||
}
|
||||
}, 100);
|
||||
// 2. Render cards using populated IndexedDB cache
|
||||
if (item && item.Id) {
|
||||
await renderCollectionsSection(item);
|
||||
}
|
||||
},
|
||||
{
|
||||
pages: ['details'],
|
||||
@@ -300,33 +252,17 @@
|
||||
LOG('Collections hook initialized');
|
||||
}
|
||||
|
||||
const maxInitAttempts = 10;
|
||||
let initAttempts = 0;
|
||||
|
||||
// Initialize cache population on script load
|
||||
function initialize() {
|
||||
if (!window.ApiClient) {
|
||||
WARN('Dependencies not available, retrying in 1 second');
|
||||
if (initAttempts < maxInitAttempts) {
|
||||
setTimeout(initialize, 1000);
|
||||
initAttempts++;
|
||||
} else {
|
||||
ERR('Dependencies not available after 10 seconds, giving up');
|
||||
}
|
||||
setTimeout(initialize, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize collections hook
|
||||
initializeCollectionsHook();
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
|
||||
LOG('Script loaded successfully');
|
||||
})();
|
||||
|
||||
})();
|
||||
Reference in new issue
Block a user