diff --git a/icons/src/cn/69shu/icon.png b/icons/src/cn/69shu/icon.png new file mode 100644 index 0000000..18f3c9a Binary files /dev/null and b/icons/src/cn/69shu/icon.png differ diff --git a/icons/src/cn/69xinshu/icon.png b/icons/src/cn/69xinshu/icon.png new file mode 100644 index 0000000..6472b5b Binary files /dev/null and b/icons/src/cn/69xinshu/icon.png differ diff --git a/plugins/chinese/69shu.ts b/plugins/chinese/69shu.ts new file mode 100644 index 0000000..9fbb21a --- /dev/null +++ b/plugins/chinese/69shu.ts @@ -0,0 +1,227 @@ +import { 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 Shu69 implements Plugin.PluginBase { + id = "69shu"; + name = "69书吧"; + icon = "src/cn/69shu/icon.png"; + site = "https://www.69shu.xyz"; + version = "0.1.0"; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions + ): 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" },