import { getCache, setCache, getCachedExtension, cacheExtension, getExtensionTitle } from '../../shared/queries'; import { queryAll, queryOne } from '../../shared/database'; import {Anime, Episode, Extension, StreamData} from '../types'; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; export async function getAnimeById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); if (!row) { return { error: "Anime not found" }; } return JSON.parse(row.full_data); } export async function getTrendingAnime(): Promise { const rows = await queryAll("SELECT full_data FROM trending ORDER BY rank ASC LIMIT 10"); return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } export async function getTopAiringAnime(): Promise { const rows = await queryAll("SELECT full_data FROM top_airing ORDER BY rank ASC LIMIT 10"); return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } export async function searchAnimeLocal(query: string): Promise { if (!query || query.length < 2) { return []; } const sql = `SELECT full_data FROM anime WHERE full_data LIKE ? LIMIT 50`; const rows = await queryAll(sql, [`%${query}%`]); const results: Anime[] = rows.map((row: { full_data: string; }) => JSON.parse(row.full_data)); const cleanResults = results.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)); }); return cleanResults.slice(0, 10); } export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise { if (!ext) return { error: "not found" }; const extName = ext.constructor.name; const cached = await getCachedExtension(extName, id); if (cached) { try { console.log(`[${extName}] Metadata cache hit for ID: ${id}`); return JSON.parse(cached.metadata) as Anime; } catch { } } if ((ext.type === 'anime-board') && ext.getMetadata) { try { const match = await ext.getMetadata(id); if (match) { await cacheExtension(extName, id, match.title, match); return match; } } catch (e) { console.error(`Extension getMetadata failed:`, e); } } return { error: "not found" }; } export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise { 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 []; } export async function searchEpisodesInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; const cacheKey = `anime:episodes:${name}:${query}`; const cached = await 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) as Episode[]; } 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 getExtensionTitle(name, query); let mediaId: string; 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: Episode[] = chapterList.map(ep => ({ id: ep.id, number: ep.number, url: ep.url, title: ep.title })); await setCache(cacheKey, result, CACHE_TTL_MS); return result; } catch (e) { console.error(`Extension search failed for ${name}:`, e); } } return []; } export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise { const providerName = extension.constructor.name; const cacheKey = `anime:stream:${providerName}:${animeData.id}:${episode}:${server || 'default'}:${category || 'sub'}`; const cached = await 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) as StreamData; } catch (e) { console.error(`[${providerName}] Error parsing cached stream data:`, e); } } else { console.log(`[${providerName}] Stream data cache expired for episode ${episode}`); } } const searchOptions = { query: animeData.title.english || animeData.title.romaji, dub: category === 'dub', media: { romajiTitle: animeData.title.romaji, englishTitle: animeData.title.english || "", startDate: animeData.startDate || { year: 0, month: 0, day: 0 } } }; if (!extension.search || !extension.findEpisodes || !extension.findEpisodeServer) { throw new Error("Extension doesn't support required methods"); } const searchResults = await extension.search(searchOptions); if (!searchResults || searchResults.length === 0) { throw new Error("Anime not found on provider"); } const bestMatch = searchResults[0]; const episodes = await extension.findEpisodes(bestMatch.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 setCache(cacheKey, streamData, CACHE_TTL_MS); return streamData; }