* add prettier

* lint-staged

* prettier

* upgrade husky
This commit is contained in:
nyagami
2024-03-23 22:00:29 +07:00
committed by GitHub
parent c22979b10f
commit 5f9a067ab9
113 changed files with 79244 additions and 50841 deletions
+191 -186
View File
@@ -1,204 +1,209 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import qs from "qs";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import qs from 'qs';
class NovelsOnline implements Plugin.PluginBase {
id = "NO.net";
name = "novelsOnline";
site = "https://novelsonline.net";
icon = "src/en/novelsonline/icon.png";
version = "1.0.0";
id = 'NO.net';
name = 'novelsOnline';
site = 'https://novelsonline.net';
icon = 'src/en/novelsonline/icon.png';
version = '1.0.0';
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `/category/${filters.genre.value}/`;
else
link += `/top-novel/`;
link += page
link += `?change_type=${filters.sort.value}`
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '') link += `/category/${filters.genre.value}/`;
else link += `/top-novel/`;
const body = await fetchApi(link).then((r) => r.text());
link += page;
link += `?change_type=${filters.sort.value}`;
const loadedCheerio = parseHTML(body);
const novel: Plugin.NovelItem[] = [];
loadedCheerio('.top-novel-block').each((i,el) => {
const novelName = loadedCheerio(el).find('h2').text();
const novelCover = loadedCheerio(el).find('.top-novel-cover img').attr('src');
const novelUrl = loadedCheerio(el).find('h2 a').attr('href');
if (!novelUrl) return;
const body = await fetchApi(link).then(r => r.text());
novel.push({
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
})
})
return novel;
}
const loadedCheerio = parseHTML(body);
const novel: Plugin.NovelItem[] = [];
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
let $ = parseHTML(result);
loadedCheerio('.top-novel-block').each((i, el) => {
const novelName = loadedCheerio(el).find('h2').text();
const novelCover = loadedCheerio(el)
.find('.top-novel-cover img')
.attr('src');
const novelUrl = loadedCheerio(el).find('h2 a').attr('href');
if (!novelUrl) return;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: $("h1").text() || 'Untitled',
cover: $(".novel-cover").find("a > img").attr("src"),
chapters: [],
};
novel.push({
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
});
});
return novel;
}
$('.novel-detail-item').each((i,el) => {
let detailName = $(el).find('h6').text();
let detail = $(el).find('.novel-detail-body');
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath).then(res =>
res.text(),
);
let $ = parseHTML(result);
switch(detailName){
case "Description":
novel.summary = detail.text();
break;
case "Genre":
novel.genres = detail.find('li')
.map((_, el) => $(el).text())
.get()
.join(', ');
break;
case "Author(s)":
novel.author = detail.find('li')
.map((_, el) => $(el).text())
.get()
.join(', ');
break;
case "Status":
novel.status = detail.text().trim();
break;
}
})
const novel: Plugin.SourceNovel = {
path: novelPath,
name: $('h1').text() || 'Untitled',
cover: $('.novel-cover').find('a > img').attr('src'),
chapters: [],
};
novel.chapters = $("ul.chapter-chs > li > a")
.map((_, el) => {
const chapterUrl = $(el).attr("href");
const chapterName = $(el).text();
$('.novel-detail-item').each((i, el) => {
let detailName = $(el).find('h6').text();
let detail = $(el).find('.novel-detail-body');
return {
name: chapterName,
path: chapterUrl?.replace(this.site, ''),
} as Plugin.ChapterItem;
})
.get();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath).then((res) => res.text());
const loadedCheerio = parseHTML(result);
const chapterText = loadedCheerio("#contentall").html() || "";
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(
"https://novelsonline.net/sResults.php",
{
headers: {
Accept: "*/*",
"Accept-Language": "pl,en-US;q=0.7,en;q=0.3",
"Content-Type":
"application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: qs.stringify({ q: searchTerm }),
},
).then((res) => res.text());
const $ = parseHTML(result);
const headers = $("li");
return headers
.map((i, h) => {
const novelName = $(h).text();
const novelUrl = $(h).find("a").attr("href");
const novelCover = $(h).find("img").attr("src");
if (!novelUrl) return
return {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
})
switch (detailName) {
case 'Description':
novel.summary = detail.text();
break;
case 'Genre':
novel.genres = detail
.find('li')
.map((_, el) => $(el).text())
.get()
.filter((sr) => sr !== null)
}
.join(', ');
break;
case 'Author(s)':
novel.author = detail
.find('li')
.map((_, el) => $(el).text())
.get()
.join(', ');
break;
case 'Status':
novel.status = detail.text().trim();
break;
}
});
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
novel.chapters = $('ul.chapter-chs > li > a')
.map((_, el) => {
const chapterUrl = $(el).attr('href');
const chapterName = $(el).text();
filters = {
sort: {
value: "top_rated",
label: "Sort by",
options: [
{ label: "Top Rated", value: "top_rated" },
{ label: "Most Viewed", value: "view" },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Category",
options: [
{ label: "None", value: "" },
{ label: "Action", value: "action" },
{ label: "Adventure", value: "adventure" },
{ label: "Celebrity", value: "celebrity" },
{ label: "Comedy", value: "comedy" },
{ label: "Drama", value: "drama" },
{ label: "Ecchi", value: "ecchi" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Harem", value: "harem" },
{ label: "Historical", value: "historical" },
{ label: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ label: "Martial Arts", value: "martial-arts" },
{ label: "Mature", value: "mature" },
{ label: "Mecha", value: "mecha" },
{ label: "Mystery", value: "mystery" },
{ label: "Psychological", value: "psychological" },
{ label: "Romance", value: "romance" },
{ label: "School Life", value: "school-life" },
{ label: "Sci-fi", value: "sci-fi" },
{ label: "Seinen", value: "seinen" },
{ label: "Shotacon", value: "shotacon" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Shoujo Ai", value: "shoujo-ai" },
{ label: "Shounen", value: "shounen" },
{ label: "Shounen Ai", value: "shounen-ai" },
{ label: "Slice of Life", value: "slice-of-life" },
{ label: "Sports", value: "sports" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Yaoi", value: "yaoi" },
{ label: "Yuri", value: "yuri" }
],
type: FilterTypes.Picker,
},
} satisfies Filters;
return {
name: chapterName,
path: chapterUrl?.replace(this.site, ''),
} as Plugin.ChapterItem;
})
.get();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
const chapterText = loadedCheerio('#contentall').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const result = await fetchApi('https://novelsonline.net/sResults.php', {
headers: {
Accept: '*/*',
'Accept-Language': 'pl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
method: 'POST',
body: qs.stringify({ q: searchTerm }),
}).then(res => res.text());
const $ = parseHTML(result);
const headers = $('li');
return headers
.map((i, h) => {
const novelName = $(h).text();
const novelUrl = $(h).find('a').attr('href');
const novelCover = $(h).find('img').attr('src');
if (!novelUrl) return;
return {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
})
.get()
.filter(sr => sr !== null);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
sort: {
value: 'top_rated',
label: 'Sort by',
options: [
{ label: 'Top Rated', value: 'top_rated' },
{ label: 'Most Viewed', value: 'view' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Category',
options: [
{ label: 'None', value: '' },
{ label: 'Action', value: 'action' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Celebrity', value: 'celebrity' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Drama', value: 'drama' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Harem', value: 'harem' },
{ label: 'Historical', value: 'historical' },
{ label: 'Horror', value: 'horror' },
{ label: 'Josei', value: 'josei' },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Romance', value: 'romance' },
{ label: 'School Life', value: 'school-life' },
{ label: 'Sci-fi', value: 'sci-fi' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Shotacon', value: 'shotacon' },
{ label: 'Shoujo', value: 'shoujo' },
{ label: 'Shoujo Ai', value: 'shoujo-ai' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Shounen Ai', value: 'shounen-ai' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Sports', value: 'sports' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Yaoi', value: 'yaoi' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new NovelsOnline();
+138 -114
View File
@@ -1,140 +1,164 @@
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { load as parseHTML } from "cheerio";
import { defaultCover } from "@libs/defaultCover";
import { Plugin } from '@typings/plugin';
import { fetchApi, fetchFile } from '@libs/fetch';
import { load as parseHTML } from 'cheerio';
import { defaultCover } from '@libs/defaultCover';
class StorySeedlingPlugin implements Plugin.PluginBase {
id = "storyseedling";
name = "StorySeedling";
icon = "src/en/storyseedling/icon.png";
site = "https://storyseedling.com/";
version = "1.0.0";
id = 'storyseedling';
name = 'StorySeedling';
icon = 'src/en/storyseedling/icon.png';
site = 'https://storyseedling.com/';
version = '1.0.0';
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = "https://storyseedling.com/ajax";
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = 'https://storyseedling.com/ajax';
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then((r) => r.text());
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then(r => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string;
var postValue = loadedCheerio('div[ax-load][x-data]')
.attr('x-data')
?.replace("browse('", '')
.replace("')", '') as string;
var data = new FormData();
data.set("search", "");
data.set("orderBy", "recent");
data.set("curpage", pageNo.toString());
data.set("post", postValue);
data.set("action", "fetch_browse");
var data = new FormData();
data.set('search', '');
data.set('orderBy', 'recent');
data.set('curpage', pageNo.toString());
data.set('post', postValue);
data.set('action', 'fetch_browse');
var response = await(await fetchApi(url, {body: data, method: "POST"})).json();
var response = await (
await fetchApi(url, { body: data, method: 'POST' })
).json();
response.data.posts.forEach((element: any) => {
const novel = {
name: element.title,
cover: element.thumbnail,
path: element.permalink.replace(this.site, ""),
};
novels.push(novel);
});
response.data.posts.forEach((element: any) => {
const novel = {
name: element.title,
cover: element.thumbnail,
path: element.permalink.replace(this.site, ''),
};
novels.push(novel);
});
return novels;
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
var site = this.site;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
};
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
novel.name = loadedCheerio('h1').text().trim();
//novel.artist = "";
//novel.author = "";
novel.cover = loadedCheerio(
'img[x-ref="art"].w-full.rounded.shadow-md',
).attr('src');
if (!novel.cover) {
novel.cover = defaultCover;
}
let genres: string[] = [];
loadedCheerio(
'section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a',
).each(function () {
genres.push(loadedCheerio(this).text().trim());
});
novel.genres = genres.join(', ');
// novel.status = NovelStatus.Completed;
novel.summary = loadedCheerio('div.mb-4.order-2:not(.lg\\:grid-in-buttons)')
.text()
.trim()
.replace(/(\r\n|\n|\r)/gm, '');
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
var site = this.site;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: ""
};
let chapters: Plugin.ChapterItem[] = [];
const body = await fetchApi(this.site+novelPath).then((r) => r.text());
loadedCheerio(
'div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a',
).each(function () {
if (loadedCheerio(this).find('> div').length == 2) {
return;
}
let name = loadedCheerio(this).find('.truncate').text().trim();
let url = loadedCheerio(this).attr('href') as string;
let releaseTime = loadedCheerio(this)
.find('div > div > small')
.text()
.trim();
let chapterNumber = name.split('-')[0].trim().split(' ')[1];
const chapter: Plugin.ChapterItem = {
name: name,
path: url.replace(site, ''),
releaseTime: releaseTime,
chapterNumber: parseInt(chapterNumber),
};
chapters.push(chapter);
});
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
let t = loadedCheerio('div.justify-center > div.mb-4');
let chapterText = t.html() || '';
const loadedCheerio = parseHTML(body);
novel.name = loadedCheerio("h1").text().trim();
return chapterText;
}
//novel.artist = "";
//novel.author = "";
novel.cover = loadedCheerio('img[x-ref="art"].w-full.rounded.shadow-md').attr("src");
if (!novel.cover) {
novel.cover = defaultCover;
}
let genres:string[] = [];
loadedCheerio('section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a').each(function(){
genres.push(loadedCheerio(this).text().trim());
});
novel.genres = genres.join(", ");
// novel.status = NovelStatus.Completed;
novel.summary = loadedCheerio("div.mb-4.order-2:not(.lg\\:grid-in-buttons)").text().trim().replace(/(\r\n|\n|\r)/gm, "");
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = 'https://storyseedling.com/ajax';
let chapters: Plugin.ChapterItem[] = [];
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then(r => r.text());
loadedCheerio("div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a").each(function() {
if(loadedCheerio(this).find("> div").length == 2){
return;
}
let name = loadedCheerio(this).find(".truncate").text().trim();
let url = loadedCheerio(this).attr("href") as string;
let releaseTime = loadedCheerio(this).find("div > div > small").text().trim();
let chapterNumber = name.split("-")[0].trim().split(" ")[1];
const chapter: Plugin.ChapterItem = {
name: name,
path: url.replace(site, ""),
releaseTime: releaseTime,
chapterNumber: parseInt(chapterNumber),
};
chapters.push(chapter);
});
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site+chapterPath).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
let t = loadedCheerio("div.justify-center > div.mb-4");
let chapterText = t.html() || '';
var postValue = loadedCheerio('div[ax-load][x-data]')
.attr('x-data')
?.replace("browse('", '')
.replace("')", '') as string;
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = "https://storyseedling.com/ajax";
var data = new FormData();
data.set('search', searchTerm);
data.set('orderBy', 'recent');
data.set('curpage', pageNo.toString());
data.set('post', postValue);
data.set('action', 'fetch_browse');
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then((r) => r.text());
var response = await (
await fetchApi(url, { body: data, method: 'POST' })
).json();
const loadedCheerio = parseHTML(body);
response.data.posts.forEach((element: any) => {
const novel = {
name: element.title,
cover: element.thumbnail,
path: element.permalink.replace(this.site, ''),
};
novels.push(novel);
});
var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string;
var data = new FormData();
data.set("search", searchTerm);
data.set("orderBy", "recent");
data.set("curpage", pageNo.toString());
data.set("post", postValue);
data.set("action", "fetch_browse");
var response = await(await fetchApi(url, {body: data, method: "POST"})).json();
response.data.posts.forEach((element: any) => {
const novel = {
name: element.title,
cover: element.thumbnail,
path: element.permalink.replace(this.site, ""),
};
novels.push(novel);
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new StorySeedlingPlugin();
+191 -192
View File
@@ -1,215 +1,214 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
class AllNovelFullPlugin implements Plugin.PluginBase {
id = "anf.net";
name = "AllNovelFull";
icon = "src/en/allnovelfull/icon.png";
site = "https://allnovelfull.net";
version = "1.0.0";
id = 'anf.net';
name = 'AllNovelFull';
icon = 'src/en/allnovelfull/icon.png';
site = 'https://allnovelfull.net';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".col-truyen-main .list-truyen .row").each((i,el) => {
const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
if (!novelUrl) return;
loadedCheerio('.col-truyen-main .list-truyen .row').each((i, el) => {
const novelUrl = loadedCheerio(el)
.find('h3.truyen-title > a')
.attr('href');
if (!novelUrl) return;
const novelName = loadedCheerio(el)
.find("h3.truyen-title > a")
.text();
const novelName = loadedCheerio(el).find('h3.truyen-title > a').text();
let novelCover = loadedCheerio(el).find("img.cover").attr("src");
if (novelCover && !isUrlAbsolute(novelCover)){
novelCover = this.site + novelCover;
}
let novelCover = loadedCheerio(el).find('img.cover').attr('src');
if (novelCover && !isUrlAbsolute(novelCover)) {
novelCover = this.site + novelCover;
}
const novel = {
path: novelUrl,
name: novelName,
cover: novelCover,
};
novels.push(novel);
const novel = {
path: novelUrl,
name: novelName,
cover: novelCover,
};
novels.push(novel);
});
return novels;
}
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genres.value.length) link += filters.genres.value;
else link += filters.order.value;
link += `?page=${pageNo}`;
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.book > img').attr('alt') || 'Untitled',
cover: this.site + loadedCheerio('.book > img').attr('src'),
summary: loadedCheerio('.desc-text').text().trim(),
chapters: [],
};
loadedCheerio('.info > div').each((i, el) => {
const detailName = loadedCheerio(el).find('h3').text();
const detail = loadedCheerio(el)
.find('a')
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
switch (detailName) {
case 'Author:':
novel.author = detail;
break;
case 'Status:':
novel.status = detail;
break;
case 'Genre:':
novel.genres = detail;
break;
}
});
const novelId = loadedCheerio('#rating').attr('data-novel-id');
const getChapters = async (id: string) => {
const chaptersUrl = this.site + '/ajax/chapter-option?novelId=' + id;
const data = await fetchApi(chaptersUrl);
const chapters = await data.text();
loadedCheerio = parseHTML(chapters);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('select > option').each((i, el) => {
const chapterName = loadedCheerio(el).text();
const chapterUrl = loadedCheerio(el).attr('value');
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
return novels;
});
return chapter;
};
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genres.value.length)
link += filters.genres.value;
else
link += filters.order.value;
link += `?page=${pageNo}`;
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
if (novelId) {
novel.chapters = await getChapters(novelId);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
return novel;
}
let loadedCheerio = parseHTML(body);
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".book > img").attr("alt") || 'Untitled',
cover: this.site + loadedCheerio(".book > img").attr("src"),
summary: loadedCheerio(".desc-text").text().trim(),
chapters: [],
};
const loadedCheerio = parseHTML(body);
loadedCheerio(".info > div").each((i,el) => {
const detailName = loadedCheerio(el).find("h3").text();
const detail = loadedCheerio(el)
.find('a')
.map((a,ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
const chapterText = loadedCheerio('#chapter-content').html() || '';
switch (detailName) {
case 'Author:':
novel.author = detail;
break;
case 'Status:':
novel.status = detail;
break;
case 'Genre:':
novel.genres = detail;
break;
}});
return chapterText;
}
const novelId = loadedCheerio("#rating").attr("data-novel-id");
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`;
const result = await fetchApi(url);
const body = await result.text();
const getChapters = async (id: string) => {
const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id;
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const data = await fetchApi(chaptersUrl);
const chapters = await data.text();
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
loadedCheerio = parseHTML(chapters);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("select > option").each((i,el) => {
const chapterName = loadedCheerio(el).text();
const chapterUrl = loadedCheerio(el).attr("value");
if (!chapterUrl ) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
return chapter;
}
if (novelId) {
novel.chapters = await getChapters(novelId);
}
return novel;
};
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapter-content").html() || "";
return chapterText;
};
async searchNovels(
searchTerm: string,
page: number
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
order: {
value: "/most-popular",
label: "Order by",
options: [
{ label: "Latest Release", value: "/latest-release-novel" },
{ label: "Hot Novel", value: "/hot-novel" },
{ label: "Completed Novel", value: "/completed-novel" },
{ label: "Most Popular", value: "/most-popular" },
],
type: FilterTypes.Picker,
},
genres: {
value: "",
label: "Genre",
options: [
{ label: "None", value: "" },
{ label: "Shounen", value: "/genre/Shounen" },
{ label: "Harem", value: "/genre/Harem" },
{ label: "Comedy", value: "/genre/Comedy" },
{ label: "Martial Arts", value: "/genre/Martial+Arts" },
{ label: "School Life", value: "/genre/School+Life" },
{ label: "Mystery", value: "/genre/Mystery" },
{ label: "Shoujo", value: "/genre/Shoujo" },
{ label: "Romance", value: "/genre/Romance" },
{ label: "Sci-fi", value: "/genre/Sci-fi" },
{ label: "Gender Bender", value: "/genre/Gender+Bender" },
{ label: "Mature", value: "/genre/Mature" },
{ label: "Fantasy", value: "/genre/Fantasy" },
{ label: "Horror", value: "/genre/Horror" },
{ label: "Drama", value: "/genre/Drama" },
{ label: "Tragedy", value: "/genre/Tragedy" },
{ label: "Supernatural", value: "/genre/Supernatural" },
{ label: "Ecchi", value: "/genre/Ecchi" },
{ label: "Xuanhuan", value: "/genre/Xuanhuan" },
{ label: "Adventure", value: "/genre/Adventure" },
{ label: "Action", value: "/genre/Action" },
{ label: "Psychological", value: "/genre/Psychological" },
{ label: "Xianxia", value: "/genre/Xianxia" },
{ label: "Wuxia", value: "/genre/Wuxia" },
{ label: "Historical", value: "/genre/Historical" },
{ label: "Slice of Life", value: "/genre/Slice+of+Life" },
{ label: "Seinen", value: "/genre/Seinen" },
{ label: "Lolicon", value: "/genre/Lolicon" },
{ label: "Adult", value: "/genre/Adult" },
{ label: "Josei", value: "/genre/Josei" },
{ label: "Sports", value: "/genre/Sports" },
{ label: "Smut", value: "/genre/Smut" },
{ label: "Mecha", value: "/genre/Mecha" },
{ label: "Yaoi", value: "/genre/Yaoi" },
{ label: "Shounen Ai", value: "/genre/Shounen+Ai" },
{ label: "Magical Realism", value: "/genre/Magical+Realism" },
{ label: "Video Games", value: "/genre/Video+Games" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: '/most-popular',
label: 'Order by',
options: [
{ label: 'Latest Release', value: '/latest-release-novel' },
{ label: 'Hot Novel', value: '/hot-novel' },
{ label: 'Completed Novel', value: '/completed-novel' },
{ label: 'Most Popular', value: '/most-popular' },
],
type: FilterTypes.Picker,
},
genres: {
value: '',
label: 'Genre',
options: [
{ label: 'None', value: '' },
{ label: 'Shounen', value: '/genre/Shounen' },
{ label: 'Harem', value: '/genre/Harem' },
{ label: 'Comedy', value: '/genre/Comedy' },
{ label: 'Martial Arts', value: '/genre/Martial+Arts' },
{ label: 'School Life', value: '/genre/School+Life' },
{ label: 'Mystery', value: '/genre/Mystery' },
{ label: 'Shoujo', value: '/genre/Shoujo' },
{ label: 'Romance', value: '/genre/Romance' },
{ label: 'Sci-fi', value: '/genre/Sci-fi' },
{ label: 'Gender Bender', value: '/genre/Gender+Bender' },
{ label: 'Mature', value: '/genre/Mature' },
{ label: 'Fantasy', value: '/genre/Fantasy' },
{ label: 'Horror', value: '/genre/Horror' },
{ label: 'Drama', value: '/genre/Drama' },
{ label: 'Tragedy', value: '/genre/Tragedy' },
{ label: 'Supernatural', value: '/genre/Supernatural' },
{ label: 'Ecchi', value: '/genre/Ecchi' },
{ label: 'Xuanhuan', value: '/genre/Xuanhuan' },
{ label: 'Adventure', value: '/genre/Adventure' },
{ label: 'Action', value: '/genre/Action' },
{ label: 'Psychological', value: '/genre/Psychological' },
{ label: 'Xianxia', value: '/genre/Xianxia' },
{ label: 'Wuxia', value: '/genre/Wuxia' },
{ label: 'Historical', value: '/genre/Historical' },
{ label: 'Slice of Life', value: '/genre/Slice+of+Life' },
{ label: 'Seinen', value: '/genre/Seinen' },
{ label: 'Lolicon', value: '/genre/Lolicon' },
{ label: 'Adult', value: '/genre/Adult' },
{ label: 'Josei', value: '/genre/Josei' },
{ label: 'Sports', value: '/genre/Sports' },
{ label: 'Smut', value: '/genre/Smut' },
{ label: 'Mecha', value: '/genre/Mecha' },
{ label: 'Yaoi', value: '/genre/Yaoi' },
{ label: 'Shounen Ai', value: '/genre/Shounen+Ai' },
{ label: 'Magical Realism', value: '/genre/Magical+Realism' },
{ label: 'Video Games', value: '/genre/Video+Games' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new AllNovelFullPlugin();
+276 -279
View File
@@ -1,279 +1,276 @@
import { CheerioAPI, load as parseHTML} from "cheerio";
import { Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover";
import { fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterTypes, Filters } from "@libs/filterInputs";
class BLN implements Plugin.PluginBase {
id = "BLN";
name = "BestLightNovel";
icon = "src/en/bestlightnovel/icon.png";
site = "https://bestlightnovel.com/";
version = "1.0.0";
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".update_item.list_category").each((i,el) => {
const novelUrl = loadedCheerio(el).find("h3 > a").attr("href");
if (!novelUrl) {
// TODO: Handle error
console.error("No novel url!");
return;
}
const novelName = loadedCheerio(el).find("h3 > a").text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novel = {
name: novelName,
path: novelUrl.replace(this.site, ''),
cover: novelCover,
};
novels.push(novel);
});
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novel_list?"
link += 'type=' + filters.type.value;
link += '&category=' + filters.category.value;
link += '&state=' + filters.status.value;
link += '&page=' + page;
const result = await fetch(link);
if (!result.ok) {
console.error(await result.text());
// TODO: Cloudflare protection or other error
return [];
}
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetch(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".truyen_info_right h1").text().trim() || "",
cover: loadedCheerio(".info_image img").attr("src") || defaultCover,
summary: loadedCheerio("#noidungm").text()?.trim() || "",
chapters: [],
};
loadedCheerio('ul.truyen_info_right > li').each(function() {
const detailName = loadedCheerio(this).find('span').text();
const detail = loadedCheerio(this)
.find('a')
.map((a,ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
switch(detailName) {
case "Author(s): ":
novel.author = detail;
break;
case "GENRES: ":
novel.genres = detail;
break;
case "STATUS : ":
novel.status =
detail === "Ongoing"
? NovelStatus.Ongoing
: detail === "Completed"
? NovelStatus.Completed
: NovelStatus.Unknown;
}
})
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".chapter-list div.row").each((i, el) => {
const chapterName = loadedCheerio(el).find("a").text().trim();
const releaseDate = loadedCheerio(el)
.find("span:last")
.text()
.trim();
const months = [
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
].join("|");
const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`,'i')
.exec(releaseDate);
if (!rx) return;
const year = 2000 + +rx[3];
const month = months.indexOf(rx[1].toLowerCase());
const day = +rx[2];
const chapterUrl = loadedCheerio(el).find("a").attr("href");
if (!chapterUrl) {
// TODO: Handle error
console.error("No chapter url!");
return;
}
chapter.push({
name: chapterName,
releaseTime: new Date(year, month, day).toISOString(),
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#vung_doc").html() || "";
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
status: {
label: "Status",
value: "all",
options: [
{ label: "ALL", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" },
],
type: FilterTypes.Picker,
},
type: {
value: "topview",
label: "Type",
options: [
{ label: "Recently updated", value: "latest" },
{ label: "Newest", value: "newest" },
{ label: "Top view", value: "topview" },
],
type: FilterTypes.Picker,
},
category: {
label: "Category",
value: "all",
options: [
{ label: "ALL", value: "all" },
{ label: "Action", value: "1" },
{ label: "Adventure", value: "2" },
{ label: "Animals", value: "65" },
{ label: "Arts", value: "40" },
{ label: "Biographies", value: "41" },
{ label: "Business", value: "42" },
{ label: "Chinese", value: "3" },
{ label: "Comedy", value: "4" },
{ label: "Computers", value: "43" },
{ label: "Crafts, Hobbies", value: "45" },
{ label: "Drama", value: "5" },
{ label: "Education", value: "46" },
{ label: "English", value: "6" },
{ label: "Entertainment", value: "47" },
{ label: "Fantasy", value: "7" },
{ label: "Fiction", value: "48" },
{ label: "Gender Bender", value: "8" },
{ label: "Harem", value: "9" },
{ label: "Historical", value: "10" },
{ label: "History", value: "49" },
{ label: "Home", value: "50" },
{ label: "Horror", value: "11" },
{ label: "Humor", value: "51" },
{ label: "Investing", value: "52" },
{ label: "Josei", value: "12" },
{ label: "Korean", value: "13" },
{ label: "Literature", value: "53" },
{ label: "Lolicon", value: "14" },
{ label: "Martial Arts", value: "15" },
{ label: "Mature", value: "16" },
{ label: "Mecha", value: "17" },
{ label: "Memoirs", value: "54" },
{ label: "Mystery", value: "18" },
{ label: "Original", value: "19" },
{ label: "Other Books", value: "66" },
{ label: "Philosophy", value: "55" },
{ label: "Photography", value: "56" },
{ label: "Politics", value: "57" },
{ label: "Professional", value: "58" },
{ label: "Psychological", value: "20" },
{ label: "Reference", value: "59" },
{ label: "Reincarnation", value: "21" },
{ label: "Religion", value: "60" },
{ label: "Romance", value: "22" },
{ label: "School Life", value: "23" },
{ label: "School Stories", value: "67" },
{ label: "Sci-Fi", value: "24" },
{ label: "Seinen", value: "25" },
{ label: "Short Stories", value: "68" },
{ label: "Shotacon", value: "26" },
{ label: "Shoujo", value: "27" },
{ label: "Shoujo Ai", value: "28" },
{ label: "Shounen", value: "29" },
{ label: "Shounen Ai", value: "30" },
{ label: "Slice Of Life", value: "31" },
{ label: "Smut", value: "32" },
{ label: "Social Science", value: "61" },
{ label: "Spirituality", value: "62" },
{ label: "Sports", value: "33" },
{ label: "Supernatural", value: "34" },
{ label: "Teaser", value: "69" },
{ label: "Technical", value: "63" },
{ label: "Technology", value: "64" },
{ label: "Tragedy", value: "35" },
{ label: "Virtual Reality", value: "36" },
{ label: "Wuxia", value: "37" },
{ label: "Xianxia", value: "38" },
{ label: "Xuanhuan", value: "39" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new BLN();
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { Plugin } from '@typings/plugin';
import { defaultCover } from '@libs/defaultCover';
import { fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { FilterTypes, Filters } from '@libs/filterInputs';
class BLN implements Plugin.PluginBase {
id = 'BLN';
name = 'BestLightNovel';
icon = 'src/en/bestlightnovel/icon.png';
site = 'https://bestlightnovel.com/';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.update_item.list_category').each((i, el) => {
const novelUrl = loadedCheerio(el).find('h3 > a').attr('href');
if (!novelUrl) {
// TODO: Handle error
console.error('No novel url!');
return;
}
const novelName = loadedCheerio(el).find('h3 > a').text();
const novelCover = loadedCheerio(el).find('img').attr('src');
const novel = {
name: novelName,
path: novelUrl.replace(this.site, ''),
cover: novelCover,
};
novels.push(novel);
});
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + 'novel_list?';
link += 'type=' + filters.type.value;
link += '&category=' + filters.category.value;
link += '&state=' + filters.status.value;
link += '&page=' + page;
const result = await fetch(link);
if (!result.ok) {
console.error(await result.text());
// TODO: Cloudflare protection or other error
return [];
}
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetch(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.truyen_info_right h1').text().trim() || '',
cover: loadedCheerio('.info_image img').attr('src') || defaultCover,
summary: loadedCheerio('#noidungm').text()?.trim() || '',
chapters: [],
};
loadedCheerio('ul.truyen_info_right > li').each(function () {
const detailName = loadedCheerio(this).find('span').text();
const detail = loadedCheerio(this)
.find('a')
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
switch (detailName) {
case 'Author(s): ':
novel.author = detail;
break;
case 'GENRES: ':
novel.genres = detail;
break;
case 'STATUS : ':
novel.status =
detail === 'Ongoing'
? NovelStatus.Ongoing
: detail === 'Completed'
? NovelStatus.Completed
: NovelStatus.Unknown;
}
});
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list div.row').each((i, el) => {
const chapterName = loadedCheerio(el).find('a').text().trim();
const releaseDate = loadedCheerio(el).find('span:last').text().trim();
const months = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
].join('|');
const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`, 'i').exec(
releaseDate,
);
if (!rx) return;
const year = 2000 + +rx[3];
const month = months.indexOf(rx[1].toLowerCase());
const day = +rx[2];
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) {
// TODO: Handle error
console.error('No chapter url!');
return;
}
chapter.push({
name: chapterName,
releaseTime: new Date(year, month, day).toISOString(),
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('#vung_doc').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
status: {
label: 'Status',
value: 'all',
options: [
{ label: 'ALL', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
type: {
value: 'topview',
label: 'Type',
options: [
{ label: 'Recently updated', value: 'latest' },
{ label: 'Newest', value: 'newest' },
{ label: 'Top view', value: 'topview' },
],
type: FilterTypes.Picker,
},
category: {
label: 'Category',
value: 'all',
options: [
{ label: 'ALL', value: 'all' },
{ label: 'Action', value: '1' },
{ label: 'Adventure', value: '2' },
{ label: 'Animals', value: '65' },
{ label: 'Arts', value: '40' },
{ label: 'Biographies', value: '41' },
{ label: 'Business', value: '42' },
{ label: 'Chinese', value: '3' },
{ label: 'Comedy', value: '4' },
{ label: 'Computers', value: '43' },
{ label: 'Crafts, Hobbies', value: '45' },
{ label: 'Drama', value: '5' },
{ label: 'Education', value: '46' },
{ label: 'English', value: '6' },
{ label: 'Entertainment', value: '47' },
{ label: 'Fantasy', value: '7' },
{ label: 'Fiction', value: '48' },
{ label: 'Gender Bender', value: '8' },
{ label: 'Harem', value: '9' },
{ label: 'Historical', value: '10' },
{ label: 'History', value: '49' },
{ label: 'Home', value: '50' },
{ label: 'Horror', value: '11' },
{ label: 'Humor', value: '51' },
{ label: 'Investing', value: '52' },
{ label: 'Josei', value: '12' },
{ label: 'Korean', value: '13' },
{ label: 'Literature', value: '53' },
{ label: 'Lolicon', value: '14' },
{ label: 'Martial Arts', value: '15' },
{ label: 'Mature', value: '16' },
{ label: 'Mecha', value: '17' },
{ label: 'Memoirs', value: '54' },
{ label: 'Mystery', value: '18' },
{ label: 'Original', value: '19' },
{ label: 'Other Books', value: '66' },
{ label: 'Philosophy', value: '55' },
{ label: 'Photography', value: '56' },
{ label: 'Politics', value: '57' },
{ label: 'Professional', value: '58' },
{ label: 'Psychological', value: '20' },
{ label: 'Reference', value: '59' },
{ label: 'Reincarnation', value: '21' },
{ label: 'Religion', value: '60' },
{ label: 'Romance', value: '22' },
{ label: 'School Life', value: '23' },
{ label: 'School Stories', value: '67' },
{ label: 'Sci-Fi', value: '24' },
{ label: 'Seinen', value: '25' },
{ label: 'Short Stories', value: '68' },
{ label: 'Shotacon', value: '26' },
{ label: 'Shoujo', value: '27' },
{ label: 'Shoujo Ai', value: '28' },
{ label: 'Shounen', value: '29' },
{ label: 'Shounen Ai', value: '30' },
{ label: 'Slice Of Life', value: '31' },
{ label: 'Smut', value: '32' },
{ label: 'Social Science', value: '61' },
{ label: 'Spirituality', value: '62' },
{ label: 'Sports', value: '33' },
{ label: 'Supernatural', value: '34' },
{ label: 'Teaser', value: '69' },
{ label: 'Technical', value: '63' },
{ label: 'Technology', value: '64' },
{ label: 'Tragedy', value: '35' },
{ label: 'Virtual Reality', value: '36' },
{ label: 'Wuxia', value: '37' },
{ label: 'Xianxia', value: '38' },
{ label: 'Xuanhuan', value: '39' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new BLN();
+154 -158
View File
@@ -1,184 +1,180 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import dayjs from "dayjs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import dayjs from 'dayjs';
class ComradeMaoPlugin implements Plugin.PluginBase {
id = "comrademao";
name = "Comrade Mao";
site = "https://comrademao.com/";
version = "1.0.0";
icon = "src/en/comrademao/icon.png";
id = 'comrademao';
name = 'Comrade Mao';
site = 'https://comrademao.com/';
version = '1.0.0';
icon = 'src/en/comrademao/icon.png';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".listupd")
.find("div.bs")
.each(function () {
const novelName = loadedCheerio(this).find(".tt").text().trim();
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelUrl = loadedCheerio(this).find("a").attr("href");
loadedCheerio('.listupd')
.find('div.bs')
.each(function () {
const novelName = loadedCheerio(this).find('.tt').text().trim();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
url: novelUrl,
};
novels.push(novel);
});
return novels;
}
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
link += filters.category.value;
link += "page/" + pageNo + "/?post_type=novel";
const body = await fetchApi(link).then((result) => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((result) => result.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
url: novelUrl,
chapters: [],
const novel = {
name: novelName,
cover: novelCover,
url: novelUrl,
};
novels.push(novel);
});
return novels;
}
novel.name = loadedCheerio(".entry-title").text().trim();
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
novel.cover = loadedCheerio("div.thumbook > div > img").attr("src");
link += filters.category.value;
loadedCheerio(".infox .wd-full").each((i, el) => {
const detailName = loadedCheerio(el).find("b").text();
const detail = loadedCheerio(el).find("span").text();
link += 'page/' + pageNo + '/?post_type=novel';
switch (detailName) {
case "Publisher: ":
novel.author = detail.trim();
break;
case "Status: ":
novel.status = detail.trim();
break;
case "Genres:":
novel.genres = detail.trim().replace(/ , /g, ",");
break;
case "Description: ":
novel.summary = detail.trim();
break;
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then(result => result.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
url: novelUrl,
chapters: [],
};
novel.name = loadedCheerio('.entry-title').text().trim();
novel.cover = loadedCheerio('div.thumbook > div > img').attr('src');
loadedCheerio('.infox .wd-full').each((i, el) => {
const detailName = loadedCheerio(el).find('b').text();
const detail = loadedCheerio(el).find('span').text();
switch (detailName) {
case 'Publisher: ':
novel.author = detail.trim();
break;
case 'Status: ':
novel.status = detail.trim();
break;
case 'Genres:':
novel.genres = detail.trim().replace(/ , /g, ',');
break;
case 'Description: ':
novel.summary = detail.trim();
break;
}
});
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('#chapterlist')
.find('li')
.each(function () {
const chapterUrl = loadedCheerio(this).find('a').attr('href');
if (!chapterUrl) return;
const chapterName = loadedCheerio(this).find('.chapternum').text();
let releaseTime = loadedCheerio(this).find('.chapterdate').text();
if (releaseTime?.includes?.('ago')) {
const timeAgo = releaseTime.match(/\d+/)?.[0] || '0';
const timeAgoInt = parseInt(timeAgo, 10);
if (timeAgoInt) {
const dayJSDate = dayjs(); // today
if (
releaseTime.includes('hours ago') ||
releaseTime.includes('hour ago')
) {
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
if (
releaseTime.includes('days ago') ||
releaseTime.includes('day ago')
) {
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
if (
releaseTime.includes('months ago') ||
releaseTime.includes('month ago')
) {
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
releaseTime = dayJSDate.format('LL');
}
}
chapter.push({
name: chapterName,
url: chapterUrl,
releaseTime,
});
});
const chapter: Plugin.ChapterItem[] = [];
novel.chapters = chapter.reverse();
loadedCheerio("#chapterlist")
.find("li")
.each(function () {
const chapterUrl = loadedCheerio(this).find("a").attr("href");
if (!chapterUrl) return;
return novel;
}
const chapterName = loadedCheerio(this)
.find(".chapternum")
.text();
let releaseTime = loadedCheerio(this)
.find(".chapterdate")
.text();
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then(result => result.text());
const loadedCheerio = parseHTML(body);
if (releaseTime?.includes?.("ago")) {
const timeAgo = releaseTime.match(/\d+/)?.[0] || "0";
const timeAgoInt = parseInt(timeAgo, 10);
const chapterText = loadedCheerio('#chaptercontent').html()?.trim() || '';
if (timeAgoInt) {
const dayJSDate = dayjs(); // today
if (
releaseTime.includes("hours ago") ||
releaseTime.includes("hour ago")
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
}
return chapterText;
}
if (
releaseTime.includes("days ago") ||
releaseTime.includes("day ago")
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
if (
releaseTime.includes("months ago") ||
releaseTime.includes("month ago")
) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
}
const body = await fetchApi(url).then(result => result.text());
releaseTime = dayJSDate.format("LL");
}
}
chapter.push({
name: chapterName,
url: chapterUrl,
releaseTime,
});
});
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
novel.chapters = chapter.reverse();
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((result) => result.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || "";
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
const body = await fetchApi(url).then((result) => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
category: {
value: "",
label: "Type",
options: [
{ label: "None", value: "" },
{ label: "Chinese", value: "/mtype/chinese/" },
{ label: "Japanese", value: "/mtype/japanese/" },
{ label: "Korean", value: "/mtype/korean/" },
{ label: "Manhua", value: "/mtype/manhua/" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
category: {
value: '',
label: 'Type',
options: [
{ label: 'None', value: '' },
{ label: 'Chinese', value: '/mtype/chinese/' },
{ label: 'Japanese', value: '/mtype/japanese/' },
{ label: 'Korean', value: '/mtype/korean/' },
{ label: 'Manhua', value: '/mtype/manhua/' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new ComradeMaoPlugin();
+125 -126
View File
@@ -1,138 +1,137 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { defaultCover } from '@libs/defaultCover';
class DDLPlugin implements Plugin.PluginBase {
id = "DDL.com";
name = "Divine Dao Library";
site = "https://www.divinedaolibrary.com/";
version = "1.0.1";
icon = "src/en/divinedaolibrary/icon.png";
filters?: undefined;
id = 'DDL.com';
name = 'Divine Dao Library';
site = 'https://www.divinedaolibrary.com/';
version = '1.0.1';
icon = 'src/en/divinedaolibrary/icon.png';
filters?: undefined;
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string){
let novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("#main")
.find("li")
.each((i,el) => {
const novelName = loadedCheerio(el).find("a").text();
const novelCover = defaultCover;
const novelUrl = loadedCheerio(el).find("a").attr("href");
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
if (searchTerm) {
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return novels;
};
loadedCheerio('#main')
.find('li')
.each((i, el) => {
const novelName = loadedCheerio(el).find('a').text();
const novelCover = defaultCover;
const novelUrl = loadedCheerio(el).find('a').attr('href');
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novels";
if (!novelUrl) return;
const body = await fetchApi(link).then((res) => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.entry-title").text().trim() || "Untitled",
cover:
loadedCheerio(".entry-content").find("img").attr("data-ezsrc") ||
defaultCover,
chapters: [],
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)")
.text()
.trim();
novels.push(novel);
});
novel.author = loadedCheerio("#main > article > div > h3:nth-child(2)")
.text()
.replace(/Author:/g, "")
.trim();
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("#main")
.find("li > span > a")
.each((i, el) => {
const chapterName = loadedCheerio(el).text().trim();
const chapterUrl = loadedCheerio(el).attr("href");
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter;
return novel;
};
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterName = loadedCheerio(".entry-title").text().trim();
let chapterText = loadedCheerio(".entry-content").html();
if (!chapterText) {
chapterText = loadedCheerio(".page-header").html();
}
chapterText = `<p><h1>${chapterName}</h1></p>` + chapterText;
return chapterText;
};
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
let url = this.site + "novels";
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio, searchTerm);
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
if (searchTerm) {
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return novels;
}
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let link = this.site + 'novels';
const body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h1.entry-title').text().trim() || 'Untitled',
cover:
loadedCheerio('.entry-content').find('img').attr('data-ezsrc') ||
defaultCover,
chapters: [],
};
novel.summary = loadedCheerio('#main > article > div > p:nth-child(6)')
.text()
.trim();
novel.author = loadedCheerio('#main > article > div > h3:nth-child(2)')
.text()
.replace(/Author:/g, '')
.trim();
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('#main')
.find('li > span > a')
.each((i, el) => {
const chapterName = loadedCheerio(el).text().trim();
const chapterUrl = loadedCheerio(el).attr('href');
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterName = loadedCheerio('.entry-title').text().trim();
let chapterText = loadedCheerio('.entry-content').html();
if (!chapterText) {
chapterText = loadedCheerio('.page-header').html();
}
chapterText = `<p><h1>${chapterName}</h1></p>` + chapterText;
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let url = this.site + 'novels';
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio, searchTerm);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
}
export default new DDLPlugin();
export default new DDLPlugin();
+187 -194
View File
@@ -1,213 +1,206 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
class EarlyNovelPlugin implements Plugin.PagePlugin {
id = "earlynovel";
name = "Early Novel";
version = "1.0.0";
icon = "multisrc/madara/latestnovel.png";
site = "https://earlynovel.net/";
id = 'earlynovel';
name = 'Early Novel';
version = '1.0.0';
icon = 'multisrc/madara/latestnovel.png';
site = 'https://earlynovel.net/';
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".col-truyen-main > .list-truyen > .row").each(
(i, el) => {
const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
const novelName = loadedCheerio(el)
.find("h3.truyen-title > a")
.text();
const novelCover = loadedCheerio(el).find('.lazyimg').attr("data-image");
if (!novelUrl) return;
novels.push({
path: novelUrl,
name: novelName,
cover: novelCover,
});
}
);
return novels;
loadedCheerio('.col-truyen-main > .list-truyen > .row').each((i, el) => {
const novelUrl = loadedCheerio(el)
.find('h3.truyen-title > a')
.attr('href');
const novelName = loadedCheerio(el).find('h3.truyen-title > a').text();
const novelCover = loadedCheerio(el).find('.lazyimg').attr('data-image');
if (!novelUrl) return;
novels.push({
path: novelUrl,
name: novelName,
cover: novelCover,
});
});
return novels;
}
parseChapters(loadedCheerio: CheerioAPI) {
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('ul.list-chapter > li').each((i, el) => {
const chapterName = loadedCheerio(el).find('.chapter-text').text().trim();
const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
return chapter;
}
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genres.value.length) link += filters.genres.value;
else link += filters.order.value;
link += `?page=${pageNo}`;
const body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
loadedCheerio('.glyphicon-menu-right').closest('li').remove();
const pagenav = loadedCheerio('.page-nav').prev().find('a');
const lastPageStr = pagenav.attr('title')?.match(/(\d+)/);
const lastPage = Number(lastPageStr?.[1] || '0');
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('.book > img').attr('alt') || 'Untitled',
cover: loadedCheerio('.book > img').attr('src'),
summary: loadedCheerio('.desc-text').text().trim(),
chapters: [],
totalPages: lastPage,
};
parseChapters(loadedCheerio:CheerioAPI){
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("ul.list-chapter > li").each((i,el) => {
const chapterName = loadedCheerio(el)
.find(".chapter-text")
.text()
.trim();
const chapterUrl = loadedCheerio(el)
.find("a")
.attr("href")
?.slice(1);
if (!chapterUrl) return;
loadedCheerio('.info > div > h3').each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
chapter.push({
name: chapterName,
path: chapterUrl
})});
return chapter;
}
switch (detailName) {
case 'Author:':
novel.author = detail;
break;
case 'Status:':
novel.status = detail;
break;
case 'Genre:':
novel.genres = detail;
break;
}
});
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = this.site;
novel.chapters = this.parseChapters(loadedCheerio);
return novel;
}
if (filters.genres.value.length)
link += filters.genres.value;
else
link += filters.order.value;
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '?page=' + page;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapters = this.parseChapters(loadedCheerio);
return { chapters };
}
link += `?page=${pageNo}`;
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const body = await fetchApi(link).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const chapterText = loadedCheerio('#chapter-c').html() || '';
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
return chapterText;
}
let loadedCheerio = parseHTML(body);
loadedCheerio('.glyphicon-menu-right').closest('li').remove();
const pagenav = loadedCheerio('.page-nav').prev().find('a');
const lastPageStr = pagenav.attr('title')?.match(/(\d+)/);
const lastPage = Number(lastPageStr?.[1] || "0");
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?keyword=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.text();
const novel: Plugin.SourceNovel & {totalPages: number}= {
path: novelPath,
name: loadedCheerio(".book > img").attr("alt") || "Untitled",
cover: loadedCheerio(".book > img").attr("src"),
summary: loadedCheerio(".desc-text").text().trim(),
chapters: [],
totalPages: lastPage
};
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
loadedCheerio(".info > div > h3").each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
switch (detailName) {
case "Author:":
novel.author = detail;
break;
case "Status:":
novel.status = detail;
break;
case "Genre:":
novel.genres = detail;
break;
}
});
novel.chapters = this.parseChapters(loadedCheerio);
return novel;
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '?page=' + page;
const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const chapters = this.parseChapters(loadedCheerio);
return {chapters}
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapter-c").html() || "";
return chapterText;
};
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?keyword=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
order: {
value: "/most-popular",
label: "Order by",
options: [
{ label: "Latest Release", value: "/latest-release-novel" },
{ label: "Hot Novel", value: "/hot-novel" },
{ label: "Completed Novel", value: "/completed-novel" },
{ label: "Most Popular", value: "/most-popular" },
],
type: FilterTypes.Picker,
},
genres: {
value: "",
label: "Genre",
options: [
{ label: "None", value: "" },
{ label: "Action", value: "/genre/action-1" },
{ label: "Adult", value: "/genre/adult-2" },
{ label: "Adventure", value: "/genre/adventure-3" },
{ label: "Comedy", value: "/genre/comedy-4" },
{ label: "Drama", value: "/genre/drama-5" },
{ label: "Ecchi", value: "/genre/ecchi-6" },
{ label: "Fantasy", value: "/genre/fantasy-7" },
{ label: "Gender Bender", value: "/genre/gender-bender-8" },
{ label: "Harem", value: "/genre/harem-9" },
{ label: "Historical", value: "/genre/historical-10" },
{ label: "Horror", value: "/genre/horror-11" },
{ label: "Josei", value: "/genre/josei-12" },
{ label: "Martial Arts", value: "/genre/martial-arts-13" },
{ label: "Mature", value: "/genre/mature-14" },
{ label: "Mecha", value: "/genre/mecha-15" },
{ label: "Mystery", value: "/genre/mystery-16" },
{ label: "Psychological", value: "/genre/psychological-17" },
{ label: "Romance", value: "/genre/romance-18" },
{ label: "School Life", value: "/genre/school-life-19" },
{ label: "Sci-fi", value: "/genre/sci-fi-20" },
{ label: "Seinen", value: "/genre/seinen-21" },
{ label: "Shoujo", value: "/genre/shoujo-22" },
{ label: "Shoujo Ai", value: "/genre/shoujo-ai-23" },
{ label: "Shounen", value: "/genre/shounen-24" },
{ label: "Shounen Ai", value: "/genre/shounen-ai-25" },
{ label: "Slice of Life", value: "/genre/slice-of-life-26" },
{ label: "Smut", value: "/genre/smut-27" },
{ label: "Sports", value: "/genre/sports-28" },
{ label: "Supernatural", value: "/genre/supernatural-29" },
{ label: "Tragedy", value: "/genre/tragedy-30" },
{ label: "Wuxia", value: "/genre/wuxia-31" },
{ label: "Xianxia", value: "/genre/xianxia-32" },
{ label: "Xuanhuan", value: "/genre/xuanhuan-33" },
{ label: "Yaoi", value: "/genre/yaoi-34" },
{ label: "Yuri", value: "/genre/yuri-35" },
{ label: "Video Games", value: "/genre/video-games-36" },
{ label: "Magical Realism", value: "/genre/magical-realism-37" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: '/most-popular',
label: 'Order by',
options: [
{ label: 'Latest Release', value: '/latest-release-novel' },
{ label: 'Hot Novel', value: '/hot-novel' },
{ label: 'Completed Novel', value: '/completed-novel' },
{ label: 'Most Popular', value: '/most-popular' },
],
type: FilterTypes.Picker,
},
genres: {
value: '',
label: 'Genre',
options: [
{ label: 'None', value: '' },
{ label: 'Action', value: '/genre/action-1' },
{ label: 'Adult', value: '/genre/adult-2' },
{ label: 'Adventure', value: '/genre/adventure-3' },
{ label: 'Comedy', value: '/genre/comedy-4' },
{ label: 'Drama', value: '/genre/drama-5' },
{ label: 'Ecchi', value: '/genre/ecchi-6' },
{ label: 'Fantasy', value: '/genre/fantasy-7' },
{ label: 'Gender Bender', value: '/genre/gender-bender-8' },
{ label: 'Harem', value: '/genre/harem-9' },
{ label: 'Historical', value: '/genre/historical-10' },
{ label: 'Horror', value: '/genre/horror-11' },
{ label: 'Josei', value: '/genre/josei-12' },
{ label: 'Martial Arts', value: '/genre/martial-arts-13' },
{ label: 'Mature', value: '/genre/mature-14' },
{ label: 'Mecha', value: '/genre/mecha-15' },
{ label: 'Mystery', value: '/genre/mystery-16' },
{ label: 'Psychological', value: '/genre/psychological-17' },
{ label: 'Romance', value: '/genre/romance-18' },
{ label: 'School Life', value: '/genre/school-life-19' },
{ label: 'Sci-fi', value: '/genre/sci-fi-20' },
{ label: 'Seinen', value: '/genre/seinen-21' },
{ label: 'Shoujo', value: '/genre/shoujo-22' },
{ label: 'Shoujo Ai', value: '/genre/shoujo-ai-23' },
{ label: 'Shounen', value: '/genre/shounen-24' },
{ label: 'Shounen Ai', value: '/genre/shounen-ai-25' },
{ label: 'Slice of Life', value: '/genre/slice-of-life-26' },
{ label: 'Smut', value: '/genre/smut-27' },
{ label: 'Sports', value: '/genre/sports-28' },
{ label: 'Supernatural', value: '/genre/supernatural-29' },
{ label: 'Tragedy', value: '/genre/tragedy-30' },
{ label: 'Wuxia', value: '/genre/wuxia-31' },
{ label: 'Xianxia', value: '/genre/xianxia-32' },
{ label: 'Xuanhuan', value: '/genre/xuanhuan-33' },
{ label: 'Yaoi', value: '/genre/yaoi-34' },
{ label: 'Yuri', value: '/genre/yuri-35' },
{ label: 'Video Games', value: '/genre/video-games-36' },
{ label: 'Magical Realism', value: '/genre/magical-realism-37' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new EarlyNovelPlugin();
export default new EarlyNovelPlugin();
+145 -119
View File
@@ -1,142 +1,168 @@
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { CheerioAPI, load as parseHTML } from "cheerio";
import { NovelStatus } from "@libs/novelStatus";
import { Plugin } from '@typings/plugin';
import { fetchApi, fetchFile } from '@libs/fetch';
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { NovelStatus } from '@libs/novelStatus';
class FaqWikiUs implements Plugin.PluginBase {
id = "FWK.US";
name = "Faq Wiki";
site = "https://faqwiki.us/";
version = "1.1.1";
icon = "src/en/faqwikius/icon.png";
id = 'FWK.US';
name = 'Faq Wiki';
site = 'https://faqwiki.us/';
version = '1.1.1';
icon = 'src/en/faqwikius/icon.png';
parseNovels(loadedCheerio:CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("figure.wp-block-image").each((index, element) => {
const name = loadedCheerio(element).text();
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
// Remove the appended query string
if (cover) {
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
} else {
cover = loadedCheerio(element).find("img").attr("src");
}
loadedCheerio('figure.wp-block-image').each((index, element) => {
const name = loadedCheerio(element).text();
let cover = loadedCheerio(element).find('img').attr('data-ezsrc');
const path = loadedCheerio(element).find("a").attr("href")?.replace('tp:', 'tps:')?.slice(this.site.length);
if (!path) return;
// Remove the appended query string
if (cover) {
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
} else {
cover = loadedCheerio(element).find('img').attr('src');
}
novels.push({ name, cover, path });
})
const path = loadedCheerio(element)
.find('a')
.attr('href')
?.replace('tp:', 'tps:')
?.slice(this.site.length);
if (!path) return;
if (searchTerm) {
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
);
novels.push({ name, cover, path });
});
if (searchTerm) {
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return novels;
}
async popularNovels(
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + path).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path,
name: '',
};
novel.name = loadedCheerio('.entry-title')
.text()
.replace('Novel – All Chapters', '')
.trim();
novel.cover = loadedCheerio('.wp-block-image')
.find('img')
.attr('data-ezsrc')
?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
const status = loadedCheerio(
"#lcp_instance_0 +:icontains('complete')",
).text();
novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing;
const div = loadedCheerio('.book-review-block__meta-item-value');
div.html(div.html()?.replace(/(?<=>)([^<]+)(?=<br\s*\/?>)/g, '<p>$1</p>')!);
loadedCheerio('.book-review-block__meta-item-value strong').each(
(i, el) => {
const key = loadedCheerio(el)
.text()
.replace(':', '')
.trim()
.toLowerCase();
const value = loadedCheerio(el).next().text().replace(':', '').trim();
switch (key) {
case 'description':
novel.summary = value;
break;
case 'author(s)':
novel.author = value;
break;
case 'genre':
novel.genres = value.split(' ').join(',');
}
},
);
return novels;
}
const chapters: Plugin.ChapterItem[] = loadedCheerio(
'.lcp_catlist > li > a',
)
.map((chapterIndex, element) => ({
name: loadedCheerio(element)
.text()
.replace(novel.name + '', '')
.replace('Novel' + '', '')
.trim(),
path: '' + loadedCheerio(element).attr('href')?.slice(this.site.length),
chapterNumber: chapterIndex + 1,
}))
.get();
async popularNovels(
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((res) => res.text());
const loadedCheerio = parseHTML(body);
novel.chapters = chapters;
return novel;
}
return this.parseNovels(loadedCheerio);
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
loadedCheerio('span').remove();
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + path).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const chapterParagraphs = loadedCheerio('.entry-content p');
const novel: Plugin.SourceNovel = {
path,
name: ""
};
let chapterContent; // Variable to store the result
novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim();
novel.cover = loadedCheerio(".wp-block-image")
.find("img")
.attr("data-ezsrc")
?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
const status = loadedCheerio("#lcp_instance_0 +:icontains('complete')").text();
novel.status = (status ? NovelStatus.Completed : NovelStatus.Ongoing)
const div = loadedCheerio(".book-review-block__meta-item-value");
div.html(div.html()?.replace(/(?<=>)([^<]+)(?=<br\s*\/?>)/g, '<p>$1</p>')!);
loadedCheerio(".book-review-block__meta-item-value strong").each((i,el) => {
const key = loadedCheerio(el).text().replace(':', '').trim().toLowerCase();
const value = loadedCheerio(el).next().text().replace(':', '').trim();
switch(key){
case "description":
novel.summary = value;
break;
case "author(s)":
novel.author = value;
break;
case "genre":
novel.genres = value.split(' ').join(',');
}
if (chapterParagraphs.length < 5) {
//some chapter in this site store their whole text in 1-4 <p>,
chapterContent = chapterParagraphs
.map((index, element) => {
const text = loadedCheerio(element).html();
return text;
})
const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a")
.map((chapterIndex, element) => ({
name: loadedCheerio(element).text().replace(novel.name + "", "").replace("Novel" + "", "").trim(),
path: ""+loadedCheerio(element).attr("href")?.slice(this.site.length),
chapterNumber: chapterIndex + 1,
}))
.get();
novel.chapters = chapters;
return novel;
.get()
.join('\n\n');
} else {
// Multiple paragraphs case
chapterContent = chapterParagraphs
.map((index, element) => {
const text = loadedCheerio(element).text().trim();
return `<p>${text}</p>`;
})
.get()
.join('\n\n');
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
const loadedCheerio = parseHTML(body);
loadedCheerio("span").remove();
return chapterContent;
}
const chapterParagraphs = loadedCheerio(".entry-content p");
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
let chapterContent; // Variable to store the result
return this.parseNovels(loadedCheerio, searchTerm);
}
if (chapterParagraphs.length < 5) { //some chapter in this site store their whole text in 1-4 <p>,
chapterContent = chapterParagraphs.map((index, element) => {
const text = loadedCheerio(element).html()
return text;
}).get().join('\n\n');
}
else {
// Multiple paragraphs case
chapterContent = chapterParagraphs.map((index, element) => {
const text = loadedCheerio(element).text().trim();
return `<p>${text}</p>`;
}).get().join('\n\n');
}
return chapterContent;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((res) => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio, searchTerm);
}
fetchImage = fetchFile;
fetchImage = fetchFile;
}
export default new FaqWikiUs();
+51 -44
View File
@@ -1,73 +1,78 @@
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import qs from "qs";
import { Plugin } from '@typings/plugin';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import qs from 'qs';
class FreeWebNovel implements Plugin.PluginBase {
id = "FWN.com";
name = "Free Web Novel";
site = "https://freewebnovel.com";
version = "1.0.0";
icon = "src/en/freewebnovel/icon.png";
id = 'FWN.com';
name = 'Free Web Novel';
site = 'https://freewebnovel.com';
version = '1.0.0';
icon = 'src/en/freewebnovel/icon.png';
async popularNovels(
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const sort = showLatestNovels
? "/latest-release-novels/"
: "/completed-novels/";
? '/latest-release-novels/'
: '/completed-novels/';
const body = await fetchApi(this.site + sort + page).then((res) => res.text());
const body = await fetchApi(this.site + sort + page).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row")
const novels: Plugin.NovelItem[] = loadedCheerio('.li-row')
.map((index, element) => ({
name: loadedCheerio(element).find(".tit").text() || "",
cover: loadedCheerio(element).find("img").attr("src"),
path: loadedCheerio(element).find("h3 > a").attr("href") || "",
name: loadedCheerio(element).find('.tit').text() || '',
cover: loadedCheerio(element).find('img').attr('src'),
path: loadedCheerio(element).find('h3 > a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.tit").text(),
cover: loadedCheerio(".pic > img").attr("src"),
summary: loadedCheerio(".inner").text().trim(),
name: loadedCheerio('h1.tit').text(),
cover: loadedCheerio('.pic > img').attr('src'),
summary: loadedCheerio('.inner').text().trim(),
};
novel.genres = loadedCheerio("[title=Genre]")
novel.genres = loadedCheerio('[title=Genre]')
.next()
.text()
.replace(/[\t\n]/g, "");
.replace(/[\t\n]/g, '');
novel.author = loadedCheerio("[title=Author]")
novel.author = loadedCheerio('[title=Author]')
.next()
.text()
.replace(/[\t\n]/g, "");
.replace(/[\t\n]/g, '');
novel.status = loadedCheerio("[title=Status]")
novel.status = loadedCheerio('[title=Status]')
.next()
.text()
.replace(/[\t\n]/g, "");
.replace(/[\t\n]/g, '');
const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
const chapters: Plugin.ChapterItem[] = loadedCheerio('#idData > li > a')
.map((chapterIndex, element) => ({
name:
loadedCheerio(element).attr("title") ||
"Chapter " + (chapterIndex + 1),
loadedCheerio(element).attr('title') ||
'Chapter ' + (chapterIndex + 1),
path:
loadedCheerio(element).attr("href") ||
novelPath.replace(".html", "/chapter-" + (chapterIndex + 1) + ".html"),
loadedCheerio(element).attr('href') ||
novelPath.replace(
'.html',
'/chapter-' + (chapterIndex + 1) + '.html',
),
releaseTime: null,
chapterNumber: chapterIndex + 1,
}))
@@ -78,33 +83,35 @@ class FreeWebNovel implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("div.txt").html() || "";
const chapterText = loadedCheerio('div.txt').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site + "/search/", {
const body = await fetchApi(this.site + '/search/', {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site,
Origin: this.site,
},
method: "POST",
method: 'POST',
body: qs.stringify({ searchkey: searchTerm }),
}).then((res) => res.text());
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
const novels: Plugin.NovelItem[] = loadedCheerio('.li-row > .li > .con')
.map((index, element) => ({
name: loadedCheerio(element).find(".tit").text() || "",
cover: loadedCheerio(element).find(".pic > a > img").attr("src"),
path: loadedCheerio(element).find("h3 > a").attr("href") || "",
name: loadedCheerio(element).find('.tit').text() || '',
cover: loadedCheerio(element).find('.pic > a > img').attr('src'),
path: loadedCheerio(element).find('h3 > a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
+222 -221
View File
@@ -1,247 +1,248 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import dayjs from "dayjs";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import dayjs from 'dayjs';
class LightNovelPub implements Plugin.PagePlugin {
id = "lightnovelpub";
name = "LightNovelPub";
version = "1.0.1";
icon = "src/en/lightnovelpub/icon.png";
site = "https://www.lightnovelpub.com/";
headers = {
Accept: "application/json",
"Content-Type": "application/json",
id = 'lightnovelpub';
name = 'LightNovelPub';
version = '1.0.1';
icon = 'src/en/lightnovelpub/icon.png';
site = 'https://www.lightnovelpub.com/';
headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
link += `${filters.order.value}/`;
link += `${filters.status.value}/`;
link += page;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.novel-item.ads').remove();
loadedCheerio('.novel-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('.novel-title').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('data-src');
const novelUrl = loadedCheerio(ele)
.find('.novel-title > a')
.attr('href')
?.substring(1);
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
});
return novels;
}
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const totalChapters = parseInt(
loadedCheerio('.header-stats span:first strong').text(),
10,
);
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('h1.novel-title').text().trim() || 'Untitled',
cover: loadedCheerio('figure.cover > img').attr('data-src'),
author: loadedCheerio('.author > a > span').text(),
summary: loadedCheerio('.summary > .content').text().trim(),
status: loadedCheerio('.header-stats span:last strong').text(),
totalPages: Math.ceil(totalChapters / 100),
chapters: [],
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
link += `${filters.order.value}/`;
link += `${filters.status.value}/`;
link += page;
novel.genres = loadedCheerio('.categories ul li')
.map((a, ex) => loadedCheerio(ex).text().trim())
.toArray()
.join(',');
const body = await fetchApi(link).then((r) => r.text());
return novel;
}
const loadedCheerio = parseHTML(body);
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
const chapterName =
'Chapter ' +
loadedCheerio(this).find('.chapter-no').text().trim() +
' - ' +
loadedCheerio(this).find('.chapter-title').text().trim();
const novels: Plugin.NovelItem[] = [];
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.attr('datetime');
loadedCheerio(".novel-item.ads").remove();
const chapterUrl = loadedCheerio(this)
.find('a')
.attr('href')
?.substring(1);
if (!chapterUrl) return;
loadedCheerio(".novel-item").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find(".novel-title")
.text()
.trim();
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
const novelUrl =
loadedCheerio(ele)
.find(".novel-title > a")
.attr("href")
?.substring(1);
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
});
});
const chapters = chapter;
return { chapters };
}
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
novels.push(novel);
});
const loadedCheerio = parseHTML(body);
return novels;
}
let chapterText = loadedCheerio('#chapter-container').html() || '';
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
return chapterText;
}
const loadedCheerio = parseHTML(body);
const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then(r => r.text());
const token = parseHTML(response);
let verifytoken = token('#novelSearchForm > input').attr('value');
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio("h1.novel-title").text().trim() || "Untitled",
cover: loadedCheerio("figure.cover > img").attr("data-src"),
author: loadedCheerio(".author > a > span").text(),
summary: loadedCheerio(".summary > .content").text().trim(),
status: loadedCheerio(".header-stats span:last strong").text(),
totalPages: Math.ceil(totalChapters / 100),
chapters: [],
};
let formData = new FormData();
formData.append('inputContent', searchTerm);
novel.genres = loadedCheerio('.categories ul li')
.map((a, ex) => loadedCheerio(ex).text().trim())
.toArray()
.join(',');
const body = await fetchApi(url, {
method: 'POST',
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
}).then(r => r.text());
return novel;
}
let loadedCheerio = parseHTML(body);
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
const chapterName =
'Chapter ' +
loadedCheerio(this).find('.chapter-no').text().trim() +
' - ' +
loadedCheerio(this).find('.chapter-title').text().trim();
let novels: Plugin.NovelItem[] = [];
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.attr('datetime');
let results = JSON.parse(loadedCheerio('body').text());
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
if (!chapterUrl) return;
loadedCheerio = parseHTML(results.resultview);
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
})
});
const chapters = chapter;
return { chapters };
}
loadedCheerio('.novel-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1);
if (!novelUrl) return;
novels.push({
name: novelName,
path: novelUrl,
cover: novelCover,
});
});
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
return novels;
}
const loadedCheerio = parseHTML(body);
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url, { headers: this.headers });
}
let chapterText = loadedCheerio("#chapter-container").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then((r) => r.text());
const token = parseHTML(response);
let verifytoken = token("#novelSearchForm > input").attr("value");
let formData = new FormData();
formData.append("inputContent", searchTerm);
const body = await fetchApi(
url,
{
method: "POST",
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
},
).then((r) => r.text());
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
let results = JSON.parse(loadedCheerio("body").text());
loadedCheerio = parseHTML(results.resultview);
loadedCheerio(".novel-item").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h4.novel-title")
.text()
.trim();
const novelCover = loadedCheerio(ele).find("img").attr("src");
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
if (!novelUrl) return;
novels.push({
name: novelName,
path: novelUrl,
cover: novelCover,
});
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url, { headers: this.headers });
}
filters = {
order: {
value: "popular",
label: "Order by",
options: [
{ label: "New", value: "new" },
{ label: "Popular", value: "popular" },
{ label: "Updates", value: "updated" },
],
type: FilterTypes.Picker,
},
status: {
value: "all",
label: "Status",
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" }
],
type: FilterTypes.Picker,
},
genres: {
value: "all",
label: "Genre",
options: [
{ label: "All", value: "all" },
{ label: "Action", value: "action" },
{ label: "Adventure", value: "adventure" },
{ label: "Drama", value: "drama" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Harem", value: "harem" },
{ label: "Martial Arts", value: "martial-arts" },
{ label: "Mature", value: "mature" },
{ label: "Romance", value: "romance" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Ecchi", value: "ecchi" },
{ label: "Comedy", value: "comedy" },
{ label: "Slice of Life", value: "slice-of-life" },
{ label: "Mystery", value: "mystery" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Psychological", value: "psychological" },
{ label: "Sci-fi", value: "sci-fi" },
{ label: "Xianxia", value: "xianxia" },
{ label: "School Life", value: "school-life" },
{ label: "Josei", value: "josei" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Shounen", value: "shounen" },
{ label: "Horror", value: "horror" },
{ label: "Mecha", value: "mecha" },
{ label: "Historical", value: "historical" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Adult", value: "adult" },
{ label: "Seinen", value: "seinen" },
{ label: "Sports", value: "sports" },
{ label: "Lolicon", value: "lolicon" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Shounen Ai", value: "shounen-ai" },
{ label: "Yaoi", value: "yaoi" },
{ label: "Video Games", value: "video-games" },
{ label: "Smut", value: "smut" },
{ label: "Magical Realism", value: "magical-realism" },
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
{ label: "Contemporary Romance", value: "contemporary-romance" },
{ label: "Fantasy Romance", value: "fantasy-romance" },
{ label: "Shoujo Ai", value: "shoujo-ai" },
{ label: "Yuri", value: "yuri" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: 'popular',
label: 'Order by',
options: [
{ label: 'New', value: 'new' },
{ label: 'Popular', value: 'popular' },
{ label: 'Updates', value: 'updated' },
],
type: FilterTypes.Picker,
},
status: {
value: 'all',
label: 'Status',
options: [
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
genres: {
value: 'all',
label: 'Genre',
options: [
{ label: 'All', value: 'all' },
{ label: 'Action', value: 'action' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Drama', value: 'drama' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Harem', value: 'harem' },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Romance', value: 'romance' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Sci-fi', value: 'sci-fi' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'School Life', value: 'school-life' },
{ label: 'Josei', value: 'josei' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Horror', value: 'horror' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Historical', value: 'historical' },
{ label: 'Shoujo', value: 'shoujo' },
{ label: 'Adult', value: 'adult' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Sports', value: 'sports' },
{ label: 'Lolicon', value: 'lolicon' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Shounen Ai', value: 'shounen-ai' },
{ label: 'Yaoi', value: 'yaoi' },
{ label: 'Video Games', value: 'video-games' },
{ label: 'Smut', value: 'smut' },
{ label: 'Magical Realism', value: 'magical-realism' },
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
{ label: 'Contemporary Romance', value: 'contemporary-romance' },
{ label: 'Fantasy Romance', value: 'fantasy-romance' },
{ label: 'Shoujo Ai', value: 'shoujo-ai' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new LightNovelPub();
export default new LightNovelPub();
+221 -220
View File
@@ -1,247 +1,248 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import dayjs from "dayjs";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import dayjs from 'dayjs';
class LightNovelWorld implements Plugin.PagePlugin {
id = "lightnovelworld";
name = "LightNovelWorld";
site = "https://www.webnovelworld.org/";
version = "1.0.1";
icon = "src/en/lightnovelworld/icon.png";
headers = {
Accept: "application/json",
"Content-Type": "application/json",
id = 'lightnovelworld';
name = 'LightNovelWorld';
site = 'https://www.webnovelworld.org/';
version = '1.0.1';
icon = 'src/en/lightnovelworld/icon.png';
headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
link += `${filters.order.value}/`;
link += `${filters.status.value}/`;
link += page;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.novel-item.ads').remove();
loadedCheerio('.novel-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('.novel-title').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('data-src');
const novelUrl = loadedCheerio(ele)
.find('.novel-title > a')
.attr('href')
?.substring(1);
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
});
return novels;
}
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const totalChapters = parseInt(
loadedCheerio('.header-stats span:first strong').text(),
10,
);
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('h1.novel-title').text().trim() || 'Untitled',
cover: loadedCheerio('figure.cover > img').attr('data-src'),
author: loadedCheerio('.author > a > span').text(),
summary: loadedCheerio('.summary > .content').text().trim(),
status: loadedCheerio('.header-stats span:last strong').text(),
totalPages: Math.ceil(totalChapters / 100),
chapters: [],
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
link += `${filters.order.value}/`;
link += `${filters.status.value}/`;
link += page;
novel.genres = loadedCheerio('.categories ul li')
.map((a, ex) => loadedCheerio(ex).text().trim())
.toArray()
.join(',');
const body = await fetchApi(link).then((r) => r.text());
return novel;
}
const loadedCheerio = parseHTML(body);
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
const chapterName =
'Chapter ' +
loadedCheerio(this).find('.chapter-no').text().trim() +
' - ' +
loadedCheerio(this).find('.chapter-title').text().trim();
const novels: Plugin.NovelItem[] = [];
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.attr('datetime');
loadedCheerio(".novel-item.ads").remove();
const chapterUrl = loadedCheerio(this)
.find('a')
.attr('href')
?.substring(1);
if (!chapterUrl) return;
loadedCheerio(".novel-item").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find(".novel-title")
.text()
.trim();
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
const novelUrl =
loadedCheerio(ele)
.find(".novel-title > a")
.attr("href")
?.substring(1);
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
});
});
const chapters = chapter;
return { chapters };
}
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
novels.push(novel);
});
const loadedCheerio = parseHTML(body);
return novels;
}
let chapterText = loadedCheerio('#chapter-container').html() || '';
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
return chapterText;
}
const loadedCheerio = parseHTML(body);
const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then(r => r.text());
const token = parseHTML(response);
let verifytoken = token('#novelSearchForm > input').attr('value');
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio("h1.novel-title").text().trim() || "Untitled",
cover: loadedCheerio("figure.cover > img").attr("data-src"),
author: loadedCheerio(".author > a > span").text(),
summary: loadedCheerio(".summary > .content").text().trim(),
status: loadedCheerio(".header-stats span:last strong").text(),
totalPages: Math.ceil(totalChapters / 100),
chapters: [],
};
let formData = new FormData();
formData.append('inputContent', searchTerm);
novel.genres = loadedCheerio('.categories ul li')
.map((a, ex) => loadedCheerio(ex).text().trim())
.toArray()
.join(',');
const body = await fetchApi(url, {
method: 'POST',
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
}).then(r => r.text());
return novel;
}
let loadedCheerio = parseHTML(body);
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
const chapterName =
'Chapter ' +
loadedCheerio(this).find('.chapter-no').text().trim() +
' - ' +
loadedCheerio(this).find('.chapter-title').text().trim();
let novels: Plugin.NovelItem[] = [];
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.attr('datetime');
let results = JSON.parse(loadedCheerio('body').text());
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
if (!chapterUrl) return;
loadedCheerio = parseHTML(results.resultview);
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
})
});
const chapters = chapter;
return { chapters };
}
loadedCheerio('.novel-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1);
if (!novelUrl) return;
novels.push({
name: novelName,
path: novelUrl,
cover: novelCover,
});
});
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
return novels;
}
const loadedCheerio = parseHTML(body);
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url, { headers: this.headers });
}
let chapterText = loadedCheerio("#chapter-container").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then((r) => r.text());
const token = parseHTML(response);
let verifytoken = token("#novelSearchForm > input").attr("value");
let formData = new FormData();
formData.append("inputContent", searchTerm);
const body = await fetchApi(
url,
{
method: "POST",
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
},
).then((r) => r.text());
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
let results = JSON.parse(loadedCheerio("body").text());
loadedCheerio = parseHTML(results.resultview);
loadedCheerio(".novel-item").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h4.novel-title")
.text()
.trim();
const novelCover = loadedCheerio(ele).find("img").attr("src");
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
if (!novelUrl) return;
novels.push({
name: novelName,
path: novelUrl,
cover: novelCover,
});
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url, { headers: this.headers });
}
filters = {
order: {
value: "popular",
label: "Order by",
options: [
{ label: "New", value: "new" },
{ label: "Popular", value: "popular" },
{ label: "Updates", value: "updated" },
],
type: FilterTypes.Picker,
},
status: {
value: "all",
label: "Status",
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" }
],
type: FilterTypes.Picker,
},
genres: {
value: "all",
label: "Genre",
options: [
{ label: "All", value: "all" },
{ label: "Action", value: "action" },
{ label: "Adventure", value: "adventure" },
{ label: "Drama", value: "drama" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Harem", value: "harem" },
{ label: "Martial Arts", value: "martial-arts" },
{ label: "Mature", value: "mature" },
{ label: "Romance", value: "romance" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Ecchi", value: "ecchi" },
{ label: "Comedy", value: "comedy" },
{ label: "Slice of Life", value: "slice-of-life" },
{ label: "Mystery", value: "mystery" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Psychological", value: "psychological" },
{ label: "Sci-fi", value: "sci-fi" },
{ label: "Xianxia", value: "xianxia" },
{ label: "School Life", value: "school-life" },
{ label: "Josei", value: "josei" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Shounen", value: "shounen" },
{ label: "Horror", value: "horror" },
{ label: "Mecha", value: "mecha" },
{ label: "Historical", value: "historical" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Adult", value: "adult" },
{ label: "Seinen", value: "seinen" },
{ label: "Sports", value: "sports" },
{ label: "Lolicon", value: "lolicon" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Shounen Ai", value: "shounen-ai" },
{ label: "Yaoi", value: "yaoi" },
{ label: "Video Games", value: "video-games" },
{ label: "Smut", value: "smut" },
{ label: "Magical Realism", value: "magical-realism" },
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
{ label: "Contemporary Romance", value: "contemporary-romance" },
{ label: "Fantasy Romance", value: "fantasy-romance" },
{ label: "Shoujo Ai", value: "shoujo-ai" },
{ label: "Yuri", value: "yuri" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: 'popular',
label: 'Order by',
options: [
{ label: 'New', value: 'new' },
{ label: 'Popular', value: 'popular' },
{ label: 'Updates', value: 'updated' },
],
type: FilterTypes.Picker,
},
status: {
value: 'all',
label: 'Status',
options: [
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
genres: {
value: 'all',
label: 'Genre',
options: [
{ label: 'All', value: 'all' },
{ label: 'Action', value: 'action' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Drama', value: 'drama' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Harem', value: 'harem' },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Romance', value: 'romance' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Sci-fi', value: 'sci-fi' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'School Life', value: 'school-life' },
{ label: 'Josei', value: 'josei' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Horror', value: 'horror' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Historical', value: 'historical' },
{ label: 'Shoujo', value: 'shoujo' },
{ label: 'Adult', value: 'adult' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Sports', value: 'sports' },
{ label: 'Lolicon', value: 'lolicon' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Shounen Ai', value: 'shounen-ai' },
{ label: 'Yaoi', value: 'yaoi' },
{ label: 'Video Games', value: 'video-games' },
{ label: 'Smut', value: 'smut' },
{ label: 'Magical Realism', value: 'magical-realism' },
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
{ label: 'Contemporary Romance', value: 'contemporary-romance' },
{ label: 'Fantasy Romance', value: 'fantasy-romance' },
{ label: 'Shoujo Ai', value: 'shoujo-ai' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new LightNovelWorld();
+227 -229
View File
@@ -1,247 +1,245 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
class LnMTLPlugin implements Plugin.PagePlugin {
id = "lnmtl";
name = "LnMTL";
icon = "src/en/lnmtl/icon.png";
site = "https://lnmtl.com/";
version = "1.0.1";
id = 'lnmtl';
name = 'LnMTL';
icon = 'src/en/lnmtl/icon.png';
site = 'https://lnmtl.com/';
version = '1.0.1';
async sleep (ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
async sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + 'novel?';
link += `orderBy=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&filter=${filters.storyStatus.value}`;
link += `&page=${page}`;
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.media').each((i, el) => {
const novelName = loadedCheerio(el).find('h4').text();
const novelCover = loadedCheerio(el).find('img').attr('src');
const novelUrl = loadedCheerio(el).find('h4 > a').attr('href');
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
return novels;
}
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio('main')
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace('lnmtl.volumes = ', '') || '',
);
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('.novel-name').text() || 'Untitled',
cover: loadedCheerio('div.novel').find('img').attr('src'),
summary: loadedCheerio('div.description').text().trim(),
totalPages: volumes.length,
chapters: [],
};
loadedCheerio('.panel-body > dl').each(function () {
const detailName = loadedCheerio(this).find('dt').text().trim();
const detail = loadedCheerio(this).find('dd').text().trim();
switch (detailName) {
case 'Authors':
novel.author = detail;
break;
case 'Current status':
novel.status = detail;
break;
}
});
novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")')
.next()
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
return novel;
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio('main')
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace('lnmtl.volumes = ', '') || '',
)[+page - 1];
const chapter: Plugin.ChapterItem[] = [];
await this.sleep(1000);
const volumeData = await fetchApi(
`${this.site}chapter?volumeId=${volumes.id}`,
);
const volumePage = await volumeData.json();
const firstPage = volumePage.data.map((chapter: ChapterEntry) => ({
name: `#${chapter.number} - ${chapter.title}`,
path: `chapter/${chapter.slug}`,
releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
}));
chapter.push(...firstPage);
for (let i = 2; i <= volumePage.last_page; i++) {
await this.sleep(1000);
const chapterData = await fetchApi(
`${this.site}chapter?page=${i}&volumeId=${volumes.id}`,
);
const chapterInfo = await chapterData.json();
const chapterDetails = chapterInfo.data.map((chapter: ChapterEntry) => ({
name: `#${chapter.number} ${chapter.title}`,
path: `chapter/${chapter.slug}`,
releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
}));
chapter.push(...chapterDetails);
}
const chapters = chapter;
return { chapters };
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio('.original, script').remove();
loadedCheerio('sentence.translated').wrap('<p></p>');
let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“');
if (!chapterText) {
chapterText = loadedCheerio('.alert.alert-warning').text();
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novel?";
link += `orderBy=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&filter=${filters.storyStatus.value}`;
link += `&page=${page}`;
return chapterText;
}
const body = await fetchApi(link).then((result) =>
result.text()
);
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(r => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
const list = loadedCheerio('footer')
.next()
.next()
.html()
?.match(/prefetch: '\/(.*json)/)![1];
loadedCheerio(".media").each((i,el) => {
const novelName = loadedCheerio(el).find("h4").text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novelUrl = loadedCheerio(el).find("h4 > a").attr("href");
const search = await fetch(`${this.site}${list}`);
const data = await search.json();
if (!novelUrl) return;
const nov = data.filter((res: { name: string }) =>
res.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
const novels: Plugin.NovelItem[] = [];
nov.map((res: { name: string; slug: string; image: string }) => {
const novelName = res.name;
const novelUrl = `novel/${res.slug}`;
const novelCover = res.image;
novels.push(novel);
});
const novel = {
path: novelUrl,
name: novelName,
cover: novelCover,
};
novels.push(novel);
});
return novels;
}
return novels;
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio("main")
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace("lnmtl.volumes = ", "") || ""
);
const novel: Plugin.SourceNovel & {totalPages: number} = {
path: novelPath,
name: loadedCheerio(".novel-name").text() || 'Untitled',
cover: loadedCheerio("div.novel").find("img").attr("src"),
summary: loadedCheerio("div.description").text().trim(),
totalPages: volumes.length,
chapters: [],
};
loadedCheerio(".panel-body > dl").each(function () {
const detailName = loadedCheerio(this).find("dt").text().trim();
const detail = loadedCheerio(this).find("dd").text().trim();
switch (detailName) {
case "Authors":
novel.author = detail;
break;
case "Current status":
novel.status = detail;
break;
}
});
novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")')
.next()
.find('a')
.map((i,el) => loadedCheerio(el).text())
.toArray()
.join(",");
return novel;
};
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio("main")
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace("lnmtl.volumes = ", "") || ""
)[+page-1];
const chapter: Plugin.ChapterItem[] = [];
await this.sleep(1000);
const volumeData = await fetchApi(
`${this.site}chapter?volumeId=${volumes.id}`
);
const volumePage = await volumeData.json();
const firstPage = volumePage.data.map(
(chapter: ChapterEntry) => ({
name: `#${chapter.number} - ${chapter.title}`,
path: `chapter/${chapter.slug}`,
releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
}))
chapter.push(...firstPage);
for (let i = 2; i <= volumePage.last_page; i++) {
await this.sleep(1000);
const chapterData = await fetchApi(
`${this.site}chapter?page=${i}&volumeId=${volumes.id}`
);
const chapterInfo = await chapterData.json();
const chapterDetails = chapterInfo.data.map(
(chapter: ChapterEntry) => ({
name: `#${chapter.number} ${chapter.title}`,
path: `chapter/${chapter.slug}`,
releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
}));
chapter.push(...chapterDetails);
}
const chapters = chapter;
return {chapters};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio('.original, script').remove();
loadedCheerio('sentence.translated').wrap('<p></p>');
let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“');
if (!chapterText) {
chapterText = loadedCheerio(".alert.alert-warning").text();
}
return chapterText;
};
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const list = loadedCheerio('footer')
.next()
.next()
.html()
?.match(/prefetch: '\/(.*json)/)![1];
const search = await fetch(`${this.site}${list}`);
const data = await search.json();
const nov = data.filter((res: { name: string; }) =>
res.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const novels: Plugin.NovelItem[] = [];
nov.map((res: { name: string; slug: string; image: string; }) => {
const novelName = res.name;
const novelUrl = `novel/${res.slug}`;
const novelCover = res.image;
const novel = {
path: novelUrl,
name: novelName,
cover: novelCover,
};
novels.push(novel);
});
return novels;
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
order: {
value: "favourites",
label: "Order by",
options: [
{ label: "Favourites", value: "favourites" },
{ label: "Name", value: "name" },
{ label: "Addition Date", value: "date" },
],
type: FilterTypes.Picker,
},
sort: {
value: "desc",
label: "Sort by",
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Finished", value: "finished" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: 'favourites',
label: 'Order by',
options: [
{ label: 'Favourites', value: 'favourites' },
{ label: 'Name', value: 'name' },
{ label: 'Addition Date', value: 'date' },
],
type: FilterTypes.Picker,
},
sort: {
value: 'desc',
label: 'Sort by',
options: [
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: 'all',
label: 'Status',
options: [
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Finished', value: 'finished' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new LnMTLPlugin();
interface ChapterEntry {
number: number;
title: string;
slug: string;
created_at: string;
}
number: number;
title: string;
slug: string;
created_at: string;
}
+170 -174
View File
@@ -1,191 +1,187 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class MTLNovel implements Plugin.PluginBase {
id = "mtlnovel";
name = "MTL Novel";
version = "1.0.0";
icon = "src/en/mtlnovel/icon.png";
site = "https://www.mtlnovel.com/";
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}novel-list/?`;
link += `orderby=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&status=${filters.storyStatus.value}`;
link += `&pg=${page}`
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(link, {headers}).then((result) => result.text());
id = 'mtlnovel';
name = 'MTL Novel';
version = '1.0.0';
icon = 'src/en/mtlnovel/icon.png';
site = 'https://www.mtlnovel.com/';
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}novel-list/?`;
link += `orderby=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&status=${filters.storyStatus.value}`;
link += `&pg=${page}`;
const loadedCheerio = parseHTML(body);
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(link, { headers }).then(result => result.text());
const novels: Plugin.NovelItem[] = [];
const loadedCheerio = parseHTML(body);
loadedCheerio("div.box.wide").each((i,el) => {
const novelName = loadedCheerio(el)
.find("a.list-title")
.text()
.slice(4);
const novelCover = loadedCheerio(el).find("amp-img").attr("src");
const novelUrl = loadedCheerio(el).find("a.list-title").attr("href");
const novels: Plugin.NovelItem[] = [];
if (!novelUrl) return;
loadedCheerio('div.box.wide').each((i, el) => {
const novelName = loadedCheerio(el).find('a.list-title').text().slice(4);
const novelCover = loadedCheerio(el).find('amp-img').attr('src');
const novelUrl = loadedCheerio(el).find('a.list-title').attr('href');
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
if (!novelUrl) return;
novels.push(novel);
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const headers = new Headers();
headers.append('Referer', `${this.site}novel-list/`);
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(this.site + novelPath, {
headers,
}).then(result => result.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h1.entry-title').text() || 'Untitled',
cover: loadedCheerio('.nov-head > amp-img').attr('src'),
summary: loadedCheerio('div.desc > h2').next().text().trim(),
author: loadedCheerio('#author').text(),
status: loadedCheerio('#status').text(),
genres: loadedCheerio('#genre').text().replace(/\s+/g, ''),
chapters: [],
};
const chapterListUrl = this.site + novelPath + 'chapter-list/';
const getChapters = async () => {
const listResult = await fetch(chapterListUrl, { headers });
const listBody = await listResult.text();
loadedCheerio = parseHTML(listBody);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('div.ch-list')
.find('a.ch-link')
.each((i, el) => {
const chapterName = loadedCheerio(el).text().replace('~ ', '');
const releaseDate = null;
const chapterUrl = loadedCheerio(el).attr('href');
if (!chapterUrl) return;
chapter.push({
path: chapterUrl.replace(this.site, ''),
name: chapterName,
releaseTime: releaseDate,
});
});
return chapter.reverse();
};
return novels;
novel.chapters = await getChapters();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(this.site + chapterPath, { headers }).then(r =>
r.text(),
);
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('div.par').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const searchUrl =
this.site +
'wp-admin/admin-ajax.php?action=autosuggest&q=' +
searchTerm +
'&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com';
const res = await fetch(searchUrl, { headers });
const result = await res.json();
const novels: Plugin.NovelItem[] = [];
interface SearchEntry {
title: string;
thumbnail: string;
permalink: string;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const headers = new Headers();
headers.append('Referer', `${this.site}novel-list/`)
headers.append('Alt-Used', 'www.mtlnovel.com');
result.items[0].results.map((item: SearchEntry) => {
const novelName = item.title.replace(/<\/?strong>/g, '');
const novelCover = item.thumbnail;
const novelUrl = item.permalink.replace(this.site, '');
const body = await fetch(
this.site + novelPath,
{
headers
},
).then((result) => result.text());
const novel = { name: novelName, cover: novelCover, path: novelUrl };
let loadedCheerio = parseHTML(body);
novels.push(novel);
});
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.entry-title").text() || 'Untitled',
cover: loadedCheerio(".nov-head > amp-img").attr("src"),
summary: loadedCheerio("div.desc > h2").next().text().trim(),
author: loadedCheerio("#author").text(),
status: loadedCheerio("#status").text(),
genres: loadedCheerio("#genre").text().replace(/\s+/g, ""),
chapters: [],
};
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
return fetchFile(url, { headers });
}
const chapterListUrl = this.site + novelPath + "chapter-list/";
const getChapters = async () => {
const listResult = await fetch(chapterListUrl, {headers});
const listBody = await listResult.text();
loadedCheerio = parseHTML(listBody);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("div.ch-list")
.find("a.ch-link")
.each((i,el) => {
const chapterName = loadedCheerio(el)
.text()
.replace("~ ", "");
const releaseDate = null;
const chapterUrl = loadedCheerio(el).attr("href");
if (!chapterUrl) return;
chapter.push({
path: chapterUrl.replace(this.site, ''),
name: chapterName,
releaseTime: releaseDate,
});
});
return chapter.reverse();
}
novel.chapters = await getChapters();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(this.site + chapterPath, {headers}).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("div.par").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const searchUrl =
this.site +
"wp-admin/admin-ajax.php?action=autosuggest&q=" +
searchTerm +
"&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com";
const res = await fetch(searchUrl, {headers});
const result = await res.json();
const novels: Plugin.NovelItem[] = [];
interface SearchEntry {
title: string;
thumbnail: string;
permalink: string;
}
result.items[0].results.map((item: SearchEntry) => {
const novelName = item.title.replace(/<\/?strong>/g, "");
const novelCover = item.thumbnail;
const novelUrl = item.permalink.replace(this.site, '');
const novel = { name: novelName, cover: novelCover, path: novelUrl };
novels.push(novel);
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
return fetchFile(url, {headers});
}
filters = {
order: {
value: "view",
label: "Order by",
options: [
{ label: "Date", value: "date" },
{ label: "Name", value: "name" },
{ label: "Rating", value: "rating" },
{ label: "View", value: "view" },
],
type: FilterTypes.Picker,
},
sort: {
value: "desc",
label: "Sort by",
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Complete", value: "completed" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
order: {
value: 'view',
label: 'Order by',
options: [
{ label: 'Date', value: 'date' },
{ label: 'Name', value: 'name' },
{ label: 'Rating', value: 'rating' },
{ label: 'View', value: 'view' },
],
type: FilterTypes.Picker,
},
sort: {
value: 'desc',
label: 'Sort by',
options: [
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: 'all',
label: 'Status',
options: [
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Complete', value: 'completed' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new MTLNovel();
+84 -79
View File
@@ -1,106 +1,111 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
class MTLReader implements Plugin.PluginBase {
id = "mtlreader";
name = "MTL Reader";
version = "1.0.0";
icon = "src/en/mtlreader/icon.png";
site = "https://mtlreader.com/";
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
id = 'mtlreader';
name = 'MTL Reader';
version = '1.0.0';
icon = 'src/en/mtlreader/icon.png';
site = 'https://mtlreader.com/';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".col-md-4").each((i,el) => {
const novelName = loadedCheerio(el).find("h5").text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novelUrl = loadedCheerio(el).find("h5 > a").attr("href");
loadedCheerio('.col-md-4').each((i, el) => {
const novelName = loadedCheerio(el).find('h5').text();
const novelCover = loadedCheerio(el).find('img').attr('src');
const novelUrl = loadedCheerio(el).find('h5 > a').attr('href');
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
novels.push(novel);
});
return novels;
}
return novels;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const url = `${this.site}novels?page=${pageNo}`;
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}novels?page=${pageNo}`;
const body = await fetchApi(url).then((r) => r.text());
const body = await fetchApi(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".agent-title").text().trim() || "Untitled",
cover: loadedCheerio(".agent-p-img > img").attr("src"),
summary: loadedCheerio("#editdescription").text().trim(),
chapters: [],
};
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.agent-title').text().trim() || 'Untitled',
cover: loadedCheerio('.agent-p-img > img').attr('src'),
summary: loadedCheerio('#editdescription').text().trim(),
chapters: [],
};
novel.author = loadedCheerio("i.fa-user")
.parent()
.text()
.replace("Author: ", "")
.trim();
novel.author = loadedCheerio('i.fa-user')
.parent()
.text()
.replace('Author: ', '')
.trim();
const chapter: Plugin.ChapterItem[] = [];
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("tr.spaceUnder").each((i,el) => {
const chapterName = loadedCheerio(el).find("a").text().trim();
const chapterUrl = loadedCheerio(el).find("a").attr("href");
loadedCheerio('tr.spaceUnder').each((i, el) => {
const chapterName = loadedCheerio(el).find('a').text().trim();
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter;
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
loadedCheerio(".container ins,script,p.mtlreader").remove();
const chapterText = loadedCheerio(".container").html() || '';
loadedCheerio('.container ins,script,p.mtlreader').remove();
const chapterText = loadedCheerio('.container').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((r) => r.text());
const tokenCheerio = parseHTML(body);
const token = tokenCheerio('input[name="_token"]').attr('value');
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(r => r.text());
const tokenCheerio = parseHTML(body);
const token = tokenCheerio('input[name="_token"]').attr('value');
const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`;
const seacrhBody = await fetchApi(searchUrl).then((r) => r.text());
const loadedCheerio = parseHTML(seacrhBody);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`;
const seacrhBody = await fetchApi(searchUrl).then(r => r.text());
const loadedCheerio = parseHTML(seacrhBody);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new MTLReader();
+212 -210
View File
@@ -1,231 +1,233 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class NovelBin implements Plugin.PluginBase {
id = "novelbin";
name = "Novel Bin";
icon = "src/en/novelbin/icon.png";
site = "https://binnovel.com/";
version = "1.0.0";
parseNovels(loadedCheerio:CheerioAPI){
const novels: Plugin.NovelItem[] = [];
id = 'novelbin';
name = 'Novel Bin';
icon = 'src/en/novelbin/icon.png';
site = 'https://binnovel.com/';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".col-novel-main .list-novel .row").each((i,el) => {
const novelName = loadedCheerio(el).find("h3.novel-title > a").text();
const novelCover = loadedCheerio(el).find("img.cover").attr("src");
const novelUrl = loadedCheerio(el)
.find("h3.novel-title > a")
.attr("href");
loadedCheerio('.col-novel-main .list-novel .row').each((i, el) => {
const novelName = loadedCheerio(el).find('h3.novel-title > a').text();
const novelCover = loadedCheerio(el).find('img.cover').attr('src');
const novelUrl = loadedCheerio(el)
.find('h3.novel-title > a')
.attr('href');
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
novels.push(novel);
});
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '') link += `gen/${filters.genre.value}`;
else link += `sort/${filters.sort.value}`;
if (filters.complete.value) link += `/completed`;
link += `?page=${page}`;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(r => r.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('div.book > img').attr('alt') || 'Untitled',
cover: loadedCheerio('div.book > img').attr('src'),
summary: loadedCheerio('div.desc-text').text().trim(),
chapters: [],
};
loadedCheerio('ul.info > li > h3').each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
switch (detailName) {
case 'Author:':
novel.author = detail;
break;
case 'Status:':
novel.status = detail;
break;
case 'Genre:':
novel.genres = detail;
break;
}
});
const novelId = loadedCheerio('#rating').attr('data-novel-id');
const getChapters = async (id: string) => {
const chapterListUrl = this.site + 'ajax/chapter-archive?novelId=' + id;
const data = await fetchApi(chapterListUrl);
const chapterdata = await data.text();
loadedCheerio = parseHTML(chapterdata);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('ul.list-chapter > li').each((i, el) => {
const chapterName = loadedCheerio(el).find('a').attr('title');
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterName || !chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
return chapter;
};
return novels;
if (novelId) {
novel.chapters = await getChapters(novelId);
}
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `gen/${filters.genre.value}`;
else
link += `sort/${filters.sort.value}`;
return novel;
}
if (filters.complete.value)
link += `/completed`;
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
link += `?page=${page}`;
const loadedCheerio = parseHTML(body);
const body = await fetchApi(link).then((r) => r.text());
loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
const chapterText = loadedCheerio('#chr-content').html() || '';
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
return chapterText;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search/?keyword=${searchTerm}`;
const body = await fetchApi(url).then(r => r.text());
let loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("div.book > img").attr("alt") || "Untitled",
cover: loadedCheerio("div.book > img").attr("src"),
summary: loadedCheerio("div.desc-text").text().trim(),
chapters: [],
};
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
loadedCheerio("ul.info > li > h3").each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
switch (detailName) {
case "Author:":
novel.author = detail;
break;
case "Status:":
novel.status = detail;
break;
case "Genre:":
novel.genres = detail;
break;
}
});
const novelId = loadedCheerio("#rating").attr("data-novel-id");
const getChapters = async (id: string) => {
const chapterListUrl =
this.site + "ajax/chapter-archive?novelId=" + id;
const data = await fetchApi(chapterListUrl);
const chapterdata = await data.text();
loadedCheerio = parseHTML(chapterdata);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("ul.list-chapter > li").each((i,el) => {
const chapterName = loadedCheerio(el).find("a").attr("title");
const chapterUrl = loadedCheerio(el).find("a").attr("href");
if (!chapterName || !chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
return chapter;
}
if (novelId) {
novel.chapters = await getChapters(novelId);
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const loadedCheerio = parseHTML(body);
loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
const chapterText = loadedCheerio("#chr-content").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search/?keyword=${searchTerm}`;
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
sort: {
value: "popular-novels",
label: "Sort by",
options: [
{ label: "Latest Release", value: "update" },
{ label: "Hot Novel", value: "hot-novels" },
{ label: "Completed Novel", value: "c" },
{ label: "Most Popular", value: "popular-novels" },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Genres",
options: [
{ label: "None", value: "" },
{ label: "Action", value: "action" },
{ label: "Adult", value: "adult" },
{ label: "Adventure", value: "adventure" },
{ label: "Anime", value: "anime" },
{ label: "Arts", value: "arts" },
{ label: "Comedy", value: "comedy" },
{ label: "Drama", value: "drama" },
{ label: "Eastern", value: "eastern" },
{ label: "Ecchi", value: "ecchi" },
{ label: "Fan-fiction", value: "fan-fiction" },
{ label: "Fanfiction", value: "fanfiction" },
{ label: "Fantas", value: "fantas" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Game", value: "game" },
{ label: "Gender bender", value: "gender-bender" },
{ label: "Harem", value: "harem" },
{ label: "Hentai", value: "hentai" },
{ label: "Historical", value: "historical" },
{ label: "Horror", value: "horror" },
{ label: "Isekai", value: "isekai" },
{ label: "Josei", value: "josei" },
{ label: "Lgbt+", value: "lgbt+" },
{ label: "Litrpg", value: "litrpg" },
{ label: "Magic", value: "magic" },
{ label: "Magical realism", value: "magical-realism" },
{ label: "Manhua", value: "manhua" },
{ label: "Martial arts", value: "martial-arts" },
{ label: "Mature", value: "mature" },
{ label: "Mecha", value: "mecha" },
{ label: "Military", value: "military" },
{ label: "Modern life", value: "modern-life" },
{ label: "Movies", value: "movies" },
{ label: "Mystery", value: "mystery" },
{ label: "Portal fantasy", value: "portal-fantasy" },
{ label: "Psychological", value: "psychological" },
{ label: "Realistic fiction", value: "realistic-fiction" },
{ label: "Reincarnation", value: "reincarnation" },
{ label: "Romance", value: "romance" },
{ label: "School life", value: "school-life" },
{ label: "Sci-fi", value: "sci-fi" },
{ label: "Seinen", value: "seinen" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Shoujo ai", value: "shoujo-ai" },
{ label: "Shounen", value: "shounen" },
{ label: "Shounen ai", value: "shounen-ai" },
{ label: "Slice of life", value: "slice-of-life" },
{ label: "Smut", value: "smut" },
{ label: "Sports", value: "sports" },
{ label: "Supernatural", value: "supernatural" },
{ label: "System", value: "system" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Urban life", value: "urban-life" },
{ label: "Video games", value: "video-games" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Yaoi", value: "yaoi" },
{ label: "Yuri", value: "yuri" }
],
type: FilterTypes.Picker,
},
complete: {
label: "Show Completed Novels Only",
value: false,
type: FilterTypes.Switch,
},
} satisfies Filters;
filters = {
sort: {
value: 'popular-novels',
label: 'Sort by',
options: [
{ label: 'Latest Release', value: 'update' },
{ label: 'Hot Novel', value: 'hot-novels' },
{ label: 'Completed Novel', value: 'c' },
{ label: 'Most Popular', value: 'popular-novels' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Genres',
options: [
{ label: 'None', value: '' },
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Anime', value: 'anime' },
{ label: 'Arts', value: 'arts' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Drama', value: 'drama' },
{ label: 'Eastern', value: 'eastern' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Fan-fiction', value: 'fan-fiction' },
{ label: 'Fanfiction', value: 'fanfiction' },
{ label: 'Fantas', value: 'fantas' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Game', value: 'game' },
{ label: 'Gender bender', value: 'gender-bender' },
{ label: 'Harem', value: 'harem' },
{ label: 'Hentai', value: 'hentai' },
{ label: 'Historical', value: 'historical' },
{ label: 'Horror', value: 'horror' },
{ label: 'Isekai', value: 'isekai' },
{ label: 'Josei', value: 'josei' },
{ label: 'Lgbt+', value: 'lgbt+' },
{ label: 'Litrpg', value: 'litrpg' },
{ label: 'Magic', value: 'magic' },
{ label: 'Magical realism', value: 'magical-realism' },
{ label: 'Manhua', value: 'manhua' },
{ label: 'Martial arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Military', value: 'military' },
{ label: 'Modern life', value: 'modern-life' },
{ label: 'Movies', value: 'movies' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Portal fantasy', value: 'portal-fantasy' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Realistic fiction', value: 'realistic-fiction' },
{ label: 'Reincarnation', value: 'reincarnation' },
{ label: 'Romance', value: 'romance' },
{ label: 'School life', value: 'school-life' },
{ label: 'Sci-fi', value: 'sci-fi' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Shoujo', value: 'shoujo' },
{ label: 'Shoujo ai', value: 'shoujo-ai' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Shounen ai', value: 'shounen-ai' },
{ label: 'Slice of life', value: 'slice-of-life' },
{ label: 'Smut', value: 'smut' },
{ label: 'Sports', value: 'sports' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'System', value: 'system' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Urban life', value: 'urban-life' },
{ label: 'Video games', value: 'video-games' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Yaoi', value: 'yaoi' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.Picker,
},
complete: {
label: 'Show Completed Novels Only',
value: false,
type: FilterTypes.Switch,
},
} satisfies Filters;
}
export default new NovelBin();
+156 -149
View File
@@ -1,171 +1,178 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
class NovelBuddy implements Plugin.PluginBase {
id = "novelbuddy";
name = "NovelBuddy.io";
site = "https://novelbuddy.io/";
version = "1.0.0";
icon = "src/en/novelbuddy/icon.png";
filters?: undefined; //TODO: Filters, Filters in Filters required
id = 'novelbuddy';
name = 'NovelBuddy.io';
site = 'https://novelbuddy.io/';
version = '1.0.0';
icon = 'src/en/novelbuddy/icon.png';
filters?: undefined; //TODO: Filters, Filters in Filters required
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".book-item").each((idx, ele) => {
const novelName = loadedCheerio(ele).find(".title").text();
const novelCover =
"https:" + loadedCheerio(ele).find("img").attr("data-src");
const novelUrl = loadedCheerio(ele).find(".title a").attr("href")?.substring(1);
loadedCheerio('.book-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('.title').text();
const novelCover =
'https:' + loadedCheerio(ele).find('img').attr('data-src');
const novelUrl = loadedCheerio(ele)
.find('.title a')
.attr('href')
?.substring(1);
if (!novelUrl) return;
if (!novelUrl) return;
const novel = { name: novelName, cover: novelCover, path: novelUrl };
const novel = { name: novelName, cover: novelCover, path: novelUrl };
novels.push(novel);
novels.push(novel);
});
return novels;
}
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}popular?page=${pageNo}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.name h1').text().trim() || 'Untitled',
cover: 'https:' + loadedCheerio('.img-cover img').attr('data-src'),
summary: loadedCheerio('.section-body.summary').text().trim(),
chapters: [],
};
loadedCheerio('.meta.box p').each((i, el) => {
const detailName = loadedCheerio(el).find('strong').text();
const detail = loadedCheerio(el).find('a');
switch (detailName) {
case 'Authors :':
novel.author = detail
.find('span')
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
break;
case 'Status :':
novel.status = detail.text();
break;
case 'Genres :':
novel.genres = detail.text().trim();
break;
}
});
const novelId = loadedCheerio('script')
.text()
.match(/bookId = (\d+);/)![1];
const chapter: Plugin.ChapterItem[] = [];
const getChapters = async (id: string) => {
const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`;
const data = await fetchApi(chapterListUrl);
const chapterlist = await data.text();
loadedCheerio = parseHTML(chapterlist);
loadedCheerio('li').each((i, el) => {
const chapterName = loadedCheerio(el)
.find('.chapter-title')
.text()
.trim();
const releaseDate = loadedCheerio(el)
.find('.chapter-update')
.text()
.trim();
const months = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
].join('|');
const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`, 'i').exec(
releaseDate,
);
if (!rx) return;
const year = +rx[3];
const month = months.indexOf(rx[1].toLowerCase());
const day = +rx[2];
const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: new Date(year, month, day).toISOString(),
path: chapterUrl,
});
});
return chapter;
};
return novels;
}
novel.chapters = (await getChapters(novelId)).reverse();
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}popular?page=${pageNo}`;
return novel;
}
const result = await fetchApi(url);
const body = await result.text();
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
loadedCheerio('#listen-chapter').remove();
loadedCheerio('#google_translate_element').remove();
let loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('.chapter__content').html() || '';
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".name h1").text().trim() || "Untitled",
cover: "https:" + loadedCheerio(".img-cover img").attr("data-src"),
summary: loadedCheerio('.section-body.summary').text().trim(),
chapters: [],
};
return chapterText;
}
loadedCheerio('.meta.box p').each((i,el) => {
const detailName = loadedCheerio(el).find('strong').text();
const detail = loadedCheerio(el).find('a');
switch(detailName){
case "Authors :":
novel.author = detail.find('span')
.map((a,ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
break;
case "Status :":
novel.status = detail.text();
break;
case "Genres :":
novel.genres = detail.text().trim();
break;
}
})
const novelId = loadedCheerio('script')
.text()
.match(/bookId = (\d+);/)![1];
const chapter: Plugin.ChapterItem[] = [];
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?q=${searchTerm}&page=${page}`;
const getChapters = async (id: string) => {
const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`;
const data = await fetchApi(chapterListUrl);
const chapterlist = await data.text();
const result = await fetchApi(url);
const body = await result.text();
loadedCheerio = parseHTML(chapterlist);
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
loadedCheerio("li").each((i,el) => {
const chapterName = loadedCheerio(el)
.find(".chapter-title")
.text()
.trim();
const releaseDate = loadedCheerio(el)
.find(".chapter-update")
.text()
.trim();
const months = [
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
].join("|");
const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`,'i')
.exec(releaseDate);
if (!rx) return;
const year = +rx[3];
const month = months.indexOf(rx[1].toLowerCase());
const day = +rx[2];
const chapterUrl = loadedCheerio(el).find("a").attr("href")?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: new Date(year, month, day).toISOString(),
path: chapterUrl,
});
});
return chapter;
}
novel.chapters = (await getChapters(novelId)).reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio("#listen-chapter").remove();
loadedCheerio("#google_translate_element").remove();
const chapterText = loadedCheerio(".chapter__content").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?q=${searchTerm}&page=${page}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new NovelBuddy();
+177 -176
View File
@@ -1,200 +1,201 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class NovelFull implements Plugin.PluginBase {
id = "novelfull";
name = "NovelFull";
version = "1.0.0";
icon = "src/en/novelfull/icon.png";
site = "https://novelfull.com/";
id = 'novelfull';
name = 'NovelFull';
version = '1.0.0';
icon = 'src/en/novelfull/icon.png';
site = 'https://novelfull.com/';
parseNovels(loadedCheerio:CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h3.truyen-title > a")
.text();
loadedCheerio('.col-truyen-main .list-truyen .row').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text();
const novelCover =
this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1);
const novelCover =
this.site + loadedCheerio(ele).find('img').attr('src')?.slice(1);
const novelUrl = loadedCheerio(ele)
.find("h3.truyen-title > a")
.attr("href")
?.slice(1);
const novelUrl = loadedCheerio(ele)
.find('h3.truyen-title > a')
.attr('href')
?.slice(1);
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
novels.push(novel);
});
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '') link += `genre/${filters.genre.value}`;
else link += filters.sort.value;
link += `?page=${page}`;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('div.book > img').attr('alt') || 'Untitled',
cover: this.site + loadedCheerio('div.book > img').attr('src'),
summary: loadedCheerio('div.desc-text').text().trim(),
status: loadedCheerio('h3:contains("Status")').next().text(),
chapters: [],
};
novel.author = loadedCheerio('h3:contains("Author")')
.parent()
.contents()
.text()
.replace('Author:', '');
novel.genres = loadedCheerio('h3:contains("Genre")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
const novelId = loadedCheerio('#rating').attr('data-novel-id')!;
const chapter: Plugin.ChapterItem[] = [];
const getChapters = async (id: string) => {
const chapterListUrl = this.site + 'ajax/chapter-option?novelId=' + id;
const data = await fetchApi(chapterListUrl);
const chapterlist = await data.text();
loadedCheerio = parseHTML(chapterlist);
loadedCheerio('select > option').each(function () {
const chapterName = loadedCheerio(this).text();
const chapterUrl = loadedCheerio(this).attr('value')?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
return chapter;
};
return novels;
}
novel.chapters = await getChapters(novelId);
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `genre/${filters.genre.value}`;
else
link += filters.sort.value
return novel;
}
link += `?page=${page}`;
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const body = await fetchApi(link).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
loadedCheerio('#chapter-content div.ads').remove();
const chapterText = loadedCheerio('#chapter-content').html() || '';
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
return chapterText;
}
let loadedCheerio = parseHTML(body);
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("div.book > img").attr("alt") || "Untitled",
cover: this.site + loadedCheerio("div.book > img").attr("src"),
summary: loadedCheerio("div.desc-text").text().trim(),
status: loadedCheerio('h3:contains("Status")').next().text(),
chapters: [],
};
const result = await fetchApi(searchUrl);
const body = await result.text();
novel.author = loadedCheerio('h3:contains("Author")')
.parent()
.contents()
.text()
.replace("Author:", "");
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
novel.genres = loadedCheerio('h3:contains("Genre")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
const novelId = loadedCheerio("#rating").attr("data-novel-id")!;
const chapter: Plugin.ChapterItem[] = [];
const getChapters = async (id: string) => {
const chapterListUrl = this.site + "ajax/chapter-option?novelId=" + id;
const data = await fetchApi(chapterListUrl);
const chapterlist = await data.text();
loadedCheerio = parseHTML(chapterlist);
loadedCheerio("select > option").each(function () {
const chapterName = loadedCheerio(this).text();
const chapterUrl = loadedCheerio(this).attr("value")?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
return chapter;
}
novel.chapters = await getChapters(novelId);
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio("#chapter-content div.ads").remove();
const chapterText = loadedCheerio("#chapter-content").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
const result = await fetchApi(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
sort: {
value: "most-popular",
label: "Sort by",
options: [
{ label: "Latest Release", value: "latest-release-novel" },
{ label: "Hot Novel", value: "hot-novel" },
{ label: "Completed Novel", value: "completed-novel" },
{ label: "Most Popular", value: "most-popular" },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Genres",
options: [
{ label: "None", value: "" },
{ label: "Shounen", value: "Shounen" },
{ label: "Harem", value: "Harem" },
{ label: "Comedy", value: "Comedy" },
{ label: "Martial Arts", value: "Martial+Arts" },
{ label: "School Life", value: "School+Life" },
{ label: "Mystery", value: "Mystery" },
{ label: "Shoujo", value: "Shoujo" },
{ label: "Romance", value: "Romance" },
{ label: "Sci-fi", value: "Sci-fi" },
{ label: "Gender Bender", value: "Gender+Bender" },
{ label: "Mature", value: "Mature" },
{ label: "Fantasy", value: "Fantasy" },
{ label: "Horror", value: "Horror" },
{ label: "Drama", value: "Drama" },
{ label: "Tragedy", value: "Tragedy" },
{ label: "Supernatural", value: "Supernatural" },
{ label: "Ecchi", value: "Ecchi" },
{ label: "Xuanhuan", value: "Xuanhuan" },
{ label: "Adventure", value: "Adventure" },
{ label: "Action", value: "Action" },
{ label: "Psychological", value: "Psychological" },
{ label: "Xianxia", value: "Xianxia" },
{ label: "Wuxia", value: "Wuxia" },
{ label: "Historical", value: "Historical" },
{ label: "Slice of Life", value: "Slice+of+Life" },
{ label: "Seinen", value: "Seinen" },
{ label: "Lolicon", value: "Lolicon" },
{ label: "Adult", value: "Adult" },
{ label: "Josei", value: "Josei" },
{ label: "Sports", value: "Sports" },
{ label: "Smut", value: "Smut" },
{ label: "Mecha", value: "Mecha" },
{ label: "Yaoi", value: "Yaoi" },
{ label: "Shounen Ai", value: "Shounen+Ai" },
{ label: "History", value: "History" },
{ label: "Martial", value: "Martial" }
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
sort: {
value: 'most-popular',
label: 'Sort by',
options: [
{ label: 'Latest Release', value: 'latest-release-novel' },
{ label: 'Hot Novel', value: 'hot-novel' },
{ label: 'Completed Novel', value: 'completed-novel' },
{ label: 'Most Popular', value: 'most-popular' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Genres',
options: [
{ label: 'None', value: '' },
{ label: 'Shounen', value: 'Shounen' },
{ label: 'Harem', value: 'Harem' },
{ label: 'Comedy', value: 'Comedy' },
{ label: 'Martial Arts', value: 'Martial+Arts' },
{ label: 'School Life', value: 'School+Life' },
{ label: 'Mystery', value: 'Mystery' },
{ label: 'Shoujo', value: 'Shoujo' },
{ label: 'Romance', value: 'Romance' },
{ label: 'Sci-fi', value: 'Sci-fi' },
{ label: 'Gender Bender', value: 'Gender+Bender' },
{ label: 'Mature', value: 'Mature' },
{ label: 'Fantasy', value: 'Fantasy' },
{ label: 'Horror', value: 'Horror' },
{ label: 'Drama', value: 'Drama' },
{ label: 'Tragedy', value: 'Tragedy' },
{ label: 'Supernatural', value: 'Supernatural' },
{ label: 'Ecchi', value: 'Ecchi' },
{ label: 'Xuanhuan', value: 'Xuanhuan' },
{ label: 'Adventure', value: 'Adventure' },
{ label: 'Action', value: 'Action' },
{ label: 'Psychological', value: 'Psychological' },
{ label: 'Xianxia', value: 'Xianxia' },
{ label: 'Wuxia', value: 'Wuxia' },
{ label: 'Historical', value: 'Historical' },
{ label: 'Slice of Life', value: 'Slice+of+Life' },
{ label: 'Seinen', value: 'Seinen' },
{ label: 'Lolicon', value: 'Lolicon' },
{ label: 'Adult', value: 'Adult' },
{ label: 'Josei', value: 'Josei' },
{ label: 'Sports', value: 'Sports' },
{ label: 'Smut', value: 'Smut' },
{ label: 'Mecha', value: 'Mecha' },
{ label: 'Yaoi', value: 'Yaoi' },
{ label: 'Shounen Ai', value: 'Shounen+Ai' },
{ label: 'History', value: 'History' },
{ label: 'Martial', value: 'Martial' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new NovelFull();
+104 -99
View File
@@ -1,128 +1,133 @@
import { load, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { load, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
class NovelHall implements Plugin.PluginBase {
id = "novelhall";
name = "Novel Hall";
version = "1.0.0";
icon = "src/en/novelhall/icon.png";
filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect
site = "https://novelhall.com/";
id = 'novelhall';
name = 'Novel Hall';
version = '1.0.0';
icon = 'src/en/novelhall/icon.png';
filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect
site = 'https://novelhall.com/';
async popularNovels(page: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const url = `${this.site}all2022-${page}.html`;
async popularNovels(
page: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}all2022-${page}.html`;
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const body = await fetchApi(url).then(r => r.text());
const novels: Plugin.NovelItem[] = [];
const loadedCheerio = parseHTML(body);
loadedCheerio("li.btm").each((idx, ele) => {
const novelName = loadedCheerio(ele).text().trim()
const novelUrl = loadedCheerio(ele).find("a").attr("href");
if(!novelUrl) return;
const novels: Plugin.NovelItem[] = [];
const novel = {
name: novelName,
cover: defaultCover,
path: novelUrl,
};
loadedCheerio('li.btm').each((idx, ele) => {
const novelName = loadedCheerio(ele).text().trim();
const novelUrl = loadedCheerio(ele).find('a').attr('href');
if (!novelUrl) return;
novels.push(novel);
});
const novel = {
name: novelName,
cover: defaultCover,
path: novelUrl,
};
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
novels.push(novel);
});
const loadedCheerio = parseHTML(body);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".book-info > h1").text() || 'Untitled',
cover: loadedCheerio('meta[property="og:image"]').attr('content'),
summary: loadedCheerio(".intro").text().trim(),
chapters: [],
};
const loadedCheerio = parseHTML(body);
loadedCheerio('.total').find('p').remove();
novel.author = loadedCheerio('.total span:contains("Author")')
.text()
.replace("Author:", "")
.trim();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.book-info > h1').text() || 'Untitled',
cover: loadedCheerio('meta[property="og:image"]').attr('content'),
summary: loadedCheerio('.intro').text().trim(),
chapters: [],
};
novel.status = loadedCheerio('.total span:contains("Status")')
.text()
.replace("Status:", "")
.replace("Active", "Ongoing")
.trim();
loadedCheerio('.total').find('p').remove();
novel.author = loadedCheerio('.total span:contains("Author")')
.text()
.replace('Author:', '')
.trim();
novel.genres = loadedCheerio('.total a')
.map((a,ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
novel.status = loadedCheerio('.total span:contains("Status")')
.text()
.replace('Status:', '')
.replace('Active', 'Ongoing')
.trim();
const chapter: Plugin.ChapterItem[] = [];
novel.genres = loadedCheerio('.total a')
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
loadedCheerio("#morelist ul > li").each((idx, ele) => {
const chapterName = loadedCheerio(ele).find("a").text().trim();
const chapterUrl = loadedCheerio(ele).find("a").attr("href");
if (!chapterUrl) return;
const chapter: Plugin.ChapterItem[] = [];
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
loadedCheerio('#morelist ul > li').each((idx, ele) => {
const chapterName = loadedCheerio(ele).find('a').text().trim();
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
if (!chapterUrl) return;
novel.chapters = chapter;
return novel;
}
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".content").html() || '';
return chapterText;
}
novel.chapters = chapter;
return novel;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`;
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('.content').html() || '';
return chapterText;
}
const novels: Plugin.NovelItem[] = [];
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`;
const body = await fetchApi(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
loadedCheerio("#article_list_content > li").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h3")
.text()
.replace(/\t+/g, "")
.replace(/\n/g, " ");
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.slice(1);
if (!novelUrl) return;
const novels: Plugin.NovelItem[] = [];
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
loadedCheerio('#article_list_content > li').each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find('h3')
.text()
.replace(/\t+/g, '')
.replace(/\n/g, ' ');
const novelCover = loadedCheerio(ele).find('img').attr('data-src');
const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
if (!novelUrl) return;
novels.push(novel);
});
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
novels.push(novel);
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new NovelHall();
+429 -422
View File
@@ -1,463 +1,470 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
class NovelUpdates implements Plugin.PluginBase {
id = "novelupdates";
name = "Novel Updates";
version = "0.5.1";
icon = "src/en/novelupdates/icon.png";
site = "https://www.novelupdates.com/";
id = 'novelupdates';
name = 'Novel Updates';
version = '0.5.1';
icon = 'src/en/novelupdates/icon.png';
site = 'https://www.novelupdates.com/';
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio("div.search_main_box_nu").each((idx, ele) => {
const novelCover = loadedCheerio(ele).find("img").attr("src");
const novelName = loadedCheerio(ele).find(".search_title > a").text();
const novelUrl = loadedCheerio(ele)
.find(".search_title > a")
.attr("href");
loadedCheerio('div.search_main_box_nu').each((idx, ele) => {
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelName = loadedCheerio(ele).find('.search_title > a').text();
const novelUrl = loadedCheerio(ele)
.find('.search_title > a')
.attr('href');
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
novels.push(novel);
});
return novels;
return novels;
}
async popularNovels(
page: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (
filters.language.value.length ||
filters.novelType.value.length ||
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length ||
filters.storyStatus.value !== ''
) {
link += 'series-finder/?sf=1';
} else if (showLatestNovels) {
link += 'latest-series/?st=1';
} else {
link += 'series-ranking/?rank=week';
}
async popularNovels(page: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (filters.language.value.length ||
filters.novelType.value.length ||
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length ||
filters.storyStatus.value !== '' ){
link += "series-finder/?sf=1"
} else if (showLatestNovels){
link += "latest-series/?st=1"
} else {
link += "series-ranking/?rank=week"
};
if (filters.language.value.length)
link += '&org=' + filters.language.value.join(',');
if (filters.novelType.value.length)
link += '&nt=' + filters.novelType.value.join(',');
if (filters.language.value.length)
link += '&org=' + filters.language.value.join(',');
if (filters.genres.value.include?.length)
link += '&gi=' + filters.genres.value.include.join(',');
if (filters.novelType.value.length)
link += '&nt=' + filters.novelType.value.join(',');
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.genres.value.include?.length)
link += '&gi=' + filters.genres.value.include.join(',');
if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
link += '&mgi=' + filters.genre_operator.value;
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.storyStatus.value.length)
link += '&ss=' + filters.storyStatus.value;
if (
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length
)
link += '&mgi=' + filters.genre_operator.value;
link += '&sort=' + filters.sort.value;
if (filters.storyStatus.value.length)
link += '&ss=' + filters.storyStatus.value;
link += '&order=' + filters.order.value;
link += '&sort=' + filters.sort.value;
link += '&pg=' + page;
link += '&order=' + filters.order.value;
const body = await fetchApi(link).then((result) =>
result.text()
);
link += '&pg=' + page;
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.seriestitlenu').text() || 'Untitled',
cover: loadedCheerio('.wpb_wrapper img').attr('src'),
chapters: [],
};
novel.author = loadedCheerio('#authtag')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(', ');
novel.genres = loadedCheerio('#seriesgenre')
.children('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
novel.status = loadedCheerio('#editstatus').text().includes('Ongoing')
? 'Ongoing'
: 'Completed';
const type = loadedCheerio('#showtype').text().trim();
const summary = loadedCheerio('#editdescription').text().trim();
novel.summary = summary + `\n\nType: ${type}`;
const chapter: Plugin.ChapterItem[] = [];
const novelId = loadedCheerio('input#mypostid').attr('value')!;
const formData = new FormData();
formData.append('action', 'nd_getchapters');
formData.append('mygrr', '0');
formData.append('mypostid', novelId);
const link = `${this.site}wp-admin/admin-ajax.php`;
const text = await fetchApi(link, {
method: 'POST',
body: formData,
}).then(data => data.text());
loadedCheerio = parseHTML(text);
loadedCheerio('li.sp_li_chp').each((i, el) => {
const chapterName = loadedCheerio(el).text().trim();
const chapterUrl =
'https:' + loadedCheerio(el).find('a').first().next().attr('href');
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter.reverse();
return novel;
}
getLocation(href: string) {
var match = href.match(
/^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/,
);
return match && `${match[1]}//${match[3]}`;
}
async parseChapter(chapterPath: string): Promise<string> {
let chapterText = '';
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
// console.log(result.chapterUrl);
// console.log('Redirected URL: ', result.chapterUrl);
const loadedCheerio = parseHTML(body);
let isWuxiaWorld = result.url.toLowerCase().includes('wuxiaworld');
let isBlogspot = result.url.toLowerCase().includes('blogspot');
let isTumblr = result.url.toLowerCase().includes('tumblr');
let isWattpad = result.url.toLowerCase().includes('wattpad');
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
let kofi = result.url.toLowerCase().includes('ko-fi');
let isBlossomTranslation = result.url
.toLowerCase()
.includes('blossomtranslation');
let isLightNovelsTls = result.url
.toLowerCase()
.includes('lightnovelstranslations');
let isiNovelTranslation = result.url
.toLowerCase()
.includes('inoveltranslation');
let isTravisTranslation = result.url
.toLowerCase()
.includes('travistranslations');
/**
* Checks if its a wordpress site
*/
let isWordPressStr =
loadedCheerio('meta[name="generator"]').attr('content') ||
loadedCheerio('footer').text();
let isWordPress = false;
if (isWordPressStr) {
isWordPress =
isWordPressStr.toLowerCase().includes('wordpress') ||
isWordPressStr.includes('Site Kit by Google') ||
loadedCheerio('.powered-by').text().toLowerCase().includes('wordpress');
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
let isRainOfSnow = result.url.toLowerCase().includes('rainofsnow');
let loadedCheerio = parseHTML(body);
let isWebNovel = result.url.toLowerCase().includes('webnovel');
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".seriestitlenu").text() || "Untitled",
cover: loadedCheerio('.wpb_wrapper img').attr('src'),
chapters: [],
};
let isHostedNovel = result.url.toLowerCase().includes('hostednovel');
novel.author = loadedCheerio('#authtag')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(', ');
let isScribbleHub = result.url.toLowerCase().includes('scribblehub');
novel.genres = loadedCheerio("#seriesgenre")
.children("a")
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
if (isWuxiaWorld) {
chapterText = loadedCheerio('#chapter-content').html()!;
} else if (isRainOfSnow) {
chapterText = loadedCheerio('div.content').html()!;
} else if (isTumblr) {
chapterText = loadedCheerio('.post').html()!;
} else if (isBlogspot) {
loadedCheerio('.post-share-buttons').remove();
chapterText =
loadedCheerio('.entry-content').html() ||
loadedCheerio('#post-body').html()!;
} else if (isHostedNovel) {
chapterText = loadedCheerio('.chapter').html()!;
} else if (isScribbleHub) {
chapterText = loadedCheerio('div.chp_raw').html()!;
} else if (isWattpad) {
chapterText = loadedCheerio('.container pre').html()!;
} else if (isTravisTranslation) {
chapterText = loadedCheerio('.reader-content').html()!;
} else if (isLightNovelsTls) {
chapterText = loadedCheerio('.text_story').html()!;
} else if (isBlossomTranslation) {
chapterText = loadedCheerio('.manga-child-content').html()!;
} else if (isAnomalously) {
chapterText = loadedCheerio('#comic').html()!;
} else if (kofi) {
chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
.html()
?.match(/shadowDom\.innerHTML \+= '(<div.*?)';/)![1]!;
} else if (isiNovelTranslation) {
const link = 'https://api.' + result.url.slice(8);
const json = await fetchApi(link).then(r => r.json());
chapterText = json.content.replace(/\n/g, '<br>');
if (json.notes) {
chapterText +=
'<br><hr><br>TL Notes:<br>' + json.notes.replace(/\n/g, '<br>');
}
} else if (isWordPress) {
/**
* Remove wordpress bloat tags
*/
novel.status = loadedCheerio("#editstatus").text().includes("Ongoing")
? "Ongoing"
: "Completed";
const bloatClasses = [
'.c-ads',
'#madara-comments',
'#comments',
'.content-comments',
'.sharedaddy',
'.wp-dark-mode-switcher',
'.wp-next-post-navi',
'.wp-block-buttons',
'.wp-block-columns',
'.post-cats',
'.sidebar',
'.author-avatar',
'.ezoic-ad',
];
const type = loadedCheerio("#showtype").text().trim();
bloatClasses.map(tag => loadedCheerio(tag).remove());
const summary = loadedCheerio("#editdescription").text().trim();
chapterText =
loadedCheerio('.entry-content').html() ||
loadedCheerio('.single_post').html() ||
loadedCheerio('.post-entry').html() ||
loadedCheerio('.main-content').html() ||
loadedCheerio('article.post').html() ||
loadedCheerio('.content').html() ||
loadedCheerio('#content').html() ||
loadedCheerio('.page-body').html() ||
loadedCheerio('.td-page-content').html()!;
} else if (isWebNovel) {
chapterText = loadedCheerio('.cha-words').html()!;
novel.summary = summary + `\n\nType: ${type}`;
if (!chapterText) {
chapterText = loadedCheerio('._content').html()!;
}
} else {
/**
* Remove unnecessary tags
*/
const tags = ['nav', 'header', 'footer', '.hidden'];
const chapter: Plugin.ChapterItem[] = [];
tags.map(tag => loadedCheerio(tag).remove());
const novelId = loadedCheerio("input#mypostid").attr("value")!;
const formData = new FormData();
formData.append("action", "nd_getchapters");
formData.append("mygrr", "0");
formData.append("mypostid", novelId);
const link = `${this.site}wp-admin/admin-ajax.php`;
const text = await fetchApi(
link,
{
method: "POST",
body: formData,
},
).then((data) => data.text());
loadedCheerio = parseHTML(text);
loadedCheerio("li.sp_li_chp").each((i,el) => {
const chapterName = loadedCheerio(el).text().trim();
const chapterUrl =
"https:" +
loadedCheerio(el).find("a").first().next().attr("href");
chapter.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter.reverse();
return novel;
chapterText = loadedCheerio('body').html()!;
}
getLocation(href: string) {
var match = href.match(
/^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/
);
return match && `${match[1]}//${match[3]}`;
if (chapterText) {
/**
* Convert relative urls to absolute
*/
chapterText = chapterText.replace(
/href="\//g,
`href="${this.getLocation(result.url)}/`,
);
}
async parseChapter(chapterPath: string): Promise<string> {
let chapterText = "";
return chapterText;
}
const result = await fetchApi(
this.site + chapterPath,
);
const body = await result.text();
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url =
this.site +
'?s=' +
searchTerm.replace(/\s+/g, '+') +
'&post_type=seriesplans';
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
// console.log(result.chapterUrl);
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
// console.log('Redirected URL: ', result.chapterUrl);
const loadedCheerio = parseHTML(body);
let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
let isBlogspot = result.url.toLowerCase().includes("blogspot");
let isTumblr = result.url.toLowerCase().includes("tumblr");
let isWattpad = result.url.toLowerCase().includes("wattpad");
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
let kofi = result.url.toLowerCase().includes('ko-fi');
let isBlossomTranslation = result.url
.toLowerCase()
.includes('blossomtranslation');
let isLightNovelsTls = result.url
.toLowerCase()
.includes("lightnovelstranslations");
let isiNovelTranslation = result.url
.toLowerCase()
.includes("inoveltranslation");
let isTravisTranslation = result.url
.toLowerCase()
.includes("travistranslations");
/**
* Checks if its a wordpress site
*/
let isWordPressStr =
loadedCheerio('meta[name="generator"]').attr("content") ||
loadedCheerio("footer").text();
let isWordPress = false;
if (isWordPressStr) {
isWordPress =
isWordPressStr.toLowerCase().includes("wordpress") ||
isWordPressStr.includes("Site Kit by Google") ||
loadedCheerio(".powered-by")
.text()
.toLowerCase()
.includes("wordpress");
}
let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow");
let isWebNovel = result.url.toLowerCase().includes("webnovel");
let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
if (isWuxiaWorld) {
chapterText = loadedCheerio("#chapter-content").html()!;
} else if (isRainOfSnow) {
chapterText = loadedCheerio("div.content").html()!;
} else if (isTumblr) {
chapterText = loadedCheerio(".post").html()!;
} else if (isBlogspot) {
loadedCheerio('.post-share-buttons').remove();
chapterText =
loadedCheerio('.entry-content').html() ||
loadedCheerio('#post-body').html()!;
} else if (isHostedNovel) {
chapterText = loadedCheerio(".chapter").html()!;
} else if (isScribbleHub) {
chapterText = loadedCheerio("div.chp_raw").html()!;
} else if (isWattpad) {
chapterText = loadedCheerio(".container pre").html()!;
} else if (isTravisTranslation) {
chapterText = loadedCheerio(".reader-content").html()!;
} else if (isLightNovelsTls) {
chapterText = loadedCheerio('.text_story').html()!;
} else if (isBlossomTranslation) {
chapterText = loadedCheerio('.manga-child-content').html()!;
} else if (isAnomalously) {
chapterText = loadedCheerio('#comic').html()!;
} else if (kofi) {
chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
.html()
?.match(/shadowDom\.innerHTML \+= '(<div.*?)';/)![1]!;
} else if (isiNovelTranslation) {
const link = 'https://api.' + result.url.slice(8);
const json = await fetchApi(link).then(r => r.json());
chapterText = json.content.replace(/\n/g, '<br>');
if (json.notes) {
chapterText +=
'<br><hr><br>TL Notes:<br>' + json.notes.replace(/\n/g, '<br>');
}
} else if (isWordPress) {
/**
* Remove wordpress bloat tags
*/
const bloatClasses = [
".c-ads",
"#madara-comments",
"#comments",
".content-comments",
".sharedaddy",
".wp-dark-mode-switcher",
".wp-next-post-navi",
".wp-block-buttons",
".wp-block-columns",
".post-cats",
".sidebar",
".author-avatar",
".ezoic-ad",
];
bloatClasses.map((tag) => loadedCheerio(tag).remove());
chapterText =
loadedCheerio(".entry-content").html() ||
loadedCheerio(".single_post").html() ||
loadedCheerio(".post-entry").html() ||
loadedCheerio(".main-content").html() ||
loadedCheerio("article.post").html() ||
loadedCheerio(".content").html() ||
loadedCheerio("#content").html() ||
loadedCheerio(".page-body").html() ||
loadedCheerio(".td-page-content").html()!;
} else if (isWebNovel) {
chapterText = loadedCheerio(".cha-words").html()!;
if (!chapterText) {
chapterText = loadedCheerio("._content").html()!;
}
} else {
/**
* Remove unnecessary tags
*/
const tags = ["nav", "header", "footer", ".hidden"];
tags.map((tag) => loadedCheerio(tag).remove());
chapterText = loadedCheerio("body").html()!;
}
if (chapterText) {
/**
* Convert relative urls to absolute
*/
chapterText = chapterText.replace(
/href="\//g,
`href="${this.getLocation(result.url)}/`
);
}
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const url = this.site + '?s=' + searchTerm.replace(/\s+/g, '+') + '&post_type=seriesplans';
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
sort: {
label: "Sort Results By",
value: "sdate",
options: [
{ label: "Last Updated", value: "sdate" },
{ label: "Rating", value: "srate" },
{ label: "Rank", value: "srank" },
{ label: "Reviews", value: "sreview" },
{ label: "Chapters", value: "srel" },
{ label: "Title", value: "abc" },
{ label: "Readers", value: "sread" },
{ label: "Frequency", value: "sfrel" },
],
type: FilterTypes.Picker,
},
order: {
label: "Order",
value: "desc",
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: "Story Status (Translation)",
value: "",
options: [
{ label: "All", value: "" },
{ label: "Completed", value: "2" },
{ label: "Ongoing", value: "3" },
{ label: "Hiatus", value: "4" },
],
type: FilterTypes.Picker,
},
language: {
label: "Language",
value: [],
options: [
{ label: "Chinese", value: "495" },
{ label: "Filipino", value: "9181" },
{ label: "Indonesian", value: "9179" },
{ label: "Japanese", value: "496" },
{ label: "Khmer", value: "18657" },
{ label: "Korean", value: "497" },
{ label: "Malaysian", value: "9183" },
{ label: "Thai", value: "9954" },
{ label: "Vietnamese", value: "9177" },
],
type: FilterTypes.CheckboxGroup,
},
novelType: {
label: "Novel Type",
value: [],
options: [
{ label: "Light Novel", value: "2443" },
{ label: "Published Novel", value: "26874" },
{ label: "Web Novel", value: "2444" },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: "Genres",
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [],
exclude: [],
},
options: [
{ label: "Action", value: "8" },
{ label: "Adult", value: "280" },
{ label: "Adventure", value: "13" },
{ label: "Comedy", value: "17" },
{ label: "Drama", value: "9" },
{ label: "Ecchi", value: "292" },
{ label: "Fantasy", value: "5" },
{ label: "Gender Bender", value: "168" },
{ label: "Harem", value: "3" },
{ label: "Historical", value: "330" },
{ label: "Horror", value: "343" },
{ label: "Josei", value: "324" },
{ label: "Martial Arts", value: "14" },
{ label: "Mature", value: "4" },
{ label: "Mecha", value: "10" },
{ label: "Mystery", value: "245" },
{ label: "Psychoical", value: "486" },
{ label: "Romance", value: "15" },
{ label: "School Life", value: "6" },
{ label: "Sci-fi", value: "11" },
{ label: "Seinen", value: "18" },
{ label: "Shoujo", value: "157" },
{ label: "Shoujo Ai", value: "851" },
{ label: "Shounen", value: "12" },
{ label: "Shounen Ai", value: "1692" },
{ label: "Slice of Life", value: "7" },
{ label: "Smut", value: "281" },
{ label: "Sports", value: "1357" },
{ label: "Supernatural", value: "16" },
{ label: "Tragedy", value: "132" },
{ label: "Wuxia", value: "479" },
{ label: "Xianxia", value: "480" },
{ label: "Xuanhuan", value: "3954" },
{ label: "Yaoi", value: "560" },
{ label: "Yuri", value: "922" },
],
},
genre_operator: {
label: "Genre (AND/OR)",
value: "and",
options: [
{ label: "AND", value: "and" },
{ label: "OR", value: "or" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
filters = {
sort: {
label: 'Sort Results By',
value: 'sdate',
options: [
{ label: 'Last Updated', value: 'sdate' },
{ label: 'Rating', value: 'srate' },
{ label: 'Rank', value: 'srank' },
{ label: 'Reviews', value: 'sreview' },
{ label: 'Chapters', value: 'srel' },
{ label: 'Title', value: 'abc' },
{ label: 'Readers', value: 'sread' },
{ label: 'Frequency', value: 'sfrel' },
],
type: FilterTypes.Picker,
},
order: {
label: 'Order',
value: 'desc',
options: [
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: 'Story Status (Translation)',
value: '',
options: [
{ label: 'All', value: '' },
{ label: 'Completed', value: '2' },
{ label: 'Ongoing', value: '3' },
{ label: 'Hiatus', value: '4' },
],
type: FilterTypes.Picker,
},
language: {
label: 'Language',
value: [],
options: [
{ label: 'Chinese', value: '495' },
{ label: 'Filipino', value: '9181' },
{ label: 'Indonesian', value: '9179' },
{ label: 'Japanese', value: '496' },
{ label: 'Khmer', value: '18657' },
{ label: 'Korean', value: '497' },
{ label: 'Malaysian', value: '9183' },
{ label: 'Thai', value: '9954' },
{ label: 'Vietnamese', value: '9177' },
],
type: FilterTypes.CheckboxGroup,
},
novelType: {
label: 'Novel Type',
value: [],
options: [
{ label: 'Light Novel', value: '2443' },
{ label: 'Published Novel', value: '26874' },
{ label: 'Web Novel', value: '2444' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: 'Genres',
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [],
exclude: [],
},
options: [
{ label: 'Action', value: '8' },
{ label: 'Adult', value: '280' },
{ label: 'Adventure', value: '13' },
{ label: 'Comedy', value: '17' },
{ label: 'Drama', value: '9' },
{ label: 'Ecchi', value: '292' },
{ label: 'Fantasy', value: '5' },
{ label: 'Gender Bender', value: '168' },
{ label: 'Harem', value: '3' },
{ label: 'Historical', value: '330' },
{ label: 'Horror', value: '343' },
{ label: 'Josei', value: '324' },
{ label: 'Martial Arts', value: '14' },
{ label: 'Mature', value: '4' },
{ label: 'Mecha', value: '10' },
{ label: 'Mystery', value: '245' },
{ label: 'Psychoical', value: '486' },
{ label: 'Romance', value: '15' },
{ label: 'School Life', value: '6' },
{ label: 'Sci-fi', value: '11' },
{ label: 'Seinen', value: '18' },
{ label: 'Shoujo', value: '157' },
{ label: 'Shoujo Ai', value: '851' },
{ label: 'Shounen', value: '12' },
{ label: 'Shounen Ai', value: '1692' },
{ label: 'Slice of Life', value: '7' },
{ label: 'Smut', value: '281' },
{ label: 'Sports', value: '1357' },
{ label: 'Supernatural', value: '16' },
{ label: 'Tragedy', value: '132' },
{ label: 'Wuxia', value: '479' },
{ label: 'Xianxia', value: '480' },
{ label: 'Xuanhuan', value: '3954' },
{ label: 'Yaoi', value: '560' },
{ label: 'Yuri', value: '922' },
],
},
genre_operator: {
label: 'Genre (AND/OR)',
value: 'and',
options: [
{ label: 'AND', value: 'and' },
{ label: 'OR', value: 'or' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new NovelUpdates();
+195 -191
View File
@@ -1,226 +1,230 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class PawRead implements Plugin.PluginBase {
id = "pawread";
name = "PawRead";
version = "2.0.0";
icon = "src/en/pawread/icon.png";
site = "https://m.pawread.com/";
id = 'pawread';
name = 'PawRead';
version = '2.0.0';
icon = 'src/en/pawread/icon.png';
site = 'https://m.pawread.com/';
parseNovels(loadedCheerio:CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".list-comic, .itemBox").each((idx, ele) => {
loadedCheerio(ele).find('.serialise').remove();
const novelName = loadedCheerio(ele).find('a').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
loadedCheerio('.list-comic, .itemBox').each((idx, ele) => {
loadedCheerio(ele).find('.serialise').remove();
const novelName = loadedCheerio(ele).find('a').text().trim();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
});
novels.push(novel);
});
return novels;
}
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}list/`;
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}list/`;
const filterValues = [
filters.status.value,
filters.lang.value,
filters.genre.value
];
const filterValues = [
filters.status.value,
filters.lang.value,
filters.genre.value,
];
link += filterValues.filter(value => value !== '').join('-');
if (filterValues.some(value => value !== ''))
link += '/';
link += filterValues.filter(value => value !== '').join('-');
if (filterValues.some(value => value !== '')) link += '/';
if (filters.order.value)
link += '-';
link += filters.sort.value;
link += `/?page=${page}`;
if (filters.order.value) link += '-';
const body = await fetchApi(link).then((r) => r.text());
link += filters.sort.value;
link += `/?page=${page}`;
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const body = await fetchApi(link).then(r => r.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("#Cover img").attr('title') || "Untitled",
cover: loadedCheerio("#Cover img").attr('src'),
author: loadedCheerio('.icon01 <').text().trim(),
status: loadedCheerio('.txtItme:first').text().trim(),
chapters: [],
};
const loadedCheerio = parseHTML(body);
novel.summary = loadedCheerio("#full-des")
.find('br')
.replaceWith('\n')
.end()
.text()
.trim();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('#Cover img').attr('title') || 'Untitled',
cover: loadedCheerio('#Cover img').attr('src'),
author: loadedCheerio('.icon01 <').text().trim(),
status: loadedCheerio('.txtItme:first').text().trim(),
chapters: [],
};
novel.genres = loadedCheerio('a.btn-default')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(",");
novel.summary = loadedCheerio('#full-des')
.find('br')
.replaceWith('\n')
.end()
.text()
.trim();
const chapter: Plugin.ChapterItem[] = [];
novel.genres = loadedCheerio('a.btn-default')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(',');
loadedCheerio(".item-box").each((idx, ele) => {
const smallText = loadedCheerio(ele).find('span:last').text().trim();
if (smallText === "Advanced Chapter") return;
const releaseDate = smallText.split('.').map(x => Number(x));
const chapterName = loadedCheerio(ele).find("span:first").text().trim();
const chapterUrl = loadedCheerio(ele).attr("onclick")?.match(/\d+/)![0];
if (!chapterUrl) return;
const chapter: Plugin.ChapterItem[] = [];
chapter.push({
name: chapterName,
path: `${novelPath}${chapterUrl}.html`,
releaseTime: new Date(releaseDate[0], releaseDate[1], releaseDate[2]).toISOString(),
});
});
loadedCheerio('.item-box').each((idx, ele) => {
const smallText = loadedCheerio(ele).find('span:last').text().trim();
if (smallText === 'Advanced Chapter') return;
const releaseDate = smallText.split('.').map(x => Number(x));
const chapterName = loadedCheerio(ele).find('span:first').text().trim();
const chapterUrl = loadedCheerio(ele).attr('onclick')?.match(/\d+/)![0];
if (!chapterUrl) return;
novel.chapters = chapter;
return novel;
}
chapter.push({
name: chapterName,
path: `${novelPath}${chapterUrl}.html`,
releaseTime: new Date(
releaseDate[0],
releaseDate[1],
releaseDate[2],
).toISOString(),
});
});
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const steal = ['bit.ly', 'tinyurl', 'pawread'];
steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove());
const chapterText = loadedCheerio('.main').html() || "";
const steal = ['bit.ly', 'tinyurl', 'pawread'];
steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove());
const chapterText = loadedCheerio('.main').html() || '';
return chapterText;
}
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`;
const result = await fetchApi(searchUrl);
const body = await result.text();
const result = await fetchApi(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
status: {
value: "",
label: "Status",
options: [
{ label: "All", value: "" },
{ label: "Completed", value: "wanjie" },
{ label: "Ongoing", value: "lianzai" },
{ label: "Hiatus", value: "hiatus" },
],
type: FilterTypes.Picker,
},
lang: {
value: "",
label: "Languages",
options: [
{ label: "All", value: "" },
{ label: "Chinese", value: "chinese" },
{ label: "Korean", value: "korean" },
{ label: "Japanese", value: "japanese" },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Genres",
options: [
{ label: "All", value: "" },
{ label: "Fantasy", value: "Fantasy" },
{ label: "Action", value: "Action" },
{ label: "Xuanhuan", value: "Xuanhuan" },
{ label: "Romance", value: "Romance" },
{ label: "Comedy", value: "Comedy" },
{ label: "Mystery", value: "Mystery" },
{ label: "Mature", value: "Mature" },
{ label: "Harem", value: "Harem" },
{ label: "Wuxia", value: "Wuxia" },
{ label: "Xianxia", value: "Xianxia" },
{ label: "Tragedy", value: "Tragedy" },
{ label: "Sci-fi", value: "Scifi" },
{ label: "Historical", value: "Historical" },
{ label: "Ecchi", value: "Ecchi" },
{ label: "Adventure", value: "Adventure" },
{ label: "Adult", value: "Adult" },
{ label: "Supernatural", value: "Supernatural" },
{ label: "Psychological", value: "Psychological" },
{ label: "Drama", value: "Drama" },
{ label: "Horror", value: "Horror" },
{ label: "Josei", value: "Josei" },
{ label: "Mecha", value: "Mecha" },
{ label: "Seinen", value: "Seinen" },
{ label: "Shoujo", value: "Shoujo" },
{ label: "Shounen", value: "Shounen" },
{ label: "Smut", value: "Smut" },
{ label: "Yaoi", value: "Yaoi" },
{ label: "Yuri", value: "Yuri" },
{ label: "Martial Arts", value: "MartialArts" },
{ label: "School Life", value: "SchoolLife" },
{ label: "Shoujo Ai", value: "ShoujoAi" },
{ label: "Shounen Ai", value: "ShounenAi" },
{ label: "Slice of Life", value: "SliceofLife" },
{ label: "Gender Bender", value: "GenderBender" },
{ label: "Sports", value: "Sports" },
{ label: "Urban", value: "Urban" },
{ label: "Adventurer", value: "Adventurer" }
],
type: FilterTypes.Picker,
},
sort: {
value: "click",
label: "Sort By",
options: [
{ label: "Time Updated", value: "update" },
{ label: "Time Posted", value: "post" },
{ label: "Clicks", value: "click" },
],
type: FilterTypes.Picker,
},
order: {
value: false,
label: "Order ↑ ↓",
type: FilterTypes.Switch,
},
} satisfies Filters;
filters = {
status: {
value: '',
label: 'Status',
options: [
{ label: 'All', value: '' },
{ label: 'Completed', value: 'wanjie' },
{ label: 'Ongoing', value: 'lianzai' },
{ label: 'Hiatus', value: 'hiatus' },
],
type: FilterTypes.Picker,
},
lang: {
value: '',
label: 'Languages',
options: [
{ label: 'All', value: '' },
{ label: 'Chinese', value: 'chinese' },
{ label: 'Korean', value: 'korean' },
{ label: 'Japanese', value: 'japanese' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Genres',
options: [
{ label: 'All', value: '' },
{ label: 'Fantasy', value: 'Fantasy' },
{ label: 'Action', value: 'Action' },
{ label: 'Xuanhuan', value: 'Xuanhuan' },
{ label: 'Romance', value: 'Romance' },
{ label: 'Comedy', value: 'Comedy' },
{ label: 'Mystery', value: 'Mystery' },
{ label: 'Mature', value: 'Mature' },
{ label: 'Harem', value: 'Harem' },
{ label: 'Wuxia', value: 'Wuxia' },
{ label: 'Xianxia', value: 'Xianxia' },
{ label: 'Tragedy', value: 'Tragedy' },
{ label: 'Sci-fi', value: 'Scifi' },
{ label: 'Historical', value: 'Historical' },
{ label: 'Ecchi', value: 'Ecchi' },
{ label: 'Adventure', value: 'Adventure' },
{ label: 'Adult', value: 'Adult' },
{ label: 'Supernatural', value: 'Supernatural' },
{ label: 'Psychological', value: 'Psychological' },
{ label: 'Drama', value: 'Drama' },
{ label: 'Horror', value: 'Horror' },
{ label: 'Josei', value: 'Josei' },
{ label: 'Mecha', value: 'Mecha' },
{ label: 'Seinen', value: 'Seinen' },
{ label: 'Shoujo', value: 'Shoujo' },
{ label: 'Shounen', value: 'Shounen' },
{ label: 'Smut', value: 'Smut' },
{ label: 'Yaoi', value: 'Yaoi' },
{ label: 'Yuri', value: 'Yuri' },
{ label: 'Martial Arts', value: 'MartialArts' },
{ label: 'School Life', value: 'SchoolLife' },
{ label: 'Shoujo Ai', value: 'ShoujoAi' },
{ label: 'Shounen Ai', value: 'ShounenAi' },
{ label: 'Slice of Life', value: 'SliceofLife' },
{ label: 'Gender Bender', value: 'GenderBender' },
{ label: 'Sports', value: 'Sports' },
{ label: 'Urban', value: 'Urban' },
{ label: 'Adventurer', value: 'Adventurer' },
],
type: FilterTypes.Picker,
},
sort: {
value: 'click',
label: 'Sort By',
options: [
{ label: 'Time Updated', value: 'update' },
{ label: 'Time Posted', value: 'post' },
{ label: 'Clicks', value: 'click' },
],
type: FilterTypes.Picker,
},
order: {
value: false,
label: 'Order ↑ ↓',
type: FilterTypes.Switch,
},
} satisfies Filters;
}
export default new PawRead();
export default new PawRead();
+159 -155
View File
@@ -1,184 +1,188 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
interface ChapterEntry {
id: number;
title: string;
date: string;
link: string;
id: number;
title: string;
date: string;
link: string;
}
class RanobesPlugin implements Plugin.PagePlugin {
id = 'ranobes';
name = 'Ranobes';
icon = 'src/en/ranobes/icon.png';
site = 'https://ranobes.top';
filters?: Filters | undefined;
version = '1.0.0';
id = 'ranobes';
name = 'Ranobes';
icon = 'src/en/ranobes/icon.png';
site = 'https://ranobes.top';
filters?: Filters | undefined;
version = '1.0.0';
async sleep (ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
async sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.short-cont').each((i, el) => {
const novelName = loadedCheerio(el).find('h2.title').text();
const novelUrl = loadedCheerio(el).find('h2.title a').attr('href');
const novelCover = loadedCheerio(el)
.find('figure')
.attr('style')
?.replace(/.*url\((.*?)\)./g, '$1');
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl?.replace(this.site, ''),
};
novels.push(novel);
});
return novels;
}
parseChapters(data: { chapters: ChapterEntry[] }) {
const chapter: Plugin.ChapterItem[] = [];
data.chapters.map((item: ChapterEntry) => {
chapter.push({
name: item.title,
releaseTime: new Date(item.date).toISOString(),
path: item.link.replace(this.site, ''),
});
});
return chapter.reverse();
}
async popularNovels(
page: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}/novels/page/${page}/`;
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('.poster img').attr('alt') || 'Untitled',
cover: this.site + loadedCheerio('.poster img').attr('src'),
author: loadedCheerio('[itemprop="creator"]').text(),
chapters: [],
totalPages: 1,
};
novel.summary = loadedCheerio('.moreless')
.find('br')
.replaceWith('\n')
.end()
.text()
.trim();
novel.status =
loadedCheerio('li:contains("Status") span').text() === 'Ongoing'
? 'Ongoing'
: 'Completed';
novel.genres = loadedCheerio('div#mc-fs-genre').text().trim();
let chapterListUrl = loadedCheerio('.read-continue:last').attr('href');
if (!chapterListUrl?.startsWith('http')) {
chapterListUrl = this.site + chapterListUrl;
}
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r =>
r.text(),
);
const pageCheerio = parseHTML(chaptersHtmlToString);
loadedCheerio('.short-cont').each((i,el) => {
const novelName = loadedCheerio(el).find('h2.title').text();
const novelUrl = loadedCheerio(el).find('h2.title a').attr('href');
const novelCover = loadedCheerio(el)
.find('figure')
.attr('style')
?.replace(/.*url\((.*?)\)./g, '$1');
if (!novelUrl) return;
const json = pageCheerio('#dle-content main').next().html()!;
const data = JSON.parse(json.replace('window.__DATA__ =', ''));
novel.totalPages = Number(data.pages_count);
novel.chapters = this.parseChapters(data);
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl?.replace(this.site, ''),
}
novels.push(novel);
});
return novels;
}
return novel;
}
parseChapters(data: {chapters: ChapterEntry[]}){
const chapter: Plugin.ChapterItem[] = [];
data.chapters.map((item: ChapterEntry) => {
chapter.push({
name: item.title,
releaseTime: new Date(item.date).toISOString(),
path: item.link.replace(this.site, ''),
});
});
return chapter.reverse();
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const pagePath = novelPath.split('-')[0];
const firstUrl = this.site + '/chapters' + pagePath.replace('novels/', '');
const pageUrl = firstUrl + '/page/' + page;
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}/novels/page/${page}/`;
const body = await fetchApi(link).then((result) =>
result.text()
);
const body = await fetchApi(firstUrl).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
let loadedCheerio = parseHTML(body);
let json = loadedCheerio('#dle-content main').next().html()!;
let data = JSON.parse(json.replace('window.__DATA__ =', ''));
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
const latestChapterUrl = data.chapters[0].link;
const latestChapterName = data.chapters[0].title;
const latestChapterDate = data.chapters[0].date;
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel & { totalPages: number; } = {
path: novelPath,
name: loadedCheerio('.poster img').attr('alt') || "Untitled",
cover: this.site + loadedCheerio('.poster img').attr('src'),
author: loadedCheerio('[itemprop="creator"]').text(),
chapters: [],
totalPages: 1,
};
novel.summary = loadedCheerio('.moreless')
.find('br')
.replaceWith('\n')
.end()
.text()
.trim();
novel.status = loadedCheerio('li:contains("Status") span')
.text() === 'Ongoing'
? 'Ongoing'
: 'Completed';
novel.genres = loadedCheerio('div#mc-fs-genre').text().trim();
let chapterListUrl = loadedCheerio('.read-continue:last').attr('href');
if (!chapterListUrl?.startsWith('http')) {
chapterListUrl = this.site + chapterListUrl;
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl
? {
path: latestChapterUrl.replace(this.site, ''),
name: latestChapterName,
releaseTime: new Date(latestChapterDate).toISOString(),
}
: undefined;
const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r => r.text());
const pageCheerio = parseHTML(chaptersHtmlToString);
await this.sleep(1000);
const json = pageCheerio('#dle-content main').next().html()!;
const data = JSON.parse(json.replace('window.__DATA__ =', ''));
novel.totalPages = Number(data.pages_count);
novel.chapters = this.parseChapters(data);
return novel;
const pageBody = await fetchApi(pageUrl).then(r => r.text());
loadedCheerio = parseHTML(pageBody);
json = loadedCheerio('#dle-content main').next().html()!;
data = JSON.parse(json.replace('window.__DATA__ =', ''));
const chapters = this.parseChapters(data);
return {
chapters,
latestChapter,
};
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const pagePath = novelPath.split('-')[0];
const firstUrl = this.site +
'/chapters' +
pagePath.replace('novels/', '');
const pageUrl = firstUrl + '/page/' + page;
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const body = await fetchApi(firstUrl).then(r => r.text());
let loadedCheerio = parseHTML(body);
let json = loadedCheerio('#dle-content main').next().html()!;
let data = JSON.parse(json.replace('window.__DATA__ =', ''));
const loadedCheerio = parseHTML(body);
const latestChapterUrl = data.chapters[0].link;
const latestChapterName = data.chapters[0].title;
const latestChapterDate = data.chapters[0].date;
const chapterText = loadedCheerio('#arrticle').html() || '';
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? {
path: latestChapterUrl.replace(this.site, ''),
name: latestChapterName,
releaseTime: new Date(latestChapterDate).toISOString(),
} : undefined
return chapterText;
}
await this.sleep(1000);
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}/search/${searchTerm}/page/${page}`;
const pageBody = await fetchApi(pageUrl).then(r => r.text());
loadedCheerio = parseHTML(pageBody);
json = loadedCheerio('#dle-content main').next().html()!;
data = JSON.parse(json.replace('window.__DATA__ =', ''));
const chapters = this.parseChapters(data);
return {
chapters,
latestChapter
}
}
const body = await fetchApi(link).then(r => r.text());
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('#arrticle').html() || "";
return chapterText;
};
async searchNovels(
searchTerm: string,
page: number
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}/search/${searchTerm}/page/${page}`;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
};
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
}
export default new RanobesPlugin();
+182 -175
View File
@@ -1,189 +1,196 @@
import { load as parseHTML } from "cheerio";
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import dayjs from "dayjs";
import { load as parseHTML } from 'cheerio';
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import dayjs from 'dayjs';
class ReadLiteNovel implements Plugin.PluginBase {
id = "rln.app";
name = "ReadLiteNovel";
version = "1.0.0";
icon = "src/en/readlitenovel/icon.png";
site = "https://rln.app";
id = 'rln.app';
name = 'ReadLiteNovel';
version = '1.0.0';
icon = 'src/en/readlitenovel/icon.png';
site = 'https://rln.app';
parseAgoDate(date: string | undefined){ //parseMadaraDate
if (date?.includes("ago")) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
const timeAgoInt = parseInt(timeAgo, 10);
if (!timeAgo) return null; // there is no number!
if (date.includes("hours ago") || date.includes("hour ago")) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
}
if (date.includes("days ago") || date.includes("day ago")) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
}
if (date.includes("months ago") || date.includes("month ago")) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
}
parseAgoDate(date: string | undefined) {
//parseMadaraDate
if (date?.includes('ago')) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || '';
const timeAgoInt = parseInt(timeAgo, 10);
if (date.includes("years ago") || date.includes("year ago")) {
dayJSDate.subtract(timeAgoInt, "years"); // go back N years
}
if (!timeAgo) return null; // there is no number!
return dayJSDate.toISOString();
}
return null; // there is no "ago" so give up
};
if (date.includes('hours ago') || date.includes('hour ago')) {
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
const link = `${this.site}/ranking/${filters.order.value}/${page}`;
const result = await fetchApi(link);
const body = await result.text();
if (date.includes('days ago') || date.includes('day ago')) {
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
const loadedCheerio = parseHTML(body);
if (date.includes('months ago') || date.includes('month ago')) {
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
const novels: Plugin.NovelItem[] = [];
if (date.includes('years ago') || date.includes('year ago')) {
dayJSDate.subtract(timeAgoInt, 'years'); // go back N years
}
loadedCheerio(".category-items li").each((i,el) => {
const novelUrl = loadedCheerio(el)
.find(".category-name a")
.attr("href");
if (!novelUrl) return;
const novelName = loadedCheerio(el)
.find(".category-name a")
.text()
.trim();
let novelCover = loadedCheerio(el)
.find(".category-img img")
.attr("src");
if (novelCover && !isUrlAbsolute(novelCover)) {
novelCover = this.site + novelCover;
}
const novel = {
path: novelUrl?.replace(this.site, ''),
name: novelName,
cover: novelCover,
};
novels.push(novel);
})
return novels;
};
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".novel-title").text() || "Untitled",
cover: loadedCheerio(".novels-detail img").attr("src"),
summary: loadedCheerio('.empty-box').text().trim(),
chapters: [],
};
loadedCheerio('.novels-detail-right li').each((i,el) => {
const detailName = loadedCheerio(el).find('div:first').text();
const detail = loadedCheerio(el).find('div:last');
switch(detailName){
case "Status:":
novel.status = detail.text();
break;
case "Genres:":
novel.genres = detail.find('a')
.map((i,el) => loadedCheerio(el).text())
.toArray()
.join(', ') || detail.text();
break;
case "Author(s):":
novel.author = detail.find('a')
.map((i,el) => loadedCheerio(el).text())
.toArray()
.join(', ') || detail.text();
break;
case "Translator:":
novel.artist = detail.text();
break;
}
})
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".cm-tabs-content li").each((i,el) => {
const chapterUrl = loadedCheerio(el).find("a").attr("href");
if (!chapterUrl) return;
const chapterName = loadedCheerio(el).find("a").text().trim();
const releaseDate = this.parseAgoDate(
loadedCheerio(el).find('svg').attr('data-bs-original-title')!
);
chapter.push({
name: chapterName,
path: chapterUrl?.replace(this.site, ''),
releaseTime: releaseDate,
})})
novel.chapters = chapter;
return novel;
};
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapterText").html() || "";
return chapterText;
};
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.json();
const novels: Plugin.NovelItem[] = [];
body.results.forEach((item: {link: string; original_title:string; image:string;}) =>
novels.push({
path: item.link,
name: item.original_title,
cover: item.image,
})
);
return novels;
};
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
return dayJSDate.toISOString();
}
return null; // there is no "ago" so give up
}
filters = {
order: {
value: "top-rated",
label: "Order by",
options: [
{ label: "MOST VIEWED", value: "most-viewed" },
{ label: "TOP RATED", value: "top-rated" },
{ label: "BOOKMARKS", value: "subscribers" },
{ label: "NEW", value: "new" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const link = `${this.site}/ranking/${filters.order.value}/${page}`;
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.category-items li').each((i, el) => {
const novelUrl = loadedCheerio(el).find('.category-name a').attr('href');
if (!novelUrl) return;
const novelName = loadedCheerio(el)
.find('.category-name a')
.text()
.trim();
let novelCover = loadedCheerio(el).find('.category-img img').attr('src');
if (novelCover && !isUrlAbsolute(novelCover)) {
novelCover = this.site + novelCover;
}
const novel = {
path: novelUrl?.replace(this.site, ''),
name: novelName,
cover: novelCover,
};
novels.push(novel);
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.novel-title').text() || 'Untitled',
cover: loadedCheerio('.novels-detail img').attr('src'),
summary: loadedCheerio('.empty-box').text().trim(),
chapters: [],
};
loadedCheerio('.novels-detail-right li').each((i, el) => {
const detailName = loadedCheerio(el).find('div:first').text();
const detail = loadedCheerio(el).find('div:last');
switch (detailName) {
case 'Status:':
novel.status = detail.text();
break;
case 'Genres:':
novel.genres =
detail
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(', ') || detail.text();
break;
case 'Author(s):':
novel.author =
detail
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(', ') || detail.text();
break;
case 'Translator:':
novel.artist = detail.text();
break;
}
});
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.cm-tabs-content li').each((i, el) => {
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
const chapterName = loadedCheerio(el).find('a').text().trim();
const releaseDate = this.parseAgoDate(
loadedCheerio(el).find('svg').attr('data-bs-original-title')!,
);
chapter.push({
name: chapterName,
path: chapterUrl?.replace(this.site, ''),
releaseTime: releaseDate,
});
});
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('#chapterText').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.json();
const novels: Plugin.NovelItem[] = [];
body.results.forEach(
(item: { link: string; original_title: string; image: string }) =>
novels.push({
path: item.link,
name: item.original_title,
cover: item.image,
}),
);
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
order: {
value: 'top-rated',
label: 'Order by',
options: [
{ label: 'MOST VIEWED', value: 'most-viewed' },
{ label: 'TOP RATED', value: 'top-rated' },
{ label: 'BOOKMARKS', value: 'subscribers' },
{ label: 'NEW', value: 'new' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new ReadLiteNovel();
+153 -147
View File
@@ -1,185 +1,191 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class RoyalRoad implements Plugin.PluginBase {
id = "royalroad";
name = "Royal Road";
version = "1.0.1";
icon = "src/en/royalroad/icon.png";
site = "https://www.royalroad.com/";
id = 'royalroad';
name = 'Royal Road';
version = '1.0.1';
icon = 'src/en/royalroad/icon.png';
site = 'https://www.royalroad.com/';
parseNovels(loadedCheerio:CheerioAPI){
const novels: Plugin.NovelItem[] = [];
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".fiction-list-item").each((idx, ele) => {
const novelName = loadedCheerio(ele).find('a.bold').text();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1);
loadedCheerio('.fiction-list-item').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('a.bold').text();
const novelCover = loadedCheerio(ele).find('img').attr('src');
const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1);
if (!novelUrl) return;
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
});
novels.push(novel);
});
return novels;
}
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}fictions/`;
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}fictions/`;
link += filters.order.value;
link += `?page=${page}`;
link += filters.order.value;
link += `?page=${page}`;
if (filters.genre.value !== '')
link += `&genre=${filters.genre.value}`;
if (filters.genre.value !== '') link += `&genre=${filters.genre.value}`;
const body = await fetchApi(link).then((r) => r.text());
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1").text() || "Untitled",
cover: loadedCheerio('.cover-art-container img').attr('src'),
author: loadedCheerio('h4 a').text(),
summary: loadedCheerio(".description").text().trim(),
status: loadedCheerio('span.label-sm:last').text().trim(),
chapters: [],
};
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h1').text() || 'Untitled',
cover: loadedCheerio('.cover-art-container img').attr('src'),
author: loadedCheerio('h4 a').text(),
summary: loadedCheerio('.description').text().trim(),
status: loadedCheerio('span.label-sm:last').text().trim(),
chapters: [],
};
novel.genres = loadedCheerio('span.tags a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
novel.genres = loadedCheerio('span.tags a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
const chapterJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.chapters = (.+])(?=;)/)![1] || "");
const chapterJson = JSON.parse(
loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.chapters = (.+])(?=;)/)![1] || '',
);
const volumeJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.volumes = (.+])(?=;)/)![1] || "");
const volumeJson = JSON.parse(
loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.volumes = (.+])(?=;)/)![1] || '',
);
const chapter : Plugin.ChapterItem[] = chapterJson.map(
(chapter: ChapterEntry) => {
const matchingVolume = volumeJson.find(
(volume: VolumeEntry) => volume.id === chapter.volumeId);
return {
name: chapter.title,
path: chapter.url.slice(1),
releaseTime: chapter.date,
chapterNumber: chapter?.order,
page: matchingVolume ? matchingVolume.title : null,
};
}
const chapter: Plugin.ChapterItem[] = chapterJson.map(
(chapter: ChapterEntry) => {
const matchingVolume = volumeJson.find(
(volume: VolumeEntry) => volume.id === chapter.volumeId,
);
return {
name: chapter.title,
path: chapter.url.slice(1),
releaseTime: chapter.date,
chapterNumber: chapter?.order,
page: matchingVolume ? matchingVolume.title : null,
};
},
);
novel.chapters = chapter;
return novel;
}
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('.chapter-content').html() || '';
const chapterText = loadedCheerio(".chapter-content").html() || '';
return chapterText;
}
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`;
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`;
const result = await fetchApi(searchUrl);
const body = await result.text();
const result = await fetchApi(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
filters = {
order: {
value: "weekly-popular",
label: "Order By",
options: [
{ label: "Best Rated", value: "best-rated" },
{ label: "Trending", value: "trending" },
{ label: "Ongoing Fictions", value: "active-popular" },
{ label: "Complete", value: "complete" },
{ label: "Popular this week", value: "weekly-popular" },
{ label: "Latest Updates", value: "latest-updates" },
{ label: "Newest Fictions", value: "new" },
{ label: "Rising Stars", value: "rising-stars" },
{ label: "Writathon", value: "writathon" },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Genres",
options: [
{ label: "ALL", value: "" },
{ label: "Action", value: "action" },
{ label: "Adventure", value: "adventure" },
{ label: "Comedy", value: "comedy" },
{ label: "Contemporary", value: "contemporary" },
{ label: "Drama", value: "drama" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Historical", value: "historical" },
{ label: "Horror", value: "horror" },
{ label: "Mystery", value: "mystery" },
{ label: "Psychological", value: "psychological" },
{ label: "Romance", value: "romance" },
{ label: "Satire", value: "satire" },
{ label: "Sci-fi", value: "sci_fi" },
{ label: "Short Story", value: "one_shot" },
{ label: "Tragedy", value: "tragedy" }
],
type: FilterTypes.Picker,
}
} satisfies Filters;
filters = {
order: {
value: 'weekly-popular',
label: 'Order By',
options: [
{ label: 'Best Rated', value: 'best-rated' },
{ label: 'Trending', value: 'trending' },
{ label: 'Ongoing Fictions', value: 'active-popular' },
{ label: 'Complete', value: 'complete' },
{ label: 'Popular this week', value: 'weekly-popular' },
{ label: 'Latest Updates', value: 'latest-updates' },
{ label: 'Newest Fictions', value: 'new' },
{ label: 'Rising Stars', value: 'rising-stars' },
{ label: 'Writathon', value: 'writathon' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Genres',
options: [
{ label: 'ALL', value: '' },
{ label: 'Action', value: 'action' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Contemporary', value: 'contemporary' },
{ label: 'Drama', value: 'drama' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Historical', value: 'historical' },
{ label: 'Horror', value: 'horror' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Romance', value: 'romance' },
{ label: 'Satire', value: 'satire' },
{ label: 'Sci-fi', value: 'sci_fi' },
{ label: 'Short Story', value: 'one_shot' },
{ label: 'Tragedy', value: 'tragedy' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new RoyalRoad();
interface ChapterEntry {
id: number;
volumeId: number;
title: string;
date: string;
order: number;
url: string;
id: number;
volumeId: number;
title: string;
date: string;
order: number;
url: string;
}
interface VolumeEntry {
id: number;
title: string;
cover: string;
order: number;
}
id: number;
title: string;
cover: string;
order: number;
}
+271 -270
View File
@@ -1,306 +1,307 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import dayjs from "dayjs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import dayjs from 'dayjs';
class ScribbleHubPlugin implements Plugin.PluginBase {
id = "scribblehub";
name = "ScribbleHub";
icon = "src/en/scribblehub/icon.png";
site = "https://www.scribblehub.com/";
version = "1.0.0";
parseNovels(loadedCheerio: CheerioAPI){
const novels: Plugin.NovelItem[] = [];
id = 'scribblehub';
name = 'ScribbleHub';
icon = 'src/en/scribblehub/icon.png';
site = 'https://www.scribblehub.com/';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.search_main_box').each((i,el) => {
const novelName = loadedCheerio(el).find('.search_title > a').text();
const novelCover = loadedCheerio(el).find('.search_img > img').attr('src');
const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href');
if (!novelUrl) return;
loadedCheerio('.search_main_box').each((i, el) => {
const novelName = loadedCheerio(el).find('.search_title > a').text();
const novelCover = loadedCheerio(el)
.find('.search_img > img')
.attr('src');
const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href');
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
}
novels.push(novel);
});
return novels;
}
if (!novelUrl) return;
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (showLatestNovels)
link += "latest-series/"
else // if (filters.genres.value.include?.length ||
// filters.genres.value.exclude?.length)
link += "series-finder/?sf=1"
// else
// link += "series-ranking/";
// TODO, series-ranking filters when hideOnSelect come out
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
return novels;
}
if (filters.genres.value.include?.length)
link += '&gi=' + filters.genres.value.include.join(',');
async popularNovels(
page: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (showLatestNovels) link += 'latest-series/';
// if (filters.genres.value.include?.length ||
// filters.genres.value.exclude?.length)
else link += 'series-finder/?sf=1';
// else
// link += "series-ranking/";
// TODO, series-ranking filters when hideOnSelect come out
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.genres.value.include?.length)
link += '&gi=' + filters.genres.value.include.join(',');
if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
link += '&mgi=' + filters.genre_operator.value;
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.content_warning.value.include?.length)
link += '&cti=' + filters.content_warning.value.include.join(',');
if (
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length
)
link += '&mgi=' + filters.genre_operator.value;
if (filters.content_warning.value.exclude?.length)
link += '&cte=' + filters.content_warning.value.exclude.join(',');
if (filters.content_warning.value.include?.length)
link += '&cti=' + filters.content_warning.value.include.join(',');
if (filters.content_warning.value.include?.length ||
filters.content_warning.value.exclude?.length)
link += '&mct' + filters.content_warning_operator.value;
if (filters.content_warning.value.exclude?.length)
link += '&cte=' + filters.content_warning.value.exclude.join(',');
link += '&sort=' + filters.sort.value;
link += '&order=' + filters.order.value;
if (
filters.content_warning.value.include?.length ||
filters.content_warning.value.exclude?.length
)
link += '&mct' + filters.content_warning_operator.value;
const body = await fetchApi(link).then((result) =>
result.text()
);
link += '&sort=' + filters.sort.value;
link += '&order=' + filters.order.value;
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
const body = await fetchApi(link).then(result => result.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
let loadedCheerio = parseHTML(body);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.fic_title').text() || 'Untitled',
cover: loadedCheerio('.fic_image > img').attr('src'),
summary: loadedCheerio('.wi_fic_desc').text(),
author: loadedCheerio('.auth_name_fic').text(),
chapters: [],
};
novel.genres = loadedCheerio('.fic_genre')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
novel.status = loadedCheerio('.rnd_stats')
.next()
.text()
.includes('Ongoing')
? 'Ongoing'
: 'Completed';
let loadedCheerio = parseHTML(body);
const formData = new FormData();
formData.append('action', 'wi_getreleases_pagination');
formData.append('pagenum', '-1');
formData.append('mypostid', novelPath.split('/')[1]);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.fic_title').text() || 'Untitled',
cover: loadedCheerio('.fic_image > img').attr('src'),
summary: loadedCheerio('.wi_fic_desc').text(),
author: loadedCheerio('.auth_name_fic').text(),
chapters: [],
};
const data = await fetchApi(
`${this.site}wp-admin/admin-ajax.php`,
{
method: 'POST',
body: formData,
},
);
const text = await data.text();
novel.genres = loadedCheerio('.fic_genre')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
loadedCheerio = parseHTML(text);
novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing')
? 'Ongoing'
: 'Completed';
const chapter: Plugin.ChapterItem[] = [];
const formData = new FormData();
formData.append('action', 'wi_getreleases_pagination');
formData.append('pagenum', '-1');
formData.append('mypostid', novelPath.split('/')[1]);
const parseISODate = (date: string) => {
if (date.includes("ago")) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
const timeAgoInt = parseInt(timeAgo, 10);
if (!timeAgo) return null; // there is no number!
if (date.includes("hours ago") || date.includes("hour ago")) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
}
if (date.includes("days ago") || date.includes("day ago")) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
}
if (date.includes("months ago") || date.includes("month ago")) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
}
return dayJSDate.toISOString();
}
return null;
};
const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, {
method: 'POST',
body: formData,
});
const text = await data.text();
loadedCheerio('.toc_w').each((i,el) => {
const chapterName = loadedCheerio(el).find('.toc_a').text();
const releaseDate = loadedCheerio(el).find('.fic_date_pub').text();
const chapterUrl = loadedCheerio(el).find('a').attr('href');
loadedCheerio = parseHTML(text);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: parseISODate(releaseDate),
path: chapterUrl.replace(this.site, ''),
});
});
const chapter: Plugin.ChapterItem[] = [];
novel.chapters = chapter.reverse();
const parseISODate = (date: string) => {
if (date.includes('ago')) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || '';
const timeAgoInt = parseInt(timeAgo, 10);
return novel;
}
if (!timeAgo) return null; // there is no number!
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
if (date.includes('hours ago') || date.includes('hour ago')) {
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
const loadedCheerio = parseHTML(body);
if (date.includes('days ago') || date.includes('day ago')) {
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
const chapterText = loadedCheerio('div.chp_raw').html() || '';
return chapterText;
}
if (date.includes('months ago') || date.includes('month ago')) {
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
async searchNovels(
searchTerm: string,
pageNo: number
): Promise<Plugin.NovelItem[]> {
const url =`${this.site}?s=${searchTerm}&post_type=fictionposts`;
const result = await fetchApi(url);
const body = await result.text();
return dayJSDate.toISOString();
}
return null;
};
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
loadedCheerio('.toc_w').each((i, el) => {
const chapterName = loadedCheerio(el).find('.toc_a').text();
const releaseDate = loadedCheerio(el).find('.fic_date_pub').text();
const chapterUrl = loadedCheerio(el).find('a').attr('href');
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: parseISODate(releaseDate),
path: chapterUrl.replace(this.site, ''),
});
});
filters = {
sort: {
label: "Sort Results By",
value: "ratings",
options: [
{ label: "Chapters", value: "chapters" },
{ label: "Chapters per week", value: "frequency" },
{ label: "Date Added", value: "dateadded" },
{ label: "Favorites", value: "favorites" },
{ label: "Last Updated", value: "lastchdate" },
{ label: "Number of Ratings", value: "numofrate" },
{ label: "Pages", value: "pages" },
{ label: "Pageviews", value: "pageviews" },
{ label: "Ratings", value: "ratings" },
{ label: "Readers", value: "readers" },
{ label: "Reviews", value: "reviews" },
{ label: "Total Words", value: "totalwords" },
],
type: FilterTypes.Picker,
},
order: {
label: "Order by",
value: "desc",
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: "Story Status (Translation)",
value: "",
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Hiatus", value: "hiatus" },
],
type: FilterTypes.Picker,
},
genres: {
label: 'Genres',
value: {
include: [],
exclude: [],
},
options: [
{ label: 'Action', value: '9' },
{ label: 'Adult', value: '902' },
{ label: 'Adventure', value: '8' },
{ label: 'Boys Love', value: '891' },
{ label: 'Comedy', value: '7' },
{ label: 'Drama', value: '903' },
{ label: 'Ecchi', value: '904' },
{ label: 'Fanfiction', value: '38' },
{ label: 'Fantasy', value: '19' },
{ label: 'Gender Bender', value: '905' },
{ label: 'Girls Love', value: '892' },
{ label: 'Harem', value: '1015' },
{ label: 'Historical', value: '21' },
{ label: 'Horror', value: '22' },
{ label: 'Isekai', value: '37' },
{ label: 'Josei', value: '906' },
{ label: 'LitRPG', value: '1180' },
{ label: 'Martial Arts', value: '907' },
{ label: 'Mature', value: '20' },
{ label: 'Mecha', value: '908' },
{ label: 'Mystery', value: '909' },
{ label: 'Psychological', value: '910' },
{ label: 'Romance', value: '6' },
{ label: 'School Life', value: '911' },
{ label: 'Sci-fi', value: '912' },
{ label: 'Seinen', value: '913' },
{ label: 'Slice of Life', value: '914' },
{ label: 'Smut', value: '915' },
{ label: 'Sports', value: '916' },
{ label: 'Supernatural', value: '5' },
{ label: 'Tragedy', value: '901' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
genre_operator : {
value: 'and',
label: 'Genre And/Or',
options: [
{ label: 'OR', value: 'or' },
{ label: 'AND', value: 'and' },
],
type: FilterTypes.Picker,
},
content_warning: {
value: {
include: [],
exclude: [],
},
label: "Mature Content",
options: [
{ label: 'Gore', value: '48' },
{ label: 'Sexual Content', value: '50' },
{ label: 'Strong Language', value: '49' },
],
type: FilterTypes.ExcludableCheckboxGroup
},
content_warning_operator: {
value: 'and',
label: 'Mature Content And/Or',
options: [
{ label: 'OR', value: 'or' },
{ label: 'AND', value: 'and' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('div.chp_raw').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}?s=${searchTerm}&post_type=fictionposts`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
filters = {
sort: {
label: 'Sort Results By',
value: 'ratings',
options: [
{ label: 'Chapters', value: 'chapters' },
{ label: 'Chapters per week', value: 'frequency' },
{ label: 'Date Added', value: 'dateadded' },
{ label: 'Favorites', value: 'favorites' },
{ label: 'Last Updated', value: 'lastchdate' },
{ label: 'Number of Ratings', value: 'numofrate' },
{ label: 'Pages', value: 'pages' },
{ label: 'Pageviews', value: 'pageviews' },
{ label: 'Ratings', value: 'ratings' },
{ label: 'Readers', value: 'readers' },
{ label: 'Reviews', value: 'reviews' },
{ label: 'Total Words', value: 'totalwords' },
],
type: FilterTypes.Picker,
},
order: {
label: 'Order by',
value: 'desc',
options: [
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: 'Story Status (Translation)',
value: '',
options: [
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Hiatus', value: 'hiatus' },
],
type: FilterTypes.Picker,
},
genres: {
label: 'Genres',
value: {
include: [],
exclude: [],
},
options: [
{ label: 'Action', value: '9' },
{ label: 'Adult', value: '902' },
{ label: 'Adventure', value: '8' },
{ label: 'Boys Love', value: '891' },
{ label: 'Comedy', value: '7' },
{ label: 'Drama', value: '903' },
{ label: 'Ecchi', value: '904' },
{ label: 'Fanfiction', value: '38' },
{ label: 'Fantasy', value: '19' },
{ label: 'Gender Bender', value: '905' },
{ label: 'Girls Love', value: '892' },
{ label: 'Harem', value: '1015' },
{ label: 'Historical', value: '21' },
{ label: 'Horror', value: '22' },
{ label: 'Isekai', value: '37' },
{ label: 'Josei', value: '906' },
{ label: 'LitRPG', value: '1180' },
{ label: 'Martial Arts', value: '907' },
{ label: 'Mature', value: '20' },
{ label: 'Mecha', value: '908' },
{ label: 'Mystery', value: '909' },
{ label: 'Psychological', value: '910' },
{ label: 'Romance', value: '6' },
{ label: 'School Life', value: '911' },
{ label: 'Sci-fi', value: '912' },
{ label: 'Seinen', value: '913' },
{ label: 'Slice of Life', value: '914' },
{ label: 'Smut', value: '915' },
{ label: 'Sports', value: '916' },
{ label: 'Supernatural', value: '5' },
{ label: 'Tragedy', value: '901' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
genre_operator: {
value: 'and',
label: 'Genre And/Or',
options: [
{ label: 'OR', value: 'or' },
{ label: 'AND', value: 'and' },
],
type: FilterTypes.Picker,
},
content_warning: {
value: {
include: [],
exclude: [],
},
label: 'Mature Content',
options: [
{ label: 'Gore', value: '48' },
{ label: 'Sexual Content', value: '50' },
{ label: 'Strong Language', value: '49' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
content_warning_operator: {
value: 'and',
label: 'Mature Content And/Or',
options: [
{ label: 'OR', value: 'or' },
{ label: 'AND', value: 'and' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new ScribbleHubPlugin();
+167 -154
View File
@@ -1,230 +1,247 @@
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { load as parseHTML } from "cheerio";
import { Plugin } from '@typings/plugin';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { load as parseHTML } from 'cheerio';
class WTRLAB implements Plugin.PluginBase {
id = "WTRLAB";
name = "WTR-LAB";
site = "https://wtr-lab.com/";
version = "1.0.0";
icon = "src/en/wtrlab/icon.png";
sourceLang = "en/"
id = 'WTRLAB';
name = 'WTR-LAB';
site = 'https://wtr-lab.com/';
version = '1.0.0';
icon = 'src/en/wtrlab/icon.png';
sourceLang = 'en/';
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + this.sourceLang + "novel-list?";
link += `orderBy=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&filter=${filters.storyStatus.value}`;
link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy.
if(showLatestNovels){
let link = this.site + this.sourceLang + 'novel-list?';
link += `orderBy=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&filter=${filters.storyStatus.value}`;
link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy.
if (showLatestNovels) {
const response = await fetch('https://wtr-lab.com/api/home/recent', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({ page: page })
body: JSON.stringify({ page: page }),
});
const recentNovel: JsonNovel = await response.json();
// Parse novels from JSON
const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({
name: datum.serie.data.title || "",
cover: datum.serie.data.image,
path: this.sourceLang + "serie-" + datum.serie.raw_id + "/" + datum.serie.slug || "",
}));
const novels: Plugin.NovelItem[] = recentNovel.data.map(
(datum: Datum) => ({
name: datum.serie.data.title || '',
cover: datum.serie.data.image,
path:
this.sourceLang +
'serie-' +
datum.serie.raw_id +
'/' +
datum.serie.slug || '',
}),
);
return novels;
}
else {
const body = await fetchApi(link).then((res) => res.text());
} else {
const body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
//console.log(link);
const novels: Plugin.NovelItem[] = loadedCheerio(".serie-item")
const novels: Plugin.NovelItem[] = loadedCheerio('.serie-item')
.map((index, element) => ({
name: loadedCheerio(element).find(".title-wrap > a").text().replace(loadedCheerio(element).find(".rawtitle").text(), "") || "",
cover: loadedCheerio(element).find("img").attr("src"),
path: loadedCheerio(element).find("a").attr("href") || "",
name:
loadedCheerio(element)
.find('.title-wrap > a')
.text()
.replace(loadedCheerio(element).find('.rawtitle').text(), '') ||
'',
cover: loadedCheerio(element).find('img').attr('src'),
path: loadedCheerio(element).find('a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
return novels;
.filter(novel => novel.name && novel.path);
return novels;
}
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
//console.log(this.site + novelPath);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.text-uppercase").text(),
cover: loadedCheerio(".img-wrap > img").attr("src"),
summary: loadedCheerio(".lead").text().trim(),
name: loadedCheerio('h1.text-uppercase').text(),
cover: loadedCheerio('.img-wrap > img').attr('src'),
summary: loadedCheerio('.lead').text().trim(),
};
novel.genres = loadedCheerio('td:contains("Genre")')
.next()
.find('a')
.map((i,el) => loadedCheerio(el).text())
.toArray()
.join(",");
novel.genres = loadedCheerio('td:contains("Genre")')
.next()
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
novel.author = loadedCheerio('td:contains("Author")')
.next()
.text()
.replace(/[\t\n]/g, "");
.replace(/[\t\n]/g, '');
novel.status = loadedCheerio('td:contains("Status")')
.next()
.text()
.replace(/[\t\n]/g, "");
.replace(/[\t\n]/g, '');
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + '';
const jsonData: NovelJson = JSON.parse(chapterJson);
const chapters: Plugin.ChapterItem[] = jsonData.props.pageProps.serie.chapters.map((jsonChapter, chapterIndex) => ({
name: "Chapter " + jsonChapter.slug + " " + jsonChapter.title,
path: this.sourceLang + "serie-" +jsonData.props.pageProps.serie.serie_data.raw_id + "/" + jsonData.props.pageProps.serie.serie_data.slug + "/chapter-" + jsonChapter.slug, // Assuming 'slug' is the intended path
releaseTime: jsonChapter.created_at.substring(0, 10),
chapterNumber: chapterIndex + 1
}));
const chapters: Plugin.ChapterItem[] =
jsonData.props.pageProps.serie.chapters.map(
(jsonChapter, chapterIndex) => ({
name: 'Chapter ' + jsonChapter.slug + ' ' + jsonChapter.title,
path:
this.sourceLang +
'serie-' +
jsonData.props.pageProps.serie.serie_data.raw_id +
'/' +
jsonData.props.pageProps.serie.serie_data.slug +
'/chapter-' +
jsonChapter.slug, // Assuming 'slug' is the intended path
releaseTime: jsonChapter.created_at.substring(0, 10),
chapterNumber: chapterIndex + 1,
}),
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + '';
const jsonData: NovelJson = JSON.parse(chapterJson);
//console.log(body);
const chapterContent = JSON.stringify(jsonData.props.pageProps.serie.chapter_data.data.body);
const chapterContent = JSON.stringify(
jsonData.props.pageProps.serie.chapter_data.data.body,
);
//console.log(chapterContent);
const parsedArray = JSON.parse(chapterContent);
let htmlString = '';
for (const text of parsedArray) {
htmlString += `<p>${text}</p>`;
}
}
return htmlString;
}
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const response = await fetch(this.site + "api/search", {
const response = await fetch(this.site + 'api/search', {
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
Referer: this.site + this.sourceLang,
Origin: this.site,
},
method: "POST",
body: JSON.stringify({ text: searchTerm}),
method: 'POST',
body: JSON.stringify({ text: searchTerm }),
});
const recentNovel: JsonNovel = await response.json();
// Parse novels from JSON
const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({
name: datum.data.title || "",
cover: datum.data.image,
path: this.sourceLang + "serie-" + datum.raw_id + "/" + datum.slug || "",
}));
// Parse novels from JSON
const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({
name: datum.data.title || '',
cover: datum.data.image,
path: this.sourceLang + 'serie-' + datum.raw_id + '/' + datum.slug || '',
}));
return novels;
}
fetchImage = fetchFile;
filters = {
order: {
value: "chapter",
label: "Order by",
options: [
{ label: "View", value: "view" },
{ label: "Name", value: "name" },
{ label: "Addition Date", value: "date" },
{ label: "Reader", value: "reader" },
{ label: "Chapter", value: "chapter" },
],
type: FilterTypes.Picker,
value: 'chapter',
label: 'Order by',
options: [
{ label: 'View', value: 'view' },
{ label: 'Name', value: 'name' },
{ label: 'Addition Date', value: 'date' },
{ label: 'Reader', value: 'reader' },
{ label: 'Chapter', value: 'chapter' },
],
type: FilterTypes.Picker,
},
sort: {
value: "desc",
label: "Sort by",
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
],
type: FilterTypes.Picker,
value: 'desc',
label: 'Sort by',
options: [
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Completed", value: "completed" },
],
type: FilterTypes.Picker,
value: 'all',
label: 'Status',
options: [
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
interface NovelJson {
props: Props;
page: string;
props: Props;
page: string;
}
interface Props {
pageProps: PageProps;
__N_SSP: boolean;
__N_SSP: boolean;
}
interface PageProps {
serie: Serie;
serie: Serie;
server_time: Date;
}
interface Serie {
serie_data: SerieData;
chapters: Chapter[];
serie_data: SerieData;
chapters: Chapter[];
recommendation: SerieData[];
chapter_data: ChapterData;
id: number;
raw_id: number;
slug: string;
data: Data;
chapter_data: ChapterData;
id: number;
raw_id: number;
slug: string;
data: Data;
is_default: boolean;
raw_type: string;
raw_type: string;
}
interface Chapter {
serie_id: number;
id: number;
slug: string;
title: string;
name: string;
serie_id: number;
id: number;
slug: string;
title: string;
name: string;
created_at: string;
}
interface ChapterData {
@@ -236,51 +253,47 @@ interface ChapterContent {
}
interface SerieData {
serie_id?: number;
serie_id?: number;
recommendation_id?: number;
score?: string;
id: number;
slug: string;
search_text: string;
status: number;
data: Data;
created_at: string;
updated_at: string;
view: number;
in_library: number;
rating: number | null;
chapter_count: number;
power: number;
total_rate: number;
user_status: number;
verified: boolean;
from: null;
raw_id: number;
genres?: number[];
score?: string;
id: number;
slug: string;
search_text: string;
status: number;
data: Data;
created_at: string;
updated_at: string;
view: number;
in_library: number;
rating: number | null;
chapter_count: number;
power: number;
total_rate: number;
user_status: number;
verified: boolean;
from: null;
raw_id: number;
genres?: number[];
}
interface Data {
title: string;
author: string;
title: string;
author: string;
description: string;
image: string;
image: string;
}
interface JsonNovel {
success: boolean;
data: Datum[];
data: Datum[];
}
interface Datum {
serie: Serie;
chapters: Chapter[];
serie: Serie;
chapters: Chapter[];
updated_at: Date;
raw_id: number;
slug: string;
data: Data;
raw_id: number;
slug: string;
data: Data;
}
export default new WTRLAB();
+230 -218
View File
@@ -1,283 +1,295 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile, fetchProto } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile, fetchProto } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
interface StringValue {
value: string;
value: string;
}
interface BoolValue {
value: boolean;
value: boolean;
}
interface DecimalValue {
units: bigint;
nanos: number;
units: bigint;
nanos: number;
}
interface Timestamp {
seconds: number;
nanos: number;
seconds: number;
nanos: number;
}
interface NovelEntry {
name: string;
coverUrl: string;
slug: string;
name: string;
coverUrl: string;
slug: string;
}
interface RelatedChapterUserInfo {
isChapterUnlocked?: BoolValue;
isChapterUnlocked?: BoolValue;
}
interface ChapterItem {
entityId: number;
name: string;
slug: string;
number?: DecimalValue;
content?: StringValue;
relatedUserInfo?: RelatedChapterUserInfo;
offset: number;
publishedAt?: Timestamp;
entityId: number;
name: string;
slug: string;
number?: DecimalValue;
content?: StringValue;
relatedUserInfo?: RelatedChapterUserInfo;
offset: number;
publishedAt?: Timestamp;
}
interface GetChapterResponse {
item?: ChapterItem;
item?: ChapterItem;
}
interface ChapterGroupCounts {
total: number;
advance: number;
normal: number;
total: number;
advance: number;
normal: number;
}
interface ChapterGroupItem {
id: number;
title: string;
order: number;
fromChapterNumber?: DecimalValue;
toChapterNumber?: DecimalValue;
chapterList: ChapterItem[];
counts?: ChapterGroupCounts;
id: number;
title: string;
order: number;
fromChapterNumber?: DecimalValue;
toChapterNumber?: DecimalValue;
chapterList: ChapterItem[];
counts?: ChapterGroupCounts;
}
interface GetChapterListResponse {
items: ChapterGroupItem[];
items: ChapterGroupItem[];
}
enum NovelItem_Status {
Finished = 0,
Active = 1,
Hiatus = 2,
All = -1
Finished = 0,
Active = 1,
Hiatus = 2,
All = -1,
}
interface NovelKarmaInfo {
isActive: boolean;
isFree: boolean;
maxFreeChapter?: DecimalValue;
canUnlockWithVip: boolean;
isActive: boolean;
isFree: boolean;
maxFreeChapter?: DecimalValue;
canUnlockWithVip: boolean;
}
interface NovelItem {
id: number;
name: string;
slug: string;
status: NovelItem_Status;
visible: boolean;
description?: StringValue;
synopsis?: StringValue;
coverUrl?: StringValue;
translatorName?: StringValue;
authorName?: StringValue;
karmaInfo?: NovelKarmaInfo;
genres: string[];
id: number;
name: string;
slug: string;
status: NovelItem_Status;
visible: boolean;
description?: StringValue;
synopsis?: StringValue;
coverUrl?: StringValue;
translatorName?: StringValue;
authorName?: StringValue;
karmaInfo?: NovelKarmaInfo;
genres: string[];
}
interface GetNovelResponse {
item?: NovelItem;
item?: NovelItem;
}
class WuxiaWorld implements Plugin.PluginBase {
id = "wuxiaworld";
name = "Wuxia World";
icon = "src/en/wuxiaworld/icon.png";
site = "https://www.wuxiaworld.com/";
filters?: Filters | undefined;
version = "0.5.0";
id = 'wuxiaworld';
name = 'Wuxia World';
icon = 'src/en/wuxiaworld/icon.png';
site = 'https://www.wuxiaworld.com/';
filters?: Filters | undefined;
version = '0.5.0';
parseNovels(data: {items: NovelEntry[]}){
const novels: Plugin.NovelItem[] = [];
parseNovels(data: { items: NovelEntry[] }) {
const novels: Plugin.NovelItem[] = [];
data.items.map((novel: NovelEntry) => {
const name = novel.name;
const cover = novel.coverUrl;
const path = `novel/${novel.slug}/`;
data.items.map((novel: NovelEntry) => {
const name = novel.name;
const cover = novel.coverUrl;
const path = `novel/${novel.slug}/`;
novels.push({
name,
cover,
path,
});
});
novels.push({
name,
cover,
path,
});
});
return novels;
return novels;
}
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const link = `${this.site}api/novels`;
const result = await fetchApi(link);
const data = await result.json();
return this.parseNovels(data);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const data = await fetchProto<GetNovelResponse>(
{
proto: this.proto,
requestType: 'GetNovelRequest',
responseType: 'GetNovelResponse',
requestData: { slug: novelPath.split('/')[1] },
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel',
{
headers: {
'Content-Type': 'application/grpc-web+proto',
},
},
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: data.item?.name || 'Untitled',
cover: data.item?.coverUrl?.value,
summary: parseHTML(
data.item?.description?.value + '\n\n' + data.item?.synopsis?.value,
).text(),
author: data.item?.authorName?.value,
genres: data.item?.genres.join(','),
chapters: [],
};
const status = data.item?.status;
switch (status) {
case NovelItem_Status.Active:
novel.status = NovelStatus.Ongoing;
break;
case NovelItem_Status.Hiatus:
novel.status = NovelStatus.OnHiatus;
break;
case NovelItem_Status.All:
novel.status = NovelStatus.Unknown;
break;
default:
novel.status = NovelStatus.Completed;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const link = `${this.site}api/novels`;
// novel.summary = loadedCheerio('.relative > .absolute:first')
// .children('span')
// .map((i,el) => loadedCheerio(el).text().trim())
// .toArray()
// .join('\n');
const result = await fetchApi(link);
const data = await result.json();
// novel.genres = loadedCheerio("a.MuiLink-underlineNone")
// .map((i,el) => loadedCheerio(el).text())
// .toArray()
// .join(',');
return this.parseNovels(data)
}
// novel.status = loadedCheerio("div.font-set-b10").text();
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const data = await fetchProto<GetNovelResponse>(
{
proto: this.proto,
requestType: 'GetNovelRequest',
responseType: 'GetNovelResponse',
requestData: {slug: novelPath.split('/')[1]},
const list = await fetchProto<GetChapterListResponse>(
{
proto: this.proto,
requestType: 'GetChapterListRequest',
responseType: 'GetChapterListResponse',
requestData: { novelId: data.item?.id },
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList',
{
headers: {
'Content-Type': 'application/grpc-web+proto',
},
},
);
const freeChapter =
Number(data.item?.karmaInfo?.maxFreeChapter?.units) +
(data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50;
const chapter: Plugin.ChapterItem[] = list.items.flatMap(
(ChapterGroupItem: ChapterGroupItem) =>
ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({
page: ChapterGroupItem.title,
name:
chapterItem.name +
(chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false ||
(!chapterItem.relatedUserInfo &&
Number(chapterItem.number?.units) +
(chapterItem.number?.nanos || 0) / 1000000000 >
freeChapter)
? ' 🔒'
: ''),
path: novelPath + chapterItem.slug,
chapterNumber: chapterItem.offset,
releaseTime: new Date(
(chapterItem.publishedAt?.seconds || 0) * 1000 +
(chapterItem.publishedAt?.nanos || 0) / 1000000,
).toISOString(),
})),
);
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const paths = chapterPath.split('/');
const data = await fetchProto<GetChapterResponse>(
{
proto: this.proto,
requestType: 'GetChapterRequest',
responseType: 'GetChapterResponse',
requestData: {
chapterProperty: {
slugs: {
novelSlug: paths[1],
chapterSlug: paths[2],
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
},
},
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter',
{
headers: {
'Content-Type': 'application/grpc-web+proto',
},
},
);
// loadedCheerio(".chapter-nav").remove();
// loadedCheerio("#chapter-content > script").remove();
// const chapterText = loadedCheerio("#chapter-content").html() || '';
const novel: Plugin.SourceNovel = {
path: novelPath,
name: data.item?.name || "Untitled",
cover: data.item?.coverUrl?.value,
summary: parseHTML(data.item?.description?.value + '\n\n' + data.item?.synopsis?.value).text(),
author: data.item?.authorName?.value,
genres: data.item?.genres.join(','),
chapters: [],
};
const chapterText = data.item?.content?.value || '';
return chapterText;
}
const status = data.item?.status;
switch (status){
case NovelItem_Status.Active:
novel.status = NovelStatus.Ongoing;
break;
case NovelItem_Status.Hiatus:
novel.status = NovelStatus.OnHiatus;
break;
case NovelItem_Status.All:
novel.status = NovelStatus.Unknown;
break;
default: novel.status = NovelStatus.Completed;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = 'https://www.wuxiaworld.com/api/novels/search?query=';
// novel.summary = loadedCheerio('.relative > .absolute:first')
// .children('span')
// .map((i,el) => loadedCheerio(el).text().trim())
// .toArray()
// .join('\n');
const url = searchUrl + searchTerm;
// novel.genres = loadedCheerio("a.MuiLink-underlineNone")
// .map((i,el) => loadedCheerio(el).text())
// .toArray()
// .join(',');
const result = await fetchApi(url);
const data = await result.json();
// novel.status = loadedCheerio("div.font-set-b10").text();
return this.parseNovels(data);
}
const list = await fetchProto<GetChapterListResponse>(
{
proto: this.proto,
requestType: 'GetChapterListRequest',
responseType: 'GetChapterListResponse',
requestData: {novelId: data.item?.id},
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
const freeChapter = Number(data.item?.karmaInfo?.maxFreeChapter?.units) +
(data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50;
const chapter: Plugin.ChapterItem[] = list.items.flatMap((ChapterGroupItem: ChapterGroupItem) =>
ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({
page: ChapterGroupItem.title,
name: chapterItem.name + (
(chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false) ||
(!chapterItem.relatedUserInfo &&
(Number(chapterItem.number?.units) +
(chapterItem.number?.nanos || 0) / 1000000000) > freeChapter)
? ' 🔒'
: ''
),
path: novelPath + chapterItem.slug,
chapterNumber: chapterItem.offset,
releaseTime: new Date(
(chapterItem.publishedAt?.seconds || 0) * 1000 +
(chapterItem.publishedAt?.nanos || 0) / 1000000
).toISOString(),
})
));
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const paths = chapterPath.split('/');
const data = await fetchProto<GetChapterResponse>(
{
proto: this.proto,
requestType: 'GetChapterRequest',
responseType: 'GetChapterResponse',
requestData: {
chapterProperty: {
slugs: {
novelSlug: paths[1],
chapterSlug: paths[2],
}
}
},
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
// loadedCheerio(".chapter-nav").remove();
// loadedCheerio("#chapter-content > script").remove();
// const chapterText = loadedCheerio("#chapter-content").html() || '';
const chapterText = data.item?.content?.value || "";
return chapterText
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const searchUrl = "https://www.wuxiaworld.com/api/novels/search?query=";
const url = searchUrl + searchTerm;
const result = await fetchApi(url);
const data = await result.json();
return this.parseNovels(data);
}
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
proto = `
proto = `
syntax = "proto3";
option optimize_for = CODE_SIZE;
package wuxiaworld.api.v2;
@@ -460,7 +472,7 @@ class WuxiaWorld implements Plugin.PluginBase {
service Novels {
rpc GetNovel(GetNovelRequest) returns (GetNovelResponse);
}
`
`;
}
export default new WuxiaWorld();
export default new WuxiaWorld();