better auto matching on anime and books when anilist is the source

This commit is contained in:
2026-01-02 02:45:12 +01:00
parent edb8a93395
commit a1d70193fa
2 changed files with 127 additions and 10 deletions

View File

@@ -276,7 +276,6 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
if (!ext) return { error: "not found" }; if (!ext) return { error: "not found" };
const extName = ext.constructor.name; const extName = ext.constructor.name;
const cached = await getCachedExtension(extName, id); const cached = await getCachedExtension(extName, id);
if (cached) { if (cached) {
try { try {
@@ -387,22 +386,41 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str
startDate: { year: 0, month: 0, day: 0 } startDate: { year: 0, month: 0, day: 0 }
} }
}); });
if (!matches || matches.length === 0) return []; 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 []; if (!res?.id) return [];
mediaId = res.id; mediaId = res.id;
} else { } else {
mediaId = query; mediaId = query;
} }
const chapterList = await ext.findEpisodes(mediaId); const chapterList = await ext.findEpisodes(mediaId);
if (!Array.isArray(chapterList)) return []; if (!Array.isArray(chapterList)) return [];
const result: Episode[] = chapterList.map(ep => ({ const result: Episode[] = chapterList.map(ep => ({
id: ep.id, id: ep.id,
number: ep.number, number: ep.number,
@@ -467,3 +485,46 @@ export async function getStreamData(extension: Extension, episode: string, id: s
await setCache(cacheKey, streamData, CACHE_TTL_MS); await setCache(cacheKey, streamData, CACHE_TTL_MS);
return streamData; 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(/&#39;/g, "'") // decodificar entidades HTML
.replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios
.replace(/\s+/g, ' ') // normalizar espacios
.trim();
}

View File

@@ -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 { } else {
const match = await ext.getMetadata(searchTitle); const match = await ext.getMetadata(searchTitle);
@@ -549,3 +562,46 @@ export async function getChapterContent(bookId: string, chapterId: string, provi
throw err; 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(/&#39;/g, "'") // decodificar entidades HTML
.replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios
.replace(/\s+/g, ' ') // normalizar espacios
.trim();
}