From 3a1e0c43eb1b36bcfdd2936c9f714dca06c8b6d3 Mon Sep 17 00:00:00 2001 From: uy/sun Date: Fri, 26 Jan 2024 19:46:47 +0800 Subject: [PATCH] Add 69shu and 69xinshu (#726) * fix(linovelib): use NovelStatus and translate filter label * feat(69xinshu): add 69xinshu * feat(69shu): add 69shu * remove: empty userAgent --- icons/src/cn/69shu/icon.png | Bin 0 -> 843 bytes icons/src/cn/69xinshu/icon.png | Bin 0 -> 852 bytes plugins/chinese/69shu.ts | 227 +++++++++++++++++++++++++++++++++ plugins/chinese/69xinshu.ts | 212 ++++++++++++++++++++++++++++++ plugins/chinese/linovelib.ts | 18 +-- 5 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 icons/src/cn/69shu/icon.png create mode 100644 icons/src/cn/69xinshu/icon.png create mode 100644 plugins/chinese/69shu.ts create mode 100644 plugins/chinese/69xinshu.ts diff --git a/icons/src/cn/69shu/icon.png b/icons/src/cn/69shu/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..18f3c9ad736de28dbfe474976ba20868dc682815 GIT binary patch literal 843 zcmV-R1GM~!P)Px&1xZ9fR5(v{(|JhKaTv$(_ivj!)GVgaTsh4anys)XrNHQd%+l-+>7p9ZDV0)G z&MjwCGco^)YTb8+7)s5qKZ-8wQqoE+O05I=5Ig~AHh{yE-@im=yn%^0RDXjnw|u3 zs(K@-7d-?LfCLc>ur~e)=#sQtE6-%OB2tVq}3*^0*!);e+WJdjVN=;_vZ1&d9_OJ(`%+>v{X4nd;)BWSlSB2W<8N zqt;H~#UVX1w<;X>l*>rN#{CZ0Wv0_vaS&Z%F!k|;ycWAidYZ-FN{6GQg~>u_AZP6+ zmi-8ufHcvDpHdBOJG9zpD7a6@rL$-!!LvH}7@b5+_BNEY8wh>bNaP_(g=vvAt(wTl z7M;aoHt!s0srLr2R?MOO0jQ(k-Fs-(%wo7HkMJ-J*8TfDW1EYFV~=>{4NL^)W@Q_* z3JdZBz1#9;+gjSok1$vb-@Bku0+Nkp!p0;MtBYb!aY<*sCASc`;TvL5w$b>xV3$SS zWZXns=>eoc&?kmkkwDi}4{<36iJh?qnV*;(v)P$vu?z*ee1O`JYk6ZyOO!vI5r;<( z{z|yxgpPQDRGo*YrB%38NytQC+L_}l$j=)IeDVRRE!zz(C8l&)<$M$`+&3)#*DJm%pE+wUQSI_8B>SeV_Kwlpab9N z>s%X7%{&weuodN?H{~q?%K8EA5H3|^#U_7ofiidy+6X`V+HQgi5Cd*t*i3#(H3+>T zMDJ@Dy}hogxr6!rK!}2KtK*n2kpfca5ksFpAOj@cT?UZ@a1PuBHynGy)h`Yf{{sFx V4ZxHd{Vf0h002ovPDHLkV1fY;i8}xQ literal 0 HcmV?d00001 diff --git a/icons/src/cn/69xinshu/icon.png b/icons/src/cn/69xinshu/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6472b5bc788a6a2706c4a5d2530b0e49d42f0e71 GIT binary patch literal 852 zcmV-a1FQUrP)Px&4oO5oR5(vvlS^z=RUF2D=iJxKKx>t0B5G{fricXwgVZLHNNw;DUo9rCG;z}e z7)adVMxzVEMq@~bQ3_NxCN4@)u!=EY1WQUt>LU@52^Oe4LY%&s&g0HK=Wp&@f{8hc zbI#)XoiFGAeM&^+>VjqDw>m#f8y0GU2v|sV0g2b0^HNW-vCSdB&ZnbAqpr*w*>yVP z>;zq6iYF-p-1r_B)&&ha8=?ajIG^q_ez(G zdisk+nX4t(TvpTH?qZE$c(TI04R8_qA9i@I!$oWO@p8mhg@~F}H2Vs*Zlomf%WJdn zcCTjY#tgq+D|5JDDf!EIp({gvEsXtsle!U}?QZ4ZImO{iptV9hcAzAP<=rPervJXq zrxP<69pJbwj&}I?<>j2YROG-fi&!*+kED2UO_sNQs8Dy)sNDXNq_p9~K97-~uX3&^ zI9fAP6&x|_8MvRZ3yX|is3B;cY4;g=qJ^Qai&RtwwI&8A!|qK!)N5pXjZ%v7D^>QN zDe~H?G%u~rvitNrp@D6^_mJ}7jnRK7TNkx@Ur7RO7=Av5t(Mq(>N<|2*uL>0#x7R* zaWx7Tt_T>&Wyv48MQAL` zyofjhD_YZR-Ox(@+l9ozqror8KYSGW2z1*zrw~Q29g_w1WKl zrF7n>*?FQ2+C{D0ds`x78E6Z6GnZ!UVi~Ozo4Yf7HhzoIg3y$5d9`~9k3SI5)gruq ze3pr#kLzeu&*vqXtqPv4vbn8Bf4kpEvJUoyJH9R|crRW{tdavc|+UlY~45 zg%zY;O~gvw-Q)2#AWGBwpvSKMC8)U0Q*)M2CPR*2PU_q!a161rvqTV!V^~uV_a$IQ ePnynq9sUL9@N^<9PC%Rh0000 + ): Promise { + let url: string; + if (filters.sort.value === 'none') { + url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`; + } else { + url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`; + } + + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.book-coverlist').each((i, el) => { + let url = loadedCheerio(el).find('a.cover').attr('href'); + + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + const novelUrl = this.site + url; + + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url, + chapters: [], + }; + + novel.name = loadedCheerio('h1').text().trim(); + + novel.cover = loadedCheerio('div.cover > img').attr('src'); + + novel.summary = loadedCheerio('#bookIntro').text().trim(); + + const bookInfo = loadedCheerio('div.caption-bookinfo > p') + + novel.author = bookInfo.find("a").attr('title'); + + novel.artist = undefined; + + novel.status = bookInfo.text().includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed; + + novel.genres = ''; + + // Table of Content is on a different page than the summary page + let chapters: Plugin.ChapterItem[] = []; + + const allUrl = loadedCheerio('dd.all > a').attr('href') + if (allUrl) { + const chaptersUrl = this.site + allUrl; + const chaptersBody = await fetchText(chaptersUrl); + + const chaptersLoadedCheerio = parseHTML(chaptersBody); + + chaptersLoadedCheerio('dd').each((i, el) => { + const chapterUrl = this.site + chaptersLoadedCheerio(el).find('a').attr('href'); + const chapterName = chaptersLoadedCheerio(el).find('a').text().trim(); + + chapters.push({ + name: chapterName, + url: chapterUrl, + }); + }); + } else { + loadedCheerio('div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd').each((i, el) => { + const chapterUrl = this.site + loadedCheerio(el).find('a').attr('href'); + const chapterName = loadedCheerio(el).find('a').text().trim(); + + chapters.push({ + name: chapterName, + url: chapterUrl, + }); + }); + } + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; + const body = await fetchText(url); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chaptercontent').html() || ''; + + return chapterText; + }; + + async searchNovels( + searchTerm: string, + pageNo?: number | undefined + ): Promise { + const searchUrl = `${this.site}/search`; + const formData = new FormData(); + formData.append('searchkey', searchTerm); + + const body = await fetchText(searchUrl, { + method: 'post', + body: formData, + }); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + + let loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.book-coverlist').each((i, el) => { + let url = loadedCheerio(el).find('a.cover').attr('href'); + + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + const novelUrl = this.site + url; + + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + fetchImage = fetchFile; + + filters = { + rank: { + label: "排行榜", + value: "allvisit", + options: [ + { label: "总排行榜", value: "allvisit" }, + + { label: "月排行榜", value: "monthvisit" }, + + { label: "周排行榜", value: "weekvisit" }, + + { label: "日排行榜", value: "dayvisit" }, + + { label: "收藏榜", value: "goodnum" }, + + { label: "字数榜", value: "words" }, + + { label: "推荐榜", value: "allvote" }, + + { label: "新书榜", value: "postdate" }, + + { label: "更新榜", value: "lastupdate" }, + ], + type: FilterTypes.Picker, + }, + sort: { + label: "分类", + value: "none", + options: [ + { label: "无", value: "none" }, + + { label: "全部", value: "all" }, + + { label: "玄幻", value: "xuanhuan" }, + + { label: "仙侠", value: "xianxia" }, + + { label: "都市", value: "dushi" }, + + { label: "历史", value: "lishi" }, + + { label: "游戏", value: "youxi" }, + + { label: "科幻", value: "kehuan" }, + + { label: "灵异", value: "kongbu" }, + + { label: "言情", value: "nvsheng" }, + + { label: "其它", value: "qita" }, + ], + type: FilterTypes.Picker, + } + } satisfies Filters; +} + +export default new Shu69(); diff --git a/plugins/chinese/69xinshu.ts b/plugins/chinese/69xinshu.ts new file mode 100644 index 0000000..14a0179 --- /dev/null +++ b/plugins/chinese/69xinshu.ts @@ -0,0 +1,212 @@ +import { load as parseHTML } from "cheerio"; +import { fetchText, fetchFile } from "@libs/fetch"; +import { FilterTypes, Filters } from "@libs/filterInputs"; +import { Plugin } from "@typings/plugin"; +import { encode } from "urlencode"; +import { NovelStatus } from "@libs/novelStatus"; + +class XinShu69 implements Plugin.PluginBase { + id = "69xinshu"; + name = "69书吧"; + icon = "src/cn/69xinshu/icon.png"; + site = "https://www.69xinshu.com"; + version = "0.1.0"; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let url: string; + if (pageNo === 1) { + url = `${this.site}/novels/class/${filters.class.value}.htm`; + } else { + url = `${this.site}/ajax_novels/class/${filters.class.value}/${pageNo}.htm`; + } + + const body = await fetchText(url, {}, 'gbk'); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + const novelsList = pageNo === 1 ? loadedCheerio('div.newbox > ul > li') : loadedCheerio('li'); + novelsList.each(function () { + const novelUrl = loadedCheerio(this).find('li > div > h3 > a:nth-child(2)').attr('href'); + + if (novelUrl) { + const novelName = loadedCheerio(this).find('li > div > h3 > a:nth-child(2)').text(); + const novelCover = loadedCheerio(this) + .find('li > a > img') + .attr('data-src'); + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + } + }); + + return novels; + } + + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + + const body = await fetchText(url, {}, 'gbk'); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url, + chapters: [], + }; + + novel.name = loadedCheerio('h1 > a').text(); + + novel.cover = loadedCheerio('div.bookimg2 > img').attr( + 'src', + ); + + novel.summary = loadedCheerio('div.navtxt').text().trim(); + + const bookInfo = loadedCheerio('div.booknav2').text() + + const authorMatch = bookInfo.match(/作者:(.*)/); + if (authorMatch) { + novel.author = authorMatch[1]; + } + + novel.artist = undefined; + + novel.status = bookInfo.includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed; + + const genresMatch = bookInfo.match(/分类:(.*)/); + if (genresMatch) { + novel.genres = genresMatch[1]; + } + + // Table of Content is on a different page than the summary page + let chapters: Plugin.ChapterItem[] = []; + + const chaptersUrl = loadedCheerio('a.more-btn').attr('href'); + if (chaptersUrl) { + const chaptersBody = await fetchText(chaptersUrl, {}, 'gbk'); + + const chaptersLoadedCheerio = parseHTML(chaptersBody); + + chaptersLoadedCheerio('li').each(function () { + const chapterUrl = chaptersLoadedCheerio(this).find('a').attr('href'); + + if (chapterUrl?.startsWith("https://")) { + const chapterName = chaptersLoadedCheerio(this).find('a').text().trim(); + + chapters.push({ + name: chapterName, + url: chapterUrl, + }); + } + }); + } + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; + const body = await fetchText(url, {}, 'gbk'); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.txtnav').prop('innerText') ?? ''; + // remove empty lines + let lines = chapterText.split('\n').map((line: string) => line.trim()).filter((line: string) => line.length > 0); + // remove the first two lines which are the chapter name and author name + lines = lines.slice(2); + + let parsedText = ''; + for (let line of lines) { + parsedText += `

${line}

`; + } + + return parsedText; + }; + + async searchNovels( + searchTerm: string, + pageNo?: number | undefined + ): Promise { + const searchUrl = `${this.site}/modules/article/search.php`; + const formData = new FormData(); + formData.append('searchkey', encode(searchTerm, 'gbk')); + formData.append('searchtype', 'all'); + + const body = await fetchText(searchUrl, { method: 'post', body: formData, }, 'gbk'); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.newbox > ul > li').each(function () { + const novelUrl = loadedCheerio(this).find('a').attr('href')!; + const novelName = loadedCheerio(this).find('div.newnav > h3').text(); + const novelCover = loadedCheerio(this).find('img').attr('data-src'); + + const novel = { + name: novelName, + url: novelUrl, + cover: novelCover, + }; + + novels.push(novel); + }); + + return novels; + } + + fetchImage = fetchFile; + + filters = { + class: { + label: "分类", + value: "0", + options: [ + { label: "全部分类", value: "0" }, + + { label: "言情小说", value: "3" }, + + { label: "玄幻魔法", value: "1" }, + + { label: "修真武侠", value: "2" }, + + { label: "穿越时空", value: "11" }, + + { label: "都市小说", value: "9" }, + + { label: "历史军事", value: "4" }, + + { label: "游戏竞技", value: "5" }, + + { label: "科幻空间", value: "6" }, + + { label: "悬疑惊悚", value: "7" }, + + { label: "同人小说", value: "8" }, + + { label: "官场职场", value: "10" }, + + { label: "青春校园", value: "12" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new XinShu69(); diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index c463941..afac987 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -2,25 +2,21 @@ import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchText, fetchFile } from "@libs/fetch"; import { FilterTypes, Filters } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; +import { NovelStatus } from "@libs/novelStatus"; class Linovelib implements Plugin.PluginBase { id = "linovelib"; name = "Linovelib"; icon = "src/cn/linovelib/icon.png"; site = "https://www.bilinovel.com"; - version = "1.0.1"; + version = "1.0.2"; async popularNovels( pageNo: number, { filters }: Plugin.PopularNovelsOptions ): Promise { - let link = `${this.site}/top/`; - - link += filters.sort.value; - - link += `/${pageNo}.html`; - - const body = await fetchText(link); + const url = `${this.site}/top/${filters.rank.value}/${pageNo}.html`; + const body = await fetchText(url); const loadedCheerio = parseHTML(body); @@ -73,7 +69,7 @@ class Linovelib implements Plugin.PluginBase { ).text(); const meta = loadedCheerio("#bookDetailWrapper .book-meta").text() - novel.status = meta.includes("完结") ? "完结" : "连载"; + novel.status = meta.includes("完结") ? NovelStatus.Completed : NovelStatus.Ongoing; novel.genres = loadedCheerio(".tag-small.red") .children("a") @@ -528,8 +524,8 @@ class Linovelib implements Plugin.PluginBase { } filters = { - sort: { - label: "Sort By", + rank: { + label: "排行榜", value: "monthvisit", options: [ { label: "月点击榜", value: "monthvisit" },