From 5169df24c4b734005e77ed645db6a440cb60ed2f Mon Sep 17 00:00:00 2001 From: binarygeek119 <72405030+binarygeek119@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:00:18 -0500 Subject: [PATCH 1/2] Fix on-demand toggle and empty Sonarr/Radarr Coming Soon decks. Gate cache-backed library slides on enableOD; treat premiere flag as a string; include Radarr titles with any release date; clear Sonarr cards on save. Co-authored-by: Cursor --- classes/arr/radarr.js | 32 +++++++++++++++++++++----------- classes/arr/sonarr.js | 12 ++++++------ index.js | 6 ++++-- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/classes/arr/radarr.js b/classes/arr/radarr.js index 03a5a70..07a1e66 100644 --- a/classes/arr/radarr.js +++ b/classes/arr/radarr.js @@ -72,18 +72,27 @@ class Radarr { if (raw != null) { // move through results and populate media cards await raw.data.reduce(async (memo, md) => { - let noReleaseDate = false; await memo; const medCard = new mediaCard(); - let releaseDate; - if(!await util.isEmpty(md.digitalRelease)){ - let digitalRelease = new Date(md.digitalRelease); - releaseDate = digitalRelease.toISOString().split("T")[0]; - noReleaseDate = true; + // Prefer digital, then physical, then theatrical — calendar already scoped the window. + let releaseDate = "No release date"; + let hasReleaseDate = false; + let releaseCandidate = null; + if (!(await util.isEmpty(md.digitalRelease))) { + releaseCandidate = md.digitalRelease; + } else if (!(await util.isEmpty(md.physicalRelease))) { + releaseCandidate = md.physicalRelease; + } else if (!(await util.isEmpty(md.inCinemas))) { + releaseCandidate = md.inCinemas; } - else { - releaseDate = "No digital release date"; - noReleaseDate = false; + if (releaseCandidate) { + try { + releaseDate = new Date(releaseCandidate).toISOString().split("T")[0]; + hasReleaseDate = true; + } catch (e) { + releaseDate = String(releaseCandidate); + hasReleaseDate = true; + } } medCard.tagLine = md.title + " (" + releaseDate + ")"; @@ -199,8 +208,9 @@ class Radarr { // if(medCard.theme.includes("undefined")) medCard.theme=""; // } - // add media card to array, only if not released yet (caters to old movies being released digitally) - if (md.hasFile == false && md.status != "released" && noReleaseDate != false){ //&& !await util.isEmpty(md.digitalRelease) ) { + // Include upcoming movies not yet downloaded. Require some release date + // (digital/physical/theatrical) so taglines are useful; do not require digital-only. + if (md.hasFile == false && hasReleaseDate) { csrCards.push(medCard); } diff --git a/classes/arr/sonarr.js b/classes/arr/sonarr.js index e3fdd44..4538324 100644 --- a/classes/arr/sonarr.js +++ b/classes/arr/sonarr.js @@ -253,13 +253,13 @@ class Sonarr { medCard.posterAR = 1.47; - // add media card to array (taking into account premieres option) - if (md.hasFile == false && premieres && md.episodeNumber == 1) { + // add media card to array (taking into account premieres option). + // premieres is a string ("true"/"false") — must compare explicitly; "false" is truthy in JS. + const premieresOnly = premieres == "true"; + if (md.hasFile == false && premieresOnly && md.episodeNumber == 1) { + csCards.push(medCard); + } else if (md.hasFile == false && !premieresOnly) { csCards.push(medCard); - } else { - if (!premieres) { - csCards.push(medCard); - } } }, undefined); } diff --git a/index.js b/index.js index 21b8fa5..1ed3cdb 100644 --- a/index.js +++ b/index.js @@ -1391,6 +1391,8 @@ async function buildTmdbNowShowingListCards() { /** Library-style slides: cached poster DB first when enabled; live on-demand only as backup. */ function buildLibrarySlideDeckFromPosterCache() { + // Master ON-DEMAND toggle must gate both live fetches and cache-backed library slides. + if (!isOnDemandEnabled) return []; if (!preferCachedPostersEnabled()) return odCards; const kind = loadedSettings ? getMediaServerKind(loadedSettings.mediaServerType) @@ -1408,7 +1410,7 @@ function buildLibrarySlideDeckFromPosterCache() { * Now Playing / on-demand network work finishes (first paint on /posters). */ async function warmCachedPosterDeckEarlyIfPossible() { - if (!loadedSettings || !preferCachedPostersEnabled()) return; + if (!loadedSettings || !isOnDemandEnabled || !preferCachedPostersEnabled()) return; if (!cachedPosterDbHasRows()) return; const kind = getMediaServerKind(loadedSettings.mediaServerType); const warmCount = Math.min(12, primaryCachedPosterSlideCount()); @@ -3261,7 +3263,7 @@ async function saveReset(formObject) { // clear cards nsCards = []; odCards = []; - csrCards = []; + csCards = []; csrCards = []; picCards = []; adSlideCards = []; From db89c149663d18657b37ad148ecf59d97a2504f0 Mon Sep 17 00:00:00 2001 From: binarygeek119 <72405030+binarygeek119@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:07:03 -0500 Subject: [PATCH 2/2] Fix Emby/Jellyfin Filter by user(s) bypassing to all sessions. Stop returning unfiltered fallback cards when include filters exclude everyone; harden username matching and document exact Emby user names. Co-authored-by: Cursor --- classes/mediaservers/embyJellyfinBase.js | 82 ++++++++++++++++++------ myviews/settings.ejs | 2 +- test/embyUserFilter.test.js | 34 ++++++++++ 3 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 test/embyUserFilter.test.js diff --git a/classes/mediaservers/embyJellyfinBase.js b/classes/mediaservers/embyJellyfinBase.js index 5597d29..9649609 100644 --- a/classes/mediaservers/embyJellyfinBase.js +++ b/classes/mediaservers/embyJellyfinBase.js @@ -862,6 +862,45 @@ class EmbyJellyfinBase { }); } + /** Comma-separated include list → trimmed lowercase tokens (allows spaces inside names). */ + static parseIncludeFilterList(raw) { + return String(raw || "") + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter(Boolean); + } + + /** + * Emby/Jellyfin session display names for user include-filter matching. + * Prefer UserName; also match UserId (GUID) if pasted into the filter field. + */ + static sessionUserMatchKeys(session) { + const keys = []; + const push = (v) => { + const s = String(v || "") + .trim() + .toLowerCase(); + if (s && !keys.includes(s)) keys.push(s); + }; + push(session && (session.UserName || session.userName)); + push(session && (session.UserId || session.userId)); + const extra = (session && (session.AdditionalUsers || session.additionalUsers)) || []; + if (Array.isArray(extra)) { + for (const u of extra) { + push(u && (u.UserName || u.userName)); + push(u && (u.UserId || u.userId)); + } + } + return keys; + } + + static sessionMatchesUserFilter(session, wantedUsers) { + if (!wantedUsers || wantedUsers.length === 0) return true; + const keys = EmbyJellyfinBase.sessionUserMatchKeys(session); + if (keys.length === 0) return false; + return wantedUsers.some((want) => keys.includes(want)); + } + static pickStreams(item) { const ms = item.MediaSources && item.MediaSources[0]; if (!ms || !ms.MediaStreams) return { resCodec: "", audioCodec: "" }; @@ -1017,20 +1056,8 @@ class EmbyJellyfinBase { return nsCards; } - const devices = (filterDevices || "") - .toLowerCase() - .replace(/, /g, ",") - .replace(/ ,/g, ",") - .replace(/,+$/, "") - .split(",") - .filter(Boolean); - const users = (filterUsers || "") - .toLowerCase() - .replace(/, /g, ",") - .replace(/ ,/g, ",") - .replace(/,+$/, "") - .split(",") - .filter(Boolean); + const devices = EmbyJellyfinBase.parseIncludeFilterList(filterDevices); + const users = EmbyJellyfinBase.parseIncludeFilterList(filterUsers); const libNameCache = new Map(); let userIdForLibs = null; @@ -1385,11 +1412,12 @@ class EmbyJellyfinBase { if (wantRemote && medCard.playerLocal === false) okToAdd = true; if (wantLocal && medCard.playerLocal === true) okToAdd = true; } - if (users.length > 0 && users[0] !== "") { - const un = (session.UserName || session.userName || "").toLowerCase(); - if (!users.includes(un)) okToAdd = false; + if (users.length > 0) { + if (!EmbyJellyfinBase.sessionMatchesUserFilter(session, users)) { + okToAdd = false; + } } - if (devices.length > 0 && devices[0] !== "") { + if (devices.length > 0) { if (!EmbyJellyfinBase.sessionDeviceMatchesFilter(session, medCard.playerDevice, devices)) { okToAdd = false; } @@ -1466,13 +1494,27 @@ class EmbyJellyfinBase { } } + // Never bypass user/device include-filters with unfiltered sessions. That made + // "Filter by user(s)" look broken on Emby/Jellyfin (empty filtered set → show everyone). if (nsCards.length === 0 && fallbackCards.length > 0) { + const hasIncludeFilter = users.length > 0 || devices.length > 0; + if (!hasIncludeFilter) { + const now = new Date(); + console.log( + now.toLocaleString() + + " *Now Scrn. - All Emby/Jellyfin sessions excluded by local/remote filter; using unfiltered fallback cards" + ); + return fallbackCards; + } const now = new Date(); console.log( now.toLocaleString() + - " *Now Scrn. - All Jellyfin sessions filtered; using fallback unfiltered session cards" + " *Now Scrn. - No Emby/Jellyfin sessions matched user/device filter (" + + (users.length ? "users=[" + users.join(",") + "]" : "") + + (users.length && devices.length ? " " : "") + + (devices.length ? "devices=[" + devices.join(",") + "]" : "") + + "); showing none" ); - return fallbackCards; } return nsCards; diff --git a/myviews/settings.ejs b/myviews/settings.ejs index a5f9a96..877137d 100644 --- a/myviews/settings.ejs +++ b/myviews/settings.ejs @@ -947,7 +947,7 @@ <%=settings.filterUsers%>"<%}%>> - Enter user names to include (Comma seperated and no spaces. Leave blank for all users) + Include only these Emby/Jellyfin/Plex usernames (comma-separated). Use the exact name from the server’s user list (spaces allowed). Leave blank for all users. When set, other household accounts are hidden even if they are the only ones playing.
diff --git a/test/embyUserFilter.test.js b/test/embyUserFilter.test.js new file mode 100644 index 0000000..e9b9071 --- /dev/null +++ b/test/embyUserFilter.test.js @@ -0,0 +1,34 @@ +const EmbyJellyfinBase = require("../classes/mediaservers/embyJellyfinBase"); + +describe("Emby/Jellyfin now-playing user filter helpers", () => { + test("parseIncludeFilterList trims and allows spaces in names", () => { + expect( + EmbyJellyfinBase.parseIncludeFilterList(" Matt , Mother In Law ,fred") + ).toEqual(["matt", "mother in law", "fred"]); + }); + + test("sessionMatchesUserFilter matches UserName case-insensitively", () => { + const session = { UserName: "Matt", UserId: "abc-123" }; + expect( + EmbyJellyfinBase.sessionMatchesUserFilter(session, ["matt"]) + ).toBe(true); + expect( + EmbyJellyfinBase.sessionMatchesUserFilter(session, ["mother"]) + ).toBe(false); + }); + + test("sessionMatchesUserFilter can match UserId GUID", () => { + const session = { UserName: "Matt", UserId: "a490a918d5234bc990088cfee3c4e245" }; + expect( + EmbyJellyfinBase.sessionMatchesUserFilter(session, [ + "a490a918d5234bc990088cfee3c4e245", + ]) + ).toBe(true); + }); + + test("empty filter list allows all sessions", () => { + expect( + EmbyJellyfinBase.sessionMatchesUserFilter({ UserName: "Anyone" }, []) + ).toBe(true); + }); +});