diff --git a/classes/mediaservers/embyJellyfinBase.js b/classes/mediaservers/embyJellyfinBase.js index 9649609..e2f30e8 100644 --- a/classes/mediaservers/embyJellyfinBase.js +++ b/classes/mediaservers/embyJellyfinBase.js @@ -2644,6 +2644,7 @@ class EmbyJellyfinBase { ); } } else if (recentlyAdded > 0) { + // Recently-added only when numberOnDemand is 0; both modes when numberOnDemand > 0. odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, @@ -2653,9 +2654,22 @@ class EmbyJellyfinBase { false, false ); - if (odRaw !== undefined) { - odRaw = odRaw.concat( - await this.GetOnDemandRawData( + const alsoRandomOd = Number(numberOnDemand) > 0; + if (alsoRandomOd) { + if (odRaw !== undefined) { + odRaw = odRaw.concat( + await this.GetOnDemandRawData( + onDemandLibraries, + numberOnDemand, + genres, + 0, + contentRatings, + false, + false + ) + ); + } else { + odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, genres, @@ -2663,18 +2677,10 @@ class EmbyJellyfinBase { contentRatings, false, false - ) - ); - } else { - odRaw = await this.GetOnDemandRawData( - onDemandLibraries, - numberOnDemand, - genres, - 0, - contentRatings, - false, - false - ); + ); + } + } else if (odRaw === undefined) { + odRaw = []; } } else { odRaw = await this.GetOnDemandRawData( diff --git a/classes/mediaservers/kodi.js b/classes/mediaservers/kodi.js index 30af55f..ae8f033 100644 --- a/classes/mediaservers/kodi.js +++ b/classes/mediaservers/kodi.js @@ -792,6 +792,7 @@ class Kodi { sp ); } else if (recentlyAdded > 0) { + // Recently-added only when numberOnDemand is 0; both modes when numberOnDemand > 0. odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, @@ -800,26 +801,31 @@ class Kodi { contentRatings, false ); - if (odRaw !== undefined) { - odRaw = odRaw.concat( - await this.GetOnDemandRawData( + const alsoRandomOd = Number(numberOnDemand) > 0; + if (alsoRandomOd) { + if (odRaw !== undefined) { + odRaw = odRaw.concat( + await this.GetOnDemandRawData( + onDemandLibraries, + numberOnDemand, + genres, + 0, + contentRatings, + false + ) + ); + } else { + odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, genres, 0, contentRatings, false - ) - ); - } else { - odRaw = await this.GetOnDemandRawData( - onDemandLibraries, - numberOnDemand, - genres, - 0, - contentRatings, - false - ); + ); + } + } else if (odRaw === undefined) { + odRaw = []; } } else { odRaw = await this.GetOnDemandRawData( diff --git a/classes/mediaservers/plex.js b/classes/mediaservers/plex.js index 798f995..4f3dedb 100644 --- a/classes/mediaservers/plex.js +++ b/classes/mediaservers/plex.js @@ -716,6 +716,7 @@ class Plex { sp ); } else if (recentlyAdded > 0) { + // Recently-added only when numberOnDemand is 0; both modes when numberOnDemand > 0. odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, @@ -724,26 +725,31 @@ class Plex { contentRatings, false ); - if (odRaw !== undefined) { - odRaw = odRaw.concat( - await this.GetOnDemandRawData( + const alsoRandomOd = Number(numberOnDemand) > 0; + if (alsoRandomOd) { + if (odRaw !== undefined) { + odRaw = odRaw.concat( + await this.GetOnDemandRawData( + onDemandLibraries, + numberOnDemand, + genres, + 0, + contentRatings, + false + ) + ); + } else { + odRaw = await this.GetOnDemandRawData( onDemandLibraries, numberOnDemand, genres, 0, contentRatings, false - ) - ); - } else { - odRaw = await this.GetOnDemandRawData( - onDemandLibraries, - numberOnDemand, - genres, - 0, - contentRatings, - false - ); + ); + } + } else if (odRaw === undefined) { + odRaw = []; } } else { odRaw = await this.GetOnDemandRawData( diff --git a/index.js b/index.js index 1ed3cdb..b76e22a 100644 --- a/index.js +++ b/index.js @@ -894,6 +894,35 @@ function preferCachedPostersEnabled() { return s !== "false" && s !== "0" && s !== "off" && s !== "no"; } +/** True when on-demand should use live “recently added in n days” filtering (cache cannot apply that filter). */ +function recentlyAddedDaysActive() { + if (!loadedSettings) return false; + const n = parseInt(loadedSettings.recentlyAddedDays, 10); + return Number.isFinite(n) && n > 0; +} + +/** + * Spread feature cards (Coming Soon, pictures, trivia, …) through a large library deck + * so they are not buried after dozens of on-demand/cache slides. + */ +function interleaveFeatureCardsThroughLibrary(libraryCards, featureCards) { + const lib = Array.isArray(libraryCards) ? libraryCards.slice() : []; + const feat = Array.isArray(featureCards) ? featureCards.slice() : []; + if (!feat.length) return lib; + if (!lib.length) return feat; + const out = []; + const step = Math.max(1, Math.ceil(lib.length / (feat.length + 1))); + let fi = 0; + for (let i = 0; i < lib.length; i++) { + out.push(lib[i]); + if ((i + 1) % step === 0 && fi < feat.length) { + out.push(feat[fi++]); + } + } + while (fi < feat.length) out.push(feat[fi++]); + return out; +} + /** True when poster metadata DB has at least one row (sync may have run while server was up). */ function cachedPosterDbHasRows() { try { @@ -1393,7 +1422,8 @@ async function buildTmdbNowShowingListCards() { function buildLibrarySlideDeckFromPosterCache() { // Master ON-DEMAND toggle must gate both live fetches and cache-backed library slides. if (!isOnDemandEnabled) return []; - if (!preferCachedPostersEnabled()) return odCards; + // Recently-added day filter requires a live media-server query; the poster cache has no added dates. + if (recentlyAddedDaysActive() || !preferCachedPostersEnabled()) return odCards; const kind = loadedSettings ? getMediaServerKind(loadedSettings.mediaServerType) : ""; @@ -1411,6 +1441,7 @@ function buildLibrarySlideDeckFromPosterCache() { */ async function warmCachedPosterDeckEarlyIfPossible() { if (!loadedSettings || !isOnDemandEnabled || !preferCachedPostersEnabled()) return; + if (recentlyAddedDaysActive()) return; if (!cachedPosterDbHasRows()) return; const kind = getMediaServerKind(loadedSettings.mediaServerType); const warmCount = Math.min(12, primaryCachedPosterSlideCount()); @@ -1745,18 +1776,22 @@ async function loadNowScreening() { } if (loadedSettings.pinNS !== "true") { + const featureCards = csCards + .concat(csrCards) + .concat(cslCards) + .concat(picCards) + .concat(csbCards) + .concat(trivCards) + .concat(linkCards); if (loadedSettings.shuffleSlides !== undefined && loadedSettings.shuffleSlides == "true") { - mCards = nsCards.concat(librarySlideCards.concat(csCards.concat(csrCards).concat(cslCards).concat(picCards).concat(linkCards).concat(csbCards).concat(trivCards)).sort(() => Math.random() - 0.5)); + mCards = nsCards.concat( + librarySlideCards.concat(featureCards).sort(() => Math.random() - 0.5) + ); } else { - mCards = nsCards.concat(librarySlideCards); - mCards = mCards.concat(picCards); - mCards = mCards.concat(csCards); - mCards = mCards.concat(csrCards); - mCards = mCards.concat(cslCards); - mCards = mCards.concat(csbCards); - mCards = mCards.concat(trivCards); - mCards = mCards.concat(linkCards); + mCards = nsCards.concat( + interleaveFeatureCardsThroughLibrary(librarySlideCards, featureCards) + ); } pinnedMode = false; } @@ -1785,17 +1820,18 @@ async function loadNowScreening() { pinnedMode = false; if (librarySlideCards.length > 0) { + const featureCards = csCards + .concat(csrCards) + .concat(cslCards) + .concat(picCards) + .concat(csbCards) + .concat(trivCards) + .concat(linkCards); if (loadedSettings.shuffleSlides !== undefined && loadedSettings.shuffleSlides == "true") { - mCards = librarySlideCards.concat(csCards.concat(csrCards).concat(cslCards).concat(picCards).concat(csbCards).concat(linkCards).concat(trivCards)).sort(() => Math.random() - 0.5); + mCards = librarySlideCards.concat(featureCards).sort(() => Math.random() - 0.5); } else { - mCards = librarySlideCards.concat(csCards); - mCards = mCards.concat(picCards); - mCards = mCards.concat(csrCards); - mCards = mCards.concat(cslCards); - mCards = mCards.concat(csbCards); - mCards = mCards.concat(trivCards); - mCards = mCards.concat(linkCards); + mCards = interleaveFeatureCardsThroughLibrary(librarySlideCards, featureCards); } globalPage.cards = mCards; } else { @@ -2018,7 +2054,8 @@ async function fetchOnDemandCardsFromServer(numberOnDemandOverride) { ? numberOnDemandOverride : loadedSettings.numberOnDemand; - if (preferCachedPostersEnabled()) { + // Prefer-cache skips live GetOnDemand — but recently-added days need a live filter. + if (preferCachedPostersEnabled() && !recentlyAddedDaysActive()) { const kind = loadedSettings ? getMediaServerKind(loadedSettings.mediaServerType) : ""; @@ -2132,7 +2169,9 @@ async function loadOnDemand() { // Changing timings if media server unavailable (live OD only; cache-backed OD ignores this). let odCheckMinutes = loadedSettings.onDemandRefresh; - if (!preferCachedPostersEnabled() && isMediaServerUnavailable) { + const needsLiveServer = + !preferCachedPostersEnabled() || recentlyAddedDaysActive(); + if (needsLiveServer && isMediaServerUnavailable) { odCheckMinutes = 1; console.log("✘✘ WARNING ✘✘ - Next on-demand query will run in 1 minute."); // restart interval timer diff --git a/myviews/settings.ejs b/myviews/settings.ejs index 877137d..b1f5ba1 100644 --- a/myviews/settings.ejs +++ b/myviews/settings.ejs @@ -776,7 +776,7 @@ <% if(typeof formData !== 'undefined' && errors){%><%=(formData.preferCachedPosters == 'true') ? 'checked' : '' %><%}else{%><%=(settings.preferCachedPosters == 'true') ? 'checked' : '' %><%}%>> - When on (default), slideshow library slides come from the poster cache and metadata database. Plex/Jellyfin/Emby/Kodi on-demand is only used if the cache is empty, and still runs in the background to fill the cache. Now Playing slides are unchanged. + When on (default), slideshow library slides come from the poster cache and metadata database. Automatically off for filtering when Recently Added In Last n Days is set (cache has no added dates). Plex/Jellyfin/Emby/Kodi on-demand is used if the cache is empty. Now Playing slides are unchanged. Coming Soon / pictures are interleaved through large library decks so they are not buried at the end.
@@ -1032,7 +1032,7 @@ <%=settings.recentlyAddedDays%>"<%}%>> Show recently added titles from - each library / media folder / Kodi video source. 'Content rating' and 'Genre' are ignored if this is set to a non-zero value. + each library / media folder / Kodi video source. Set to a non-zero value to use a live server query (not the full poster cache). With Number to Display = 0, only recently added titles are shown; with a number above 0, those titles are shown plus that many random on-demand titles. 'Content rating' and 'Genre' are ignored if this is set to a non-zero value.
diff --git a/test/deckInterleave.test.js b/test/deckInterleave.test.js new file mode 100644 index 0000000..552ad6a --- /dev/null +++ b/test/deckInterleave.test.js @@ -0,0 +1,43 @@ +/** + * Mirror of index.js interleaveFeatureCardsThroughLibrary for unit testing. + */ +function interleaveFeatureCardsThroughLibrary(libraryCards, featureCards) { + const lib = Array.isArray(libraryCards) ? libraryCards.slice() : []; + const feat = Array.isArray(featureCards) ? featureCards.slice() : []; + if (!feat.length) return lib; + if (!lib.length) return feat; + const out = []; + const step = Math.max(1, Math.ceil(lib.length / (feat.length + 1))); + let fi = 0; + for (let i = 0; i < lib.length; i++) { + out.push(lib[i]); + if ((i + 1) % step === 0 && fi < feat.length) { + out.push(feat[fi++]); + } + } + while (fi < feat.length) out.push(feat[fi++]); + return out; +} + +describe("interleaveFeatureCardsThroughLibrary", () => { + test("keeps Coming Soon from sitting only at the end of a large library deck", () => { + const lib = Array.from({ length: 12 }, (_, i) => "L" + i); + const feat = ["CS1", "CS2", "CS3"]; + const out = interleaveFeatureCardsThroughLibrary(lib, feat); + expect(out).toHaveLength(15); + expect(out.filter((x) => String(x).startsWith("CS"))).toEqual([ + "CS1", + "CS2", + "CS3", + ]); + // First feature card should appear before the last library card + expect(out.indexOf("CS1")).toBeLessThan(out.indexOf("L11")); + }); + + test("returns features alone when library is empty", () => { + expect(interleaveFeatureCardsThroughLibrary([], ["A", "B"])).toEqual([ + "A", + "B", + ]); + }); +});