Fixed bundling into an exe not working as intended.
This commit is contained in:
140
electron/api/books/books.controller.js
Normal file
140
electron/api/books/books.controller.js
Normal file
@@ -0,0 +1,140 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getBook = getBook;
|
||||
exports.getTrending = getTrending;
|
||||
exports.getPopular = getPopular;
|
||||
exports.searchBooks = searchBooks;
|
||||
exports.searchBooksInExtension = searchBooksInExtension;
|
||||
exports.getChapters = getChapters;
|
||||
exports.getChapterContent = getChapterContent;
|
||||
const booksService = __importStar(require("./books.service"));
|
||||
const extensions_1 = require("../../shared/extensions");
|
||||
async function getBook(req, reply) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const source = req.query.source;
|
||||
let book;
|
||||
if (source === 'anilist') {
|
||||
book = await booksService.getBookById(id);
|
||||
}
|
||||
else {
|
||||
const ext = (0, extensions_1.getExtension)(source);
|
||||
const result = await booksService.getBookInfoExtension(ext, id);
|
||||
book = result || null;
|
||||
}
|
||||
return book;
|
||||
}
|
||||
catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
}
|
||||
async function getTrending(req, reply) {
|
||||
try {
|
||||
const results = await booksService.getTrendingBooks();
|
||||
return { results };
|
||||
}
|
||||
catch (err) {
|
||||
return { results: [] };
|
||||
}
|
||||
}
|
||||
async function getPopular(req, reply) {
|
||||
try {
|
||||
const results = await booksService.getPopularBooks();
|
||||
return { results };
|
||||
}
|
||||
catch (err) {
|
||||
return { results: [] };
|
||||
}
|
||||
}
|
||||
async function searchBooks(req, reply) {
|
||||
try {
|
||||
const query = req.query.q;
|
||||
const dbResults = await booksService.searchBooksLocal(query);
|
||||
if (dbResults.length > 0) {
|
||||
return { results: dbResults };
|
||||
}
|
||||
console.log(`[Books] Local DB miss for "${query}", fetching live...`);
|
||||
const anilistResults = await booksService.searchBooksAniList(query);
|
||||
if (anilistResults.length > 0) {
|
||||
return { results: anilistResults };
|
||||
}
|
||||
return { results: [] };
|
||||
}
|
||||
catch (e) {
|
||||
const error = e;
|
||||
console.error("Search Error:", error.message);
|
||||
return { results: [] };
|
||||
}
|
||||
}
|
||||
async function searchBooksInExtension(req, reply) {
|
||||
try {
|
||||
const extensionName = req.params.extension;
|
||||
const query = req.query.q;
|
||||
const ext = (0, extensions_1.getExtension)(extensionName);
|
||||
if (!ext)
|
||||
return { results: [] };
|
||||
const results = await booksService.searchBooksInExtension(ext, extensionName, query);
|
||||
return { results };
|
||||
}
|
||||
catch (e) {
|
||||
const error = e;
|
||||
console.error("Search Error:", error.message);
|
||||
return { results: [] };
|
||||
}
|
||||
}
|
||||
async function getChapters(req, reply) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const source = req.query.source || 'anilist';
|
||||
const isExternal = source !== 'anilist';
|
||||
return await booksService.getChaptersForBook(id, isExternal);
|
||||
}
|
||||
catch {
|
||||
return { chapters: [] };
|
||||
}
|
||||
}
|
||||
async function getChapterContent(req, reply) {
|
||||
try {
|
||||
const { bookId, chapter, provider } = req.params;
|
||||
const source = req.query.source || 'anilist';
|
||||
const content = await booksService.getChapterContent(bookId, chapter, provider, source);
|
||||
return reply.send(content);
|
||||
}
|
||||
catch (err) {
|
||||
console.error("getChapterContent error:", err.message);
|
||||
return reply.code(500).send({ error: "Error loading chapter" });
|
||||
}
|
||||
}
|
||||
46
electron/api/books/books.routes.js
Normal file
46
electron/api/books/books.routes.js
Normal file
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const controller = __importStar(require("./books.controller"));
|
||||
async function booksRoutes(fastify) {
|
||||
fastify.get('/book/:id', controller.getBook);
|
||||
fastify.get('/books/trending', controller.getTrending);
|
||||
fastify.get('/books/popular', controller.getPopular);
|
||||
fastify.get('/search/books', controller.searchBooks);
|
||||
fastify.get('/search/books/:extension', controller.searchBooksInExtension);
|
||||
fastify.get('/book/:id/chapters', controller.getChapters);
|
||||
fastify.get('/book/:bookId/:chapter/:provider', controller.getChapterContent);
|
||||
}
|
||||
exports.default = booksRoutes;
|
||||
482
electron/api/books/books.service.js
Normal file
482
electron/api/books/books.service.js
Normal file
@@ -0,0 +1,482 @@
|
||||
"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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user