mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-22 18:47:44 +00:00
Merge branch 'NuvioMedia:main' into Mal
This commit is contained in:
commit
640b45835c
5 changed files with 124 additions and 46 deletions
|
|
@ -722,8 +722,45 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}
|
||||
}
|
||||
|
||||
// If normalizedType is not a known type (e.g. "other" from Gemini/AI search),
|
||||
// resolve the correct type via TMDB before fetching addon metadata.
|
||||
let effectiveType = normalizedType;
|
||||
if (normalizedType !== 'movie' && normalizedType !== 'series') {
|
||||
try {
|
||||
if (actualId.startsWith('tt')) {
|
||||
// Use TMDB /find endpoint which returns tv_results + movie_results simultaneously
|
||||
// — gives definitive type in one call with no sequential guessing
|
||||
const tmdbSvc = TMDBService.getInstance();
|
||||
const resolved = await tmdbSvc.findTypeAndIdByIMDB(actualId);
|
||||
if (resolved) {
|
||||
effectiveType = resolved.type;
|
||||
setTmdbId(resolved.tmdbId);
|
||||
if (__DEV__) console.log(`🔍 [useMetadata] Resolved unknown type "${normalizedType}" → "${effectiveType}" via TMDB /find`);
|
||||
}
|
||||
} else if (actualId.startsWith('tmdb:')) {
|
||||
// For tmdb: IDs try both in parallel, prefer series
|
||||
const tmdbSvc = TMDBService.getInstance();
|
||||
const tmdbRaw = parseInt(actualId.split(':')[1]);
|
||||
if (!isNaN(tmdbRaw)) {
|
||||
const [movieResult, seriesResult] = await Promise.allSettled([
|
||||
tmdbSvc.getMovieDetails(String(tmdbRaw)).catch(() => null),
|
||||
tmdbSvc.getTVShowDetails(tmdbRaw).catch(() => null),
|
||||
]);
|
||||
const hasMovie = movieResult.status === 'fulfilled' && !!movieResult.value;
|
||||
const hasSeries = seriesResult.status === 'fulfilled' && !!seriesResult.value;
|
||||
// Prefer series when both exist (anime/TV tagged as "other" is usually a series)
|
||||
if (hasSeries) effectiveType = 'series';
|
||||
else if (hasMovie) effectiveType = 'movie';
|
||||
if (__DEV__) console.log(`🔍 [useMetadata] Resolved unknown type "${normalizedType}" → "${effectiveType}" via TMDB parallel check`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (__DEV__) console.log('🔍 [useMetadata] Failed to resolve type via TMDB, using fallback:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Load all data in parallel
|
||||
if (__DEV__) logger.log('[loadMetadata] fetching addon metadata', { type, actualId, addonId });
|
||||
if (__DEV__) logger.log('[loadMetadata] fetching addon metadata', { type: effectiveType, actualId, addonId });
|
||||
|
||||
let contentResult: any = null;
|
||||
let lastError = null;
|
||||
|
|
@ -758,7 +795,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
for (const addon of externalMetaAddons) {
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
stremioService.getMetaDetails(normalizedType, actualId, addon.id),
|
||||
stremioService.getMetaDetails(effectiveType, actualId, addon.id),
|
||||
API_TIMEOUT
|
||||
);
|
||||
|
||||
|
|
@ -775,7 +812,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
// If no external addon worked, fall back to catalog addon
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(normalizedType, actualId, addonId),
|
||||
catalogService.getEnhancedContentDetails(effectiveType, actualId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
if (actualId.startsWith('tt')) {
|
||||
|
|
@ -804,7 +841,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Load content with timeout and retry
|
||||
withRetry(async () => {
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(normalizedType, actualId, addonId),
|
||||
catalogService.getEnhancedContentDetails(effectiveType, actualId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
// Store the actual ID used (could be IMDB)
|
||||
|
|
@ -841,7 +878,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
const [content, castData] = await Promise.allSettled([
|
||||
withRetry(async () => {
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(normalizedType, stremioId, addonId),
|
||||
catalogService.getEnhancedContentDetails(effectiveType, stremioId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
if (stremioId.startsWith('tt')) {
|
||||
|
|
|
|||
|
|
@ -38,11 +38,8 @@ export async function getContentDetails(
|
|||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
|
||||
const isValidId = await stremioService.isValidContentId(type, id);
|
||||
|
||||
if (!isValidId) {
|
||||
break;
|
||||
}
|
||||
// isValidContentId gate removed — getMetaDetails uses addonCanServeId()
|
||||
// for per-addon prefix matching, avoiding false negatives for custom ID types.
|
||||
meta = await stremioService.getMetaDetails(type, id, preferredAddonId);
|
||||
|
||||
if (meta) {
|
||||
|
|
@ -102,10 +99,8 @@ export async function getBasicContentDetails(
|
|||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
if (!(await stremioService.isValidContentId(type, id))) {
|
||||
break;
|
||||
}
|
||||
|
||||
// isValidContentId gate removed — getMetaDetails uses addonCanServeId()
|
||||
// for per-addon prefix matching, avoiding false negatives for custom ID types.
|
||||
meta = await stremioService.getMetaDetails(type, id, preferredAddonId);
|
||||
if (meta) {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -321,13 +321,9 @@ async function searchAddonCatalog(
|
|||
|
||||
const items = metas.map(meta => {
|
||||
const content = convertMetaToStreamingContent(meta, library);
|
||||
const addonSupportsMeta = Array.isArray(manifest.resources) && manifest.resources.some((resource: any) =>
|
||||
resource === 'meta' || (typeof resource === 'object' && resource?.name === 'meta')
|
||||
);
|
||||
|
||||
if (addonSupportsMeta) {
|
||||
content.addonId = manifest.id;
|
||||
}
|
||||
// Do NOT set addonId from search results — let getMetaDetails resolve the correct
|
||||
// meta addon by ID prefix matching. Setting it here causes 404s when two addons
|
||||
// are installed and one returns IDs the other can't serve metadata for.
|
||||
|
||||
const normalizedCatalogType = type ? type.toLowerCase() : type;
|
||||
if (normalizedCatalogType && content.type !== normalizedCatalogType) {
|
||||
|
|
|
|||
|
|
@ -177,30 +177,49 @@ export function getCatalogHasMore(
|
|||
return ctx.catalogHasMore.get(`${manifestId}|${type}|${id}`);
|
||||
}
|
||||
|
||||
function addonSupportsMetaResource(addon: Manifest, type: string, id: string): boolean {
|
||||
let hasMetaSupport = false;
|
||||
let supportsIdPrefix = false;
|
||||
|
||||
/**
|
||||
* Check if an addon can serve metadata for this ID by matching ID prefix.
|
||||
* Does NOT require a type match — type is resolved separately via resolveTypeForAddon.
|
||||
*/
|
||||
function addonCanServeId(addon: Manifest, id: string): boolean {
|
||||
for (const resource of addon.resources || []) {
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const typedResource = resource as ResourceObject;
|
||||
if (typedResource.name === 'meta' && typedResource.types?.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
supportsIdPrefix =
|
||||
!typedResource.idPrefixes?.length ||
|
||||
typedResource.idPrefixes.some(prefix => id.startsWith(prefix));
|
||||
break;
|
||||
}
|
||||
} else if (resource === 'meta' && addon.types?.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
supportsIdPrefix =
|
||||
!addon.idPrefixes?.length || addon.idPrefixes.some(prefix => id.startsWith(prefix));
|
||||
break;
|
||||
const r = resource as ResourceObject;
|
||||
if (r.name !== 'meta') continue;
|
||||
if (!r.idPrefixes?.length) return true;
|
||||
if (r.idPrefixes.some(p => id.startsWith(p))) return true;
|
||||
} else if (resource === 'meta') {
|
||||
if (!addon.idPrefixes?.length) return true;
|
||||
if (addon.idPrefixes.some(p => id.startsWith(p))) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const requiresIdPrefix = !!addon.idPrefixes?.length;
|
||||
return hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
||||
/**
|
||||
* Resolve the correct type to use in the metadata URL for a given addon.
|
||||
* Looks at what types the addon declares for its meta resource matching this ID prefix,
|
||||
* rather than blindly trusting the passed-in type (which may be "other", "Movie", etc.).
|
||||
* Falls back to lowercased passed-in type if no better match found.
|
||||
*/
|
||||
function resolveTypeForAddon(addon: Manifest, type: string, id: string): string {
|
||||
const lowerFallback = type ? type.toLowerCase() : type;
|
||||
for (const resource of addon.resources || []) {
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const r = resource as ResourceObject;
|
||||
if (r.name !== 'meta' || !r.types?.length) continue;
|
||||
const prefixMatch = !r.idPrefixes?.length || r.idPrefixes.some(p => id.startsWith(p));
|
||||
if (prefixMatch) {
|
||||
const exact = r.types.find(t => t.toLowerCase() === lowerFallback);
|
||||
return exact ?? r.types[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addon.types?.length) {
|
||||
const exact = addon.types.find(t => t.toLowerCase() === lowerFallback);
|
||||
return exact ?? addon.types[0];
|
||||
}
|
||||
return lowerFallback;
|
||||
}
|
||||
|
||||
async function fetchMetaFromAddon(
|
||||
|
|
@ -209,11 +228,12 @@ async function fetchMetaFromAddon(
|
|||
type: string,
|
||||
id: string
|
||||
): Promise<MetaDetails | null> {
|
||||
const resolvedType = resolveTypeForAddon(addon, type, id);
|
||||
const { baseUrl, queryParams } = ctx.getAddonBaseURL(addon.url || '');
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = queryParams
|
||||
? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}`
|
||||
: `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
? `${baseUrl}/meta/${resolvedType}/${encodedId}.json?${queryParams}`
|
||||
: `${baseUrl}/meta/${resolvedType}/${encodedId}.json`;
|
||||
|
||||
const response = await ctx.retryRequest(() => axios.get(url, createSafeAxiosConfig(10000)));
|
||||
return response.data?.meta?.id ? response.data.meta : null;
|
||||
|
|
@ -226,7 +246,11 @@ export async function getMetaDetails(
|
|||
preferredAddonId?: string
|
||||
): Promise<MetaDetails | null> {
|
||||
try {
|
||||
if (!(await ctx.isValidContentId(type, id))) {
|
||||
// isValidContentId gate removed — addonCanServeId() handles per-addon ID prefix
|
||||
// filtering correctly. The gate caused false negatives when type was non-standard
|
||||
// or prefixes weren't indexed yet, silently returning null before any addon was tried.
|
||||
const lowerId = (id || '').toLowerCase();
|
||||
if (!id || lowerId === 'null' || lowerId === 'undefined' || lowerId === 'moviebox' || lowerId === 'torbox') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +258,7 @@ export async function getMetaDetails(
|
|||
|
||||
if (preferredAddonId) {
|
||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||
if (preferredAddon?.resources && addonSupportsMetaResource(preferredAddon, type, id)) {
|
||||
if (preferredAddon?.resources && addonCanServeId(preferredAddon, id)) {
|
||||
try {
|
||||
const meta = await fetchMetaFromAddon(ctx, preferredAddon, type, id);
|
||||
if (meta) {
|
||||
|
|
@ -249,7 +273,7 @@ export async function getMetaDetails(
|
|||
for (const baseUrl of ['https://v3-cinemeta.strem.io', 'http://v3-cinemeta.strem.io']) {
|
||||
try {
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
const url = `${baseUrl}/meta/${type ? type.toLowerCase() : type}/${encodedId}.json`;
|
||||
const response = await ctx.retryRequest(() => axios.get(url, createSafeAxiosConfig(10000)));
|
||||
if (response.data?.meta?.id) {
|
||||
return response.data.meta;
|
||||
|
|
@ -264,7 +288,7 @@ export async function getMetaDetails(
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!addonSupportsMetaResource(addon, type, id)) {
|
||||
if (!addonCanServeId(addon, id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -582,6 +582,32 @@ export class TMDBService {
|
|||
/**
|
||||
* Find TMDB ID by IMDB ID
|
||||
*/
|
||||
/**
|
||||
* Resolve both the TMDB ID and the correct content type ('movie' | 'series') for an IMDb ID.
|
||||
* Uses TMDB's /find endpoint which returns tv_results and movie_results simultaneously,
|
||||
* giving a definitive type without sequential guessing.
|
||||
* TV results take priority since "other"-typed search results are usually series/anime.
|
||||
*/
|
||||
async findTypeAndIdByIMDB(imdbId: string): Promise<{ tmdbId: number; type: 'movie' | 'series' } | null> {
|
||||
try {
|
||||
const baseImdbId = imdbId.split(':')[0];
|
||||
const response = await axios.get(`${BASE_URL}/find/${baseImdbId}`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({ external_source: 'imdb_id' }),
|
||||
});
|
||||
|
||||
if (response.data.tv_results?.length > 0) {
|
||||
return { tmdbId: response.data.tv_results[0].id, type: 'series' };
|
||||
}
|
||||
if (response.data.movie_results?.length > 0) {
|
||||
return { tmdbId: response.data.movie_results[0].id, type: 'movie' };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async findTMDBIdByIMDB(imdbId: string, language: string = 'en-US'): Promise<number | null> {
|
||||
const cacheKey = this.generateCacheKey('find_imdb', { imdbId, language });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue