mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
Refactor: MalSync resolution to prioritize air-dates for series and direct ID mapping for movies
This commit is contained in:
parent
efe58005d0
commit
5f77721298
1 changed files with 82 additions and 87 deletions
|
|
@ -13,20 +13,41 @@ export const MalSync = {
|
||||||
/**
|
/**
|
||||||
* Tries to find a MAL ID using IMDb ID via MAL-Sync API.
|
* Tries to find a MAL ID using IMDb ID via MAL-Sync API.
|
||||||
*/
|
*/
|
||||||
getMalIdFromImdb: async (imdbId: string): Promise<number | null> => {
|
/**
|
||||||
if (!imdbId) return null;
|
* Tries to find a MAL ID using IMDb or TMDB ID via External APIs (MAL-Sync or ARM).
|
||||||
|
*/
|
||||||
// Fetch from MAL-Sync API
|
getMalIdFromExternal: async (id: string | number, source: 'imdb' | 'tmdb'): Promise<number | null> => {
|
||||||
|
if (!id) return null;
|
||||||
try {
|
try {
|
||||||
// Ensure ID format
|
// 1. Try ARM API (v2)
|
||||||
const cleanId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
const endpoint = source === 'imdb' ? 'imdb' : 'tmdb';
|
||||||
const response = await axios.get(`https://api.malsync.moe/mal/anime/imdb/${cleanId}`);
|
const cleanId = source === 'imdb' && typeof id === 'string' && !id.startsWith('tt') ? `tt${id}` : id;
|
||||||
|
|
||||||
if (response.data && response.data.id) {
|
try {
|
||||||
return response.data.id;
|
const armRes = await axios.get(`https://arm.haglund.dev/api/v2/${endpoint}`, {
|
||||||
|
params: { id: cleanId }
|
||||||
|
});
|
||||||
|
if (Array.isArray(armRes.data) && armRes.data.length > 0) {
|
||||||
|
const entry = armRes.data.find((e: any) => e.myanimelist);
|
||||||
|
if (entry && entry.myanimelist) return entry.myanimelist;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore errors (404, etc.)
|
// Fallback to MAL-Sync for IMDb if ARM fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try MAL-Sync API (only for IMDb)
|
||||||
|
if (source === 'imdb') {
|
||||||
|
try {
|
||||||
|
const malSyncRes = await axios.get(`https://api.malsync.moe/mal/anime/imdb/${cleanId}`);
|
||||||
|
if (malSyncRes.data && malSyncRes.data.id) {
|
||||||
|
return malSyncRes.data.id;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// General error catch
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -40,9 +61,21 @@ export const MalSync = {
|
||||||
const normalizedTitle = cleanTitle.toLowerCase();
|
const normalizedTitle = cleanTitle.toLowerCase();
|
||||||
const isGenericTitle = !normalizedTitle || normalizedTitle === 'anime' || normalizedTitle === 'movie';
|
const isGenericTitle = !normalizedTitle || normalizedTitle === 'anime' || normalizedTitle === 'movie';
|
||||||
|
|
||||||
const seasonNumber = season || 1;
|
|
||||||
|
|
||||||
if (isGenericTitle && !imdbId && !tmdbId) return null;
|
if (isGenericTitle && !imdbId && !tmdbId) return null;
|
||||||
|
|
||||||
|
// 0. Direct ID mapping for Movies (Skip date matching as movies are 1:1)
|
||||||
|
if (type === 'movie') {
|
||||||
|
if (tmdbId) {
|
||||||
|
const idFromExternal = await MalSync.getMalIdFromExternal(tmdbId, 'tmdb');
|
||||||
|
if (idFromExternal) return idFromExternal;
|
||||||
|
}
|
||||||
|
if (imdbId) {
|
||||||
|
const idFromExternal = await MalSync.getMalIdFromExternal(imdbId, 'imdb');
|
||||||
|
if (idFromExternal) return idFromExternal;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Try TMDB-based Resolution (High Accuracy)
|
// 1. Try TMDB-based Resolution (High Accuracy)
|
||||||
if (tmdbId && releaseDate) {
|
if (tmdbId && releaseDate) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -62,12 +95,6 @@ export const MalSync = {
|
||||||
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate, dayIndex);
|
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate, dayIndex);
|
||||||
if (armResult && armResult.malId) {
|
if (armResult && armResult.malId) {
|
||||||
console.log(`[MalSync] Found ARM match: ${imdbId} (${releaseDate}) -> MAL ${armResult.malId} Ep ${armResult.episode}`);
|
console.log(`[MalSync] Found ARM match: ${imdbId} (${releaseDate}) -> MAL ${armResult.malId} Ep ${armResult.episode}`);
|
||||||
// Note: ArmSyncService returns the *absolute* episode number for MAL (e.g. 76)
|
|
||||||
// but our 'episode' arg is usually relative (e.g. 1).
|
|
||||||
// scrobbleEpisode uses the malId returned here, and potentially the episode number from ArmSync
|
|
||||||
// But getMalId just returns the ID.
|
|
||||||
// Ideally, scrobbleEpisode should call ArmSyncService directly to get both ID and correct Episode number.
|
|
||||||
// For now, we return the ID.
|
|
||||||
return armResult.malId;
|
return armResult.malId;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -75,71 +102,9 @@ export const MalSync = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Try IMDb ID mapping when it is likely to be accurate, or when title is generic.
|
|
||||||
if (imdbId && (type === 'movie' || seasonNumber <= 1 || isGenericTitle)) {
|
|
||||||
const idFromImdb = await MalSync.getMalIdFromImdb(imdbId);
|
|
||||||
if (idFromImdb) return idFromImdb;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Search MAL (Skip if generic title)
|
// 3. Search MAL (Skip if generic title)
|
||||||
if (isGenericTitle) return null;
|
// Disabled title-based search and IMDb mapping fallback for series to prevent inaccurate mappings.
|
||||||
|
// Real-time resolution via ARM/TMDB is now the primary path.
|
||||||
try {
|
|
||||||
let searchQuery = cleanTitle;
|
|
||||||
// For Season 2+, explicitly search for that season
|
|
||||||
if (type === 'series' && season && season > 1) {
|
|
||||||
// Improve search query: "Attack on Titan Season 2" usually works better than just appending
|
|
||||||
searchQuery = `${cleanTitle} Season ${season}`;
|
|
||||||
} else if (type === 'series' && season === 0) {
|
|
||||||
// Improve Season 0 (Specials) lookup: "Attack on Titan Specials" or "Attack on Titan OVA"
|
|
||||||
// We search for both to find the most likely entry
|
|
||||||
searchQuery = `${cleanTitle} Specials`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await MalApiService.searchAnime(searchQuery, 10);
|
|
||||||
if (result.data.length > 0) {
|
|
||||||
let candidates = result.data;
|
|
||||||
|
|
||||||
// Filter by type first
|
|
||||||
if (type === 'movie') {
|
|
||||||
candidates = candidates.filter(r => r.node.media_type === 'movie');
|
|
||||||
} else if (season === 0) {
|
|
||||||
// For Season 0, prioritize specials, ovas, and onas
|
|
||||||
candidates = candidates.filter(r => r.node.media_type === 'special' || r.node.media_type === 'ova' || r.node.media_type === 'ona');
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
// If no specific special types found, fallback to anything containing "Special" or "OVA" in title
|
|
||||||
candidates = result.data.filter(r =>
|
|
||||||
r.node.title.toLowerCase().includes('special') ||
|
|
||||||
r.node.title.toLowerCase().includes('ova') ||
|
|
||||||
r.node.title.toLowerCase().includes('ona')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
candidates = candidates.filter(r => r.node.media_type === 'tv' || r.node.media_type === 'ona' || r.node.media_type === 'special' || r.node.media_type === 'ova');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.length === 0) candidates = result.data; // Fallback to all if type filtering removes everything
|
|
||||||
|
|
||||||
let bestMatch = candidates[0].node;
|
|
||||||
|
|
||||||
// If year is provided, try to find an exact start year match
|
|
||||||
if (year) {
|
|
||||||
const yearMatch = candidates.find(r => r.node.start_season?.year === year);
|
|
||||||
if (yearMatch) {
|
|
||||||
bestMatch = yearMatch.node;
|
|
||||||
} else {
|
|
||||||
// Fuzzy year match (+/- 1 year)
|
|
||||||
const fuzzyMatch = candidates.find(r => r.node.start_season?.year && Math.abs(r.node.start_season.year - year) <= 1);
|
|
||||||
if (fuzzyMatch) bestMatch = fuzzyMatch.node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return best match directly
|
|
||||||
return bestMatch.id;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('MAL Search failed for', title);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -170,8 +135,23 @@ export const MalSync = {
|
||||||
let malId: number | null = null;
|
let malId: number | null = null;
|
||||||
let finalEpisodeNumber = episodeNumber;
|
let finalEpisodeNumber = episodeNumber;
|
||||||
|
|
||||||
|
// Strategy 0: Direct Resolution for Movies (Skip date matching)
|
||||||
|
// We can trust providedMalId for movies as they are always 1:1.
|
||||||
|
if (type === 'movie') {
|
||||||
|
malId = providedMalId || null;
|
||||||
|
if (!malId && tmdbId) {
|
||||||
|
malId = await MalSync.getMalIdFromExternal(tmdbId, 'tmdb');
|
||||||
|
}
|
||||||
|
if (!malId && imdbId) {
|
||||||
|
malId = await MalSync.getMalIdFromExternal(imdbId, 'imdb');
|
||||||
|
}
|
||||||
|
if (malId) {
|
||||||
|
console.log(`[MalSync] Movie Resolved: ${animeTitle} -> MAL ${malId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Strategy 1: TMDB-based Resolution (High Accuracy for Specials/Seasons)
|
// Strategy 1: TMDB-based Resolution (High Accuracy for Specials/Seasons)
|
||||||
if (tmdbId && releaseDate) {
|
if (!malId && tmdbId && releaseDate) {
|
||||||
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
||||||
if (tmdbResult) {
|
if (tmdbResult) {
|
||||||
malId = tmdbResult.malId;
|
malId = tmdbResult.malId;
|
||||||
|
|
@ -190,8 +170,9 @@ export const MalSync = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to standard lookup if ARM/TMDB failed and no ID provided
|
// Fallback to standard lookup for movies if direct resolution failed.
|
||||||
if (!malId) {
|
// For series, getMalId is now redundant as it repeats the same strategies.
|
||||||
|
if (!malId && type === 'movie') {
|
||||||
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate, dayIndex, tmdbId);
|
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate, dayIndex, tmdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,8 +253,20 @@ export const MalSync = {
|
||||||
let malId: number | null = null;
|
let malId: number | null = null;
|
||||||
let finalEpisodeNumber = episodeNumber;
|
let finalEpisodeNumber = episodeNumber;
|
||||||
|
|
||||||
|
// Strategy 0: Direct Resolution for Movies (Skip date matching)
|
||||||
|
// We can trust providedMalId for movies as they are always 1:1.
|
||||||
|
if (type === 'movie') {
|
||||||
|
malId = providedMalId || null;
|
||||||
|
if (!malId && tmdbId) {
|
||||||
|
malId = await MalSync.getMalIdFromExternal(tmdbId, 'tmdb');
|
||||||
|
}
|
||||||
|
if (!malId && imdbId) {
|
||||||
|
malId = await MalSync.getMalIdFromExternal(imdbId, 'imdb');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve ID using same strategies as scrobbling
|
// Resolve ID using same strategies as scrobbling
|
||||||
if (tmdbId && releaseDate) {
|
if (!malId && tmdbId && releaseDate) {
|
||||||
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
||||||
if (tmdbResult) {
|
if (tmdbResult) {
|
||||||
malId = tmdbResult.malId;
|
malId = tmdbResult.malId;
|
||||||
|
|
@ -289,7 +282,9 @@ export const MalSync = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!malId) {
|
// Fallback to standard lookup for movies if direct resolution failed.
|
||||||
|
// For series, getMalId is now redundant as it repeats the same strategies.
|
||||||
|
if (!malId && type === 'movie') {
|
||||||
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate, dayIndex, tmdbId);
|
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate, dayIndex, tmdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue