Files
KefinTweaks/scripts/itemDetailsCollections.js
T

332 lines
12 KiB
JavaScript

// 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';
const LOG = (...args) => console.log('[KefinTweaks ItemDetailsCollections]', ...args);
const WARN = (...args) => console.warn('[KefinTweaks ItemDetailsCollections]', ...args);
const ERR = (...args) => console.error('[KefinTweaks ItemDetailsCollections]', ...args);
const COLLECTIONS_CHILD_COUNT_KEY = `kefinTweaks_${ApiClient.getCurrentUserId()}_allCollectionsChildCount`;
const CACHE_NAME = 'collections';
const state = {
allCollections: null,
}
// 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;
}
const cache = window.IndexedDBCache;
if (!cache) {
WARN('IndexedDBCache instance not available');
return;
}
if (!state.allCollections) {
state.allCollections = await ApiClient.getItems(ApiClient.getCurrentUserId(), {
IncludeItemTypes: 'BoxSet,CollectionFolder',
Recursive: true,
Fields: 'ChildCount'
});
}
const allCollectionsChildCount = state.allCollections.Items.map(collection => collection.ChildCount).reduce((a, b) => a + b, 0);
// Get saved Collections Child Count
const cachedAllCollectionsChildCount = Number(localStorage.getItem(COLLECTIONS_CHILD_COUNT_KEY));
if (allCollectionsChildCount !== cachedAllCollectionsChildCount) {
localStorage.setItem(COLLECTIONS_CHILD_COUNT_KEY, allCollectionsChildCount);
invalidate = true;
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 {
// 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);
}
}
// 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 [];
}
const cacheData = await cache.get(CACHE_NAME);
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;
}
const activePage = document.querySelector('.libraryPage:not(.hide)');
if (!activePage) {
return;
}
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;
}
// 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;
}
try {
// Render collections section using renderCards with cached item data
const collectionsSection = window.cardBuilder.renderCards(
parentCollectionItems,
'Included In',
null,
true,
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');
}
const sectionTitleContainer = collectionsSection.querySelector('.sectionTitleContainer');
if (sectionTitleContainer) {
sectionTitleContainer.classList.add('no-padding');
}
// Add to detailPageContent
const similarSection = detailPageContent.querySelector('#similarCollapsible');
if (similarSection) {
similarSection.before(collectionsSection);
} else {
detailPageContent.appendChild(collectionsSection);
}
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');
setTimeout(initializeCollectionsHook, 1000);
return;
}
LOG('Registering collections handler with KefinTweaksUtils');
window.KefinTweaksUtils.onViewPage(
async (view, element, hash, itemPromise) => {
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' ];
if (!supportedTypes.includes(item.Type)) {
LOG(`Item type ${item.Type} not supported for collections, skipping...`);
return;
}
await populateCollectionsCache();
// Small delay to ensure details DOM is ready
setTimeout(async () => {
if (item && item.Id) {
await renderCollectionsSection(item);
}
}, 100);
},
{
pages: ['details']
}
);
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');
}
return;
}
// Initialize collections hook
initializeCollectionsHook();
}
// Start initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
LOG('Script loaded successfully');
})();