d818d1758f
* beta host * Refactor: url -> path (#757) * refactor: url -> path * clean up * fix: json branch * PagePlugin (#769) * paginated novel support * fix: truyenfull * fix: tunovel, remove pageList * parsePage support for webtest (#773) * add parsePage api * add parsePage UI * add totalPages * (FR src) Beta plugin & Added filters (#775) * noveldeglace: - added filters - added latest - defaultCover fallback - change id to noveldeglace.com - added complementary infos in summary - added cover artist - chapters are now parts - show volume number for multi volume novels - better search - fix search page number - refactored for plugin beta - releaseDate as Date.toString() - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * chireads: - fix different parsing for mobile version - added filters - added author - added status - added released date - refactored for plugin beta - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * wpmangastream => lightnovelwp rework & new sources (#776) * Added sources: - Knoxt (EN) - Baca Light Novel (ID) - AllNovelRead (ES) - DaoTranslate (EN) - جنة الروايات (AR) - Universal Novel (EN) - NovelTR (TR) old sources auto-generated: - GenesisTls (EN) - NobleMTL (EN) wpmangastream => lightnovelwp: - added doc - added filters in a separte file - semi-automatic filter generator - made it compatible with version 1.0.X and 1.1.X versions of the theme - loadedCheerio => $ - icons in "/icons/lightnovel/id.png" - code cleanup * removed auto-implemented sources * Added README.md to lightnovelwp * removed ai generated text for kolnovel * added novelsknight * added centralNovel * pandamtl accual name * adding folder plugin/arabic * Fix: Vietnamese plugins (#777) * fix: lightnovelvn * add nettruyen * add truyenchu * fix: truyenconect * Fix: Migrate plugins (#771) * Add novel.ovh (#756) * fix * test * fix * date fix + master merge (#761) * date fix * Added Brazilian Portuguese E-Novel (#758) * fixed single digit dates (#767) Co-authored-by: NaddH1 <naderhorani@hotmail.com> * fix * test * fix order * Update renovels.ts * require_chapters * Delete plugins/english/genesistl.broken.ts * Update novelTL.ts --------- Co-authored-by: NaddH1 <naderhorani@hotmail.com> * (src update) ENG (#772) * allnovelfull * bestlightnovel * divinedaolibrary * earlynovel * lightnovelpub * L * M * novelbin * novelfull * novelbuddy * N S * novel updates * async sleep * rename file * pagePlugin * Feat: minify js dir (#781) * Multisrc: HotNovelPub (#784) * HotNovelPub * fix search * Squashed commit of the following: commit 3b759c68eddc06c0adfb959f775de069a85adca7 Author: nyagami <hoangquan05112002@gmail.com> Date: Sat Feb 17 20:33:47 2024 +0700 Feat: minify js dir (#781) * jsmin changed to terser * test * why I did it. * test * Update ranobelib.ts * fix page * Update ranobelib.ts * updated modules * Update terser.json Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Update scripts/multisrc/hotnovelpub/template.ts Co-authored-by: nyagami <hoangquan05112002@gmail.com> * terser api --------- Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Added sources & fix sources (#787) * lightnovelwp: - fix author name - added more localisation of strings - README typo - error on !r.ok new message * noveldeglace: - fix chapterNumber for parts - better dates - better search * Added sources: - White Moonlight Novels (EN) - System Translation (EN) * Resized centralnovel.png * Add Faqwiki.us source (#792) * Add Faqwiki.us source (beta branch) * Fix Path, Fix Chapter Parsing, Fix Novel Title * search novel path fix * fix chapter parsing. some chapter in this site store their whole text in 1-4 <p>, * Add WTR-LAB Source (#797) * Add WTR-LAB Source (Beta Branch) * Add other language from WTR-LAB source * change lang to sourceLang * remove id es tr lang from WTR-LAB * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami <hoangquan05112002@gmail.com> --------- Co-authored-by: nyagami <hoangquan05112002@gmail.com> * revert: scripts to master --------- Co-authored-by: Justin COLLON <48163201+error7404@users.noreply.github.com> Co-authored-by: Rider21 <58046032+Rider21@users.noreply.github.com> Co-authored-by: NaddH1 <naderhorani@hotmail.com> Co-authored-by: K1ngfish3r <trulyihu@gmail.com> Co-authored-by: bregassa <bregas.sat1@gmail.com>
285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
import { load, load as loadCheerio } from "cheerio";
|
||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||
import { Plugin } from "@typings/plugin";
|
||
import { defaultCover } from "@libs/defaultCover";
|
||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||
import { NovelItem } from "../../test_web/static/js";
|
||
// const novelStatus = require('@libs/novelStatus');
|
||
// const isUrlAbsolute = require('@libs/isAbsoluteUrl');
|
||
// const parseDate = require('@libs/parseDate');
|
||
|
||
class Syosetu implements Plugin.PluginBase {
|
||
id = "yomou.syosetu";
|
||
name = "Syosetu";
|
||
icon = "src/jp/syosetu/icon.png";
|
||
site = "https://yomou.syosetu.com/";
|
||
version = "1.1.0";
|
||
headers = {
|
||
"User-Agent":
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||
};
|
||
searchUrl = (pagenum?: number, order?: string) => {
|
||
return `https://yomou.syosetu.com/search.php?order=${order || "hyoka"}${
|
||
pagenum !== undefined
|
||
? `&p=${pagenum <= 1 || pagenum > 100 ? "1" : pagenum}` // check if pagenum is between 1 and 100
|
||
: "" // if isn't don't set ?p
|
||
}`;
|
||
};
|
||
async popularNovels(
|
||
pageNo: number,
|
||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||
): Promise<Plugin.NovelItem[]> {
|
||
const getNovelsFromPage = async (pagenumber: number) => {
|
||
// load page
|
||
let url = this.site;
|
||
|
||
if (!filters.genre.value) {
|
||
url += `rank/list/type/${filters.ranking.value}_${filters.modifier.value}/?p=${pagenumber}`;
|
||
} else {
|
||
url += `rank/${
|
||
filters.genre.value.length === 1
|
||
? "isekailist"
|
||
: "genrelist"
|
||
}/type/${filters.ranking.value}_${filters.genre.value}${
|
||
filters.modifier.value === "total"
|
||
? ""
|
||
: `_${filters.modifier.value}`
|
||
}/?p=${pagenumber}`;
|
||
}
|
||
const html = await (await fetchApi(url)).text();
|
||
|
||
const loadedCheerio = loadCheerio(html, {
|
||
decodeEntities: false,
|
||
});
|
||
|
||
if (
|
||
parseInt(loadedCheerio(".is-current").html() || "1") !==
|
||
pagenumber
|
||
)
|
||
return [];
|
||
|
||
const novels: NovelItem[] = [];
|
||
loadedCheerio(".c-card").each((_, e) => {
|
||
const anchor = loadedCheerio(e).find(
|
||
".p-ranklist-item__title a"
|
||
);
|
||
const url = anchor.attr("href");
|
||
if (!url) return;
|
||
const name = anchor.text();
|
||
const novel: NovelItem = {
|
||
url,
|
||
name,
|
||
cover: defaultCover,
|
||
};
|
||
novels.push(novel);
|
||
});
|
||
return novels;
|
||
};
|
||
const novels = await getNovelsFromPage(pageNo);
|
||
return novels;
|
||
}
|
||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||
let chapters: Plugin.ChapterItem[] = [];
|
||
const result = await fetch(novelUrl, { headers: this.headers });
|
||
const body = await result.text();
|
||
const loadedCheerio = loadCheerio(body, { decodeEntities: false });
|
||
|
||
// create novel object
|
||
let novel: Plugin.SourceNovel = {
|
||
url: novelUrl,
|
||
name: loadedCheerio(".novel_title").text(),
|
||
author: loadedCheerio(".novel_writername")
|
||
.text()
|
||
.replace("作者:", ""),
|
||
cover: defaultCover,
|
||
};
|
||
|
||
// Get all the chapters
|
||
const cqGetChapters = loadedCheerio(".novel_sublist2");
|
||
if (cqGetChapters.length !== 0) {
|
||
// has more than 1 chapter
|
||
novel.summary = loadedCheerio("#novel_ex")
|
||
.text()
|
||
.replace(/<\s*br.*?>/g, "\n");
|
||
cqGetChapters.each(function (i, e) {
|
||
const chapterA = loadedCheerio(this).find("a");
|
||
const [chapterName, releaseDate, chapterUrl] = [
|
||
// set the variables
|
||
chapterA.text(),
|
||
loadedCheerio(this)
|
||
.find("dt") // get title
|
||
.text() // get text
|
||
.replace(/(.)/g, "") // remove "(edited)" mark
|
||
.trim(), // trim spaces
|
||
"https://ncode.syosetu.com" + chapterA.attr("href"),
|
||
];
|
||
chapters.push({
|
||
name: chapterName,
|
||
releaseTime: releaseDate,
|
||
url: chapterUrl,
|
||
});
|
||
});
|
||
} else {
|
||
/**
|
||
* Because there are oneshots on the site, they have to be treated with special care
|
||
* that's what pisses me off in Shosetsu app. They have this extension,
|
||
* but every oneshot is set as "there are no chapters" and all contents are thrown into the description!!
|
||
*/
|
||
// get summary for oneshot chapter
|
||
|
||
const nameResult = await fetch(
|
||
this.searchUrl() + `&word=${novel.name}`,
|
||
{ headers: this.headers }
|
||
);
|
||
const nameBody = await nameResult.text();
|
||
const summaryQuery = loadCheerio(nameBody, {
|
||
decodeEntities: false,
|
||
});
|
||
const foundText = summaryQuery(".searchkekka_box")
|
||
.first()
|
||
.find(".ex")
|
||
.text()
|
||
.replace(/\s{2,}/g, "\n");
|
||
novel.summary = foundText;
|
||
|
||
// add single chapter
|
||
chapters.push({
|
||
name: "Oneshot",
|
||
releaseTime: loadedCheerio("head")
|
||
.find("meta[name='WWWC']")
|
||
.attr("content"), // get date from metadata
|
||
url: novelUrl, // set chapterUrl to oneshot so that chapterScraper knows it's a one-shot,
|
||
});
|
||
}
|
||
novel.chapters = chapters;
|
||
return novel;
|
||
}
|
||
async parseChapter(chapterUrl: string): Promise<string> {
|
||
const result = await fetch(chapterUrl, { headers: this.headers });
|
||
const body = await result.text();
|
||
|
||
// create cheerioQuery
|
||
const cheerioQuery = loadCheerio(body, {
|
||
decodeEntities: false,
|
||
});
|
||
|
||
let chapterText =
|
||
cheerioQuery("#novel_honbun") // get chapter text
|
||
.html() || "";
|
||
return chapterText;
|
||
}
|
||
async searchNovels(
|
||
searchTerm: string,
|
||
pageNo: number
|
||
): Promise<Plugin.NovelItem[]> {
|
||
let novels = [];
|
||
|
||
// returns list of novels from given page
|
||
const getNovelsFromPage = async (pagenumber: number) => {
|
||
// load page
|
||
const url = this.searchUrl(pagenumber) + `&word=${searchTerm}`;
|
||
console.log(url);
|
||
const result = await fetch(url, { headers: this.headers });
|
||
const body = await result.text();
|
||
// Cheerio it!
|
||
const cheerioQuery = loadCheerio(body, { decodeEntities: false });
|
||
|
||
let pageNovels: Plugin.NovelItem[] = [];
|
||
// find class=searchkekka_box
|
||
cheerioQuery(".searchkekka_box").each(function (i, e) {
|
||
// get div with link and name
|
||
const novelDIV = cheerioQuery(this).find(".novel_h");
|
||
// get link element
|
||
const novelA = novelDIV.children()[0];
|
||
// add new novel to array
|
||
pageNovels.push({
|
||
name: novelDIV.text(), // get the name
|
||
url: novelA.attribs.href, // get last part of the link
|
||
cover: defaultCover,
|
||
});
|
||
});
|
||
// return all novels from this page
|
||
return pageNovels;
|
||
};
|
||
|
||
// counter of loaded pages
|
||
// let pagesLoaded = 0;
|
||
// do {
|
||
// // always load first one
|
||
// novels.push(...(await getNovelsFromPage(pagesLoaded + 1)));
|
||
// pagesLoaded++;
|
||
// } while (pagesLoaded < maxPageLoad && isNext); // check if we should load more
|
||
|
||
novels = await getNovelsFromPage(pageNo);
|
||
|
||
/** Use
|
||
* novels.push(...(await getNovelsFromPage(pageNumber)))
|
||
* if you want to load more
|
||
*/
|
||
|
||
// respond with novels!
|
||
return novels;
|
||
}
|
||
async fetchImage(url: string): Promise<string | undefined> {
|
||
return fetchFile(url);
|
||
}
|
||
filters = {
|
||
ranking: {
|
||
type: FilterTypes.Picker,
|
||
label: "Ranked by",
|
||
options: [
|
||
{ label: "日間", value: "daily" },
|
||
{ label: "週間", value: "weekly" },
|
||
{ label: "月間", value: "monthly" },
|
||
{ label: "四半期", value: "quarter" },
|
||
{ label: "年間", value: "yearly" },
|
||
{ label: "累計", value: "total" },
|
||
],
|
||
value: "total",
|
||
},
|
||
genre: {
|
||
type: FilterTypes.Picker,
|
||
label: "Ranking Genre",
|
||
options: [
|
||
{ label: "総ジャンル", value: "" },
|
||
{ label: "異世界転生/転移〔恋愛〕〕", value: "1" },
|
||
{ label: "異世界転生/転移〔ファンタジー〕", value: "2" },
|
||
{ label: "異世界転生/転移〔文芸・SF・その他〕", value: "o" },
|
||
{ label: "異世界〔恋愛〕", value: "101" },
|
||
{ label: "現実世界〔恋愛〕", value: "102" },
|
||
{ label: "ハイファンタジー〔ファンタジー〕", value: "201" },
|
||
{ label: "ローファンタジー〔ファンタジー〕", value: "202" },
|
||
{ label: "純文学〔文芸〕", value: "301" },
|
||
{ label: "ヒューマンドラマ〔文芸〕", value: "302" },
|
||
{ label: "歴史〔文芸〕", value: "303" },
|
||
{ label: "推理〔文芸〕", value: "304" },
|
||
{ label: "ホラー〔文芸〕", value: "305" },
|
||
{ label: "アクション〔文芸〕", value: "306" },
|
||
{ label: "コメディー〔文芸〕", value: "307" },
|
||
{ label: "VRゲーム〔SF〕", value: "401" },
|
||
{ label: "宇宙〔SF〕", value: "402" },
|
||
{ label: "空想科学〔SF〕", value: "403" },
|
||
{ label: "パニック〔SF〕", value: "404" },
|
||
{ label: "童話〔その他〕", value: "9901" },
|
||
{ label: "詩〔その他〕", value: "9902" },
|
||
{ label: "エッセイ〔その他〕", value: "9903" },
|
||
{ label: "その他〔その他〕", value: "9999" },
|
||
],
|
||
value: "",
|
||
},
|
||
modifier: {
|
||
type: FilterTypes.Picker,
|
||
label: "Modifier",
|
||
options: [
|
||
{ label: "すべて", value: "total" },
|
||
{ label: "連載中", value: "r" },
|
||
{ label: "完結済", value: "er" },
|
||
{ label: "短編", value: "t" },
|
||
],
|
||
value: "total",
|
||
},
|
||
} satisfies Filters;
|
||
}
|
||
|
||
export default new Syosetu();
|