const { queryOne, queryAll } = require('../shared/database'); const { getAllExtensions } = require('../shared/extensions'); async function getBookById(id) { const row = await 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) } }) }); const data = await response.json(); if (data.data && data.data.Media) { return data.data.Media; } } catch(e) { console.error("Fetch error:", e); } return { error: "Book not found" }; } async function getTrendingBooks() { const rows = await queryAll("SELECT full_data FROM trending_books ORDER BY rank ASC LIMIT 10"); return rows.map(r => JSON.parse(r.full_data)); } async function getPopularBooks() { const rows = await queryAll("SELECT full_data FROM popular_books ORDER BY rank ASC LIMIT 10"); return rows.map(r => JSON.parse(r.full_data)); } 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 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 searchBooksInExtension(ext, name, query) { if (!ext) return []; if ((ext.type === 'book-board' || ext.type === 'manga-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 && matches.length > 0) { return matches.map(m => ({ id: m.id, extensionName: name, title: { romaji: m.title, english: m.title }, coverImage: { large: m.image || '' }, averageScore: m.rating || m.score || null, format: 'MANGA', seasonYear: null, isExtensionResult: true })); } } catch (e) { console.error(`Extension search failed for ${name}:`, e); } } return []; } async function searchBooksExtensions(query) { const extensions = getAllExtensions(); for (const [name, ext] of extensions) { const results = await searchBooksInExtension(ext, name, query); if (results.length > 0) return results; } return []; } async function getChaptersForBook(id) { let bookData = null; let searchTitle = null; if (typeof id === "string" && isNaN(Number(id))) { searchTitle = id.replaceAll("-", " "); } else { bookData = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]) .then(row => row ? JSON.parse(row.full_data) : null) .catch(() => null); if (!bookData) { 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(); if (d.data?.Media) bookData = d.data.Media; } catch (e) {} } if (!bookData) return { chapters: [] }; const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean); searchTitle = titles[0]; } const allChapters = []; const extensions = getAllExtensions(); const searchPromises = Array.from(extensions.entries()) .filter(([_, ext]) => (ext.type === 'book-board' || ext.type === 'manga-board') && ext.search && ext.findChapters ) .map(async ([name, ext]) => { try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); const matches = await ext.search({ query: searchTitle, media: bookData ? { romajiTitle: bookData.title.romaji, englishTitle: bookData.title.english, startDate: bookData.startDate } : {} }); if (matches?.length) { const best = matches[0]; const chaps = await ext.findChapters(best.id); if (chaps?.length) { console.log(`[${name}] Found ${chaps.length} chapters.`); chaps.forEach(ch => { allChapters.push({ id: ch.id, number: parseFloat(ch.number), title: ch.title, date: ch.releaseDate, provider: name }); }); } } else { console.log(`[${name}] No matches found for book.`); } } catch (e) { console.error(`Failed to fetch chapters from ${name}:`, e.message); } }); await Promise.all(searchPromises); return { chapters: allChapters.sort((a, b) => a.number - b.number) }; } async function getChapterContent(bookId, chapterIndex, providerName) { const extensions = getAllExtensions(); const ext = extensions.get(providerName); if (!ext) { throw new Error("Provider not found"); } const chapterList = await getChaptersForBook(bookId); 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.mediaType === "manga") { const pages = await ext.findChapterPages(chapterId); return { type: "manga", chapterId, title: chapterTitle, number: chapterNumber, provider: providerName, pages }; } if (ext.mediaType === "ln") { const content = await ext.findChapterPages(chapterId); return { type: "ln", chapterId, title: chapterTitle, number: chapterNumber, provider: providerName, content }; } throw new Error("Unknown mediaType"); } catch (err) { console.error(`[Chapter] Error loading from ${providerName}:`, err && err.message ? err.message : err); throw err; } } module.exports = { getBookById, getTrendingBooks, getPopularBooks, searchBooksLocal, searchBooksAniList, searchBooksExtensions, searchBooksInExtension, getChaptersForBook, getChapterContent };