mirror of
https://github.com/IAmParadox27/jellyfin-plugin-media-bar.git
synced 2026-11-13 16:54:19 +01:00
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.
This commit is contained in:
1 parent
073eac75e7
commit
96b0d4b099
1 file changed
+69
-9
+69
-9
@@ -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", {
|
||||
|
||||
Reference in new issue
Block a user