Files
WaifuBoard/electron/api/anime/anime.service.js
2025-12-15 16:21:06 +01:00

389 lines
13 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAnimeById = getAnimeById;
exports.getTrendingAnime = getTrendingAnime;
exports.getTopAiringAnime = getTopAiringAnime;
exports.searchAnimeLocal = searchAnimeLocal;
exports.getAnimeInfoExtension = getAnimeInfoExtension;
exports.searchAnimeInExtension = searchAnimeInExtension;
exports.searchEpisodesInExtension = searchEpisodesInExtension;
exports.getStreamData = getStreamData;
const queries_1 = require("../../shared/queries");
const database_1 = require("../../shared/database");
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const TTL = 60 * 60 * 6;
const ANILIST_URL = "https://graphql.anilist.co";
const MEDIA_FIELDS = `
id
idMal
title { romaji english native userPreferred }
type
format
status
description
startDate { year month day }
endDate { year month day }
season
seasonYear
episodes
duration
chapters
volumes
countryOfOrigin
isLicensed
source
hashtag
trailer { id site thumbnail }
updatedAt
coverImage { extraLarge large medium color }
bannerImage
genres
synonyms
averageScore
popularity
isLocked
trending
favourites
isAdult
siteUrl
tags { id name description category rank isGeneralSpoiler isMediaSpoiler isAdult }
relations {
edges {
relationType
node {
id
title { romaji }
type
format
status
}
}
}
studios {
edges {
isMain
node { id name isAnimationStudio }
}
}
nextAiringEpisode { airingAt timeUntilAiring episode }
externalLinks { id url site type language color icon notes }
rankings { id rank type format year season allTime context }
stats {
scoreDistribution { score amount }
statusDistribution { status amount }
}
recommendations(perPage: 7, sort: RATING_DESC) {
nodes {
mediaRecommendation {
id
title { romaji }
coverImage { medium }
format
type
}
}
}
`;
async function fetchAniList(query, variables) {
const res = await fetch(ANILIST_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables })
});
const json = await res.json();
return json?.data;
}
async function getAnimeById(id) {
const row = await (0, database_1.queryOne)("SELECT full_data FROM anime WHERE id = ?", [id]);
if (row)
return JSON.parse(row.full_data);
const query = `
query ($id: Int) {
Media(id: $id, type: ANIME) { ${MEDIA_FIELDS} }
}
`;
const data = await fetchAniList(query, { id: Number(id) });
if (!data?.Media)
return { error: "Anime not found" };
await (0, database_1.queryOne)("INSERT INTO anime (id, title, updatedAt, full_data) VALUES (?, ?, ?, ?)", [
data.Media.id,
data.Media.title?.english || data.Media.title?.romaji,
data.Media.updatedAt || 0,
JSON.stringify(data.Media)
]);
return data.Media;
}
async function getTrendingAnime() {
const rows = await (0, database_1.queryAll)("SELECT full_data, updated_at FROM trending ORDER BY rank ASC LIMIT 10");
if (rows.length) {
const expired = (Date.now() / 1000 - rows[0].updated_at) > TTL;
if (!expired) {
return rows.map((r) => JSON.parse(r.full_data));
}
}
const query = `
query {
Page(page: 1, perPage: 10) {
media(type: ANIME, sort: TRENDING_DESC) { ${MEDIA_FIELDS} }
}
}
`;
const data = await fetchAniList(query, {});
const list = data?.Page?.media || [];
const now = Math.floor(Date.now() / 1000);
await (0, database_1.queryOne)("DELETE FROM trending");
let rank = 1;
for (const anime of list) {
await (0, database_1.queryOne)("INSERT INTO trending (rank, id, full_data, updated_at) VALUES (?, ?, ?, ?)", [rank++, anime.id, JSON.stringify(anime), now]);
}
return list;
}
async function getTopAiringAnime() {
const rows = await (0, database_1.queryAll)("SELECT full_data, updated_at FROM top_airing ORDER BY rank ASC LIMIT 10");
if (rows.length) {
const expired = (Date.now() / 1000 - rows[0].updated_at) > TTL;
if (!expired) {
return rows.map((r) => JSON.parse(r.full_data));
}
}
const query = `
query {
Page(page: 1, perPage: 10) {
media(type: ANIME, status: RELEASING, sort: SCORE_DESC) { ${MEDIA_FIELDS} }
}
}
`;
const data = await fetchAniList(query, {});
const list = data?.Page?.media || [];
const now = Math.floor(Date.now() / 1000);
await (0, database_1.queryOne)("DELETE FROM top_airing");
let rank = 1;
for (const anime of list) {
await (0, database_1.queryOne)("INSERT INTO top_airing (rank, id, full_data, updated_at) VALUES (?, ?, ?, ?)", [rank++, anime.id, JSON.stringify(anime), now]);
}
return list;
}
async function searchAnimeLocal(query) {
if (!query || query.length < 2)
return [];
const sql = `SELECT full_data FROM anime WHERE full_data LIKE ? LIMIT 50`;
const rows = await (0, database_1.queryAll)(sql, [`%${query}%`]);
const localResults = rows
.map((r) => JSON.parse(r.full_data))
.filter((anime) => {
const q = query.toLowerCase();
const titles = [
anime.title?.english,
anime.title?.romaji,
anime.title?.native,
...(anime.synonyms || [])
]
.filter(Boolean)
.map(t => t.toLowerCase());
return titles.some(t => t.includes(q));
})
.slice(0, 10);
if (localResults.length >= 5) {
return localResults;
}
const gql = `
query ($search: String) {
Page(page: 1, perPage: 10) {
media(type: ANIME, search: $search) { ${MEDIA_FIELDS} }
}
}
`;
const data = await fetchAniList(gql, { search: query });
const remoteResults = data?.Page?.media || [];
for (const anime of remoteResults) {
await (0, database_1.queryOne)("INSERT OR IGNORE INTO anime (id, title, updatedAt, full_data) VALUES (?, ?, ?, ?)", [
anime.id,
anime.title?.english || anime.title?.romaji,
anime.updatedAt || 0,
JSON.stringify(anime)
]);
}
const merged = [...localResults];
for (const anime of remoteResults) {
if (!merged.find(a => a.id === anime.id)) {
merged.push(anime);
}
if (merged.length >= 10)
break;
}
return merged;
}
async function getAnimeInfoExtension(ext, id) {
if (!ext)
return { error: "not found" };
const extName = ext.constructor.name;
const cached = await (0, queries_1.getCachedExtension)(extName, id);
if (cached) {
try {
console.log(`[${extName}] Metadata cache hit for ID: ${id}`);
return JSON.parse(cached.metadata);
}
catch {
}
}
if ((ext.type === 'anime-board') && ext.getMetadata) {
try {
const match = await ext.getMetadata(id);
if (match) {
const normalized = {
title: match.title ?? "Unknown",
summary: match.summary ?? "No summary available",
episodes: Number(match.episodes) || 0,
characters: Array.isArray(match.characters) ? match.characters : [],
season: match.season ?? null,
status: match.status ?? "Unknown",
studio: match.studio ?? "Unknown",
score: Number(match.score) || 0,
year: match.year ?? null,
genres: Array.isArray(match.genres) ? match.genres : [],
image: match.image ?? ""
};
await (0, queries_1.cacheExtension)(extName, id, normalized.title, normalized);
return normalized;
}
}
catch (e) {
console.error(`Extension getMetadata failed:`, e);
}
}
return { error: "not found" };
}
async function searchAnimeInExtension(ext, name, query) {
if (!ext)
return [];
if (ext.type === 'anime-board' && ext.search) {
try {
console.log(`[${name}] Searching for anime: ${query}`);
const matches = await ext.search({
query: query,
media: {
romajiTitle: query,
englishTitle: query,
startDate: { year: 0, month: 0, day: 0 }
}
});
if (matches && matches.length > 0) {
return matches.map(m => ({
id: m.id,
extensionName: name,
title: { romaji: m.title, english: m.title, native: null },
coverImage: { large: m.image || '' },
averageScore: m.rating || m.score || null,
format: 'ANIME',
seasonYear: null,
isExtensionResult: true,
}));
}
}
catch (e) {
console.error(`Extension search failed for ${name}:`, e);
}
}
return [];
}
async function searchEpisodesInExtension(ext, name, query) {
if (!ext)
return [];
const cacheKey = `anime:episodes:${name}:${query}`;
const cached = await (0, queries_1.getCache)(cacheKey);
if (cached) {
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
if (!isExpired) {
console.log(`[${name}] Episodes cache hit for: ${query}`);
try {
return JSON.parse(cached.result);
}
catch (e) {
console.error(`[${name}] Error parsing cached episodes:`, e);
}
}
else {
console.log(`[${name}] Episodes cache expired for: ${query}`);
}
}
if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") {
try {
const title = await (0, queries_1.getExtensionTitle)(name, query);
let mediaId;
if (!title) {
const matches = await ext.search({
query,
media: {
romajiTitle: query,
englishTitle: query,
startDate: { year: 0, month: 0, day: 0 }
}
});
if (!matches || matches.length === 0)
return [];
const res = matches[0];
if (!res?.id)
return [];
mediaId = res.id;
}
else {
mediaId = query;
}
const chapterList = await ext.findEpisodes(mediaId);
if (!Array.isArray(chapterList))
return [];
const result = chapterList.map(ep => ({
id: ep.id,
number: ep.number,
url: ep.url,
title: ep.title
}));
await (0, queries_1.setCache)(cacheKey, result, CACHE_TTL_MS);
return result;
}
catch (e) {
console.error(`Extension search failed for ${name}:`, e);
}
}
return [];
}
async function getStreamData(extension, episode, id, source, server, category) {
const providerName = extension.constructor.name;
const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`;
const cached = await (0, queries_1.getCache)(cacheKey);
if (cached) {
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
if (!isExpired) {
console.log(`[${providerName}] Stream data cache hit for episode ${episode}`);
try {
return JSON.parse(cached.result);
}
catch (e) {
console.error(`[${providerName}] Error parsing cached stream data:`, e);
}
}
else {
console.log(`[${providerName}] Stream data cache expired for episode ${episode}`);
}
}
if (!extension.findEpisodes || !extension.findEpisodeServer) {
throw new Error("Extension doesn't support required methods");
}
let episodes;
if (source === "anilist") {
const anime = await getAnimeById(id);
episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji);
}
else {
episodes = await extension.findEpisodes(id);
}
const targetEp = episodes.find(e => e.number === parseInt(episode));
if (!targetEp) {
throw new Error("Episode not found");
}
const serverName = server || "default";
const streamData = await extension.findEpisodeServer(targetEp, serverName);
await (0, queries_1.setCache)(cacheKey, streamData, CACHE_TTL_MS);
return streamData;
}