fix: use string matching to map to correct title in Aniwatch
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { findBestMatchingTitle } from "~/libs/findBestMatchingTitle";
|
||||
|
||||
import { Episode, type EpisodesResponse } from "./episode";
|
||||
|
||||
export async function getEpisodesFromAniwatch(
|
||||
@@ -8,9 +10,12 @@ export async function getEpisodesFromAniwatch(
|
||||
.then(({ fetchTitleFromAnilist }) =>
|
||||
fetchTitleFromAnilist(aniListId, undefined),
|
||||
)
|
||||
.then((title) => title?.title?.english ?? title?.title?.userPreferred);
|
||||
.then((title) => ({
|
||||
english: title?.title?.english,
|
||||
userPreferred: title?.title?.userPreferred,
|
||||
}));
|
||||
|
||||
if (!animeTitle) {
|
||||
if (!animeTitle.english && !animeTitle.userPreferred) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -55,12 +60,23 @@ export async function getEpisodesFromAniwatch(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAniwatchId(animeTitle: string): Promise<string | undefined> {
|
||||
function getAniwatchId(
|
||||
animeTitle: Partial<{ english: string; userPreferred: string }>,
|
||||
): Promise<string | undefined> {
|
||||
return fetch(
|
||||
`https://aniwatch.up.railway.app/anime/search?q=${encodeURIComponent(animeTitle)}`,
|
||||
`https://aniwatch.up.railway.app/anime/search?q=${encodeURIComponent(animeTitle.english ?? animeTitle.userPreferred!)}`,
|
||||
)
|
||||
.then((res) => res.json<AniwatchSearchResponse>())
|
||||
.then(({ animes }) => animes[0]?.id);
|
||||
.then(({ animes }) => {
|
||||
const bestMatchingTitle = findBestMatchingTitle(
|
||||
animeTitle,
|
||||
animes.map((anime) => ({
|
||||
english: anime.name,
|
||||
userPreferred: anime.jname,
|
||||
})),
|
||||
);
|
||||
return animes.find((anime) => anime.name === bestMatchingTitle)?.id;
|
||||
});
|
||||
}
|
||||
|
||||
export interface AniwatchEpisodesResponse {
|
||||
|
||||
113
src/libs/findBestMatchingTitle.ts
Normal file
113
src/libs/findBestMatchingTitle.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
function findBestMatch(mainString: string, targets: string[]): string | null {
|
||||
if (targets.length === 0) return null;
|
||||
|
||||
let bestMatch = targets[0];
|
||||
let highestScore = stringMatchingAlgorithm(mainString, bestMatch);
|
||||
|
||||
for (let i = 1; i < targets.length; i++) {
|
||||
const currentScore = stringMatchingAlgorithm(mainString, targets[i]);
|
||||
if (currentScore > highestScore) {
|
||||
highestScore = currentScore;
|
||||
bestMatch = targets[i];
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
type Title = {
|
||||
english: string;
|
||||
userPreferred: string;
|
||||
};
|
||||
|
||||
export const findBestMatchingTitle = (
|
||||
title: Partial<Title>,
|
||||
titles: Partial<Title>[],
|
||||
): string | null => {
|
||||
const { english, userPreferred } = title;
|
||||
|
||||
const userPreferredBestMatch = userPreferred
|
||||
? findBestMatch(
|
||||
userPreferred,
|
||||
titles
|
||||
.map((title) => title.userPreferred)
|
||||
.filter((title) => title !== undefined),
|
||||
)
|
||||
: null;
|
||||
const englishBestMatch = english
|
||||
? findBestMatch(
|
||||
english,
|
||||
titles
|
||||
.map((title) => title.english)
|
||||
.filter((title) => title !== undefined),
|
||||
)
|
||||
: null;
|
||||
|
||||
const userPreferredScore = userPreferredBestMatch
|
||||
? stringMatchingAlgorithm(userPreferred!, userPreferredBestMatch)
|
||||
: 0;
|
||||
const englishScore = englishBestMatch
|
||||
? stringMatchingAlgorithm(english!, englishBestMatch)
|
||||
: 0;
|
||||
|
||||
console.log(title.english, englishScore);
|
||||
console.log(title.userPreferred, userPreferredScore);
|
||||
if (userPreferredScore >= englishScore) {
|
||||
return userPreferredBestMatch;
|
||||
} else {
|
||||
return englishBestMatch;
|
||||
}
|
||||
};
|
||||
|
||||
function stringMatchingAlgorithm(s1: string, s2: string): number {
|
||||
// current implementation is the Jaro-Winkler algorithm
|
||||
// copied from https://discord.com/channels/987492554486452315/1273988465222090783/1273988465222090783
|
||||
const m = s1.length;
|
||||
const n = s2.length;
|
||||
|
||||
if (m === 0 && n === 0) return 1.0;
|
||||
if (m === 0 || n === 0) return 0.0;
|
||||
|
||||
const matchDistance = Math.floor(Math.max(m, n) / 2) - 1;
|
||||
const s1Matches = new Array(m).fill(false);
|
||||
const s2Matches = new Array(n).fill(false);
|
||||
|
||||
let matches = 0;
|
||||
let transpositions = 0;
|
||||
for (let i = 0; i < m; i++) {
|
||||
const start = Math.max(0, i - matchDistance);
|
||||
const end = Math.min(n - 1, i + matchDistance);
|
||||
|
||||
for (let j = start; j <= end; j++) {
|
||||
if (s2Matches[j]) continue;
|
||||
if (s1[i] !== s2[j]) continue;
|
||||
s1Matches[i] = true;
|
||||
s2Matches[j] = true;
|
||||
matches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches === 0) return 0.0;
|
||||
|
||||
let k = 0;
|
||||
for (let i = 0; i < m; i++) {
|
||||
if (!s1Matches[i]) continue;
|
||||
while (!s2Matches[k]) k++;
|
||||
if (s1[i] !== s2[k]) transpositions++;
|
||||
k++;
|
||||
}
|
||||
|
||||
transpositions /= 2;
|
||||
|
||||
const jaro =
|
||||
(matches / m + matches / n + (matches - transpositions) / matches) / 3;
|
||||
|
||||
const prefix = Math.min(
|
||||
4,
|
||||
[...s1].findIndex((ch, i) => s1[i] !== s2[i]),
|
||||
);
|
||||
const p = 0.1;
|
||||
|
||||
return jaro + prefix * p * (1 - jaro);
|
||||
}
|
||||
Reference in New Issue
Block a user