mirror of
https://github.com/p-stream/providers.git
synced 2026-05-20 14:52:04 +00:00
use anilist instead of mal api
This commit is contained in:
parent
3a33b0ee78
commit
7f4a6ad5fc
6 changed files with 120 additions and 102 deletions
|
|
@ -23,7 +23,7 @@ export function makeAnimetsuEmbed(id: string, rank: number = 100) {
|
||||||
const serverName = id as (typeof ANIMETSU_SERVERS)[number];
|
const serverName = id as (typeof ANIMETSU_SERVERS)[number];
|
||||||
|
|
||||||
const query = JSON.parse(ctx.url);
|
const query = JSON.parse(ctx.url);
|
||||||
const { type, malId, episode } = query;
|
const { type, anilistId, episode } = query;
|
||||||
|
|
||||||
if (type !== 'movie' && type !== 'show') {
|
if (type !== 'movie' && type !== 'show') {
|
||||||
throw new NotFoundError('Unsupported media type');
|
throw new NotFoundError('Unsupported media type');
|
||||||
|
|
@ -34,7 +34,7 @@ export function makeAnimetsuEmbed(id: string, rank: number = 100) {
|
||||||
headers,
|
headers,
|
||||||
query: {
|
query: {
|
||||||
server: serverName,
|
server: serverName,
|
||||||
id: String(malId),
|
id: String(anilistId),
|
||||||
num: String(episode ?? 1),
|
num: String(episode ?? 1),
|
||||||
subType: 'dub',
|
subType: 'dub',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@ export function makeZunimeEmbed(id: string, rank: number = 100) {
|
||||||
const serverName = id as (typeof ZUNIME_SERVERS)[number];
|
const serverName = id as (typeof ZUNIME_SERVERS)[number];
|
||||||
|
|
||||||
const query = JSON.parse(ctx.url);
|
const query = JSON.parse(ctx.url);
|
||||||
const { malId, episode } = query;
|
const { anilistId, episode } = query;
|
||||||
|
|
||||||
const res = await ctx.proxiedFetcher(`${'/sources'}`, {
|
const res = await ctx.proxiedFetcher(`${'/sources'}`, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
headers,
|
headers,
|
||||||
query: {
|
query: {
|
||||||
id: String(malId),
|
id: String(anilistId),
|
||||||
ep: String(episode ?? 1),
|
ep: String(episode ?? 1),
|
||||||
host: serverName,
|
host: serverName,
|
||||||
type: 'dub',
|
type: 'dub',
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { getAnilistIdFromMedia } from '@/utils/anilist';
|
||||||
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
import { getMalIdFromMedia } from '@/utils/mal';
|
|
||||||
|
|
||||||
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
||||||
const malId = await getMalIdFromMedia(ctx, ctx.media);
|
const anilistId = await getAnilistIdFromMedia(ctx, ctx.media);
|
||||||
|
|
||||||
const query: any = {
|
const query: any = {
|
||||||
type: ctx.media.type,
|
type: ctx.media.type,
|
||||||
title: ctx.media.title,
|
title: ctx.media.title,
|
||||||
tmdbId: ctx.media.tmdbId,
|
tmdbId: ctx.media.tmdbId,
|
||||||
imdbId: ctx.media.imdbId,
|
imdbId: ctx.media.imdbId,
|
||||||
malId,
|
anilistId,
|
||||||
...(ctx.media.type === 'show' && {
|
...(ctx.media.type === 'show' && {
|
||||||
season: ctx.media.season.number,
|
season: ctx.media.season.number,
|
||||||
episode: ctx.media.episode.number,
|
episode: ctx.media.episode.number,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { getAnilistIdFromMedia } from '@/utils/anilist';
|
||||||
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
import { getMalIdFromMedia } from '@/utils/mal';
|
|
||||||
|
|
||||||
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
||||||
const malId = await getMalIdFromMedia(ctx, ctx.media);
|
const anilistId = await getAnilistIdFromMedia(ctx, ctx.media);
|
||||||
|
|
||||||
const query: any = {
|
const query: any = {
|
||||||
type: ctx.media.type,
|
type: ctx.media.type,
|
||||||
title: ctx.media.title,
|
title: ctx.media.title,
|
||||||
tmdbId: ctx.media.tmdbId,
|
tmdbId: ctx.media.tmdbId,
|
||||||
imdbId: ctx.media.imdbId,
|
imdbId: ctx.media.imdbId,
|
||||||
malId,
|
anilistId,
|
||||||
...(ctx.media.type === 'show' && {
|
...(ctx.media.type === 'show' && {
|
||||||
season: ctx.media.season.number,
|
season: ctx.media.season.number,
|
||||||
episode: ctx.media.episode.number,
|
episode: ctx.media.episode.number,
|
||||||
|
|
|
||||||
110
src/utils/anilist.ts
Normal file
110
src/utils/anilist.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { MovieMedia, ShowMedia } from '@/entrypoint/utils/media';
|
||||||
|
import { ScrapeContext } from '@/utils/context';
|
||||||
|
|
||||||
|
type AnilistMedia = {
|
||||||
|
id: number;
|
||||||
|
type: 'ANIME' | 'MANGA';
|
||||||
|
format: 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'MUSIC' | 'MANGA' | 'NOVEL' | 'ONE_SHOT';
|
||||||
|
seasonYear?: number;
|
||||||
|
title: {
|
||||||
|
romaji: string;
|
||||||
|
english?: string;
|
||||||
|
native?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type AnilistSearchResponse = {
|
||||||
|
data: {
|
||||||
|
Page: {
|
||||||
|
media: AnilistMedia[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cache = new Map<string, number>();
|
||||||
|
|
||||||
|
function normalizeTitle(t: string) {
|
||||||
|
return t
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesType(mediaType: 'show' | 'movie', anilist: AnilistMedia) {
|
||||||
|
if (mediaType === 'show') {
|
||||||
|
return ['TV', 'TV_SHORT', 'OVA', 'ONA', 'SPECIAL'].includes(anilist.format);
|
||||||
|
}
|
||||||
|
return anilist.format === 'MOVIE';
|
||||||
|
}
|
||||||
|
|
||||||
|
const anilistQuery = `
|
||||||
|
query ($search: String, $type: MediaType) {
|
||||||
|
Page(page: 1, perPage: 20) {
|
||||||
|
media(search: $search, type: $type, sort: POPULARITY_DESC) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
format
|
||||||
|
seasonYear
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function getAnilistIdFromMedia(ctx: ScrapeContext, media: MovieMedia | ShowMedia): Promise<number> {
|
||||||
|
const key = `${media.type}:${media.title}:${media.releaseYear}`;
|
||||||
|
const cached = cache.get(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const res = await ctx.proxiedFetcher<AnilistSearchResponse>('', {
|
||||||
|
baseUrl: 'https://graphql.anilist.co',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: anilistQuery,
|
||||||
|
variables: {
|
||||||
|
search: media.title,
|
||||||
|
type: 'ANIME',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = res.data?.Page?.media ?? [];
|
||||||
|
if (!items.length) {
|
||||||
|
throw new Error('AniList id not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTitle = normalizeTitle(media.title);
|
||||||
|
|
||||||
|
const scored = items
|
||||||
|
.filter((it) => matchesType(media.type, it))
|
||||||
|
.map((it) => {
|
||||||
|
const titles: string[] = [it.title.romaji];
|
||||||
|
if (it.title.english) titles.push(it.title.english);
|
||||||
|
if (it.title.native) titles.push(it.title.native);
|
||||||
|
const normTitles = titles.map(normalizeTitle).filter(Boolean);
|
||||||
|
const exact = normTitles.includes(targetTitle);
|
||||||
|
const partial = normTitles.some((t) => t.includes(targetTitle) || targetTitle.includes(t));
|
||||||
|
const yearDelta = it.seasonYear ? Math.abs(it.seasonYear - media.releaseYear) : 5;
|
||||||
|
let score = 0;
|
||||||
|
if (exact) score += 100;
|
||||||
|
else if (partial) score += 50;
|
||||||
|
score += Math.max(0, 20 - yearDelta * 4);
|
||||||
|
return { it, score };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
const winner = scored[0]?.it ?? items[0];
|
||||||
|
const anilistId = winner?.id;
|
||||||
|
if (!anilistId) throw new Error('AniList id not found');
|
||||||
|
|
||||||
|
cache.set(key, anilistId);
|
||||||
|
return anilistId;
|
||||||
|
}
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { MovieMedia, ShowMedia } from '@/entrypoint/utils/media';
|
|
||||||
import { ScrapeContext } from '@/utils/context';
|
|
||||||
|
|
||||||
type JikanAnime = {
|
|
||||||
mal_id: number;
|
|
||||||
type?: string;
|
|
||||||
title?: string;
|
|
||||||
title_english?: string | null;
|
|
||||||
titles?: Array<{ title: string; type: string }>;
|
|
||||||
year?: number | null;
|
|
||||||
aired?: {
|
|
||||||
prop?: { from?: { year?: number | null } };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type JikanSearchResponse = {
|
|
||||||
data: JikanAnime[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const cache = new Map<string, number>();
|
|
||||||
|
|
||||||
function normalizeTitle(t: string) {
|
|
||||||
return t
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function guessYear(a: JikanAnime): number | null {
|
|
||||||
return (typeof a.year === 'number' ? a.year : null) ?? a.aired?.prop?.from?.year ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesType(mediaType: 'show' | 'movie', t?: string) {
|
|
||||||
if (!t) return true; // be lenient
|
|
||||||
const jt = t.toLowerCase();
|
|
||||||
return mediaType === 'show' ? jt === 'tv' : jt === 'movie';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMalIdFromMedia(ctx: ScrapeContext, media: MovieMedia | ShowMedia): Promise<number> {
|
|
||||||
const key = `${media.type}:${media.title}:${media.releaseYear}`;
|
|
||||||
const cached = cache.get(key);
|
|
||||||
if (cached) return cached;
|
|
||||||
|
|
||||||
const q = media.title;
|
|
||||||
// Jikan search
|
|
||||||
const res = await ctx.proxiedFetcher<JikanSearchResponse>('/anime', {
|
|
||||||
baseUrl: 'https://api.jikan.moe/v4',
|
|
||||||
query: {
|
|
||||||
q,
|
|
||||||
// Jikan expects tv|movie etc; provide a hint but still filter manually
|
|
||||||
type: media.type === 'show' ? 'tv' : 'movie',
|
|
||||||
sfw: 'true',
|
|
||||||
limit: '20',
|
|
||||||
order_by: 'popularity',
|
|
||||||
sort: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = Array.isArray(res?.data) ? res.data : [];
|
|
||||||
if (!items.length) {
|
|
||||||
throw new Error('MAL id not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetTitle = normalizeTitle(media.title);
|
|
||||||
|
|
||||||
// Score results by title similarity and year closeness
|
|
||||||
const scored = items
|
|
||||||
.filter((it) => matchesType(media.type, it.type))
|
|
||||||
.map((it) => {
|
|
||||||
const titles: string[] = [it.title || ''];
|
|
||||||
if (it.title_english) titles.push(it.title_english);
|
|
||||||
if (Array.isArray(it.titles)) titles.push(...it.titles.map((t) => t.title));
|
|
||||||
const normTitles = titles.map(normalizeTitle).filter(Boolean);
|
|
||||||
const exact = normTitles.includes(targetTitle);
|
|
||||||
const partial = normTitles.some((t) => t.includes(targetTitle) || targetTitle.includes(t));
|
|
||||||
const y = guessYear(it);
|
|
||||||
const yearDelta = typeof y === 'number' ? Math.abs(y - media.releaseYear) : 5; // unknown year => penalize
|
|
||||||
let score = 0;
|
|
||||||
if (exact) score += 100;
|
|
||||||
else if (partial) score += 50;
|
|
||||||
score += Math.max(0, 20 - yearDelta * 4);
|
|
||||||
return { it, score };
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.score - a.score);
|
|
||||||
|
|
||||||
const winner = scored[0]?.it ?? items[0];
|
|
||||||
const malId = winner?.mal_id;
|
|
||||||
if (!malId) throw new Error('MAL id not found');
|
|
||||||
|
|
||||||
cache.set(key, malId);
|
|
||||||
return malId;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue