mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Move scripts to /scripts folder
This commit is contained in:
1 parent
584e299219
commit
216be4502a
38 files changed
+490
-112
No files matched your search
@@ -0,0 +1,358 @@
|
||||
// KefinTweaks Flatten Single Season Shows
|
||||
// Flattens series with only 1 season to show episodes directly on the series details page
|
||||
// Requires: cardBuilder.js, utils.js modules to be loaded before this script
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const LOG = (...args) => console.log('[KefinTweaks FlattenSingleSeasonShows]', ...args);
|
||||
const WARN = (...args) => console.warn('[KefinTweaks FlattenSingleSeasonShows]', ...args);
|
||||
const ERR = (...args) => console.error('[KefinTweaks FlattenSingleSeasonShows]', ...args);
|
||||
|
||||
/**
|
||||
* Fetches the Next Up episode for a given series
|
||||
* @param {string} seriesId - The series ID
|
||||
* @returns {Promise<Object|null>} - Episode number object with season and episode, or null
|
||||
*/
|
||||
async function fetchNextUpEpisode(seriesId) {
|
||||
try {
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const serverUrl = apiClient.serverAddress();
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
LOG(`Fetching Next Up episode for series: ${seriesId}`);
|
||||
const nextUpUrl = `${serverUrl}/Shows/NextUp?SeriesId=${seriesId}&UserId=${userId}&Fields=MediaSourceCount`;
|
||||
const nextUpRes = await fetch(nextUpUrl, {
|
||||
headers: { "Authorization": `MediaBrowser Token="${token}"` }
|
||||
});
|
||||
|
||||
if (!nextUpRes.ok) {
|
||||
throw new Error(`HTTP ${nextUpRes.status}: ${nextUpRes.statusText}`);
|
||||
}
|
||||
|
||||
const nextUpData = await nextUpRes.json();
|
||||
const items = nextUpData.Items || [];
|
||||
|
||||
if (items.length > 0) {
|
||||
const item = items[0];
|
||||
if (item.IndexNumber && item.ParentIndexNumber) {
|
||||
const episodeNumber = {
|
||||
season: item.ParentIndexNumber,
|
||||
episode: item.IndexNumber
|
||||
};
|
||||
LOG(`Fetched Next Up episode from API: S${episodeNumber.season}:E${episodeNumber.episode}`);
|
||||
return episodeNumber;
|
||||
}
|
||||
}
|
||||
|
||||
LOG('No Next Up episode found in API response');
|
||||
return null;
|
||||
} catch (error) {
|
||||
ERR(`Failed to fetch Next Up episode for series ${seriesId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all episodes for a given series
|
||||
* @param {string} seriesId - The series ID
|
||||
* @returns {Promise<Array>} - Array of episode items
|
||||
*/
|
||||
async function fetchEpisodesForSeries(seriesId) {
|
||||
try {
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const serverUrl = apiClient.serverAddress();
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
LOG(`Fetching episodes for series: ${seriesId}`);
|
||||
const episodesUrl = `${serverUrl}/Shows/${seriesId}/Episodes?UserId=${userId}&Fields=UserData`;
|
||||
const episodesRes = await fetch(episodesUrl, {
|
||||
headers: { "Authorization": `MediaBrowser Token="${token}"` }
|
||||
});
|
||||
|
||||
if (!episodesRes.ok) {
|
||||
throw new Error(`HTTP ${episodesRes.status}: ${episodesRes.statusText}`);
|
||||
}
|
||||
|
||||
const episodesData = await episodesRes.json();
|
||||
const episodes = episodesData.Items || [];
|
||||
|
||||
LOG(`Fetched ${episodes.length} episodes for series: ${seriesId}`);
|
||||
return episodes;
|
||||
} catch (error) {
|
||||
ERR(`Failed to fetch episodes for series ${seriesId}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens a single-season show by creating a "Season 1" section with all episodes
|
||||
* @param {string} seriesId - The series ID
|
||||
*/
|
||||
async function flattenShow(seriesId) {
|
||||
LOG(`Flattening show: ${seriesId}`);
|
||||
|
||||
// Check if already flattened for this series (avoid duplicates)
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (activePage && activePage.dataset.flattenedSeriesId === seriesId) {
|
||||
LOG(`Show ${seriesId} already flattened, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if flattened section already exists
|
||||
const existingSection = activePage?.querySelector('.flattened-season-section');
|
||||
if (existingSection) {
|
||||
LOG(`Flattened section already exists, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all episodes for the series
|
||||
const episodes = await fetchEpisodesForSeries(seriesId);
|
||||
if (episodes.length === 0) {
|
||||
WARN(`No episodes found for series: ${seriesId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for cardBuilder
|
||||
if (!window.cardBuilder || !window.cardBuilder.renderCards) {
|
||||
WARN('cardBuilder.renderCards not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the detailPageContent
|
||||
const detailPageContent = activePage?.querySelector('.detailPageContent');
|
||||
if (!detailPageContent) {
|
||||
WARN('detailPageContent not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the #childrenCollapsible div
|
||||
const childrenCollapsible = detailPageContent.querySelector('#childrenCollapsible');
|
||||
if (!childrenCollapsible) {
|
||||
WARN('#childrenCollapsible not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the season ID from the first episode's ParentId
|
||||
const seasonId = episodes[0]?.SeasonId;
|
||||
let viewMoreUrl = null;
|
||||
|
||||
if (seasonId) {
|
||||
const apiClient = window.ApiClient;
|
||||
const serverId = apiClient.serverId();
|
||||
viewMoreUrl = `${apiClient._serverAddress || apiClient.serverAddress()}/web/#/details?id=${seasonId}&serverId=${serverId}`;
|
||||
LOG(`Setting viewMoreUrl to season details: ${viewMoreUrl}`);
|
||||
} else {
|
||||
WARN('Could not determine season ID from episodes');
|
||||
}
|
||||
|
||||
// Check if Next Up section exists
|
||||
const nextUpSection = activePage?.querySelector('.nextUpSection');
|
||||
const nextUpItemsContainer = nextUpSection?.querySelector('.itemsContainer');
|
||||
|
||||
// Extract episode info from Next Up if it exists
|
||||
let targetEpisodeNumber = null;
|
||||
if (nextUpSection && nextUpItemsContainer) {
|
||||
LOG('Next Up section found, hiding nextUpSection');
|
||||
|
||||
// Get the existing card from Next Up and extract episode info
|
||||
const existingCard = nextUpItemsContainer.querySelector('.card');
|
||||
if (existingCard) {
|
||||
// Find the .cardText a element that contains the episode number
|
||||
const cardTextLinks = existingCard.querySelectorAll('.cardText a');
|
||||
for (const link of cardTextLinks) {
|
||||
const linkText = link.innerText || link.textContent;
|
||||
// Match SX:EY pattern (may be part of longer text like "S1:E5 - Episode Name")
|
||||
const match = linkText.match(/S(\d+):E(\d+)/);
|
||||
if (match) {
|
||||
targetEpisodeNumber = {
|
||||
season: parseInt(match[1], 10),
|
||||
episode: parseInt(match[2], 10)
|
||||
};
|
||||
LOG(`Found Next Up episode from DOM: S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find it from the DOM, fetch from API
|
||||
if (!targetEpisodeNumber) {
|
||||
LOG('Could not find episode number from DOM, fetching from API');
|
||||
targetEpisodeNumber = await fetchNextUpEpisode(seriesId);
|
||||
}
|
||||
|
||||
// Hide the entire Next Up section
|
||||
nextUpSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Render Season 1 section using renderCards (default behavior)
|
||||
const seasonSection = window.cardBuilder.renderCards(
|
||||
episodes,
|
||||
'Season 1',
|
||||
viewMoreUrl,
|
||||
true, // overflowCard
|
||||
null // cardFormat (use default)
|
||||
);
|
||||
|
||||
// Mark as flattened section
|
||||
seasonSection.classList.add('flattened-season-section');
|
||||
seasonSection.setAttribute('data-flattened-section', 'true');
|
||||
|
||||
const scrollerContainer = seasonSection.querySelector('.emby-scroller');
|
||||
if (scrollerContainer) {
|
||||
scrollerContainer.classList.add('no-padding');
|
||||
}
|
||||
|
||||
const sectionTitleContainer = seasonSection.querySelector('.sectionTitleContainer');
|
||||
if (sectionTitleContainer) {
|
||||
sectionTitleContainer.classList.add('no-padding');
|
||||
}
|
||||
|
||||
// Prepend the Season 1 section right before #childrenCollapsible
|
||||
childrenCollapsible.parentNode.insertBefore(seasonSection, childrenCollapsible);
|
||||
|
||||
// Check if childrenCollapsible has only one child in its itemsContainer, and hide it if so
|
||||
const childrenItemsContainer = childrenCollapsible.querySelector('.itemsContainer');
|
||||
if (childrenItemsContainer) {
|
||||
LOG('childrenCollapsible contains only one child, hiding it');
|
||||
childrenCollapsible.style.display = 'none';
|
||||
}
|
||||
|
||||
// If we found a target episode from Next Up, scroll to it
|
||||
if (targetEpisodeNumber && scrollerContainer) {
|
||||
// Wait for cards to be rendered and positioned
|
||||
setTimeout(() => {
|
||||
const scrollerItemsContainer = scrollerContainer.querySelector('.itemsContainer');
|
||||
if (!scrollerItemsContainer) {
|
||||
WARN('Could not find itemsContainer in scroller');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the card that matches the target episode number
|
||||
const cards = scrollerItemsContainer.querySelectorAll('.card');
|
||||
let targetCard = null;
|
||||
|
||||
for (const card of cards) {
|
||||
const cardTextLinks = card.querySelectorAll('.cardText a');
|
||||
for (const link of cardTextLinks) {
|
||||
const linkText = link.innerText || link.textContent;
|
||||
const match = linkText.match(/S(\d+):E(\d+)/);
|
||||
if (match &&
|
||||
parseInt(match[1], 10) === targetEpisodeNumber.season &&
|
||||
parseInt(match[2], 10) === targetEpisodeNumber.episode) {
|
||||
targetCard = card;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetCard) break;
|
||||
}
|
||||
|
||||
// Scroll to make the target card the leftmost visible element
|
||||
if (targetCard) {
|
||||
const itemsContainer = scrollerItemsContainer;
|
||||
const cardOffset = targetCard.offsetLeft;
|
||||
const scrollerPadding = parseInt(window.getComputedStyle(scrollerContainer).paddingLeft, 10) || 0;
|
||||
|
||||
// Calculate the scrollToPosition value
|
||||
// The card offset minus padding gives us how much to scroll
|
||||
const translateX = (cardOffset - scrollerPadding);
|
||||
scrollerContainer.scrollToPosition(translateX);
|
||||
|
||||
// Enable the left scroll button so users can scroll back
|
||||
const scrollButtons = scrollerContainer.closest('.emby-scroller-container')?.querySelector('.emby-scrollbuttons');
|
||||
if (scrollButtons) {
|
||||
const leftButton = scrollButtons.querySelector('button[data-direction="left"]');
|
||||
if (leftButton) {
|
||||
leftButton.removeAttribute('disabled');
|
||||
LOG('Enabled left scroll button');
|
||||
}
|
||||
}
|
||||
|
||||
LOG(`Scrolled to target episode S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode} using translateX(${translateX}px)`);
|
||||
} else {
|
||||
WARN(`Could not find target episode card S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode}`);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Mark as flattened for this series to avoid duplicates
|
||||
if (activePage) {
|
||||
activePage.dataset.flattenedSeriesId = seriesId;
|
||||
}
|
||||
|
||||
LOG(`Successfully flattened show ${seriesId} with ${episodes.length} episodes`);
|
||||
} catch (error) {
|
||||
ERR('Error rendering flattened season section:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the flatten single season shows hook
|
||||
*/
|
||||
function initializeFlattenHook() {
|
||||
if (!window.KefinTweaksUtils || !window.KefinTweaksUtils.onViewPage) {
|
||||
WARN('KefinTweaksUtils.onViewPage not available, retrying in 1 second');
|
||||
setTimeout(initializeFlattenHook, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG('Registering flatten single season shows handler with KefinTweaksUtils');
|
||||
|
||||
window.KefinTweaksUtils.onViewPage(
|
||||
async (view, element, hash, itemPromise) => {
|
||||
// Only handle details pages
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!activePage) return;
|
||||
|
||||
// Remove any existing flattened section and reset flag when page changes
|
||||
const existingSection = activePage.querySelector('.flattened-season-section');
|
||||
if (existingSection) {
|
||||
LOG('Flattened section already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Await the item promise to get the actual item data
|
||||
const item = await itemPromise;
|
||||
|
||||
// Check if item is a Series and has only 1 ChildCount
|
||||
if (item && item.Type === 'Series' && item.ChildCount === 1) {
|
||||
LOG(`Found single-season series: ${item.Id} (${item.Name})`);
|
||||
|
||||
// Small delay to ensure details DOM is ready
|
||||
setTimeout(async () => {
|
||||
await flattenShow(item.Id);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{
|
||||
pages: ['details']
|
||||
}
|
||||
);
|
||||
|
||||
LOG('Flatten single season shows hook initialized');
|
||||
}
|
||||
|
||||
// Initialize the hook when the script loads
|
||||
function initialize() {
|
||||
if (!window.KefinTweaksUtils || !window.KefinTweaksUtils.onViewPage) {
|
||||
WARN('KefinTweaksUtils not available, retrying in 1 second');
|
||||
setTimeout(initialize, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
initializeFlattenHook();
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
|
||||
LOG('Script loaded successfully');
|
||||
})();
|
||||
|
||||
Reference in new issue
Block a user