feat(aniwatch): Improves title matching logic

- Enhances title matching accuracy when fetching Aniwatch IDs.
- Prioritizes user-preferred titles and falls back to English titles.
- Ensures only one fetch call is made per title if both english and userPreferred title are same.
- Adds a score threshold to filter low-quality matches.
- Fixes a bug where the episode list was not being returned.
This commit is contained in:
2025-04-23 09:32:52 -04:00
parent 00720565b4
commit b8ae211956
3 changed files with 96 additions and 21 deletions

View File

@@ -119,17 +119,43 @@ async function fetchEpisodes(
}); });
} }
// function updateTitles(title: Partial<{ english: string; userPreferred: string }>) {
// const english = title.english?.toLowerCase();
// const userPreferred = title.userPreferred?.toLowerCase();
// if (english?.match(/my hero academia.+[0-9]$/)) {
// }
// }
function getAniwatchId( function getAniwatchId(
animeTitle: Partial<{ english: string; userPreferred: string }>, animeTitle: Partial<{ english: string; userPreferred: string }>,
): Promise<string | undefined> { ): Promise<string | undefined> {
return Promise.allSettled([ animeTitle = {
english: animeTitle?.english?.toLowerCase(),
userPreferred: animeTitle?.userPreferred?.toLowerCase(),
};
const promises = [];
if (animeTitle.userPreferred) {
promises.push(
fetch( fetch(
`https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent(animeTitle.english!)}`, `https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent(
animeTitle.userPreferred,
)}`,
), ),
);
}
if (animeTitle.english && animeTitle.english !== animeTitle.userPreferred) {
promises.push(
fetch( fetch(
`https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent(animeTitle.userPreferred!)}`, `https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent(
animeTitle.english,
)}`,
), ),
]) );
}
return Promise.allSettled(promises)
.then((responses) => { .then((responses) => {
return responses.reduce( return responses.reduce(
async (current, res) => { async (current, res) => {
@@ -164,16 +190,21 @@ function getAniwatchId(
return; return;
} }
const bestMatchingTitle = findBestMatchingTitle( const { title: bestMatchingTitle, score } = findBestMatchingTitle(
animeTitle, animeTitle,
animes.map((anime) => ({ animes.map((anime) => ({
english: anime.name, english: anime.name,
userPreferred: anime.jname, userPreferred: anime.jname,
})), })),
); );
if (score < 0.8) {
return;
}
return animes.find( return animes.find(
(anime) => (anime) =>
anime.name === bestMatchingTitle || anime.jname === bestMatchingTitle, anime.name?.toLowerCase() === bestMatchingTitle ||
anime.jname?.toLowerCase() === bestMatchingTitle,
)?.id; )?.id;
}); });
} }

View File

@@ -81,18 +81,23 @@ export function fetchEpisodes(
app.openapi(route, async (c) => { app.openapi(route, async (c) => {
const aniListId = Number(c.req.param("aniListId")); const aniListId = Number(c.req.param("aniListId"));
const { result: episodes, errorOccurred } = await fetchEpisodes( const { result, errorOccurred } = await fetchEpisodes(
aniListId, aniListId,
env(c, "workerd"), env(c, "workerd"),
); );
if (errorOccurred) { if (errorOccurred || !result) {
return c.json(ErrorResponse, { status: 500 }); return c.json(ErrorResponse, { status: 500 });
} }
const { episodes, providerId } = result;
if (!episodes || episodes.length === 0) {
return c.json(ErrorResponse, { status: 404 });
}
return c.json({ return c.json({
success: true, success: true,
result: episodes ?? [], result: { providerId, episodes },
}); });
}); });

View File

@@ -1,17 +1,40 @@
function findBestMatch(mainString: string, targets: string[]): string | null { function findBestMatch(mainString: string, targets: string[]): string | null {
if (targets.length === 0) return null; if (targets.length === 0) {
return null;
}
if (targets.length === 1) {
return targets[0];
}
let bestMatch = targets[0]; let bestMatch = targets[0];
let highestScore = stringMatchingAlgorithm(mainString, bestMatch); let highestScore = stringMatchingAlgorithm(mainString, bestMatch);
for (let i = 1; i < targets.length; i++) { for (let i = 1; i < targets.length; i++) {
const currentScore = stringMatchingAlgorithm(mainString, targets[i]); const currentScore = stringMatchingAlgorithm(mainString, targets[i]);
if (currentScore === 1) {
bestMatch = targets[i];
break;
}
console.log(
"searching best match",
`'${mainString}'`,
`'${targets[i]}'`,
currentScore,
highestScore,
);
if (currentScore > highestScore) { if (currentScore > highestScore) {
highestScore = currentScore; highestScore = currentScore;
bestMatch = targets[i]; bestMatch = targets[i];
} }
} }
console.log(
"findBestMatch",
`'${mainString}'`,
`'${bestMatch}'`,
highestScore,
);
return bestMatch; return bestMatch;
} }
@@ -23,23 +46,39 @@ type Title = {
export const findBestMatchingTitle = ( export const findBestMatchingTitle = (
title: Partial<Title>, title: Partial<Title>,
titlesToSearch: Partial<Title>[], titlesToSearch: Partial<Title>[],
): string | null => { ): { title: string | null; score: number } => {
const { english, userPreferred } = title; const { english, userPreferred } = title;
let match;
let expectedSuffix = "";
if ((match = english?.match(/my hero academia.+([0-9])$/))) {
expectedSuffix = match[1];
}
console.log("searching best user preferred title", userPreferred);
const userPreferredBestMatch = userPreferred const userPreferredBestMatch = userPreferred
? findBestMatch( ? findBestMatch(
userPreferred, userPreferred,
titlesToSearch titlesToSearch
.map((title) => title.userPreferred) .map((title) => title.userPreferred?.toLowerCase())
.filter((title) => title !== undefined), .filter(
(title) =>
title !== undefined &&
(!expectedSuffix || title.endsWith(expectedSuffix)),
),
) )
: null; : null;
console.log("searching best english title", english);
const englishBestMatch = english const englishBestMatch = english
? findBestMatch( ? findBestMatch(
english, english,
titlesToSearch titlesToSearch
.map((title) => title.english) .map((title) => title.english?.toLowerCase())
.filter((title) => title !== undefined), .filter(
(title) =>
title !== undefined &&
(!expectedSuffix || title.endsWith(expectedSuffix)),
),
) )
: null; : null;
@@ -54,10 +93,10 @@ export const findBestMatchingTitle = (
console.log(title.userPreferred, userPreferredScore); console.log(title.userPreferred, userPreferredScore);
if (userPreferredScore >= englishScore) { if (userPreferredScore >= englishScore) {
console.log("User preferred", userPreferredBestMatch); console.log("User preferred", userPreferredBestMatch);
return userPreferredBestMatch; return { title: userPreferredBestMatch, score: userPreferredScore };
} else { } else {
console.log("English", englishBestMatch); console.log("English", englishBestMatch);
return englishBestMatch; return { title: englishBestMatch, score: englishScore };
} }
}; };