Don't load next up from server if we can infer it from the DOM

This commit is contained in:
HighImKevin
2026-10-01 06:50:00 -04:00
parent c0c980622c
commit 81ef36574b
2 changed files with 73 additions and 12 deletions
+2 -1
View File
@@ -7,9 +7,10 @@
padding-left: 0;
}
.detailSection:has(.series-episodes-section) {
#itemDetailPage[data-series-episodes-item-id] .detailSection {
overflow: hidden;
}
.series-episodes-section {
padding-left: 0 !important;
margin: 0 !important;
+71 -11
View File
@@ -337,6 +337,54 @@
return host;
}
/**
* Infer Next Up episode from the native .nextUpSection card title (S#:E#).
* @param {HTMLElement} activePage
* @returns {Object|null}
*/
function inferNextUpFromDom(activePage) {
if (!activePage) return null;
const card = activePage.querySelector('.nextUpSection .itemsContainer .card');
if (!card) return null;
const titleLink = card.querySelector('.cardText-first a[title]');
if (!titleLink) return null;
const title = titleLink.getAttribute('title') || '';
const match = title.match(/S(\d+)\s*:\s*E(\d+)/i);
if (!match) return null;
const season = parseInt(match[1], 10);
const episode = parseInt(match[2], 10);
if (!Number.isFinite(season) || !Number.isFinite(episode)) return null;
let id = null;
const href = titleLink.getAttribute('href') || '';
const idMatch = href.match(/[?&]id=([^&]+)/i);
if (idMatch) {
try {
id = decodeURIComponent(idMatch[1]);
} catch (_) {
id = idMatch[1];
}
}
if (!id) {
id = card.getAttribute('data-id') || card.dataset?.id || null;
}
const nameRemainder = title.replace(/S\d+\s*:\s*E\d+\s*-?\s*/i, '').trim();
LOG(`Inferred Next Up from DOM: S${season}:E${episode}${id ? ` (${id})` : ''}`);
return {
ParentIndexNumber: season,
IndexNumber: episode,
Id: id,
SeasonId: null,
Name: nameRemainder || title
};
}
/**
* Fetches the Next Up episode for a given series
* @param {string} seriesId - The series ID
@@ -507,14 +555,24 @@
}
function performEpisodeScroll(scrollerContainer, targetCard, targetEpisodeNumber) {
const scrollerPadding = parseInt(window.getComputedStyle(scrollerContainer).paddingLeft, 10) || 0;
const translateX = targetCard.offsetLeft - scrollerPadding;
window.cardBuilder.setScrollerPosition(scrollerContainer, translateX, false);
const itemsContainer = scrollerContainer.querySelector('.itemsContainer') || scrollerContainer;
const cards = Array.from(itemsContainer.querySelectorAll(
':scope > .card:not(.card-layout-dummy):not(.skeleton-card)'
));
const index = cards.indexOf(targetCard);
if (index < 0) {
WARN(`Target episode card not in scroller card list S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode}`);
return;
}
if (window.cardBuilder?.scrollToCardIndex) {
window.cardBuilder.scrollToCardIndex(scrollerContainer, index, { animate: false });
}
const scrollButtons = scrollerContainer.closest('.emby-scroller-container')?.querySelector('.emby-scrollbuttons');
if (scrollButtons) {
const leftButton = scrollButtons.querySelector('button[data-direction="left"]');
if (leftButton) {
if (leftButton && index > 0) {
leftButton.removeAttribute('disabled');
}
}
@@ -748,8 +806,12 @@
return;
}
// Get NextUp episode
let targetEpisode = await fetchNextUpEpisode(seriesId);
// Prefer Next Up from native DOM (already painted); API only if missing
const pageForUi = document.querySelector('.libraryPage:not(.hide)') || activePage;
let targetEpisode = inferNextUpFromDom(pageForUi);
if (!targetEpisode) {
targetEpisode = await fetchNextUpEpisode(seriesId);
}
let targetEpisodeNumber = null;
// Determine target season
@@ -788,7 +850,6 @@
// Extract NextUp header text
let nextUpHeaderText = 'Next Up';
const pageForUi = document.querySelector('.libraryPage:not(.hide)') || activePage;
const nextUpSection = pageForUi.querySelector('.nextUpSection');
if (nextUpSection) {
const nextUpHeader = nextUpSection.querySelector('.sectionTitle, h2, h3');
@@ -801,6 +862,8 @@
// Render episodes section
await renderEpisodesSection(seriesId, seasons, targetEpisode, nextUpHeaderText);
const childrenCollapsible = activePage.querySelector('.detailSection #listChildrenCollapsible');
// Hide season container if option is enabled and there's only one season
if (hideSingleSeasonContainer && isSingleSeason && childrenCollapsible) {
childrenCollapsible.style.display = 'none';
@@ -894,10 +957,7 @@
LOG(`Found series: ${item.Id} (${item.Name})`);
// Small delay to ensure page is ready (Emby may still be swapping pages)
setTimeout(async () => {
await processSeriesPage(item);
}, 100);
await processSeriesPage(item);
},
{
pages: ['details']