"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getBookById = getBookById; exports.getTrendingBooks = getTrendingBooks; exports.getPopularBooks = getPopularBooks; exports.searchBooksLocal = searchBooksLocal; exports.searchBooksAniList = searchBooksAniList; exports.getBookInfoExtension = getBookInfoExtension; exports.searchBooksInExtension = searchBooksInExtension; exports.getChaptersForBook = getChaptersForBook; exports.getChapterContent = getChapterContent; const queries_1 = require("../../shared/queries"); const database_1 = require("../../shared/database"); const extensions_1 = require("../../shared/extensions"); const CACHE_TTL_MS = 24 * 60 * 60 * 1000; const TTL = 60 * 60 * 6; const ANILIST_URL = "https://graphql.anilist.co"; async function fetchAniList(query, variables) { const res = await fetch(ANILIST_URL, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ query, variables }) }); if (!res.ok) { throw new Error(`AniList error ${res.status}`); } const json = await res.json(); return json?.data; } const MEDIA_FIELDS = ` id title { romaji english native userPreferred } type format status description startDate { year month day } endDate { year month day } season seasonYear episodes chapters volumes duration genres synonyms averageScore popularity favourites isAdult siteUrl coverImage { extraLarge large medium color } bannerImage updatedAt `; async function getBookById(id) { const row = await (0, database_1.queryOne)("SELECT full_data FROM books WHERE id = ?", [id]); if (row) { return JSON.parse(row.full_data); } try { console.log(`[Book] Local miss for ID ${id}, fetching live...`); const query = ` query ($id: Int) { Media(id: $id, type: MANGA) { id idMal title { romaji english native userPreferred } type format status description startDate { year month day } endDate { year month day } season seasonYear seasonInt episodes duration chapters volumes countryOfOrigin isLicensed source hashtag trailer { id site thumbnail } updatedAt coverImage { extraLarge large medium color } bannerImage genres synonyms averageScore meanScore popularity isLocked trending favourites tags { id name description category rank isGeneralSpoiler isMediaSpoiler isAdult userId } relations { edges { relationType node { id title { romaji } } } } characters(page: 1, perPage: 10) { nodes { id name { full } } } studios { nodes { id name isAnimationStudio } } isAdult nextAiringEpisode { airingAt timeUntilAiring episode } externalLinks { url site } rankings { id rank type format year season allTime context } } }`; const response = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ query, variables: { id: parseInt(id.toString()) } }) }); const data = await response.json(); if (data?.data?.Media) { const media = data.data.Media; const insertSql = ` INSERT INTO books (id, title, updatedAt, full_data) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET title = EXCLUDED.title, updatedAt = EXCLUDED.updatedAt, full_data = EXCLUDED.full_data; `; await (0, database_1.run)(insertSql, [ media.id, media.title?.userPreferred || media.title?.romaji || media.title?.english || null, media.updatedAt || Math.floor(Date.now() / 1000), JSON.stringify(media) ]); return media; } } catch (e) { console.error("Fetch error:", e); } return { error: "Book not found" }; } async function getTrendingBooks() { const rows = await (0, database_1.queryAll)("SELECT full_data, updated_at FROM trending_books 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: MANGA, 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_books"); let rank = 1; for (const book of list) { await (0, database_1.queryOne)("INSERT INTO trending_books (rank, id, full_data, updated_at) VALUES (?, ?, ?, ?)", [rank++, book.id, JSON.stringify(book), now]); } return list; } async function getPopularBooks() { const rows = await (0, database_1.queryAll)("SELECT full_data, updated_at FROM popular_books 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: MANGA, sort: POPULARITY_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 popular_books"); let rank = 1; for (const book of list) { await (0, database_1.queryOne)("INSERT INTO popular_books (rank, id, full_data, updated_at) VALUES (?, ?, ?, ?)", [rank++, book.id, JSON.stringify(book), now]); } return list; } async function searchBooksLocal(query) { if (!query || query.length < 2) { return []; } const sql = `SELECT full_data FROM books WHERE full_data LIKE ? LIMIT 50`; const rows = await (0, database_1.queryAll)(sql, [`%${query}%`]); const results = rows.map((row) => JSON.parse(row.full_data)); const clean = results.filter(book => { const searchTerms = [ book.title.english, book.title.romaji, book.title.native, ...(book.synonyms || []) ].filter(Boolean).map(t => t.toLowerCase()); return searchTerms.some(term => term.includes(query.toLowerCase())); }); return clean.slice(0, 10); } async function searchBooksAniList(query) { const gql = ` query ($search: String) { Page(page: 1, perPage: 5) { media(search: $search, type: MANGA, isAdult: false) { id title { romaji english native } coverImage { extraLarge large } bannerImage description averageScore format seasonYear startDate { year } } } }`; const response = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ query: gql, variables: { search: query } }) }); const liveData = await response.json(); if (liveData.data && liveData.data.Page.media.length > 0) { return liveData.data.Page.media; } return []; } async function getBookInfoExtension(ext, id) { if (!ext) return []; const extName = ext.constructor.name; const cached = await (0, queries_1.getCachedExtension)(extName, id); if (cached) { try { return JSON.parse(cached.metadata); } catch { } } if (ext.type === 'book-board' && ext.getMetadata) { try { const info = await ext.getMetadata(id); if (info) { await (0, queries_1.cacheExtension)(extName, id, info.title, info); return info; } } catch (e) { console.error(`Extension getInfo failed:`, e); } } return []; } async function searchBooksInExtension(ext, name, query) { if (!ext) return []; if ((ext.type === 'book-board') && ext.search) { try { console.log(`[${name}] Searching for book: ${query}`); const matches = await ext.search({ query: query, media: { romajiTitle: query, englishTitle: query, startDate: { year: 0, month: 0, day: 0 } } }); if (matches?.length) { 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: m.format, seasonYear: null, isExtensionResult: true })); } } catch (e) { console.error(`Extension search failed for ${name}:`, e); } } return []; } async function fetchBookMetadata(id) { try { const query = `query ($id: Int) { Media(id: $id, type: MANGA) { title { romaji english } startDate { year month day } } }`; const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables: { id: parseInt(id) } }) }); const d = await res.json(); return d.data?.Media || null; } catch (e) { console.error("Failed to fetch book metadata:", e); return null; } } async function searchChaptersInExtension(ext, name, searchTitle, search, origin) { const cacheKey = `chapters:${name}:${origin}:${search ? "search" : "id"}:${searchTitle}`; 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}] Chapters cache hit for: ${searchTitle}`); try { return JSON.parse(cached.result); } catch (e) { console.error(`[${name}] Error parsing cached chapters:`, e); } } else { console.log(`[${name}] Chapters cache expired for: ${searchTitle}`); } } try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); let mediaId; if (search) { const matches = await ext.search({ query: searchTitle, media: { romajiTitle: searchTitle, englishTitle: searchTitle, startDate: { year: 0, month: 0, day: 0 } } }); const best = matches?.[0]; if (!best) { return []; } mediaId = best.id; } else { const match = await ext.getMetadata(searchTitle); mediaId = match.id; } const chaps = await ext.findChapters(mediaId); if (!chaps?.length) { return []; } console.log(`[${name}] Found ${chaps.length} chapters.`); const result = chaps.map((ch) => ({ id: ch.id, number: parseFloat(ch.number.toString()), title: ch.title, date: ch.releaseDate, provider: name, index: ch.index })); await (0, queries_1.setCache)(cacheKey, result, CACHE_TTL_MS); return result; } catch (e) { const error = e; console.error(`Failed to fetch chapters from ${name}:`, error.message); return []; } } async function getChaptersForBook(id, ext, onlyProvider) { let bookData = null; let searchTitle = ""; if (!ext) { const result = await getBookById(id); if (!result || "error" in result) return { chapters: [] }; bookData = result; const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean); searchTitle = titles[0]; } const bookExtensions = (0, extensions_1.getBookExtensionsMap)(); let extension; if (!searchTitle) { for (const [name, ext] of bookExtensions) { const title = await (0, queries_1.getExtensionTitle)(name, id); if (title) { searchTitle = title; extension = name; } } } const allChapters = []; let exts = "anilist"; if (ext) exts = "ext"; for (const [name, ext] of bookExtensions) { if (onlyProvider && name !== onlyProvider) continue; if (name == extension) { const chapters = await searchChaptersInExtension(ext, name, id, false, exts); allChapters.push(...chapters); } else { const chapters = await searchChaptersInExtension(ext, name, searchTitle, true, exts); allChapters.push(...chapters); } } return { chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) }; } async function getChapterContent(bookId, chapterIndex, providerName, source) { const extensions = (0, extensions_1.getAllExtensions)(); const ext = extensions.get(providerName); if (!ext) { throw new Error("Provider not found"); } const contentCacheKey = `content:${providerName}:${source}:${bookId}:${chapterIndex}`; const cachedContent = await (0, queries_1.getCache)(contentCacheKey); if (cachedContent) { const isExpired = Date.now() - cachedContent.created_at > CACHE_TTL_MS; if (!isExpired) { console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterIndex}`); try { return JSON.parse(cachedContent.result); } catch (e) { console.error(`[${providerName}] Error parsing cached content:`, e); } } else { console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterIndex}`); } } const isExternal = source !== 'anilist'; const chapterList = await getChaptersForBook(bookId, isExternal, providerName); if (!chapterList?.chapters || chapterList.chapters.length === 0) { throw new Error("Chapters not found"); } const providerChapters = chapterList.chapters.filter(c => c.provider === providerName); const index = parseInt(chapterIndex, 10); if (Number.isNaN(index)) { throw new Error("Invalid chapter index"); } if (!providerChapters[index]) { throw new Error("Chapter index out of range"); } const selectedChapter = providerChapters[index]; const chapterId = selectedChapter.id; const chapterTitle = selectedChapter.title || null; const chapterNumber = typeof selectedChapter.number === 'number' ? selectedChapter.number : index; try { if (!ext.findChapterPages) { throw new Error("Extension doesn't support findChapterPages"); } let contentResult; if (ext.mediaType === "manga") { const pages = await ext.findChapterPages(chapterId); contentResult = { type: "manga", chapterId, title: chapterTitle, number: chapterNumber, provider: providerName, pages }; } else if (ext.mediaType === "ln") { const content = await ext.findChapterPages(chapterId); contentResult = { type: "ln", chapterId, title: chapterTitle, number: chapterNumber, provider: providerName, content }; } else { throw new Error("Unknown mediaType"); } await (0, queries_1.setCache)(contentCacheKey, contentResult, CACHE_TTL_MS); return contentResult; } catch (err) { const error = err; console.error(`[Chapter] Error loading from ${providerName}:`, error.message); throw err; } }