new format and marketplace
This commit is contained in:
@@ -1,206 +1,206 @@
|
|||||||
class MangaDex {
|
class MangaDex {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = "https://mangadex.org";
|
this.baseUrl = "https://mangadex.org";
|
||||||
this.apiUrl = "https://api.mangadex.org";
|
this.apiUrl = "https://api.mangadex.org";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
this.mediaType = "manga";
|
this.mediaType = "manga";
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaders() {
|
getHeaders() {
|
||||||
return {
|
return {
|
||||||
'User-Agent': 'MangaDex-Client-Adapter/1.0',
|
'User-Agent': 'MangaDex-Client-Adapter/1.0',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(queryObj) {
|
async search(queryObj) {
|
||||||
const query = queryObj.query?.trim() || "";
|
const query = queryObj.query?.trim() || "";
|
||||||
const limit = 25;
|
const limit = 25;
|
||||||
const offset = (1 - 1) * limit;
|
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`;
|
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { headers: this.getHeaders() });
|
const response = await fetch(url, { headers: this.getHeaders() });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`MangaDex API Error: ${response.statusText}`);
|
console.error(`MangaDex API Error: ${response.statusText}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (!json || !Array.isArray(json.data)) {
|
if (!json || !Array.isArray(json.data)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.data.map(manga => {
|
return json.data.map(manga => {
|
||||||
const attributes = manga.attributes;
|
const attributes = manga.attributes;
|
||||||
const titleObject = attributes.title || {};
|
const titleObject = attributes.title || {};
|
||||||
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
|
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
|
||||||
|
|
||||||
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
|
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
|
||||||
const coverFileName = coverRelationship?.attributes?.fileName;
|
const coverFileName = coverRelationship?.attributes?.fileName;
|
||||||
|
|
||||||
const coverUrl = coverFileName
|
const coverUrl = coverFileName
|
||||||
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
|
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: manga.id,
|
id: manga.id,
|
||||||
image: coverUrl,
|
image: coverUrl,
|
||||||
title: title,
|
title: title,
|
||||||
rating: null,
|
rating: null,
|
||||||
type: 'book'
|
type: 'book'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error during MangaDex search:", e);
|
console.error("Error during MangaDex search:", e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(id) {
|
async getMetadata(id) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
|
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
|
||||||
if (!res.ok) throw new Error("MangaDex API error");
|
if (!res.ok) throw new Error("MangaDex API error");
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const manga = json.data;
|
const manga = json.data;
|
||||||
const attr = manga.attributes;
|
const attr = manga.attributes;
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
attr.title?.en ||
|
attr.title?.en ||
|
||||||
Object.values(attr.title || {})[0] ||
|
Object.values(attr.title || {})[0] ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
const summary =
|
const summary =
|
||||||
attr.description?.en ||
|
attr.description?.en ||
|
||||||
Object.values(attr.description || {})[0] ||
|
Object.values(attr.description || {})[0] ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
const genres = manga.relationships
|
const genres = manga.relationships
|
||||||
?.filter(r => r.type === "tag")
|
?.filter(r => r.type === "tag")
|
||||||
?.map(r =>
|
?.map(r =>
|
||||||
r.attributes?.name?.en ||
|
r.attributes?.name?.en ||
|
||||||
Object.values(r.attributes?.name || {})[0]
|
Object.values(r.attributes?.name || {})[0]
|
||||||
)
|
)
|
||||||
?.filter(Boolean) || [];
|
?.filter(Boolean) || [];
|
||||||
|
|
||||||
const coverRel = manga.relationships.find(r => r.type === "cover_art");
|
const coverRel = manga.relationships.find(r => r.type === "cover_art");
|
||||||
const coverFile = coverRel?.attributes?.fileName;
|
const coverFile = coverRel?.attributes?.fileName;
|
||||||
const image = coverFile
|
const image = coverFile
|
||||||
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
|
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const score100 = 0;
|
const score100 = 0;
|
||||||
|
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
ongoing: "Ongoing",
|
ongoing: "Ongoing",
|
||||||
completed: "Completed",
|
completed: "Completed",
|
||||||
hiatus: "Hiatus",
|
hiatus: "Hiatus",
|
||||||
cancelled: "Cancelled"
|
cancelled: "Cancelled"
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
format: "Manga",
|
format: "Manga",
|
||||||
score: score100,
|
score: score100,
|
||||||
genres,
|
genres,
|
||||||
status: statusMap[attr.status] || "",
|
status: statusMap[attr.status] || "",
|
||||||
published: attr.year ? String(attr.year) : "???",
|
published: attr.year ? String(attr.year) : "???",
|
||||||
summary,
|
summary,
|
||||||
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
|
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
|
||||||
image
|
image
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("MangaDex getMetadata error:", e);
|
console.error("MangaDex getMetadata error:", e);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: "",
|
title: "",
|
||||||
format: "Manga",
|
format: "Manga",
|
||||||
score: 0,
|
score: 0,
|
||||||
genres: [],
|
genres: [],
|
||||||
status: "",
|
status: "",
|
||||||
published: "???",
|
published: "???",
|
||||||
summary: "",
|
summary: "",
|
||||||
chapters: 0,
|
chapters: 0,
|
||||||
image: ""
|
image: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(mangaId) {
|
async findChapters(mangaId) {
|
||||||
if (!mangaId) return [];
|
if (!mangaId) return [];
|
||||||
|
|
||||||
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
|
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { headers: this.getHeaders() });
|
const response = await fetch(url, { headers: this.getHeaders() });
|
||||||
let chapters = [];
|
let chapters = [];
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json && Array.isArray(json.data)) {
|
if (json && Array.isArray(json.data)) {
|
||||||
const allChapters = json.data
|
const allChapters = json.data
|
||||||
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
|
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
|
||||||
.map((ch, index) => ({
|
.map((ch, index) => ({
|
||||||
id: ch.id,
|
id: ch.id,
|
||||||
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
|
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
|
||||||
number: ch.attributes.chapter,
|
number: ch.attributes.chapter,
|
||||||
index: index,
|
index: index,
|
||||||
language: ch.attributes.translatedLanguage
|
language: ch.attributes.translatedLanguage
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const seenChapters = new Set();
|
const seenChapters = new Set();
|
||||||
allChapters.forEach(ch => {
|
allChapters.forEach(ch => {
|
||||||
if (!seenChapters.has(ch.chapter)) {
|
if (!seenChapters.has(ch.chapter)) {
|
||||||
seenChapters.add(ch.chapter);
|
seenChapters.add(ch.chapter);
|
||||||
chapters.push(ch);
|
chapters.push(ch);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
|
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapters;
|
return chapters;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error finding MangaDex chapters:", e);
|
console.error("Error finding MangaDex chapters:", e);
|
||||||
return { chapters: [], cover: null };
|
return { chapters: [], cover: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterId) {
|
||||||
if (!chapterId) return [];
|
if (!chapterId) return [];
|
||||||
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
|
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { headers: this.getHeaders() });
|
const response = await fetch(url, { headers: this.getHeaders() });
|
||||||
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
|
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (!json || !json.baseUrl || !json.chapter) return [];
|
if (!json || !json.baseUrl || !json.chapter) return [];
|
||||||
|
|
||||||
const baseUrl = json.baseUrl;
|
const baseUrl = json.baseUrl;
|
||||||
const chapterHash = json.chapter.hash;
|
const chapterHash = json.chapter.hash;
|
||||||
const imageFilenames = json.chapter.data;
|
const imageFilenames = json.chapter.data;
|
||||||
|
|
||||||
return imageFilenames.map((filename, index) => ({
|
return imageFilenames.map((filename, index) => ({
|
||||||
url: `${baseUrl}/data/${chapterHash}/${filename}`,
|
url: `${baseUrl}/data/${chapterHash}/${filename}`,
|
||||||
index: index,
|
index: index,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': `https://mangadex.org/chapter/${chapterId}`
|
'Referer': `https://mangadex.org/chapter/${chapterId}`
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error finding MangaDex pages:", e);
|
console.error("Error finding MangaDex pages:", e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MangaDex;
|
module.exports = MangaDex;
|
||||||
@@ -1,181 +1,181 @@
|
|||||||
class nhentai {
|
class nhentai {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = "https://nhentai.net";
|
this.baseUrl = "https://nhentai.net";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
this.mediaType = "manga";
|
this.mediaType = "manga";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(queryObj) {
|
async search(queryObj) {
|
||||||
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
||||||
const url = q
|
const url = q
|
||||||
? `${this.baseUrl}/search/?q=${q}`
|
? `${this.baseUrl}/search/?q=${q}`
|
||||||
: `${this.baseUrl}/?q=`;
|
: `${this.baseUrl}/?q=`;
|
||||||
|
|
||||||
const { result: data } = await this.scrape(
|
const { result: data } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const container = document.querySelector('.container.index-container');
|
const container = document.querySelector('.container.index-container');
|
||||||
if (!container) return {results: [], hasNextPage: false};
|
if (!container) return {results: [], hasNextPage: false};
|
||||||
|
|
||||||
const galleryEls = container.querySelectorAll('.gallery');
|
const galleryEls = container.querySelectorAll('.gallery');
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
galleryEls.forEach(el => {
|
galleryEls.forEach(el => {
|
||||||
const a = el.querySelector('a.cover');
|
const a = el.querySelector('a.cover');
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
|
|
||||||
const href = a.getAttribute('href');
|
const href = a.getAttribute('href');
|
||||||
const id = href.match(/\d+/)?.[0] || null;
|
const id = href.match(/\d+/)?.[0] || null;
|
||||||
|
|
||||||
const img = a.querySelector('img.lazyload');
|
const img = a.querySelector('img.lazyload');
|
||||||
const thumbRaw = img?.dataset?.src || img?.src || "";
|
const thumbRaw = img?.dataset?.src || img?.src || "";
|
||||||
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
|
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
|
||||||
const coverUrl = thumb.replace("thumb", "cover");
|
const coverUrl = thumb.replace("thumb", "cover");
|
||||||
|
|
||||||
const caption = a.querySelector('.caption');
|
const caption = a.querySelector('.caption');
|
||||||
const title = caption?.textContent.trim() || "";
|
const title = caption?.textContent.trim() || "";
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
image: coverUrl,
|
image: coverUrl,
|
||||||
rating: null,
|
rating: null,
|
||||||
type: "book"
|
type: "book"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
||||||
return {results, hasNextPage};
|
return {results, hasNextPage};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: '.container.index-container',
|
waitSelector: '.container.index-container',
|
||||||
timeout: 55000
|
timeout: 55000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data?.results || [];
|
return data?.results || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(id) {
|
async getMetadata(id) {
|
||||||
const { result: data } = await this.scrape(
|
const { result: data } = await this.scrape(
|
||||||
`${this.baseUrl}/g/${id}/`,
|
`${this.baseUrl}/g/${id}/`,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
||||||
|
|
||||||
const img = document.querySelector('#cover img');
|
const img = document.querySelector('#cover img');
|
||||||
const image =
|
const image =
|
||||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||||
img?.src || "";
|
img?.src || "";
|
||||||
|
|
||||||
const tagBlock = document.querySelector('.tag-container.field-name');
|
const tagBlock = document.querySelector('.tag-container.field-name');
|
||||||
const genres = tagBlock
|
const genres = tagBlock
|
||||||
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const timeEl = document.querySelector('.tag-container.field-name time');
|
const timeEl = document.querySelector('.tag-container.field-name time');
|
||||||
const published =
|
const published =
|
||||||
timeEl?.getAttribute("datetime") ||
|
timeEl?.getAttribute("datetime") ||
|
||||||
timeEl?.textContent?.trim() ||
|
timeEl?.textContent?.trim() ||
|
||||||
"???";
|
"???";
|
||||||
|
|
||||||
return {title, image, genres, published};
|
return {title, image, genres, published};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: "#bigcontainer",
|
waitSelector: "#bigcontainer",
|
||||||
timeout: 55000
|
timeout: 55000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
||||||
|
|
||||||
const formattedDate = data.published
|
const formattedDate = data.published
|
||||||
? new Date(data.published).toLocaleDateString("es-ES")
|
? new Date(data.published).toLocaleDateString("es-ES")
|
||||||
: "???";
|
: "???";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: data.title || "",
|
title: data.title || "",
|
||||||
format: "Manga",
|
format: "Manga",
|
||||||
score: 0,
|
score: 0,
|
||||||
genres: Array.isArray(data.genres) ? data.genres : [],
|
genres: Array.isArray(data.genres) ? data.genres : [],
|
||||||
status: "Finished",
|
status: "Finished",
|
||||||
published: formattedDate,
|
published: formattedDate,
|
||||||
summary: "",
|
summary: "",
|
||||||
chapters: 1,
|
chapters: 1,
|
||||||
image: data.image || ""
|
image: data.image || ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(mangaId) {
|
async findChapters(mangaId) {
|
||||||
const { result: data } = await this.scrape(
|
const { result: data } = await this.scrape(
|
||||||
`${this.baseUrl}/g/${mangaId}/`,
|
`${this.baseUrl}/g/${mangaId}/`,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
||||||
|
|
||||||
const img = document.querySelector('#cover img');
|
const img = document.querySelector('#cover img');
|
||||||
const cover =
|
const cover =
|
||||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||||
img?.src || "";
|
img?.src || "";
|
||||||
|
|
||||||
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
||||||
|
|
||||||
const thumbs = document.querySelectorAll('.thumbs img');
|
const thumbs = document.querySelectorAll('.thumbs img');
|
||||||
const pages = thumbs.length;
|
const pages = thumbs.length;
|
||||||
|
|
||||||
const first = thumbs[0];
|
const first = thumbs[0];
|
||||||
const s = first?.dataset?.src || first?.src || "";
|
const s = first?.dataset?.src || first?.src || "";
|
||||||
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
||||||
|
|
||||||
const langTag = [...document.querySelectorAll('#tags .tag-container')]
|
const langTag = [...document.querySelectorAll('#tags .tag-container')]
|
||||||
.find(x => x.textContent.includes("Languages:"));
|
.find(x => x.textContent.includes("Languages:"));
|
||||||
|
|
||||||
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
||||||
|
|
||||||
return {title, cover, hash, pages, ext, language};
|
return {title, cover, hash, pages, ext, language};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: '#bigcontainer',
|
waitSelector: '#bigcontainer',
|
||||||
timeout: 55000
|
timeout: 55000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
|
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
|
||||||
|
|
||||||
const encodedChapterId = Buffer.from(JSON.stringify({
|
const encodedChapterId = Buffer.from(JSON.stringify({
|
||||||
hash: data.hash,
|
hash: data.hash,
|
||||||
pages: data.pages,
|
pages: data.pages,
|
||||||
ext: data.ext
|
ext: data.ext
|
||||||
})).toString("base64");
|
})).toString("base64");
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
id: encodedChapterId,
|
id: encodedChapterId,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
number: 1,
|
number: 1,
|
||||||
releaseDate: null,
|
releaseDate: null,
|
||||||
index: 0,
|
index: 0,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterId) {
|
||||||
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
|
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
|
||||||
|
|
||||||
const { hash, pages, ext } = decoded;
|
const { hash, pages, ext } = decoded;
|
||||||
const baseUrl = "https://i.nhentai.net/galleries";
|
const baseUrl = "https://i.nhentai.net/galleries";
|
||||||
|
|
||||||
return Array.from({ length: pages }, (_, i) => ({
|
return Array.from({ length: pages }, (_, i) => ({
|
||||||
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
||||||
index: i,
|
index: i,
|
||||||
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nhentai;
|
module.exports = nhentai;
|
||||||
@@ -1,121 +1,121 @@
|
|||||||
class NovelBin {
|
class NovelBin {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = "https://novelbin.me";
|
this.baseUrl = "https://novelbin.me";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
this.mediaType = "ln";
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(queryObj) {
|
async search(queryObj) {
|
||||||
const query = queryObj.query || "";
|
const query = queryObj.query || "";
|
||||||
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
|
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: {
|
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",
|
"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",
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"referer": this.baseUrl + "/"
|
"referer": this.baseUrl + "/"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$('h3.novel-title a').each((i, el) => {
|
$('h3.novel-title a').each((i, el) => {
|
||||||
const href = $(el).attr('href');
|
const href = $(el).attr('href');
|
||||||
const title = $(el).text().trim();
|
const title = $(el).text().trim();
|
||||||
|
|
||||||
const idMatch = href.match(/novel-book\/([^/?]+)/);
|
const idMatch = href.match(/novel-book\/([^/?]+)/);
|
||||||
const id = idMatch ? idMatch[1] : null;
|
const id = idMatch ? idMatch[1] : null;
|
||||||
|
|
||||||
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
|
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
image: img,
|
image: img,
|
||||||
rating: null,
|
rating: null,
|
||||||
type: "book"
|
type: "book"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(id) {
|
async getMetadata(id) {
|
||||||
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
|
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
|
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
|
||||||
|
|
||||||
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
|
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
|
||||||
const summary = $('meta[name="description"]').attr('content') || "";
|
const summary = $('meta[name="description"]').attr('content') || "";
|
||||||
const genresRaw = getMeta("og:novel:genre");
|
const genresRaw = getMeta("og:novel:genre");
|
||||||
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
|
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
|
||||||
const status = getMeta("og:novel:status") || "";
|
const status = getMeta("og:novel:status") || "";
|
||||||
const image = getMeta("og:image");
|
const image = getMeta("og:image");
|
||||||
|
|
||||||
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
|
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
|
||||||
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
|
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
|
||||||
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
|
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
format: "Light Novel",
|
format: "Light Novel",
|
||||||
score: 0,
|
score: 0,
|
||||||
genres,
|
genres,
|
||||||
status,
|
status,
|
||||||
published: "???",
|
published: "???",
|
||||||
summary,
|
summary,
|
||||||
chapters,
|
chapters,
|
||||||
image
|
image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
|
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"user-agent": "Mozilla/5.0"
|
"user-agent": "Mozilla/5.0"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const chapters = [];
|
const chapters = [];
|
||||||
|
|
||||||
$('a[title]').each((i, el) => {
|
$('a[title]').each((i, el) => {
|
||||||
const fullUrl = $(el).attr('href');
|
const fullUrl = $(el).attr('href');
|
||||||
const title = $(el).attr('title').trim();
|
const title = $(el).attr('title').trim();
|
||||||
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
|
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
|
||||||
|
|
||||||
chapters.push({
|
chapters.push({
|
||||||
id: fullUrl,
|
id: fullUrl,
|
||||||
title,
|
title,
|
||||||
number: numMatch ? numMatch[1] : "0",
|
number: numMatch ? numMatch[1] : "0",
|
||||||
releaseDate: null,
|
releaseDate: null,
|
||||||
index: i
|
index: i
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return chapters;
|
return chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterUrl) {
|
async findChapterPages(chapterUrl) {
|
||||||
const {result} = await this.scrape(chapterUrl, async (page) => {
|
const {result} = await this.scrape(chapterUrl, async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove());
|
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);
|
const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7);
|
||||||
return ps.join("\n");
|
return ps.join("\n");
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
renderWaitTime: 300
|
renderWaitTime: 300
|
||||||
});
|
});
|
||||||
return result || "<p>Error: chapter text not found</p>";
|
return result || "<p>Error: chapter text not found</p>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = NovelBin;
|
module.exports = NovelBin;
|
||||||
@@ -1,137 +1,137 @@
|
|||||||
class NovelFire {
|
class NovelFire {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = "https://novelfire.net";
|
this.baseUrl = "https://novelfire.net";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
this.mediaType = "ln";
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(queryObj) {
|
async search(queryObj) {
|
||||||
const query = queryObj.query;
|
const query = queryObj.query;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
|
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
|
||||||
{ headers: { "accept": "application/json" } }
|
{ headers: { "accept": "application/json" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.data) return [];
|
if (!data.data) return [];
|
||||||
|
|
||||||
return data.data.map(item => ({
|
return data.data.map(item => ({
|
||||||
id: item.slug,
|
id: item.slug,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
image: `https://novelfire.net/${item.image}`,
|
image: `https://novelfire.net/${item.image}`,
|
||||||
rating: item.rank ?? null,
|
rating: item.rank ?? null,
|
||||||
type: "book"
|
type: "book"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(id) {
|
async getMetadata(id) {
|
||||||
const url = `https://novelfire.net/book/${id}`;
|
const url = `https://novelfire.net/book/${id}`;
|
||||||
const html = await (await fetch(url)).text();
|
const html = await (await fetch(url)).text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const title = $('h1[itemprop="name"]').first().text().trim() || null;
|
const title = $('h1[itemprop="name"]').first().text().trim() || null;
|
||||||
const summary = $('meta[itemprop="description"]').attr('content') || null;
|
const summary = $('meta[itemprop="description"]').attr('content') || null;
|
||||||
const image =
|
const image =
|
||||||
$('figure.cover img').attr('src') ||
|
$('figure.cover img').attr('src') ||
|
||||||
$('img.cover').attr('src') ||
|
$('img.cover').attr('src') ||
|
||||||
$('img[src*="server-"]').attr('src') ||
|
$('img[src*="server-"]').attr('src') ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
const genres = $('.categories a.property-item')
|
const genres = $('.categories a.property-item')
|
||||||
.map((_, el) => $(el).attr('title') || $(el).text().trim())
|
.map((_, el) => $(el).attr('title') || $(el).text().trim())
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
let chapters = null;
|
let chapters = null;
|
||||||
const latest = $('.chapter-latest-container .latest').text();
|
const latest = $('.chapter-latest-container .latest').text();
|
||||||
if (latest) {
|
if (latest) {
|
||||||
const m = latest.match(/Chapter\s+(\d+)/i);
|
const m = latest.match(/Chapter\s+(\d+)/i);
|
||||||
if (m) chapters = Number(m[1]);
|
if (m) chapters = Number(m[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = 'unknown';
|
let status = 'unknown';
|
||||||
const statusClass = $('strong.ongoing, strong.completed').attr('class');
|
const statusClass = $('strong.ongoing, strong.completed').attr('class');
|
||||||
if (statusClass) {
|
if (statusClass) {
|
||||||
status = statusClass.toLowerCase();
|
status = statusClass.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
format: 'Light Novel',
|
format: 'Light Novel',
|
||||||
score: 0,
|
score: 0,
|
||||||
genres,
|
genres,
|
||||||
status,
|
status,
|
||||||
published: '???',
|
published: '???',
|
||||||
summary,
|
summary,
|
||||||
chapters,
|
chapters,
|
||||||
image
|
image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
const url = `https://novelfire.net/book/${bookId}/chapters`;
|
const url = `https://novelfire.net/book/${bookId}/chapters`;
|
||||||
const html = await (await fetch(url)).text();
|
const html = await (await fetch(url)).text();
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
let postId;
|
let postId;
|
||||||
|
|
||||||
$("script").each((_, el) => {
|
$("script").each((_, el) => {
|
||||||
const txt = $(el).html() || "";
|
const txt = $(el).html() || "";
|
||||||
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
|
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
|
||||||
if (m) postId = m[1];
|
if (m) postId = m[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!postId) throw new Error("post_id not found");
|
if (!postId) throw new Error("post_id not found");
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
post_id: postId,
|
post_id: postId,
|
||||||
draw: 1,
|
draw: 1,
|
||||||
"columns[0][data]": "title",
|
"columns[0][data]": "title",
|
||||||
"columns[0][orderable]": "false",
|
"columns[0][orderable]": "false",
|
||||||
"columns[1][data]": "created_at",
|
"columns[1][data]": "created_at",
|
||||||
"columns[1][orderable]": "true",
|
"columns[1][orderable]": "true",
|
||||||
"order[0][column]": 1,
|
"order[0][column]": 1,
|
||||||
"order[0][dir]": "asc",
|
"order[0][dir]": "asc",
|
||||||
start: 0,
|
start: 0,
|
||||||
length: 1000
|
length: 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://novelfire.net/listChapterDataAjax?${params}`,
|
`https://novelfire.net/listChapterDataAjax?${params}`,
|
||||||
{ headers: { "x-requested-with": "XMLHttpRequest" } }
|
{ headers: { "x-requested-with": "XMLHttpRequest" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json?.data) throw new Error("Invalid response");
|
if (!json?.data) throw new Error("Invalid response");
|
||||||
|
|
||||||
return json.data.map((c, i) => ({
|
return json.data.map((c, i) => ({
|
||||||
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
|
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
|
||||||
title: c.title,
|
title: c.title,
|
||||||
number: Number(c.n_sort),
|
number: Number(c.n_sort),
|
||||||
release_date: c.created_at ?? null,
|
release_date: c.created_at ?? null,
|
||||||
index: i,
|
index: i,
|
||||||
language: "en"
|
language: "en"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(url) {
|
async findChapterPages(url) {
|
||||||
const html = await (await fetch(url)).text();
|
const html = await (await fetch(url)).text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const $content = $("#content").clone();
|
const $content = $("#content").clone();
|
||||||
|
|
||||||
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
|
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
|
||||||
|
|
||||||
$content.find("*").each((_, el) => {
|
$content.find("*").each((_, el) => {
|
||||||
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
|
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
|
||||||
});
|
});
|
||||||
|
|
||||||
return $content.html()
|
return $content.html()
|
||||||
.replace(/adsbygoogle/gi, "")
|
.replace(/adsbygoogle/gi, "")
|
||||||
.replace(/novelfire/gi, "")
|
.replace(/novelfire/gi, "")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NovelFire;
|
module.exports = NovelFire;
|
||||||
@@ -1,119 +1,119 @@
|
|||||||
class ZeroChan {
|
class ZeroChan {
|
||||||
baseUrl = "https://zerochan.net";
|
baseUrl = "https://zerochan.net";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query = "thighs", page = 1, perPage = 48) {
|
async search(query = "thighs", page = 1, perPage = 48) {
|
||||||
const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`;
|
const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`;
|
||||||
|
|
||||||
const { result } = await this.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const list = document.querySelectorAll("#thumbs2 li");
|
const list = document.querySelectorAll("#thumbs2 li");
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return {results: [], hasNextPage: false};
|
return {results: [], hasNextPage: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
list.forEach(li => {
|
list.forEach(li => {
|
||||||
const id = li.getAttribute("data-id");
|
const id = li.getAttribute("data-id");
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
const img = li.querySelector("img");
|
const img = li.querySelector("img");
|
||||||
const imgUrl =
|
const imgUrl =
|
||||||
img?.getAttribute("data-src") ||
|
img?.getAttribute("data-src") ||
|
||||||
img?.getAttribute("src") ||
|
img?.getAttribute("src") ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
if (!imgUrl) return;
|
if (!imgUrl) return;
|
||||||
|
|
||||||
const tagLinks = li.querySelectorAll("p a");
|
const tagLinks = li.querySelectorAll("p a");
|
||||||
const tags = [...tagLinks]
|
const tags = [...tagLinks]
|
||||||
.map(a => a.textContent.trim())
|
.map(a => a.textContent.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
image: imgUrl,
|
image: imgUrl,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNextPage =
|
const hasNextPage =
|
||||||
document.querySelector('nav.pagination a[rel="next"]') !== null;
|
document.querySelector('nav.pagination a[rel="next"]') !== null;
|
||||||
|
|
||||||
return {results, hasNextPage};
|
return {results, hasNextPage};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: "#thumbs2 li",
|
waitSelector: "#thumbs2 li",
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
renderWaitTime: 3000,
|
renderWaitTime: 3000,
|
||||||
loadImages: true
|
loadImages: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: result.results.map(r => ({
|
results: result.results.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
image: r.image,
|
image: r.image,
|
||||||
tags: r.tags
|
tags: r.tags
|
||||||
})),
|
})),
|
||||||
hasNextPage: result.hasNextPage,
|
hasNextPage: result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/${id}`;
|
const url = `${this.baseUrl}/${id}`;
|
||||||
|
|
||||||
const { result } = await this.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const preview = document.querySelector("a.preview");
|
const preview = document.querySelector("a.preview");
|
||||||
if (!preview) {
|
if (!preview) {
|
||||||
return {
|
return {
|
||||||
fullImage: null,
|
fullImage: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullImage = preview.getAttribute("href") || null;
|
const fullImage = preview.getAttribute("href") || null;
|
||||||
const img = preview.querySelector("img");
|
const img = preview.querySelector("img");
|
||||||
const alt = img?.getAttribute("alt") || "";
|
const alt = img?.getAttribute("alt") || "";
|
||||||
|
|
||||||
let tags = [];
|
let tags = [];
|
||||||
if (alt.startsWith("Tags:")) {
|
if (alt.startsWith("Tags:")) {
|
||||||
tags = alt
|
tags = alt
|
||||||
.replace("Tags:", "")
|
.replace("Tags:", "")
|
||||||
.split(",")
|
.split(",")
|
||||||
.map(t => t.trim())
|
.map(t => t.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullImage,
|
fullImage,
|
||||||
tags,
|
tags,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ waitSelector: "a.preview img", timeout: 15000 }
|
{ waitSelector: "a.preview img", timeout: 15000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image: result.fullImage,
|
image: result.fullImage,
|
||||||
tags: result.tags
|
tags: result.tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ZeroChan;
|
module.exports = ZeroChan;
|
||||||
@@ -1,83 +1,83 @@
|
|||||||
class Gelbooru {
|
class Gelbooru {
|
||||||
baseUrl = "https://gelbooru.com";
|
baseUrl = "https://gelbooru.com";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query = "thighs", page = 1, perPage = 42) {
|
async search(query = "thighs", page = 1, perPage = 42) {
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${encodeURIComponent(query)}&pid=${(page - 1) * perPage}`;
|
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${encodeURIComponent(query)}&pid=${(page - 1) * perPage}`;
|
||||||
|
|
||||||
const html = await fetch(url, {
|
const html = await fetch(url, {
|
||||||
headers: { "User-Agent": "Mozilla/5.0" }
|
headers: { "User-Agent": "Mozilla/5.0" }
|
||||||
}).then(r => r.text());
|
}).then(r => r.text());
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
|
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
|
||||||
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
|
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
const img = $(el).find("img");
|
const img = $(el).find("img");
|
||||||
const image = img.attr("src");
|
const image = img.attr("src");
|
||||||
|
|
||||||
const tags = img.attr("alt")
|
const tags = img.attr("alt")
|
||||||
?.replace(/^Rule 34 \|\s*/, "")
|
?.replace(/^Rule 34 \|\s*/, "")
|
||||||
?.split(",")
|
?.split(",")
|
||||||
?.map(t => t.trim())
|
?.map(t => t.trim())
|
||||||
?.filter(Boolean) || [];
|
?.filter(Boolean) || [];
|
||||||
|
|
||||||
results.push({ id, image, tags });
|
results.push({ id, image, tags });
|
||||||
});
|
});
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
const totalPages = Math.max(
|
const totalPages = Math.max(
|
||||||
page,
|
page,
|
||||||
...$("a[href*='pid=']")
|
...$("a[href*='pid=']")
|
||||||
.map((_, el) =>
|
.map((_, el) =>
|
||||||
Math.floor(
|
Math.floor(
|
||||||
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
|
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
|
||||||
) + 1
|
) + 1
|
||||||
)
|
)
|
||||||
.get()
|
.get()
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
results,
|
||||||
page,
|
page,
|
||||||
hasNextPage: page < totalPages
|
hasNextPage: page < totalPages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const html = await fetch(
|
const html = await fetch(
|
||||||
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
|
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
|
||||||
{ headers: { "User-Agent": "Mozilla/5.0" } }
|
{ headers: { "User-Agent": "Mozilla/5.0" } }
|
||||||
).then(r => r.text());
|
).then(r => r.text());
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
const container = $("section.image-container");
|
const container = $("section.image-container");
|
||||||
|
|
||||||
let image =
|
let image =
|
||||||
container.find("#image").attr("src") ||
|
container.find("#image").attr("src") ||
|
||||||
container.attr("data-file-url") ||
|
container.attr("data-file-url") ||
|
||||||
container.attr("data-large-file-url") ||
|
container.attr("data-large-file-url") ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
const tags = container
|
const tags = container
|
||||||
.attr("data-tags")
|
.attr("data-tags")
|
||||||
?.trim()
|
?.trim()
|
||||||
?.split(/\s+/)
|
?.split(/\s+/)
|
||||||
?.filter(Boolean) || [];
|
?.filter(Boolean) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image,
|
image,
|
||||||
tags
|
tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Gelbooru;
|
module.exports = Gelbooru;
|
||||||
@@ -1,100 +1,100 @@
|
|||||||
class Giphy {
|
class Giphy {
|
||||||
baseUrl = "https://giphy.com";
|
baseUrl = "https://giphy.com";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query = "hello", page = 1, perPage = 48) {
|
async search(query = "hello", page = 1, perPage = 48) {
|
||||||
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
|
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
|
||||||
|
|
||||||
const data = await this.scrape(
|
const data = await this.scrape(
|
||||||
url,
|
url,
|
||||||
(page) => page.evaluate(() => {
|
(page) => page.evaluate(() => {
|
||||||
const items = document.querySelectorAll('a[data-giphy-id]');
|
const items = document.querySelectorAll('a[data-giphy-id]');
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
items.forEach(el => {
|
items.forEach(el => {
|
||||||
const id = el.getAttribute('data-giphy-id');
|
const id = el.getAttribute('data-giphy-id');
|
||||||
|
|
||||||
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
|
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
|
||||||
const srcImg = el.querySelector('img');
|
const srcImg = el.querySelector('img');
|
||||||
|
|
||||||
let rawSrc =
|
let rawSrc =
|
||||||
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
|
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
|
||||||
srcImg?.src ||
|
srcImg?.src ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
||||||
|
|
||||||
const alt = srcImg?.getAttribute("alt") || "";
|
const alt = srcImg?.getAttribute("alt") || "";
|
||||||
const tags = alt.trim().split(/\s+/).filter(Boolean);
|
const tags = alt.trim().split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
image: rawSrc,
|
image: rawSrc,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
results,
|
||||||
hasNextPage: false
|
hasNextPage: false
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
waitSelector: 'picture img, a[data-giphy-id] img',
|
waitSelector: 'picture img, a[data-giphy-id] img',
|
||||||
scrollToBottom: true,
|
scrollToBottom: true,
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: data.result.results.map(r => ({
|
results: data.result.results.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
image: r.image
|
image: r.image
|
||||||
})),
|
})),
|
||||||
hasNextPage: data.result.hasNextPage,
|
hasNextPage: data.result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `https://giphy.com/gifs/${id}`;
|
const url = `https://giphy.com/gifs/${id}`;
|
||||||
|
|
||||||
const data = await this.scrape(
|
const data = await this.scrape(
|
||||||
url,
|
url,
|
||||||
(page) => page.evaluate(() => {
|
(page) => page.evaluate(() => {
|
||||||
const scripts = document.querySelectorAll(
|
const scripts = document.querySelectorAll(
|
||||||
'script[type="application/ld+json"]'
|
'script[type="application/ld+json"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
let imgsrc = null;
|
let imgsrc = null;
|
||||||
|
|
||||||
scripts.forEach(script => {
|
scripts.forEach(script => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(script.textContent);
|
const json = JSON.parse(script.textContent);
|
||||||
|
|
||||||
if (json?.["@type"] === "Article" && json?.image?.url) {
|
if (json?.["@type"] === "Article" && json?.image?.url) {
|
||||||
imgsrc = json.image.url;
|
imgsrc = json.image.url;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image: imgsrc
|
image: imgsrc
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
waitSelector: 'script[type="application/ld+json"]',
|
waitSelector: 'script[type="application/ld+json"]',
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image: data.result.image
|
image: data.result.image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Giphy;
|
module.exports = Giphy;
|
||||||
@@ -1,84 +1,84 @@
|
|||||||
class Realbooru {
|
class Realbooru {
|
||||||
baseUrl = "https://realbooru.com";
|
baseUrl = "https://realbooru.com";
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": "Mozilla/5.0"
|
"User-Agent": "Mozilla/5.0"
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query = "original", page = 1, perPage = 42) {
|
async search(query = "original", page = 1, perPage = 42) {
|
||||||
const offset = (page - 1) * perPage;
|
const offset = (page - 1) * perPage;
|
||||||
|
|
||||||
const tags = query
|
const tags = query
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.join("+") + "+";
|
.join("+") + "+";
|
||||||
|
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${tags}&pid=${offset}`;
|
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${tags}&pid=${offset}`;
|
||||||
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
|
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$('div.col.thumb').each((_, el) => {
|
$('div.col.thumb').each((_, el) => {
|
||||||
const id = ($(el).attr('id') || "").replace('s', '');
|
const id = ($(el).attr('id') || "").replace('s', '');
|
||||||
const img = $(el).find('img');
|
const img = $(el).find('img');
|
||||||
|
|
||||||
let image = img.attr('src');
|
let image = img.attr('src');
|
||||||
if (image && !image.startsWith('http')) image = 'https:' + image;
|
if (image && !image.startsWith('http')) image = 'https:' + image;
|
||||||
|
|
||||||
const title = img.attr('title') || '';
|
const title = img.attr('title') || '';
|
||||||
const tags = title
|
const tags = title
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(t => t.trim())
|
.map(t => t.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (id && image) {
|
if (id && image) {
|
||||||
results.push({ id, image, tags });
|
results.push({ id, image, tags });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let totalPages = page;
|
let totalPages = page;
|
||||||
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
|
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
|
||||||
if (lastPid) {
|
if (lastPid) {
|
||||||
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
|
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
results,
|
||||||
page,
|
page,
|
||||||
hasNextPage: page < totalPages
|
hasNextPage: page < totalPages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
||||||
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
|
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
let image =
|
let image =
|
||||||
$('video source').attr('src') ||
|
$('video source').attr('src') ||
|
||||||
$('#image').attr('src') ||
|
$('#image').attr('src') ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
if (image && !image.startsWith('http')) {
|
if (image && !image.startsWith('http')) {
|
||||||
image = this.baseUrl + image;
|
image = this.baseUrl + image;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
$('#tagLink a').each((_, el) => {
|
$('#tagLink a').each((_, el) => {
|
||||||
tags.push($(el).text().trim());
|
tags.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image,
|
image,
|
||||||
tags
|
tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Realbooru;
|
module.exports = Realbooru;
|
||||||
@@ -1,99 +1,99 @@
|
|||||||
class Rule34 {
|
class Rule34 {
|
||||||
baseUrl = "https://rule34.xxx";
|
baseUrl = "https://rule34.xxx";
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
|
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
|
||||||
const offset = (page - 1) * perPage;
|
const offset = (page - 1) * perPage;
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
|
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
|
||||||
|
|
||||||
const response = await fetch(url, { headers: this.headers });
|
const response = await fetch(url, { headers: this.headers });
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
const $ = this.cheerio.load(data);
|
const $ = this.cheerio.load(data);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$('.image-list span').each((_, e) => {
|
$('.image-list span').each((_, e) => {
|
||||||
const $e = $(e);
|
const $e = $(e);
|
||||||
const id = $e.attr('id')?.replace('s', '');
|
const id = $e.attr('id')?.replace('s', '');
|
||||||
let image = $e.find('img').attr('src');
|
let image = $e.find('img').attr('src');
|
||||||
|
|
||||||
if (image && !image.startsWith('http')) {
|
if (image && !image.startsWith('http')) {
|
||||||
image = `https:${image}`;
|
image = `https:${image}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = $e.find('img')
|
const tags = $e.find('img')
|
||||||
.attr('alt')
|
.attr('alt')
|
||||||
?.trim()
|
?.trim()
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (id && image) {
|
if (id && image) {
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
image,
|
image,
|
||||||
tags
|
tags
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagination = $('#paginator .pagination');
|
const pagination = $('#paginator .pagination');
|
||||||
const lastPageLink = pagination.find('a[alt="last page"]');
|
const lastPageLink = pagination.find('a[alt="last page"]');
|
||||||
|
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
if (lastPageLink.length) {
|
if (lastPageLink.length) {
|
||||||
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
|
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
|
||||||
totalPages = Math.ceil(pid / perPage) + 1;
|
totalPages = Math.ceil(pid / perPage) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
hasNextPage: page < totalPages,
|
hasNextPage: page < totalPages,
|
||||||
results
|
results
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
||||||
|
|
||||||
const resizeCookies = {
|
const resizeCookies = {
|
||||||
'resize-notification': 1,
|
'resize-notification': 1,
|
||||||
'resize-original': 1
|
'resize-original': 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; ');
|
const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||||
|
|
||||||
const fetchHeaders = { ...this.headers };
|
const fetchHeaders = { ...this.headers };
|
||||||
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
||||||
|
|
||||||
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
||||||
fetch(url, { headers: resizeHeaders }),
|
fetch(url, { headers: resizeHeaders }),
|
||||||
fetch(url, { headers: fetchHeaders })
|
fetch(url, { headers: fetchHeaders })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]);
|
const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]);
|
||||||
|
|
||||||
const $ = this.cheerio.load(original);
|
const $ = this.cheerio.load(original);
|
||||||
|
|
||||||
let fullImage = $('#image').attr('src');
|
let fullImage = $('#image').attr('src');
|
||||||
if (fullImage && !fullImage.startsWith('http')) {
|
if (fullImage && !fullImage.startsWith('http')) {
|
||||||
fullImage = `https:${fullImage}`;
|
fullImage = `https:${fullImage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image: fullImage,
|
image: fullImage,
|
||||||
tags
|
tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Rule34;
|
module.exports = Rule34;
|
||||||
@@ -1,115 +1,115 @@
|
|||||||
class Tenor {
|
class Tenor {
|
||||||
baseUrl = "https://tenor.com";
|
baseUrl = "https://tenor.com";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
this.lastQuery = null;
|
this.lastQuery = null;
|
||||||
this.seenIds = new Set();
|
this.seenIds = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query, page = 1, perPage = 48) {
|
async search(query, page = 1, perPage = 48) {
|
||||||
query = query?.trim() || "thighs";
|
query = query?.trim() || "thighs";
|
||||||
|
|
||||||
if (query !== this.lastQuery) {
|
if (query !== this.lastQuery) {
|
||||||
this.lastQuery = query;
|
this.lastQuery = query;
|
||||||
this.seenIds.clear();
|
this.seenIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
|
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
|
||||||
|
|
||||||
const { result } = await this.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const items = document.querySelectorAll('div.GifList figure, figure');
|
const items = document.querySelectorAll('div.GifList figure, figure');
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
items.forEach(fig => {
|
items.forEach(fig => {
|
||||||
const link = fig.querySelector('a');
|
const link = fig.querySelector('a');
|
||||||
const img = fig.querySelector('img');
|
const img = fig.querySelector('img');
|
||||||
if (!link || !img) return;
|
if (!link || !img) return;
|
||||||
|
|
||||||
const href = link.getAttribute('href') || "";
|
const href = link.getAttribute('href') || "";
|
||||||
const idMatch = href.match(/-(\d+)(?:$|\/?$)/);
|
const idMatch = href.match(/-(\d+)(?:$|\/?$)/);
|
||||||
const id = idMatch ? idMatch[1] : null;
|
const id = idMatch ? idMatch[1] : null;
|
||||||
|
|
||||||
const imgUrl =
|
const imgUrl =
|
||||||
img.getAttribute('src') ||
|
img.getAttribute('src') ||
|
||||||
img.getAttribute('data-src');
|
img.getAttribute('data-src');
|
||||||
|
|
||||||
const tagsRaw = img.getAttribute('alt') || "";
|
const tagsRaw = img.getAttribute('alt') || "";
|
||||||
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
if (id && imgUrl && !imgUrl.includes("placeholder")) {
|
if (id && imgUrl && !imgUrl.includes("placeholder")) {
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
image: imgUrl,
|
image: imgUrl,
|
||||||
sampleImageUrl: imgUrl,
|
sampleImageUrl: imgUrl,
|
||||||
tags,
|
tags,
|
||||||
type: "preview"
|
type: "preview"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueResults = Array.from(
|
const uniqueResults = Array.from(
|
||||||
new Map(results.map(r => [r.id, r])).values()
|
new Map(results.map(r => [r.id, r])).values()
|
||||||
);
|
);
|
||||||
|
|
||||||
return {results: uniqueResults, hasNextPage: true};
|
return {results: uniqueResults, hasNextPage: true};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: "figure",
|
waitSelector: "figure",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
scrollToBottom: true,
|
scrollToBottom: true,
|
||||||
renderWaitTime: 3000,
|
renderWaitTime: 3000,
|
||||||
loadImages: true
|
loadImages: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
|
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
|
||||||
newResults.forEach(r => this.seenIds.add(r.id));
|
newResults.forEach(r => this.seenIds.add(r.id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: newResults.map(r => ({
|
results: newResults.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
image: r.image,
|
image: r.image,
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
})),
|
})),
|
||||||
hasNextPage: result.hasNextPage,
|
hasNextPage: result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/view/gif-${id}`;
|
const url = `${this.baseUrl}/view/gif-${id}`;
|
||||||
|
|
||||||
const { result } = await this.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
async (page) => {
|
async (page) => {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const img = document.querySelector(".Gif img");
|
const img = document.querySelector(".Gif img");
|
||||||
const fullImage = img?.src || null;
|
const fullImage = img?.src || null;
|
||||||
|
|
||||||
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
|
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
|
||||||
.map(t => t.textContent.trim())
|
.map(t => t.textContent.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return { fullImage, tags };
|
return { fullImage, tags };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ waitSelector: ".Gif img", timeout: 15000 }
|
{ waitSelector: ".Gif img", timeout: 15000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image: result.fullImage,
|
image: result.fullImage,
|
||||||
tags: result.tags,
|
tags: result.tags,
|
||||||
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
|
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
|
||||||
headers: ""
|
headers: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Tenor;
|
module.exports = Tenor;
|
||||||
@@ -1,93 +1,93 @@
|
|||||||
class WaifuPics {
|
class WaifuPics {
|
||||||
baseUrl = "https://api.waifu.pics";
|
baseUrl = "https://api.waifu.pics";
|
||||||
|
|
||||||
SFW_CATEGORIES = [
|
SFW_CATEGORIES = [
|
||||||
'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo',
|
'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo',
|
||||||
'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave',
|
'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave',
|
||||||
'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick',
|
'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick',
|
||||||
'happy', 'wink', 'poke', 'dance', 'cringe'
|
'happy', 'wink', 'poke', 'dance', 'cringe'
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query, page = 1, perPage = 42) {
|
async search(query, page = 1, perPage = 42) {
|
||||||
if (!query) query = "waifu";
|
if (!query) query = "waifu";
|
||||||
|
|
||||||
const category = query.trim().split(' ')[0];
|
const category = query.trim().split(' ')[0];
|
||||||
|
|
||||||
if (!this.SFW_CATEGORIES.includes(category)) {
|
if (!this.SFW_CATEGORIES.includes(category)) {
|
||||||
console.warn(`[WaifuPics] Category '${category}' not supported.`);
|
console.warn(`[WaifuPics] Category '${category}' not supported.`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: 0,
|
total: 0,
|
||||||
next: 0,
|
next: 0,
|
||||||
previous: 0,
|
previous: 0,
|
||||||
pages: 1,
|
pages: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
results: []
|
results: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, {
|
const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ exclude: [] }),
|
body: JSON.stringify({ exclude: [] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API returned ${response.status}: ${await response.text()}`);
|
throw new Error(`API returned ${response.status}: ${await response.text()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const results = data.files.map((url, index) => {
|
const results = data.files.map((url, index) => {
|
||||||
|
|
||||||
const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`;
|
const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`;
|
||||||
const uniqueId = `${id}`;
|
const uniqueId = `${id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uniqueId,
|
id: uniqueId,
|
||||||
image: url,
|
image: url,
|
||||||
tags: [category],
|
tags: [category],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: 30,
|
total: 30,
|
||||||
next: page + 1,
|
next: page + 1,
|
||||||
previous: page > 1 ? page - 1 : 0,
|
previous: page > 1 ? page - 1 : 0,
|
||||||
pages: page + 1,
|
pages: page + 1,
|
||||||
page: page,
|
page: page,
|
||||||
hasNextPage: true,
|
hasNextPage: true,
|
||||||
results: results
|
results: results
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WaifuPics] Error fetching images:`, error);
|
console.error(`[WaifuPics] Error fetching images:`, error);
|
||||||
return {
|
return {
|
||||||
total: 0,
|
total: 0,
|
||||||
next: 0,
|
next: 0,
|
||||||
previous: 0,
|
previous: 0,
|
||||||
pages: 1,
|
pages: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
results: []
|
results: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(id) {
|
async getInfo(id) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
image: `https://i.waifu.pics/${id}`,
|
image: `https://i.waifu.pics/${id}`,
|
||||||
tags: []
|
tags: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WaifuPics;
|
module.exports = WaifuPics;
|
||||||
155
marketplace.json
Normal file
155
marketplace.json
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"AnimeAV1": {
|
||||||
|
"name": "AnimeAV1",
|
||||||
|
"type": "anime-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"animepictures": {
|
||||||
|
"name": "Anime Pictures",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/animepictures.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"asmhentai": {
|
||||||
|
"name": "ASM Hentai",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"gelbooru": {
|
||||||
|
"name": "Gelbooru",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/gelbooru.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"giphy": {
|
||||||
|
"name": "Giphy",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/giphy.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"HiAnime": {
|
||||||
|
"name": "HiAnime",
|
||||||
|
"type": "anime-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"lightnovelworld": {
|
||||||
|
"name": "Lightnovelworld",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/lightnovelworld.js",
|
||||||
|
"domain": "",
|
||||||
|
"broken": true
|
||||||
|
},
|
||||||
|
"mangadex": {
|
||||||
|
"name": "Mangadex",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"mangapark": {
|
||||||
|
"name": "Mangapark",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"nhentai": {
|
||||||
|
"name": "nhentai",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"novelbin": {
|
||||||
|
"name": "Novelbin",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelbin.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"novelfire": {
|
||||||
|
"name": "NovelFire",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelfire.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"realbooru": {
|
||||||
|
"name": "Realbooru",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/realbooru.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"rule34": {
|
||||||
|
"name": "Rule34",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/rule34.js",
|
||||||
|
"domain": "",
|
||||||
|
"nsfw": true
|
||||||
|
},
|
||||||
|
"tenor": {
|
||||||
|
"name": "Tenor",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/tenor.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"waifupics": {
|
||||||
|
"name": "Waifupics",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/waifupics.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"wattpad": {
|
||||||
|
"name": "Wattpad",
|
||||||
|
"type": "book-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/wattpad.js",
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
|
"ZeroChan": {
|
||||||
|
"name": "ZeroChan",
|
||||||
|
"type": "image-board",
|
||||||
|
"description": ".",
|
||||||
|
"author": "lenafx",
|
||||||
|
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/ZeroChan.js",
|
||||||
|
"domain": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user