From 96b0d4b099121b38695863a41fb4b6015ab9f4e5 Mon Sep 17 00:00:00 2001 From: Russell Luther Date: Sat, 1 Aug 2026 00:30:33 -0400 Subject: [PATCH 1/2] Pick the main trailer instead of RemoteTrailers[0] RemoteTrailers arrives in metadata provider order, which is not preference order. Taking index 0 unconditionally means the bar regularly plays something other than the trailer: a promo clip, a teaser, or an accessibility variant. The clearest case is an American Sign Language cut. TMDb lists "Official American Sign Language Trailer" first for A Minecraft Movie, Mortal Kombat II and Superman, so those slides play a trailer with an interpreter composited into the corner, drawn over the backdrop. Index 0 is also not guaranteed to be a YouTube link. When it is not, searchParams.get("v") returns null and the slide loses its trailer even though a usable YouTube entry sits further down the list. Entries are now ranked by name, preferring official, final and main trailers over generic ones and teasers, with alternate cuts ranked below everything else rather than dropped so an item that has nothing else still plays something. Provider order breaks ties, so an item whose first entry is already the best keeps it. Entries that yield no video id are skipped instead of ending the search. Across a 384 item library this changes the pick for 105 items and loses the trailer for none. Avatar moves from "Back in Theatres" to "Special Edition Official Trailer", Oppenheimer from "Review" to "Official Trailer", Moana 2 from "Special Look" to "Official Trailer", and the three titles above stop playing the sign language cut. --- slideshowpure.js | 78 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/slideshowpure.js b/slideshowpure.js index b918f5c..07119b3 100644 --- a/slideshowpure.js +++ b/slideshowpure.js @@ -22,6 +22,19 @@ const CONFIG = { fadeTransitionDuration: 500, slideAnimationEnabled: true, enableTrailers: true, + // Name fragments identifying alternate cuts of a trailer: accessibility + // variants and social crops. Matched case insensitively against the + // RemoteTrailers entry name. These are ranked below every ordinary entry + // rather than removed, so an item whose only trailer is an alternate cut + // still plays something. + trailerAlternateCutTerms: [ + "sign language", + "asl trailer", + "audio description", + "audio described", + "described audio", + "vertical", + ], youtubeApiLoadTimeoutMs: 8000, }; @@ -1198,6 +1211,60 @@ const SlideCreator = { } }, + /** + * Picks the YouTube video id of the most appropriate trailer for an item. + * + * RemoteTrailers arrives in metadata provider order, which is not preference + * order. The first entry is frequently a promo clip, a teaser or an + * accessibility variant rather than the main trailer, and it is not + * guaranteed to be a YouTube link at all. Entries are ranked by name, with + * provider order breaking ties, and any entry that does not yield a video id + * is skipped instead of losing the trailer for that slide. + * + * @param {Array} remoteTrailers - RemoteTrailers array from the item + * @returns {string|null} YouTube video id, or null if no entry is usable + */ + selectTrailerVideoId(remoteTrailers) { + if (!Array.isArray(remoteTrailers) || remoteTrailers.length === 0) { + return null; + } + + const rankName = (name) => { + const text = (name || "").toLowerCase(); + const isAlternateCut = CONFIG.trailerAlternateCutTerms.some((term) => + text.includes(term), + ); + + let rank; + if (text.includes("official trailer")) rank = 5; + else if (text.includes("final trailer") || text.includes("main trailer")) + rank = 4; + else if (text.includes("trailer")) rank = 3; + else if (text.includes("teaser")) rank = 2; + else rank = 1; + + return isAlternateCut ? rank - 10 : rank; + }; + + let best = null; + + for (const trailer of remoteTrailers) { + let videoId = null; + try { + videoId = new URL(trailer.Url).searchParams.get("v"); + } catch (e) {} + + if (!videoId) continue; + + const rank = rankName(trailer.Name); + if (!best || rank > best.rank) { + best = { videoId, rank }; + } + } + + return best ? best.videoId : null; + }, + /** * Creates a slide element for an item * @param {Object} item - Item data @@ -1221,15 +1288,8 @@ const SlideCreator = { let videoId = null; let trailerContainer = null; - if ( - CONFIG.enableTrailers && - item.RemoteTrailers && - item.RemoteTrailers.length > 0 - ) { - try { - const urlObj = new URL(item.RemoteTrailers[0].Url); - videoId = urlObj.searchParams.get("v"); - } catch (e) {} + if (CONFIG.enableTrailers) { + videoId = this.selectTrailerVideoId(item.RemoteTrailers); if (videoId) { trailerContainer = SlideUtils.createElement("div", { From 049cd148d2a0ea8828bcc6e4b6d1b3ef14a74944 Mon Sep 17 00:00:00 2001 From: Russell Luther Date: Sat, 1 Aug 2026 01:16:33 -0400 Subject: [PATCH 2/2] Demote alternate cuts within tier and handle youtu.be and embed URLs A flat 10 point demotion ranked an accessibility variant of a real trailer below non-trailer junk like Behind the Scenes or Review. Subtract 0.5 instead, so an alternate cut loses to an ordinary entry of the same tier but still beats lower tier promo clips. Also parse youtu.be short links and youtube.com embed links by pathname, since neither carries a v query param. --- slideshowpure.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/slideshowpure.js b/slideshowpure.js index 07119b3..23c18e6 100644 --- a/slideshowpure.js +++ b/slideshowpure.js @@ -24,9 +24,10 @@ const CONFIG = { enableTrailers: true, // Name fragments identifying alternate cuts of a trailer: accessibility // variants and social crops. Matched case insensitively against the - // RemoteTrailers entry name. These are ranked below every ordinary entry - // rather than removed, so an item whose only trailer is an alternate cut - // still plays something. + // RemoteTrailers entry name. These are demoted within their tier, so an + // alternate cut loses to an ordinary entry of the same tier but still + // beats lower tier promo clips, and an item whose only trailer is an + // alternate cut still plays something. trailerAlternateCutTerms: [ "sign language", "asl trailer", @@ -1219,7 +1220,9 @@ const SlideCreator = { * accessibility variant rather than the main trailer, and it is not * guaranteed to be a YouTube link at all. Entries are ranked by name, with * provider order breaking ties, and any entry that does not yield a video id - * is skipped instead of losing the trailer for that slide. + * is skipped instead of losing the trailer for that slide. Alternate cuts + * are demoted within their tier: they lose to an ordinary entry of the same + * tier but still beat lower tier promo clips. * * @param {Array} remoteTrailers - RemoteTrailers array from the item * @returns {string|null} YouTube video id, or null if no entry is usable @@ -1243,7 +1246,7 @@ const SlideCreator = { else if (text.includes("teaser")) rank = 2; else rank = 1; - return isAlternateCut ? rank - 10 : rank; + return isAlternateCut ? rank - 0.5 : rank; }; let best = null; @@ -1251,7 +1254,18 @@ const SlideCreator = { for (const trailer of remoteTrailers) { let videoId = null; try { - videoId = new URL(trailer.Url).searchParams.get("v"); + const urlObj = new URL(trailer.Url); + const host = urlObj.hostname.replace(/^www\./, ""); + if (host === "youtu.be") { + videoId = urlObj.pathname.split("/")[1] || null; + } else if ( + (host === "youtube.com" || host === "m.youtube.com") && + urlObj.pathname.startsWith("/embed/") + ) { + videoId = urlObj.pathname.split("/")[2] || null; + } else { + videoId = urlObj.searchParams.get("v"); + } } catch (e) {} if (!videoId) continue;