Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions classes/mediaservers/embyJellyfinBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -2653,28 +2654,33 @@ 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,
0,
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(
Expand Down
34 changes: 20 additions & 14 deletions classes/mediaservers/kodi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
34 changes: 20 additions & 14 deletions classes/mediaservers/plex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
79 changes: 59 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
: "";
Expand All @@ -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());
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
: "";
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions myviews/settings.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@
<% if(typeof formData !== 'undefined' && errors){%><%=(formData.preferCachedPosters == 'true') ? 'checked' : '' %><%}else{%><%=(settings.preferCachedPosters == 'true') ? 'checked' : '' %><%}%>>
<label class="custom-control-label" for="preferCachedPosters">Prefer cached poster library</label>
</div>
<small class="form-text text-muted">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.</small>
<small class="form-text text-muted">When on (default), slideshow library slides come from the poster cache and metadata database. Automatically off for filtering when <strong>Recently Added In Last n Days</strong> 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.</small>
</div>
<div class="form-group">
<label for="cachedPosterSlideCount">Cached poster slide count</label>
Expand Down Expand Up @@ -1032,7 +1032,7 @@
<input type="text" class="form-control" id="recentlyAddedDays" name="recentlyAddedDays" aria-describedby="recentlyAddedDaysHelp"
placeholder="suggest no more than 45." value="<% if(typeof formData !== 'undefined' && errors){%><%=formData.recentlyAddedDays%>"<%}else{%><%=settings.recentlyAddedDays%>"<%}%>>
<small id="recentlyAddedDaysHelp" class="form-text text-muted">Show recently added titles from
each library / media folder / <strong>Kodi video source</strong>. <b>'Content rating'</b> and <b>'Genre' <u>are ignored</u> if this is set to a non-zero value.</b></small>
each library / media folder / <strong>Kodi video source</strong>. Set to a non-zero value to use a live server query (not the full poster cache). With <b>Number to Display = 0</b>, only recently added titles are shown; with a number above 0, those titles are shown <em>plus</em> that many random on-demand titles. <b>'Content rating'</b> and <b>'Genre' <u>are ignored</u> if this is set to a non-zero value.</b></small>
</div>
<div class="form-group">
<label for="numberOnDemand">Number to Display</label>
Expand Down
43 changes: 43 additions & 0 deletions test/deckInterleave.test.js
Original file line number Diff line number Diff line change
@@ -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",
]);
});
});
Loading