better auto matching on anime and books when anilist is the source
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user