new format and marketplace
This commit is contained in:
@@ -1,206 +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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,181 +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}/` }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,121 +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>";
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -1,137 +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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,119 +1,119 @@
|
||||
class ZeroChan {
|
||||
baseUrl = "https://zerochan.net";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "thighs", page = 1, perPage = 48) {
|
||||
const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelectorAll("#thumbs2 li");
|
||||
if (list.length === 0) {
|
||||
return {results: [], hasNextPage: false};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
list.forEach(li => {
|
||||
const id = li.getAttribute("data-id");
|
||||
if (!id) return;
|
||||
|
||||
const img = li.querySelector("img");
|
||||
const imgUrl =
|
||||
img?.getAttribute("data-src") ||
|
||||
img?.getAttribute("src") ||
|
||||
null;
|
||||
|
||||
if (!imgUrl) return;
|
||||
|
||||
const tagLinks = li.querySelectorAll("p a");
|
||||
const tags = [...tagLinks]
|
||||
.map(a => a.textContent.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
results.push({
|
||||
id,
|
||||
image: imgUrl,
|
||||
tags,
|
||||
});
|
||||
});
|
||||
|
||||
const hasNextPage =
|
||||
document.querySelector('nav.pagination a[rel="next"]') !== null;
|
||||
|
||||
return {results, hasNextPage};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "#thumbs2 li",
|
||||
timeout: 15000,
|
||||
renderWaitTime: 3000,
|
||||
loadImages: true
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
results: result.results.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image,
|
||||
tags: r.tags
|
||||
})),
|
||||
hasNextPage: result.hasNextPage,
|
||||
page
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/${id}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const preview = document.querySelector("a.preview");
|
||||
if (!preview) {
|
||||
return {
|
||||
fullImage: null,
|
||||
tags: [],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const fullImage = preview.getAttribute("href") || null;
|
||||
const img = preview.querySelector("img");
|
||||
const alt = img?.getAttribute("alt") || "";
|
||||
|
||||
let tags = [];
|
||||
if (alt.startsWith("Tags:")) {
|
||||
tags = alt
|
||||
.replace("Tags:", "")
|
||||
.split(",")
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return {
|
||||
fullImage,
|
||||
tags,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
});
|
||||
},
|
||||
{ waitSelector: "a.preview img", timeout: 15000 }
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: result.fullImage,
|
||||
tags: result.tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ZeroChan {
|
||||
baseUrl = "https://zerochan.net";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "thighs", page = 1, perPage = 48) {
|
||||
const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelectorAll("#thumbs2 li");
|
||||
if (list.length === 0) {
|
||||
return {results: [], hasNextPage: false};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
list.forEach(li => {
|
||||
const id = li.getAttribute("data-id");
|
||||
if (!id) return;
|
||||
|
||||
const img = li.querySelector("img");
|
||||
const imgUrl =
|
||||
img?.getAttribute("data-src") ||
|
||||
img?.getAttribute("src") ||
|
||||
null;
|
||||
|
||||
if (!imgUrl) return;
|
||||
|
||||
const tagLinks = li.querySelectorAll("p a");
|
||||
const tags = [...tagLinks]
|
||||
.map(a => a.textContent.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
results.push({
|
||||
id,
|
||||
image: imgUrl,
|
||||
tags,
|
||||
});
|
||||
});
|
||||
|
||||
const hasNextPage =
|
||||
document.querySelector('nav.pagination a[rel="next"]') !== null;
|
||||
|
||||
return {results, hasNextPage};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "#thumbs2 li",
|
||||
timeout: 15000,
|
||||
renderWaitTime: 3000,
|
||||
loadImages: true
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
results: result.results.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image,
|
||||
tags: r.tags
|
||||
})),
|
||||
hasNextPage: result.hasNextPage,
|
||||
page
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/${id}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const preview = document.querySelector("a.preview");
|
||||
if (!preview) {
|
||||
return {
|
||||
fullImage: null,
|
||||
tags: [],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const fullImage = preview.getAttribute("href") || null;
|
||||
const img = preview.querySelector("img");
|
||||
const alt = img?.getAttribute("alt") || "";
|
||||
|
||||
let tags = [];
|
||||
if (alt.startsWith("Tags:")) {
|
||||
tags = alt
|
||||
.replace("Tags:", "")
|
||||
.split(",")
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return {
|
||||
fullImage,
|
||||
tags,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
});
|
||||
},
|
||||
{ waitSelector: "a.preview img", timeout: 15000 }
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: result.fullImage,
|
||||
tags: result.tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ZeroChan;
|
||||
@@ -1,83 +1,83 @@
|
||||
class Gelbooru {
|
||||
baseUrl = "https://gelbooru.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
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 html = await fetch(url, {
|
||||
headers: { "User-Agent": "Mozilla/5.0" }
|
||||
}).then(r => r.text());
|
||||
|
||||
const $ = this.cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
|
||||
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
|
||||
if (!id) return;
|
||||
|
||||
const img = $(el).find("img");
|
||||
const image = img.attr("src");
|
||||
|
||||
const tags = img.attr("alt")
|
||||
?.replace(/^Rule 34 \|\s*/, "")
|
||||
?.split(",")
|
||||
?.map(t => t.trim())
|
||||
?.filter(Boolean) || [];
|
||||
|
||||
results.push({ id, image, tags });
|
||||
});
|
||||
|
||||
// pagination
|
||||
const totalPages = Math.max(
|
||||
page,
|
||||
...$("a[href*='pid=']")
|
||||
.map((_, el) =>
|
||||
Math.floor(
|
||||
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
|
||||
) + 1
|
||||
)
|
||||
.get()
|
||||
);
|
||||
|
||||
return {
|
||||
results,
|
||||
page,
|
||||
hasNextPage: page < totalPages
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const html = await fetch(
|
||||
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
|
||||
{ headers: { "User-Agent": "Mozilla/5.0" } }
|
||||
).then(r => r.text());
|
||||
|
||||
const $ = this.cheerio.load(html);
|
||||
const container = $("section.image-container");
|
||||
|
||||
let image =
|
||||
container.find("#image").attr("src") ||
|
||||
container.attr("data-file-url") ||
|
||||
container.attr("data-large-file-url") ||
|
||||
null;
|
||||
|
||||
// tags
|
||||
const tags = container
|
||||
.attr("data-tags")
|
||||
?.trim()
|
||||
?.split(/\s+/)
|
||||
?.filter(Boolean) || [];
|
||||
|
||||
return {
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Gelbooru {
|
||||
baseUrl = "https://gelbooru.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
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 html = await fetch(url, {
|
||||
headers: { "User-Agent": "Mozilla/5.0" }
|
||||
}).then(r => r.text());
|
||||
|
||||
const $ = this.cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
|
||||
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
|
||||
if (!id) return;
|
||||
|
||||
const img = $(el).find("img");
|
||||
const image = img.attr("src");
|
||||
|
||||
const tags = img.attr("alt")
|
||||
?.replace(/^Rule 34 \|\s*/, "")
|
||||
?.split(",")
|
||||
?.map(t => t.trim())
|
||||
?.filter(Boolean) || [];
|
||||
|
||||
results.push({ id, image, tags });
|
||||
});
|
||||
|
||||
// pagination
|
||||
const totalPages = Math.max(
|
||||
page,
|
||||
...$("a[href*='pid=']")
|
||||
.map((_, el) =>
|
||||
Math.floor(
|
||||
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
|
||||
) + 1
|
||||
)
|
||||
.get()
|
||||
);
|
||||
|
||||
return {
|
||||
results,
|
||||
page,
|
||||
hasNextPage: page < totalPages
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const html = await fetch(
|
||||
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
|
||||
{ headers: { "User-Agent": "Mozilla/5.0" } }
|
||||
).then(r => r.text());
|
||||
|
||||
const $ = this.cheerio.load(html);
|
||||
const container = $("section.image-container");
|
||||
|
||||
let image =
|
||||
container.find("#image").attr("src") ||
|
||||
container.attr("data-file-url") ||
|
||||
container.attr("data-large-file-url") ||
|
||||
null;
|
||||
|
||||
// tags
|
||||
const tags = container
|
||||
.attr("data-tags")
|
||||
?.trim()
|
||||
?.split(/\s+/)
|
||||
?.filter(Boolean) || [];
|
||||
|
||||
return {
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Gelbooru;
|
||||
@@ -1,100 +1,100 @@
|
||||
class Giphy {
|
||||
baseUrl = "https://giphy.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "hello", page = 1, perPage = 48) {
|
||||
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
|
||||
|
||||
const data = await this.scrape(
|
||||
url,
|
||||
(page) => page.evaluate(() => {
|
||||
const items = document.querySelectorAll('a[data-giphy-id]');
|
||||
const results = [];
|
||||
|
||||
items.forEach(el => {
|
||||
const id = el.getAttribute('data-giphy-id');
|
||||
|
||||
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
|
||||
const srcImg = el.querySelector('img');
|
||||
|
||||
let rawSrc =
|
||||
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
|
||||
srcImg?.src ||
|
||||
null;
|
||||
|
||||
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
||||
|
||||
const alt = srcImg?.getAttribute("alt") || "";
|
||||
const tags = alt.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
results.push({
|
||||
id,
|
||||
image: rawSrc,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
hasNextPage: false
|
||||
};
|
||||
}),
|
||||
{
|
||||
waitSelector: 'picture img, a[data-giphy-id] img',
|
||||
scrollToBottom: true,
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
results: data.result.results.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image
|
||||
})),
|
||||
hasNextPage: data.result.hasNextPage,
|
||||
page
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `https://giphy.com/gifs/${id}`;
|
||||
|
||||
const data = await this.scrape(
|
||||
url,
|
||||
(page) => page.evaluate(() => {
|
||||
const scripts = document.querySelectorAll(
|
||||
'script[type="application/ld+json"]'
|
||||
);
|
||||
|
||||
let imgsrc = null;
|
||||
|
||||
scripts.forEach(script => {
|
||||
try {
|
||||
const json = JSON.parse(script.textContent);
|
||||
|
||||
if (json?.["@type"] === "Article" && json?.image?.url) {
|
||||
imgsrc = json.image.url;
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
return {
|
||||
image: imgsrc
|
||||
};
|
||||
}),
|
||||
{
|
||||
waitSelector: 'script[type="application/ld+json"]',
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: data.result.image
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Giphy {
|
||||
baseUrl = "https://giphy.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "hello", page = 1, perPage = 48) {
|
||||
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
|
||||
|
||||
const data = await this.scrape(
|
||||
url,
|
||||
(page) => page.evaluate(() => {
|
||||
const items = document.querySelectorAll('a[data-giphy-id]');
|
||||
const results = [];
|
||||
|
||||
items.forEach(el => {
|
||||
const id = el.getAttribute('data-giphy-id');
|
||||
|
||||
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
|
||||
const srcImg = el.querySelector('img');
|
||||
|
||||
let rawSrc =
|
||||
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
|
||||
srcImg?.src ||
|
||||
null;
|
||||
|
||||
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
||||
|
||||
const alt = srcImg?.getAttribute("alt") || "";
|
||||
const tags = alt.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
results.push({
|
||||
id,
|
||||
image: rawSrc,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
hasNextPage: false
|
||||
};
|
||||
}),
|
||||
{
|
||||
waitSelector: 'picture img, a[data-giphy-id] img',
|
||||
scrollToBottom: true,
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
results: data.result.results.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image
|
||||
})),
|
||||
hasNextPage: data.result.hasNextPage,
|
||||
page
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `https://giphy.com/gifs/${id}`;
|
||||
|
||||
const data = await this.scrape(
|
||||
url,
|
||||
(page) => page.evaluate(() => {
|
||||
const scripts = document.querySelectorAll(
|
||||
'script[type="application/ld+json"]'
|
||||
);
|
||||
|
||||
let imgsrc = null;
|
||||
|
||||
scripts.forEach(script => {
|
||||
try {
|
||||
const json = JSON.parse(script.textContent);
|
||||
|
||||
if (json?.["@type"] === "Article" && json?.image?.url) {
|
||||
imgsrc = json.image.url;
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
return {
|
||||
image: imgsrc
|
||||
};
|
||||
}),
|
||||
{
|
||||
waitSelector: 'script[type="application/ld+json"]',
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: data.result.image
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Giphy;
|
||||
@@ -1,84 +1,84 @@
|
||||
class Realbooru {
|
||||
baseUrl = "https://realbooru.com";
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "original", page = 1, perPage = 42) {
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
const tags = query
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.join("+") + "+";
|
||||
|
||||
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 $ = this.cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
|
||||
$('div.col.thumb').each((_, el) => {
|
||||
const id = ($(el).attr('id') || "").replace('s', '');
|
||||
const img = $(el).find('img');
|
||||
|
||||
let image = img.attr('src');
|
||||
if (image && !image.startsWith('http')) image = 'https:' + image;
|
||||
|
||||
const title = img.attr('title') || '';
|
||||
const tags = title
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (id && image) {
|
||||
results.push({ id, image, tags });
|
||||
}
|
||||
});
|
||||
|
||||
let totalPages = page;
|
||||
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
|
||||
if (lastPid) {
|
||||
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
page,
|
||||
hasNextPage: page < totalPages
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(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 $ = this.cheerio.load(html);
|
||||
|
||||
let image =
|
||||
$('video source').attr('src') ||
|
||||
$('#image').attr('src') ||
|
||||
null;
|
||||
|
||||
if (image && !image.startsWith('http')) {
|
||||
image = this.baseUrl + image;
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
$('#tagLink a').each((_, el) => {
|
||||
tags.push($(el).text().trim());
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Realbooru {
|
||||
baseUrl = "https://realbooru.com";
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "original", page = 1, perPage = 42) {
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
const tags = query
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.join("+") + "+";
|
||||
|
||||
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 $ = this.cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
|
||||
$('div.col.thumb').each((_, el) => {
|
||||
const id = ($(el).attr('id') || "").replace('s', '');
|
||||
const img = $(el).find('img');
|
||||
|
||||
let image = img.attr('src');
|
||||
if (image && !image.startsWith('http')) image = 'https:' + image;
|
||||
|
||||
const title = img.attr('title') || '';
|
||||
const tags = title
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (id && image) {
|
||||
results.push({ id, image, tags });
|
||||
}
|
||||
});
|
||||
|
||||
let totalPages = page;
|
||||
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
|
||||
if (lastPid) {
|
||||
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
page,
|
||||
hasNextPage: page < totalPages
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(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 $ = this.cheerio.load(html);
|
||||
|
||||
let image =
|
||||
$('video source').attr('src') ||
|
||||
$('#image').attr('src') ||
|
||||
null;
|
||||
|
||||
if (image && !image.startsWith('http')) {
|
||||
image = this.baseUrl + image;
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
$('#tagLink a').each((_, el) => {
|
||||
tags.push($(el).text().trim());
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Realbooru;
|
||||
@@ -1,99 +1,99 @@
|
||||
class Rule34 {
|
||||
baseUrl = "https://rule34.xxx";
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
|
||||
const offset = (page - 1) * perPage;
|
||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
|
||||
|
||||
const response = await fetch(url, { headers: this.headers });
|
||||
const data = await response.text();
|
||||
const $ = this.cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
$('.image-list span').each((_, e) => {
|
||||
const $e = $(e);
|
||||
const id = $e.attr('id')?.replace('s', '');
|
||||
let image = $e.find('img').attr('src');
|
||||
|
||||
if (image && !image.startsWith('http')) {
|
||||
image = `https:${image}`;
|
||||
}
|
||||
|
||||
const tags = $e.find('img')
|
||||
.attr('alt')
|
||||
?.trim()
|
||||
.split(' ')
|
||||
.filter(Boolean);
|
||||
|
||||
if (id && image) {
|
||||
results.push({
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pagination = $('#paginator .pagination');
|
||||
const lastPageLink = pagination.find('a[alt="last page"]');
|
||||
|
||||
let totalPages = 1;
|
||||
if (lastPageLink.length) {
|
||||
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
|
||||
totalPages = Math.ceil(pid / perPage) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
hasNextPage: page < totalPages,
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
||||
|
||||
const resizeCookies = {
|
||||
'resize-notification': 1,
|
||||
'resize-original': 1
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||
|
||||
const fetchHeaders = { ...this.headers };
|
||||
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
||||
|
||||
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
||||
fetch(url, { headers: resizeHeaders }),
|
||||
fetch(url, { headers: fetchHeaders })
|
||||
]);
|
||||
|
||||
const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]);
|
||||
|
||||
const $ = this.cheerio.load(original);
|
||||
|
||||
let fullImage = $('#image').attr('src');
|
||||
if (fullImage && !fullImage.startsWith('http')) {
|
||||
fullImage = `https:${fullImage}`;
|
||||
}
|
||||
|
||||
const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
||||
|
||||
return {
|
||||
id,
|
||||
image: fullImage,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Rule34 {
|
||||
baseUrl = "https://rule34.xxx";
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
|
||||
const offset = (page - 1) * perPage;
|
||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
|
||||
|
||||
const response = await fetch(url, { headers: this.headers });
|
||||
const data = await response.text();
|
||||
const $ = this.cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
$('.image-list span').each((_, e) => {
|
||||
const $e = $(e);
|
||||
const id = $e.attr('id')?.replace('s', '');
|
||||
let image = $e.find('img').attr('src');
|
||||
|
||||
if (image && !image.startsWith('http')) {
|
||||
image = `https:${image}`;
|
||||
}
|
||||
|
||||
const tags = $e.find('img')
|
||||
.attr('alt')
|
||||
?.trim()
|
||||
.split(' ')
|
||||
.filter(Boolean);
|
||||
|
||||
if (id && image) {
|
||||
results.push({
|
||||
id,
|
||||
image,
|
||||
tags
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pagination = $('#paginator .pagination');
|
||||
const lastPageLink = pagination.find('a[alt="last page"]');
|
||||
|
||||
let totalPages = 1;
|
||||
if (lastPageLink.length) {
|
||||
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
|
||||
totalPages = Math.ceil(pid / perPage) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
hasNextPage: page < totalPages,
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
||||
|
||||
const resizeCookies = {
|
||||
'resize-notification': 1,
|
||||
'resize-original': 1
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||
|
||||
const fetchHeaders = { ...this.headers };
|
||||
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
||||
|
||||
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
||||
fetch(url, { headers: resizeHeaders }),
|
||||
fetch(url, { headers: fetchHeaders })
|
||||
]);
|
||||
|
||||
const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]);
|
||||
|
||||
const $ = this.cheerio.load(original);
|
||||
|
||||
let fullImage = $('#image').attr('src');
|
||||
if (fullImage && !fullImage.startsWith('http')) {
|
||||
fullImage = `https:${fullImage}`;
|
||||
}
|
||||
|
||||
const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
||||
|
||||
return {
|
||||
id,
|
||||
image: fullImage,
|
||||
tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rule34;
|
||||
@@ -1,115 +1,115 @@
|
||||
class Tenor {
|
||||
baseUrl = "https://tenor.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
this.lastQuery = null;
|
||||
this.seenIds = new Set();
|
||||
}
|
||||
|
||||
async search(query, page = 1, perPage = 48) {
|
||||
query = query?.trim() || "thighs";
|
||||
|
||||
if (query !== this.lastQuery) {
|
||||
this.lastQuery = query;
|
||||
this.seenIds.clear();
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const items = document.querySelectorAll('div.GifList figure, figure');
|
||||
const results = [];
|
||||
|
||||
items.forEach(fig => {
|
||||
const link = fig.querySelector('a');
|
||||
const img = fig.querySelector('img');
|
||||
if (!link || !img) return;
|
||||
|
||||
const href = link.getAttribute('href') || "";
|
||||
const idMatch = href.match(/-(\d+)(?:$|\/?$)/);
|
||||
const id = idMatch ? idMatch[1] : null;
|
||||
|
||||
const imgUrl =
|
||||
img.getAttribute('src') ||
|
||||
img.getAttribute('data-src');
|
||||
|
||||
const tagsRaw = img.getAttribute('alt') || "";
|
||||
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
if (id && imgUrl && !imgUrl.includes("placeholder")) {
|
||||
results.push({
|
||||
id,
|
||||
image: imgUrl,
|
||||
sampleImageUrl: imgUrl,
|
||||
tags,
|
||||
type: "preview"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueResults = Array.from(
|
||||
new Map(results.map(r => [r.id, r])).values()
|
||||
);
|
||||
|
||||
return {results: uniqueResults, hasNextPage: true};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "figure",
|
||||
timeout: 30000,
|
||||
scrollToBottom: true,
|
||||
renderWaitTime: 3000,
|
||||
loadImages: true
|
||||
}
|
||||
);
|
||||
|
||||
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
|
||||
newResults.forEach(r => this.seenIds.add(r.id));
|
||||
|
||||
return {
|
||||
results: newResults.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image,
|
||||
tags: r.tags,
|
||||
})),
|
||||
hasNextPage: result.hasNextPage,
|
||||
page
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/view/gif-${id}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const img = document.querySelector(".Gif img");
|
||||
const fullImage = img?.src || null;
|
||||
|
||||
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
|
||||
.map(t => t.textContent.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return { fullImage, tags };
|
||||
});
|
||||
},
|
||||
{ waitSelector: ".Gif img", timeout: 15000 }
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: result.fullImage,
|
||||
tags: result.tags,
|
||||
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
|
||||
headers: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Tenor {
|
||||
baseUrl = "https://tenor.com";
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
this.lastQuery = null;
|
||||
this.seenIds = new Set();
|
||||
}
|
||||
|
||||
async search(query, page = 1, perPage = 48) {
|
||||
query = query?.trim() || "thighs";
|
||||
|
||||
if (query !== this.lastQuery) {
|
||||
this.lastQuery = query;
|
||||
this.seenIds.clear();
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const items = document.querySelectorAll('div.GifList figure, figure');
|
||||
const results = [];
|
||||
|
||||
items.forEach(fig => {
|
||||
const link = fig.querySelector('a');
|
||||
const img = fig.querySelector('img');
|
||||
if (!link || !img) return;
|
||||
|
||||
const href = link.getAttribute('href') || "";
|
||||
const idMatch = href.match(/-(\d+)(?:$|\/?$)/);
|
||||
const id = idMatch ? idMatch[1] : null;
|
||||
|
||||
const imgUrl =
|
||||
img.getAttribute('src') ||
|
||||
img.getAttribute('data-src');
|
||||
|
||||
const tagsRaw = img.getAttribute('alt') || "";
|
||||
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
if (id && imgUrl && !imgUrl.includes("placeholder")) {
|
||||
results.push({
|
||||
id,
|
||||
image: imgUrl,
|
||||
sampleImageUrl: imgUrl,
|
||||
tags,
|
||||
type: "preview"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueResults = Array.from(
|
||||
new Map(results.map(r => [r.id, r])).values()
|
||||
);
|
||||
|
||||
return {results: uniqueResults, hasNextPage: true};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "figure",
|
||||
timeout: 30000,
|
||||
scrollToBottom: true,
|
||||
renderWaitTime: 3000,
|
||||
loadImages: true
|
||||
}
|
||||
);
|
||||
|
||||
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
|
||||
newResults.forEach(r => this.seenIds.add(r.id));
|
||||
|
||||
return {
|
||||
results: newResults.map(r => ({
|
||||
id: r.id,
|
||||
image: r.image,
|
||||
tags: r.tags,
|
||||
})),
|
||||
hasNextPage: result.hasNextPage,
|
||||
page
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
const url = `${this.baseUrl}/view/gif-${id}`;
|
||||
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const img = document.querySelector(".Gif img");
|
||||
const fullImage = img?.src || null;
|
||||
|
||||
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
|
||||
.map(t => t.textContent.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return { fullImage, tags };
|
||||
});
|
||||
},
|
||||
{ waitSelector: ".Gif img", timeout: 15000 }
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
image: result.fullImage,
|
||||
tags: result.tags,
|
||||
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
|
||||
headers: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tenor;
|
||||
@@ -1,93 +1,93 @@
|
||||
class WaifuPics {
|
||||
baseUrl = "https://api.waifu.pics";
|
||||
|
||||
SFW_CATEGORIES = [
|
||||
'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo',
|
||||
'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave',
|
||||
'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick',
|
||||
'happy', 'wink', 'poke', 'dance', 'cringe'
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query, page = 1, perPage = 42) {
|
||||
if (!query) query = "waifu";
|
||||
|
||||
const category = query.trim().split(' ')[0];
|
||||
|
||||
if (!this.SFW_CATEGORIES.includes(category)) {
|
||||
console.warn(`[WaifuPics] Category '${category}' not supported.`);
|
||||
|
||||
return {
|
||||
total: 0,
|
||||
next: 0,
|
||||
previous: 0,
|
||||
pages: 1,
|
||||
page: 1,
|
||||
hasNextPage: false,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ exclude: [] }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API returned ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const results = data.files.map((url, index) => {
|
||||
|
||||
const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`;
|
||||
const uniqueId = `${id}`;
|
||||
|
||||
return {
|
||||
id: uniqueId,
|
||||
image: url,
|
||||
tags: [category],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
total: 30,
|
||||
next: page + 1,
|
||||
previous: page > 1 ? page - 1 : 0,
|
||||
pages: page + 1,
|
||||
page: page,
|
||||
hasNextPage: true,
|
||||
results: results
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[WaifuPics] Error fetching images:`, error);
|
||||
return {
|
||||
total: 0,
|
||||
next: 0,
|
||||
previous: 0,
|
||||
pages: 1,
|
||||
page: 1,
|
||||
hasNextPage: false,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
return {
|
||||
id,
|
||||
image: `https://i.waifu.pics/${id}`,
|
||||
tags: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class WaifuPics {
|
||||
baseUrl = "https://api.waifu.pics";
|
||||
|
||||
SFW_CATEGORIES = [
|
||||
'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo',
|
||||
'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave',
|
||||
'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick',
|
||||
'happy', 'wink', 'poke', 'dance', 'cringe'
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.type = "image-board";
|
||||
}
|
||||
|
||||
async search(query, page = 1, perPage = 42) {
|
||||
if (!query) query = "waifu";
|
||||
|
||||
const category = query.trim().split(' ')[0];
|
||||
|
||||
if (!this.SFW_CATEGORIES.includes(category)) {
|
||||
console.warn(`[WaifuPics] Category '${category}' not supported.`);
|
||||
|
||||
return {
|
||||
total: 0,
|
||||
next: 0,
|
||||
previous: 0,
|
||||
pages: 1,
|
||||
page: 1,
|
||||
hasNextPage: false,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ exclude: [] }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API returned ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const results = data.files.map((url, index) => {
|
||||
|
||||
const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`;
|
||||
const uniqueId = `${id}`;
|
||||
|
||||
return {
|
||||
id: uniqueId,
|
||||
image: url,
|
||||
tags: [category],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
total: 30,
|
||||
next: page + 1,
|
||||
previous: page > 1 ? page - 1 : 0,
|
||||
pages: page + 1,
|
||||
page: page,
|
||||
hasNextPage: true,
|
||||
results: results
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[WaifuPics] Error fetching images:`, error);
|
||||
return {
|
||||
total: 0,
|
||||
next: 0,
|
||||
previous: 0,
|
||||
pages: 1,
|
||||
page: 1,
|
||||
hasNextPage: false,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getInfo(id) {
|
||||
return {
|
||||
id,
|
||||
image: `https://i.waifu.pics/${id}`,
|
||||
tags: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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