new format and marketplace
This commit is contained in:
104
book/asmhentai.js
Normal file
104
book/asmhentai.js
Normal file
@@ -0,0 +1,104 @@
|
||||
class asmhentai {
|
||||
constructor() {
|
||||
this.baseUrl = "https://asmhentai.com";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const q = (queryObj.query || "").trim().replace(/\s+/g, "+");
|
||||
const html = await fetch(`${this.baseUrl}/search/?q=${q}&page=1`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
|
||||
$(".preview_item").each((_, el) => {
|
||||
const href = $(el).find(".image a").attr("href");
|
||||
const id = href?.match(/\/g\/(\d+)\//)?.[1];
|
||||
if (!id) return;
|
||||
|
||||
let img = $(el).find(".image img").attr("data-src") || $(el).find(".image img").attr("src") || "";
|
||||
if (img.startsWith("//")) img = "https:" + img;
|
||||
|
||||
const image = img.replace("thumb.jpg", "1.jpg");
|
||||
const title = $(el).find("h2.caption").text().trim();
|
||||
|
||||
results.push({
|
||||
id,
|
||||
image,
|
||||
title,
|
||||
rating: null,
|
||||
type: "book"
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const html = await fetch(`${this.baseUrl}/g/${id}/`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
let image =
|
||||
$('a[href^="/gallery/"] img').attr("data-src") ||
|
||||
$('a[href^="/gallery/"] img').attr("src") ||
|
||||
"";
|
||||
|
||||
if (image.startsWith("//")) image = "https:" + image;
|
||||
|
||||
const genres = $(".tags .tag_list .badge.tag")
|
||||
.map((_, el) => $(el).clone().children().remove().end().text().trim())
|
||||
.get()
|
||||
.join(", ");
|
||||
|
||||
return {
|
||||
id,
|
||||
title: $("h1").first().text().trim(),
|
||||
format: "MANGA",
|
||||
score: 0,
|
||||
genres,
|
||||
status: "unknown",
|
||||
published: "???",
|
||||
summary: "",
|
||||
chapters: 1,
|
||||
image
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
const html = await fetch(`${this.baseUrl}/g/${mangaId}/`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const title = $("h1").first().text().trim() || "Chapter 1";
|
||||
|
||||
let thumb = $(".gallery img").first().attr("data-src") || "";
|
||||
if (thumb.startsWith("//")) thumb = "https:" + thumb;
|
||||
|
||||
const base = thumb.match(/https:\/\/[^\/]+\/\d+\/\d+\//)?.[0];
|
||||
const pages = parseInt($(".pages").text().match(/\d+/)?.[0] || "0");
|
||||
const ext = thumb.match(/\.(jpg|png|jpeg|gif)/i)?.[1] || "jpg";
|
||||
|
||||
const chapterId = Buffer.from(JSON.stringify({ base, pages, ext })).toString("base64");
|
||||
|
||||
return [{
|
||||
id: chapterId,
|
||||
title,
|
||||
number: 1,
|
||||
releaseDate: null,
|
||||
index: 0
|
||||
}];
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const { base, pages, ext } = JSON.parse(
|
||||
Buffer.from(chapterId, "base64").toString("utf8")
|
||||
);
|
||||
|
||||
return Array.from({ length: pages }, (_, i) => ({
|
||||
url: `${base}${i + 1}.${ext}`,
|
||||
index: i
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = asmhentai;
|
||||
188
book/lightnovelworld.js
Normal file
188
book/lightnovelworld.js
Normal file
@@ -0,0 +1,188 @@
|
||||
class lightnovelworld {
|
||||
constructor() {
|
||||
this.baseUrl = "https://lightnovelworld.org/api";
|
||||
|
||||
this.type = "book-board";
|
||||
this.mediaType = "ln";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query?.trim() || "";
|
||||
|
||||
if (query !== "") {
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.novels) return [];
|
||||
|
||||
return data.novels.map(n => ({
|
||||
id: n.slug,
|
||||
title: n.title,
|
||||
image: `https://lightnovelworld.org/${n.cover_path}`,
|
||||
rating: `Rank ${n.rank}`,
|
||||
format: "Light Novel"
|
||||
}));
|
||||
}
|
||||
|
||||
const res = await fetch("https://lightnovelworld.org/");
|
||||
const html = await res.text();
|
||||
|
||||
const cards = html.split('class="recommendation-card"').slice(1);
|
||||
const results = [];
|
||||
|
||||
for (const block of cards) {
|
||||
const link = block.match(/href="([^"]+)"/)?.[1] || "";
|
||||
const id = link.replace(/^\/novel\//, "").replace(/\/$/, "");
|
||||
|
||||
const title = block.match(/class="card-title"[^>]*>([^<]+)/)?.[1]?.trim() || null;
|
||||
|
||||
let img = block.match(/<img[^>]+src="([^"]+)"/)?.[1] || "";
|
||||
if (img && !img.startsWith("http"))
|
||||
img = `https://lightnovelworld.org${img}`;
|
||||
|
||||
if (id && title) {
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image: img,
|
||||
rating: null,
|
||||
format: "Light Novel"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getMetadata(id){
|
||||
const res = await fetch(`https://lightnovelworld.org/novel/${id}`);
|
||||
const html = await res.text();
|
||||
|
||||
const match = html.match(
|
||||
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/
|
||||
);
|
||||
|
||||
let data = {};
|
||||
if(match){
|
||||
try{
|
||||
data = JSON.parse(match[1]);
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
const rawScore = Number(data.aggregateRating?.ratingValue || 1);
|
||||
const score100 = Math.round((rawScore / 5) * 100);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
title: data.name || "",
|
||||
format: "Light Novel",
|
||||
score: score100,
|
||||
genres: Array.isArray(data.genre) ? data.genre : [],
|
||||
status: data.status || "",
|
||||
published: "???",
|
||||
summary: data.description || "",
|
||||
chapters: data.numberOfPages ? Number(data.numberOfPages) : 1,
|
||||
image: data.image
|
||||
? (data.image.startsWith("http")
|
||||
? data.image
|
||||
: `https://lightnovelworld.org${data.image}`)
|
||||
: ""
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(bookId) {
|
||||
const chapters = [];
|
||||
let offset = 0;
|
||||
const limit = 500;
|
||||
|
||||
while (true) {
|
||||
const res = await fetch(
|
||||
`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.chapters) break;
|
||||
|
||||
chapters.push(
|
||||
...data.chapters.map((c, i) => ({
|
||||
id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`,
|
||||
title: c.title,
|
||||
number: Number(c.number),
|
||||
releaseDate: null,
|
||||
index: offset + i
|
||||
}))
|
||||
);
|
||||
|
||||
if (!data.has_more) break;
|
||||
offset += limit;
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const data = await this.scrape(
|
||||
chapterId,
|
||||
(page) => page.evaluate(() => document.documentElement.outerHTML),
|
||||
{
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
const html = data.result;
|
||||
if (!html) return '<p>Error loading chapter</p>';
|
||||
|
||||
const cutPoints = [
|
||||
'<div class="bottom-nav"',
|
||||
'<div class="comments-section"',
|
||||
'<div class="settings-panel"',
|
||||
'©'
|
||||
];
|
||||
|
||||
let cutIndex = html.length;
|
||||
for (const marker of cutPoints) {
|
||||
const pos = html.indexOf(marker);
|
||||
if (pos !== -1 && pos < cutIndex) cutIndex = pos;
|
||||
}
|
||||
|
||||
const chapterHtml = html.substring(0, cutIndex);
|
||||
|
||||
const pMatches = [...chapterHtml.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)];
|
||||
let cleanHtml = '';
|
||||
|
||||
for (const match of pMatches) {
|
||||
let text = match[1]
|
||||
.replace(/△▼△▼△▼△/g, '')
|
||||
.replace(/[※▲▼■◆]/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
|
||||
text = text
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/“/g, '“')
|
||||
.replace(/”/g, '”')
|
||||
.replace(/‘/g, '‘')
|
||||
.replace(/’/g, '’')
|
||||
.replace(/—/g, '—')
|
||||
.replace(/–/g, '–');
|
||||
|
||||
if (!text || text.length < 3) continue;
|
||||
if (/svg|button|modal|comment|loading|default|dyslexic|roboto|lora|line spacing/i.test(text)) continue;
|
||||
|
||||
cleanHtml += `<p>${text}</p>\n`;
|
||||
}
|
||||
|
||||
return cleanHtml.trim() || '<p>Empty chapter</p>';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = lightnovelworld;
|
||||
206
book/mangadex.js
Normal file
206
book/mangadex.js
Normal file
@@ -0,0 +1,206 @@
|
||||
class MangaDex {
|
||||
constructor() {
|
||||
this.baseUrl = "https://mangadex.org";
|
||||
this.apiUrl = "https://api.mangadex.org";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
return {
|
||||
'User-Agent': 'MangaDex-Client-Adapter/1.0',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query?.trim() || "";
|
||||
const limit = 25;
|
||||
const offset = (1 - 1) * limit;
|
||||
|
||||
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers: this.getHeaders() });
|
||||
if (!response.ok) {
|
||||
console.error(`MangaDex API Error: ${response.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
if (!json || !Array.isArray(json.data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return json.data.map(manga => {
|
||||
const attributes = manga.attributes;
|
||||
const titleObject = attributes.title || {};
|
||||
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
|
||||
|
||||
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
|
||||
const coverFileName = coverRelationship?.attributes?.fileName;
|
||||
|
||||
const coverUrl = coverFileName
|
||||
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
|
||||
: '';
|
||||
|
||||
|
||||
return {
|
||||
id: manga.id,
|
||||
image: coverUrl,
|
||||
title: title,
|
||||
rating: null,
|
||||
type: 'book'
|
||||
};
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error during MangaDex search:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
try {
|
||||
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
|
||||
if (!res.ok) throw new Error("MangaDex API error");
|
||||
|
||||
const json = await res.json();
|
||||
const manga = json.data;
|
||||
const attr = manga.attributes;
|
||||
|
||||
const title =
|
||||
attr.title?.en ||
|
||||
Object.values(attr.title || {})[0] ||
|
||||
"";
|
||||
|
||||
const summary =
|
||||
attr.description?.en ||
|
||||
Object.values(attr.description || {})[0] ||
|
||||
"";
|
||||
|
||||
const genres = manga.relationships
|
||||
?.filter(r => r.type === "tag")
|
||||
?.map(r =>
|
||||
r.attributes?.name?.en ||
|
||||
Object.values(r.attributes?.name || {})[0]
|
||||
)
|
||||
?.filter(Boolean) || [];
|
||||
|
||||
const coverRel = manga.relationships.find(r => r.type === "cover_art");
|
||||
const coverFile = coverRel?.attributes?.fileName;
|
||||
const image = coverFile
|
||||
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
|
||||
: "";
|
||||
|
||||
const score100 = 0;
|
||||
|
||||
const statusMap = {
|
||||
ongoing: "Ongoing",
|
||||
completed: "Completed",
|
||||
hiatus: "Hiatus",
|
||||
cancelled: "Cancelled"
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
format: "Manga",
|
||||
score: score100,
|
||||
genres,
|
||||
status: statusMap[attr.status] || "",
|
||||
published: attr.year ? String(attr.year) : "???",
|
||||
summary,
|
||||
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
|
||||
image
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error("MangaDex getMetadata error:", e);
|
||||
return {
|
||||
id,
|
||||
title: "",
|
||||
format: "Manga",
|
||||
score: 0,
|
||||
genres: [],
|
||||
status: "",
|
||||
published: "???",
|
||||
summary: "",
|
||||
chapters: 0,
|
||||
image: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
if (!mangaId) return [];
|
||||
|
||||
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers: this.getHeaders() });
|
||||
let chapters = [];
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
if (json && Array.isArray(json.data)) {
|
||||
const allChapters = json.data
|
||||
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
|
||||
.map((ch, index) => ({
|
||||
id: ch.id,
|
||||
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
|
||||
number: ch.attributes.chapter,
|
||||
index: index,
|
||||
language: ch.attributes.translatedLanguage
|
||||
}));
|
||||
|
||||
const seenChapters = new Set();
|
||||
allChapters.forEach(ch => {
|
||||
if (!seenChapters.has(ch.chapter)) {
|
||||
seenChapters.add(ch.chapter);
|
||||
chapters.push(ch);
|
||||
}
|
||||
});
|
||||
|
||||
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
|
||||
}
|
||||
}
|
||||
|
||||
return chapters;
|
||||
} catch (e) {
|
||||
console.error("Error finding MangaDex chapters:", e);
|
||||
return { chapters: [], cover: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
if (!chapterId) return [];
|
||||
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers: this.getHeaders() });
|
||||
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
|
||||
|
||||
const json = await response.json();
|
||||
if (!json || !json.baseUrl || !json.chapter) return [];
|
||||
|
||||
const baseUrl = json.baseUrl;
|
||||
const chapterHash = json.chapter.hash;
|
||||
const imageFilenames = json.chapter.data;
|
||||
|
||||
return imageFilenames.map((filename, index) => ({
|
||||
url: `${baseUrl}/data/${chapterHash}/${filename}`,
|
||||
index: index,
|
||||
headers: {
|
||||
'Referer': `https://mangadex.org/chapter/${chapterId}`
|
||||
}
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Error finding MangaDex pages:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MangaDex;
|
||||
174
book/mangapark.js
Normal file
174
book/mangapark.js
Normal file
@@ -0,0 +1,174 @@
|
||||
class mangapark {
|
||||
constructor() {
|
||||
this.baseUrl = "https://mangapark.net/apo";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query;
|
||||
|
||||
const res = await fetch(`${this.baseUrl}/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"x-apollo-operation-name": "get_searchComic",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: "query get_searchComic($select: SearchComic_Select) { get_searchComic(select: $select) { reqPage reqSize reqSort reqWord newPage paging { total pages page init size skip limit prev next } items { id data { id dbStatus name origLang tranLang urlPath urlCover600 urlCoverOri genres altNames authors artists is_hot is_new sfw_result score_val follows reviews comments_total max_chapterNode { id data { id dateCreate dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } sser_follow sser_lastReadChap { date chapterNode { id data { id dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } } } }",
|
||||
variables: {
|
||||
select: { word: query, size: 10, page: 1, sortby: "field_score" }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.data || !data.data.get_searchComic || !data.data.get_searchComic.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.data.get_searchComic.items.map(m => ({
|
||||
id: m.data.urlPath.split('/title/')[1]?.split('-')[0] || mangaId.split('/comic/')[1]?.split('-')[0], // This identifies the book
|
||||
title: m.data.name,
|
||||
image: `https://mangapark.net/${m.data.urlCoverOri}`,
|
||||
rating: m.data.score_val ? Math.round(m.data.score_val * 10) : null,
|
||||
type: "book",
|
||||
headers: {
|
||||
referer: "https://mangapark.net"
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const res = await fetch(`https://mangapark.net/title/${id}`);
|
||||
const html = await res.text();
|
||||
|
||||
const match = html.match(
|
||||
/<script type="qwik\/json">([\s\S]*?)<\/script>/
|
||||
);
|
||||
if (!match) throw new Error("qwik json not found");
|
||||
|
||||
function decodeQwik(obj) {
|
||||
const refs = obj.refs || {};
|
||||
function walk(v) {
|
||||
if (typeof v === "string" && refs[v] !== undefined) {
|
||||
return walk(refs[v]);
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
return v.map(walk);
|
||||
}
|
||||
if (v && typeof v === "object") {
|
||||
const out = {};
|
||||
for (const k in v) out[k] = walk(v[k]);
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
return walk(obj);
|
||||
}
|
||||
|
||||
const raw = JSON.parse(match[1]);
|
||||
const data = decodeQwik(raw);
|
||||
|
||||
const comic =
|
||||
data?.objs?.find(o => o && o.name && o.summary) ||
|
||||
data?.state?.comic;
|
||||
|
||||
if (!comic) throw new Error("comic not found");
|
||||
|
||||
const score100 = comic.score_avg
|
||||
? Math.round((Number(comic.score_avg) / 10) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
id,
|
||||
title: comic.name || "",
|
||||
format: "Manga",
|
||||
score: score100,
|
||||
genres: comic.genres || [],
|
||||
status: comic.originalStatus || comic.status || "",
|
||||
published: comic.originalPubFrom
|
||||
? String(comic.originalPubFrom)
|
||||
: "???",
|
||||
summary: comic.summary || "",
|
||||
chapters: comic.chaps_normal || comic.chapters_count || 0,
|
||||
image: comic.urlCoverOri
|
||||
? `https://mangapark.net${comic.urlCoverOri}`
|
||||
: ""
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
const comicId = mangaId
|
||||
if (!comicId) {
|
||||
console.error("[MangaPark] Invalid ID format:", mangaId);
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = await fetch(this.baseUrl + "/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"x-apollo-operation-name": "get_comicChapterList",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: "query get_comicChapterList($comicId: ID!) { get_comicChapterList(comicId: $comicId){ id data { id comicId isFinal volume serial dname title urlPath sfw_result } } }\n",
|
||||
variables: { comicId }
|
||||
})
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.data || !json.data.get_comicChapterList) return [];
|
||||
|
||||
let list = json.data.get_comicChapterList;
|
||||
|
||||
list.sort((a, b) => a.data.serial - b.data.serial);
|
||||
|
||||
return list.map((c, i) => ({
|
||||
id: `https://mangapark.net${c.data.urlPath}`,
|
||||
title: c.data.dname || c.data.title || `Chapter ${c.data.serial}`,
|
||||
number: Number(c.data.serial),
|
||||
releaseDate: null,
|
||||
index: i
|
||||
}));
|
||||
}
|
||||
|
||||
async findChapterPages(chapterUrl) {
|
||||
const res = await fetch(chapterUrl, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
});
|
||||
const html = await res.text();
|
||||
|
||||
const scripts = html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) || [];
|
||||
let found = [];
|
||||
|
||||
for (const s of scripts) {
|
||||
const matches = s.match(/https?:\/\/[^"' ]+\.(?:jpg|jpeg|png|webp)/gi);
|
||||
if (matches) found.push(...matches);
|
||||
}
|
||||
|
||||
const urls = [...new Set(found)];
|
||||
const clean = urls.filter(u =>
|
||||
!u.includes("icon") &&
|
||||
!u.includes("logo") &&
|
||||
!u.includes("banner") &&
|
||||
!u.includes("thumb")
|
||||
);
|
||||
|
||||
return clean.map((url, index) => ({
|
||||
url,
|
||||
index,
|
||||
headers: { referer: 'https://mangapark.net' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mangapark;
|
||||
181
book/nhentai.js
Normal file
181
book/nhentai.js
Normal file
@@ -0,0 +1,181 @@
|
||||
class nhentai {
|
||||
constructor() {
|
||||
this.baseUrl = "https://nhentai.net";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
||||
const url = q
|
||||
? `${this.baseUrl}/search/?q=${q}`
|
||||
: `${this.baseUrl}/?q=`;
|
||||
|
||||
const { result: data } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const container = document.querySelector('.container.index-container');
|
||||
if (!container) return {results: [], hasNextPage: false};
|
||||
|
||||
const galleryEls = container.querySelectorAll('.gallery');
|
||||
const results = [];
|
||||
|
||||
galleryEls.forEach(el => {
|
||||
const a = el.querySelector('a.cover');
|
||||
if (!a) return;
|
||||
|
||||
const href = a.getAttribute('href');
|
||||
const id = href.match(/\d+/)?.[0] || null;
|
||||
|
||||
const img = a.querySelector('img.lazyload');
|
||||
const thumbRaw = img?.dataset?.src || img?.src || "";
|
||||
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
|
||||
const coverUrl = thumb.replace("thumb", "cover");
|
||||
|
||||
const caption = a.querySelector('.caption');
|
||||
const title = caption?.textContent.trim() || "";
|
||||
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image: coverUrl,
|
||||
rating: null,
|
||||
type: "book"
|
||||
});
|
||||
});
|
||||
|
||||
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
||||
return {results, hasNextPage};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: '.container.index-container',
|
||||
timeout: 55000
|
||||
}
|
||||
);
|
||||
|
||||
return data?.results || [];
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const { result: data } = await this.scrape(
|
||||
`${this.baseUrl}/g/${id}/`,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
||||
|
||||
const img = document.querySelector('#cover img');
|
||||
const image =
|
||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||
img?.src || "";
|
||||
|
||||
const tagBlock = document.querySelector('.tag-container.field-name');
|
||||
const genres = tagBlock
|
||||
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
||||
: [];
|
||||
|
||||
const timeEl = document.querySelector('.tag-container.field-name time');
|
||||
const published =
|
||||
timeEl?.getAttribute("datetime") ||
|
||||
timeEl?.textContent?.trim() ||
|
||||
"???";
|
||||
|
||||
return {title, image, genres, published};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "#bigcontainer",
|
||||
timeout: 55000
|
||||
}
|
||||
);
|
||||
|
||||
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
||||
|
||||
const formattedDate = data.published
|
||||
? new Date(data.published).toLocaleDateString("es-ES")
|
||||
: "???";
|
||||
|
||||
return {
|
||||
id,
|
||||
title: data.title || "",
|
||||
format: "Manga",
|
||||
score: 0,
|
||||
genres: Array.isArray(data.genres) ? data.genres : [],
|
||||
status: "Finished",
|
||||
published: formattedDate,
|
||||
summary: "",
|
||||
chapters: 1,
|
||||
image: data.image || ""
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
const { result: data } = await this.scrape(
|
||||
`${this.baseUrl}/g/${mangaId}/`,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
||||
|
||||
const img = document.querySelector('#cover img');
|
||||
const cover =
|
||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||
img?.src || "";
|
||||
|
||||
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
||||
|
||||
const thumbs = document.querySelectorAll('.thumbs img');
|
||||
const pages = thumbs.length;
|
||||
|
||||
const first = thumbs[0];
|
||||
const s = first?.dataset?.src || first?.src || "";
|
||||
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
||||
|
||||
const langTag = [...document.querySelectorAll('#tags .tag-container')]
|
||||
.find(x => x.textContent.includes("Languages:"));
|
||||
|
||||
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
||||
|
||||
return {title, cover, hash, pages, ext, language};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: '#bigcontainer',
|
||||
timeout: 55000
|
||||
}
|
||||
);
|
||||
|
||||
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
|
||||
|
||||
const encodedChapterId = Buffer.from(JSON.stringify({
|
||||
hash: data.hash,
|
||||
pages: data.pages,
|
||||
ext: data.ext
|
||||
})).toString("base64");
|
||||
|
||||
return [{
|
||||
id: encodedChapterId,
|
||||
title: data.title,
|
||||
number: 1,
|
||||
releaseDate: null,
|
||||
index: 0,
|
||||
}];
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
|
||||
|
||||
const { hash, pages, ext } = decoded;
|
||||
const baseUrl = "https://i.nhentai.net/galleries";
|
||||
|
||||
return Array.from({ length: pages }, (_, i) => ({
|
||||
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
||||
index: i,
|
||||
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nhentai;
|
||||
121
book/novelbin.js
Normal file
121
book/novelbin.js
Normal file
@@ -0,0 +1,121 @@
|
||||
class NovelBin {
|
||||
constructor() {
|
||||
this.baseUrl = "https://novelbin.me";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "ln";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query || "";
|
||||
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"referer": this.baseUrl + "/"
|
||||
}
|
||||
});
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
|
||||
$('h3.novel-title a').each((i, el) => {
|
||||
const href = $(el).attr('href');
|
||||
const title = $(el).text().trim();
|
||||
|
||||
const idMatch = href.match(/novel-book\/([^/?]+)/);
|
||||
const id = idMatch ? idMatch[1] : null;
|
||||
|
||||
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
|
||||
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image: img,
|
||||
rating: null,
|
||||
type: "book"
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
|
||||
|
||||
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
|
||||
const summary = $('meta[name="description"]').attr('content') || "";
|
||||
const genresRaw = getMeta("og:novel:genre");
|
||||
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
|
||||
const status = getMeta("og:novel:status") || "";
|
||||
const image = getMeta("og:image");
|
||||
|
||||
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
|
||||
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
|
||||
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
format: "Light Novel",
|
||||
score: 0,
|
||||
genres,
|
||||
status,
|
||||
published: "???",
|
||||
summary,
|
||||
chapters,
|
||||
image
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(bookId) {
|
||||
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0"
|
||||
}
|
||||
});
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const chapters = [];
|
||||
|
||||
$('a[title]').each((i, el) => {
|
||||
const fullUrl = $(el).attr('href');
|
||||
const title = $(el).attr('title').trim();
|
||||
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
|
||||
|
||||
chapters.push({
|
||||
id: fullUrl,
|
||||
title,
|
||||
number: numMatch ? numMatch[1] : "0",
|
||||
releaseDate: null,
|
||||
index: i
|
||||
});
|
||||
});
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
async findChapterPages(chapterUrl) {
|
||||
const {result} = await this.scrape(chapterUrl, async (page) => {
|
||||
return page.evaluate(() => {
|
||||
document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove());
|
||||
const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7);
|
||||
return ps.join("\n");
|
||||
});
|
||||
}, {
|
||||
waitUntil: "domcontentloaded",
|
||||
renderWaitTime: 300
|
||||
});
|
||||
return result || "<p>Error: chapter text not found</p>";
|
||||
}
|
||||
}
|
||||
module.exports = NovelBin;
|
||||
137
book/novelfire.js
Normal file
137
book/novelfire.js
Normal file
@@ -0,0 +1,137 @@
|
||||
class NovelFire {
|
||||
constructor() {
|
||||
this.baseUrl = "https://novelfire.net";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "ln";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query;
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
|
||||
{ headers: { "accept": "application/json" } }
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.data) return [];
|
||||
|
||||
return data.data.map(item => ({
|
||||
id: item.slug,
|
||||
title: item.title,
|
||||
image: `https://novelfire.net/${item.image}`,
|
||||
rating: item.rank ?? null,
|
||||
type: "book"
|
||||
}));
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const url = `https://novelfire.net/book/${id}`;
|
||||
const html = await (await fetch(url)).text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const title = $('h1[itemprop="name"]').first().text().trim() || null;
|
||||
const summary = $('meta[itemprop="description"]').attr('content') || null;
|
||||
const image =
|
||||
$('figure.cover img').attr('src') ||
|
||||
$('img.cover').attr('src') ||
|
||||
$('img[src*="server-"]').attr('src') ||
|
||||
null;
|
||||
|
||||
const genres = $('.categories a.property-item')
|
||||
.map((_, el) => $(el).attr('title') || $(el).text().trim())
|
||||
.get();
|
||||
|
||||
let chapters = null;
|
||||
const latest = $('.chapter-latest-container .latest').text();
|
||||
if (latest) {
|
||||
const m = latest.match(/Chapter\s+(\d+)/i);
|
||||
if (m) chapters = Number(m[1]);
|
||||
}
|
||||
|
||||
let status = 'unknown';
|
||||
const statusClass = $('strong.ongoing, strong.completed').attr('class');
|
||||
if (statusClass) {
|
||||
status = statusClass.toLowerCase();
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
format: 'Light Novel',
|
||||
score: 0,
|
||||
genres,
|
||||
status,
|
||||
published: '???',
|
||||
summary,
|
||||
chapters,
|
||||
image
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(bookId) {
|
||||
const url = `https://novelfire.net/book/${bookId}/chapters`;
|
||||
const html = await (await fetch(url)).text();
|
||||
|
||||
const $ = this.cheerio.load(html);
|
||||
let postId;
|
||||
|
||||
$("script").each((_, el) => {
|
||||
const txt = $(el).html() || "";
|
||||
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
|
||||
if (m) postId = m[1];
|
||||
});
|
||||
|
||||
if (!postId) throw new Error("post_id not found");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
post_id: postId,
|
||||
draw: 1,
|
||||
"columns[0][data]": "title",
|
||||
"columns[0][orderable]": "false",
|
||||
"columns[1][data]": "created_at",
|
||||
"columns[1][orderable]": "true",
|
||||
"order[0][column]": 1,
|
||||
"order[0][dir]": "asc",
|
||||
start: 0,
|
||||
length: 1000
|
||||
});
|
||||
|
||||
const res = await fetch(
|
||||
`https://novelfire.net/listChapterDataAjax?${params}`,
|
||||
{ headers: { "x-requested-with": "XMLHttpRequest" } }
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
if (!json?.data) throw new Error("Invalid response");
|
||||
|
||||
return json.data.map((c, i) => ({
|
||||
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
|
||||
title: c.title,
|
||||
number: Number(c.n_sort),
|
||||
release_date: c.created_at ?? null,
|
||||
index: i,
|
||||
language: "en"
|
||||
}));
|
||||
}
|
||||
|
||||
async findChapterPages(url) {
|
||||
const html = await (await fetch(url)).text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const $content = $("#content").clone();
|
||||
|
||||
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
|
||||
|
||||
$content.find("*").each((_, el) => {
|
||||
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
|
||||
});
|
||||
|
||||
return $content.html()
|
||||
.replace(/adsbygoogle/gi, "")
|
||||
.replace(/novelfire/gi, "")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NovelFire;
|
||||
142
book/wattpad.js
Normal file
142
book/wattpad.js
Normal file
@@ -0,0 +1,142 @@
|
||||
class wattpad {
|
||||
constructor() {
|
||||
this.baseUrl = "https://wattpad.com";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "ln";
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query?.trim() || "";
|
||||
const limit = 15;
|
||||
const offset = 0;
|
||||
|
||||
const url =
|
||||
`${this.baseUrl}/v4/search/stories?` +
|
||||
`query=${encodeURIComponent(query)}` +
|
||||
`&limit=${limit}&offset=${offset}&mature=false`;
|
||||
|
||||
const json = await fetch(url).then(r => r.json());
|
||||
|
||||
return json.stories.map(n => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
image: n.cover,
|
||||
sampleImageUrl: n.cover,
|
||||
tags: n.tags,
|
||||
type: "book"
|
||||
}));
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const html = await fetch(`${this.baseUrl}/story/${id}`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const script = $('script')
|
||||
.map((_, el) => $(el).html())
|
||||
.get()
|
||||
.find(t => t?.includes('window.__remixContext'));
|
||||
|
||||
if (!script) return null;
|
||||
|
||||
const jsonText = script.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/)?.[1];
|
||||
if (!jsonText) return null;
|
||||
|
||||
let ctx;
|
||||
try {
|
||||
ctx = JSON.parse(jsonText);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const route = ctx?.state?.loaderData?.["routes/story.$storyid"];
|
||||
const story = route?.story;
|
||||
const meta = route?.meta;
|
||||
|
||||
if (!story) return null;
|
||||
|
||||
return {
|
||||
id: story.id,
|
||||
title: story.title,
|
||||
format: "Novel",
|
||||
score: story.voteCount ?? null,
|
||||
genres: story.tags || [],
|
||||
status: story.completed ? "Completed" : "Ongoing",
|
||||
published: story.createDate?.split("T")[0] || "???",
|
||||
summary: story.description || meta?.description || "",
|
||||
chapters: story.numParts || story.parts?.length || 1,
|
||||
image: story.cover || meta?.image || "",
|
||||
language: story.language?.name?.toLowerCase() || "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(bookId) {
|
||||
const html = await fetch(`${this.baseUrl}/story/${bookId}`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const script = $('script')
|
||||
.map((_, el) => $(el).html())
|
||||
.get()
|
||||
.find(t => t?.includes('window.__remixContext'));
|
||||
|
||||
if (!script) return [];
|
||||
|
||||
const jsonText = script.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/)?.[1];
|
||||
if (!jsonText) return [];
|
||||
|
||||
let ctx;
|
||||
try {
|
||||
ctx = JSON.parse(jsonText);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const story = ctx?.state?.loaderData?.["routes/story.$storyid"]?.story;
|
||||
if (!story?.parts) return [];
|
||||
|
||||
return story.parts.map((p, i) => ({
|
||||
id: String(p.id),
|
||||
title: p.title || `Chapter ${i + 1}`,
|
||||
number: i + 1,
|
||||
language: story.language?.name?.toLowerCase() || "en",
|
||||
index: i
|
||||
}));
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const html = await fetch(`https://www.wattpad.com/amp/${chapterId}`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const title = $('h2').first().text().trim();
|
||||
|
||||
const container = $('.story-body-type');
|
||||
if (!container.length) return "";
|
||||
|
||||
container.find('[data-media-type="image"]').remove();
|
||||
|
||||
const parts = [];
|
||||
|
||||
container.find('p').each((_, el) => {
|
||||
const text = $(el)
|
||||
.html()
|
||||
.replace(/\u00A0/g, " ")
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.trim();
|
||||
|
||||
if (text) parts.push(`<p>${text}</p>`);
|
||||
});
|
||||
|
||||
container.find('amp-img').each((_, el) => {
|
||||
const src = $(el).attr('src');
|
||||
const w = $(el).attr('width');
|
||||
const h = $(el).attr('height');
|
||||
if (src) parts.push(`<img src="${src}" width="${w}" height="${h}">`);
|
||||
});
|
||||
|
||||
return (
|
||||
(title ? `<h1>${title}</h1>\n\n` : "") +
|
||||
parts.join("\n\n")
|
||||
).trim();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = wattpad;
|
||||
Reference in New Issue
Block a user