"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) { await (0, queries_1.cacheExtension)(extName, id, match.title, match); return match; } } 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; }