diff --git a/docker/src/api/anime/anime.service.ts b/docker/src/api/anime/anime.service.ts index ce1836c..ff02457 100644 --- a/docker/src/api/anime/anime.service.ts +++ b/docker/src/api/anime/anime.service.ts @@ -276,7 +276,6 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string): if (!ext) return { error: "not found" }; const extName = ext.constructor.name; - const cached = await getCachedExtension(extName, id); if (cached) { try { @@ -387,22 +386,41 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str startDate: { year: 0, month: 0, day: 0 } } }); - if (!matches || matches.length === 0) return []; - const res = matches[0]; + const normalizedQuery = normalize(query); + const scored = matches.map(match => { + const normalizedTitle = normalize(match.title); + const score = similarity(normalizedQuery, normalizedTitle); + + let bonus = 0; + if (normalizedTitle === normalizedQuery) { + bonus = 0.5; + } else if (normalizedTitle.toLowerCase().includes(normalizedQuery.toLowerCase())) { + bonus = 0.5; + } + + const finalScore = score + bonus; + + return { + match, + score: finalScore + }; + }); + + scored.sort((a, b) => b.score - a.score); + const bestMatches = scored.filter(s => s.score > 0.4); + + if (bestMatches.length === 0) return []; + const res = bestMatches[0].match; if (!res?.id) return []; - mediaId = res.id; - } else { mediaId = query; } const chapterList = await ext.findEpisodes(mediaId); - if (!Array.isArray(chapterList)) return []; - const result: Episode[] = chapterList.map(ep => ({ id: ep.id, number: ep.number, @@ -466,4 +484,47 @@ export async function getStreamData(extension: Extension, episode: string, id: s await setCache(cacheKey, streamData, CACHE_TTL_MS); return streamData; +} + +function similarity(s1: string, s2: string): number { + const str1 = normalize(s1); + const str2 = normalize(s2); + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const editDistance = levenshteinDistance(longer, shorter); + return (longer.length - editDistance) / longer.length; +} + +function levenshteinDistance(s1: string, s2: string): number { + const costs: number[] = []; + for (let i = 0; i <= s1.length; i++) { + let lastValue = i; + for (let j = 0; j <= s2.length; j++) { + if (i === 0) { + costs[j] = j; + } else if (j > 0) { + let newValue = costs[j - 1]; + if (s1.charAt(i - 1) !== s2.charAt(j - 1)) { + newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; + } + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + if (i > 0) costs[s2.length] = lastValue; + } + return costs[s2.length]; +} + +function normalize(str: string): string { + return str + .toLowerCase() + .replace(/'/g, "'") // decodificar entidades HTML + .replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios + .replace(/\s+/g, ' ') // normalizar espacios + .trim(); } \ No newline at end of file diff --git a/docker/src/api/books/books.service.ts b/docker/src/api/books/books.service.ts index 255b9aa..47d832c 100644 --- a/docker/src/api/books/books.service.ts +++ b/docker/src/api/books/books.service.ts @@ -394,11 +394,24 @@ async function searchChaptersInExtension(ext: Extension, name: string, searchTit } }); - const best = matches?.[0]; + if (!matches?.length) return []; - if (!best) { return [] } + const nq = normalize(searchTitle); - mediaId = best.id; + const scored = matches.map(m => { + const nt = normalize(m.title); + let score = similarity(nq, nt); + + if (nt === nq || nt.includes(nq)) score += 0.5; + + return { m, score }; + }); + + scored.sort((a, b) => b.score - a.score); + + if (scored[0].score < 0.4) return []; + + mediaId = scored[0].m.id; } else { const match = await ext.getMetadata(searchTitle); @@ -548,4 +561,47 @@ export async function getChapterContent(bookId: string, chapterId: string, provi console.error(`[Chapter] Error loading from ${providerName}:`, error.message); throw err; } +} + +function similarity(s1: string, s2: string): number { + const str1 = normalize(s1); + const str2 = normalize(s2); + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const editDistance = levenshteinDistance(longer, shorter); + return (longer.length - editDistance) / longer.length; +} + +function levenshteinDistance(s1: string, s2: string): number { + const costs: number[] = []; + for (let i = 0; i <= s1.length; i++) { + let lastValue = i; + for (let j = 0; j <= s2.length; j++) { + if (i === 0) { + costs[j] = j; + } else if (j > 0) { + let newValue = costs[j - 1]; + if (s1.charAt(i - 1) !== s2.charAt(j - 1)) { + newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; + } + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + if (i > 0) costs[s2.length] = lastValue; + } + return costs[s2.length]; +} + +function normalize(str: string): string { + return str + .toLowerCase() + .replace(/'/g, "'") // decodificar entidades HTML + .replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios + .replace(/\s+/g, ' ') // normalizar espacios + .trim(); } \ No newline at end of file