Merge pull request #113 from MrRussLuther/prefer-main-trailer

Pick the main trailer instead of RemoteTrailers[0]
This commit is contained in:
M0RPH3US authored and GitHub committed 2026-08-06 20:11:51 +05:30
commit b6a113ba45
1 file changed
+83 -9
+83 -9
View File
@@ -22,6 +22,20 @@ 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 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",
"audio description",
"audio described",
"described audio",
"vertical",
],
youtubeApiLoadTimeoutMs: 8000,
};
@@ -1198,6 +1212,73 @@ 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. 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
*/
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 - 0.5 : rank;
};
let best = null;
for (const trailer of remoteTrailers) {
let videoId = null;
try {
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;
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 +1302,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", {