Skip to content
Closed
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
145 changes: 114 additions & 31 deletions addon/lib/getLogo.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,105 @@
require('dotenv').config();
const fanart = require('../utils/fanart');
const moviedb = require("./getTmdb");
const moviedb = require('./getTmdb');

const TARGET_ASPECT_RATIO = 4.0;

const SKIP_LOGO_ERROR_MESSAGES = new Set([
'TMDB_API_KEY_MISSING',
'TMDB_API_KEY_INVALID',
'TMDB API key is required.',
'TMDB API key not found in config or environment.',
]);

function shouldSkipLogoError(message) {
if (!message) return false;
if (SKIP_LOGO_ERROR_MESSAGES.has(message)) return true;
if (/invalid.*api.*key/i.test(message)) return true;
return false;
}

/**
* @param {object} tmdbLogo - Raw logo from TMDB images API.
* @returns {string} Normalized language key for matching (e.g. pt-BR, pt, en).
*/
function tmdbLogoLang(tmdbLogo) {
const iso = tmdbLogo.iso_639_1;
const region = tmdbLogo.iso_3166_1;
if (iso && region) return `${iso}-${region}`;
if (iso) return iso;
return 'en';
}

/**
* @param {Array} logos - A combined list of logo objects from all sources.
* @param {string} language - The user's selected language (e.g., 'pt-BR').
* @param {string} originalLanguage - The media's original language code (e.g., 'ja').
* @returns {object|undefined} The best-matched logo object.
* @param {Array} logos - Combined list from Fanart + TMDB (with source, scores metadata).
* @param {string} language - User language (e.g. pt-BR).
* @param {string} originalLanguage - Original audio language (e.g. ja).
* @returns {object|undefined} Best logo object.
*/
function pickLogo(logos, language, originalLanguage) {
if (!logos || logos.length === 0) return undefined;

const lang = language.split("-")[0]; // 'pt-BR' -> 'pt'
const fullLang = language;
const baseLang = (language || 'en').split('-')[0];

return (
logos.find(l => l.lang === lang) ||
logos.find(l => l.lang === originalLanguage) ||
logos.find(l => l.lang === "en") ||
logos[0]
);
const sortedLogos = logos
.map((logo) => {
let score = 0;
const logoLang = logo.lang || 'en';
if (logoLang === fullLang) {
score = 4;
} else if (logoLang.startsWith(`${baseLang}-`)) {
score = 3;
} else if (logoLang === baseLang) {
score = 2;
} else if (logoLang === 'en') {
score = 1;
} else if (logoLang === originalLanguage && logoLang !== 'en') {
score = 0.5;
}

let aspectRatioDiff = 999;
if (logo.source === 'tmdb' && logo.aspect_ratio != null) {
aspectRatioDiff = Math.abs(logo.aspect_ratio - TARGET_ASPECT_RATIO);
}

return {
...logo,
score,
fanartLikes: logo.source === 'fanart' ? parseInt(logo.likes, 10) || 0 : 0,
tmdbVotes: logo.source === 'tmdb' ? logo.vote_average || 0 : 0,
aspectRatioDiff,
};
})
.sort((a, b) => {
if (a.score !== b.score) {
return b.score - a.score;
}
if (a.source === 'tmdb' && b.source === 'tmdb') {
if (a.aspectRatioDiff !== b.aspectRatioDiff) {
return a.aspectRatioDiff - b.aspectRatioDiff;
}
return b.tmdbVotes - a.tmdbVotes;
}
if (a.source === 'fanart' && b.source === 'fanart') {
return b.fanartLikes - a.fanartLikes;
}
if (a.source === 'fanart' && b.source !== 'fanart') return -1;
if (a.source !== 'fanart' && b.source === 'fanart') return 1;

return 0;
});

return sortedLogos[0];
}

/**
* @param {'movie'|'series'} type - The type of media.
* @param {{tmdbId: string, tvdbId?: string}} ids - An object containing the necessary IDs.
* @param {string} language - The user's selected language.
* @param {string} originalLanguage - The media's original language.
* @returns {Promise<string>} The URL of the best logo, or an empty string.
* @param {'movie'|'series'} type - Media type.
* @param {{tmdbId: string, tvdbId?: string}} ids - IDs for Fanart/TMDB.
* @param {string} language - User language.
* @param {string} originalLanguage - Original language code.
* @param {object} config - Addon config (API keys, etc.).
* @returns {Promise<string>} Logo URL or empty string.
*/
async function getLogo(type, ids, language, originalLanguage, config) {
try {
Expand All @@ -41,41 +113,52 @@ async function getLogo(type, ids, language, originalLanguage, config) {
tmdbPromise = moviedb.movieImages({ id: tmdbId }, config);
} else if (type === 'series' && (tmdbId || tvdbId)) {
fanartPromise = tvdbId ? fanart.getShowImages(tvdbId, config) : Promise.resolve({});

tmdbPromise = tmdbId ? moviedb.tvImages({ id: tmdbId }, config) : Promise.resolve({});
} else {
return '';
}


const [fanartRes, tmdbRes] = await Promise.all([
fanartPromise.catch(() => ({})),
tmdbPromise.catch(() => ({}))
tmdbPromise.catch(() => ({})),
]);


const fanartLogosSource = (fanartRes || {}).hdmovielogo || (fanartRes || {}).hdtvlogo || [];
const fanartLogos = fanartLogosSource.map(l => ({
const raw = fanartRes || {};
const fanartLogosSource =
type === 'movie'
? raw.hdmovielogo || []
: raw.hdtvlogo || [];

const fanartLogos = fanartLogosSource.map((l) => ({
url: l.url,
lang: l.lang || 'en',
lang: l.lang || 'en',
likes: l.likes,
source: 'fanart',
}));


const tmdbLogosSource = tmdbRes.logos || [];
const tmdbLogos = tmdbLogosSource.map(l => ({
const tmdbLogosSource = (tmdbRes && tmdbRes.logos) || [];
const tmdbLogos = tmdbLogosSource.map((l) => ({
url: `https://image.tmdb.org/t/p/original${l.file_path}`,
lang: l.iso_639_1 || 'en',
lang: tmdbLogoLang(l),
vote_average: l.vote_average,
aspect_ratio: l.aspect_ratio,
source: 'tmdb',
}));

const combined = [...fanartLogos, ...tmdbLogos];

if (combined.length === 0) return '';

const picked = pickLogo(combined, language, originalLanguage);
return picked?.url || '';
return picked?.url || '';
} catch (error) {
console.error(`Error fetching clear logo for type=${type}, ids=${JSON.stringify(ids)}:`, error.message);
return '';
if (!shouldSkipLogoError(error.message)) {
console.error(
`Error fetching clear logo for type=${type}, ids=${JSON.stringify(ids)}:`,
error.message
);
}
return '';
}
}

Expand Down