From ee5bc52b6c10fa149fef05523b2f186fea45d49d Mon Sep 17 00:00:00 2001 From: K1ngfish3r <26593485+K1ngfish3r@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:24:08 +0000 Subject: [PATCH] add: novelhi (no filter) --- plugins/english/novelhi.ts | 200 ++++++++++++++++++++++++++ public/static/src/en/novelhi/icon.png | Bin 0 -> 3108 bytes 2 files changed, 200 insertions(+) create mode 100644 plugins/english/novelhi.ts create mode 100644 public/static/src/en/novelhi/icon.png diff --git a/plugins/english/novelhi.ts b/plugins/english/novelhi.ts new file mode 100644 index 000000000..9bf9b7aff --- /dev/null +++ b/plugins/english/novelhi.ts @@ -0,0 +1,200 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; + +class NovelHi implements Plugin.PluginBase { + id = 'novelhi'; + name = 'NovelHi'; + icon = 'src/en/novelhi/icon.png'; + site = 'https://novelhi.com/'; + version = '1.0.0'; + + // flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + // Cache for storing extended metadata from the list API | ie: copypasta from readfrom.ts + loadedNovelCache: CachedNovel[] = []; + + parseNovels(novels: NovelData[]): CachedNovel[] { + const ret: CachedNovel[] = novels.map(item => ({ + name: item.bookName, + path: `s/${item.simpleName}`, + cover: item.picUrl || defaultCover, + summary: item.bookDesc, + author: item.authorName, + status: item.bookStatus, + genres: item.genres.map(g => g.genreName).join(', '), + })); + + // Manage cache size + this.loadedNovelCache.push(...ret); + if (this.loadedNovelCache.length > 100) { + this.loadedNovelCache = this.loadedNovelCache.slice(-100); + } + + return ret; + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const params = new URLSearchParams(); + + params.append('curr', `${pageNo}`); + params.append('limit', '10'); + params.append('keyword', ''); + + const jsonUrl = `${this.site}book/searchByPageInShelf?` + params.toString(); + const response = await fetchApi(jsonUrl); + const json: ApiResponse = await response.json(); + + return this.parseNovels(json.data.list); + } + + async parseNovel(novelPath: string): Promise { + const data = await fetchApi(this.site + novelPath); + const text = await data.text(); + const loadedCheerio = parseHTML(text); + + const translate = loadedCheerio('#translate <').html(); + if (translate) { + console.error('This Novel has been removed and is no longer available'); + throw Error('This Novel has been removed and is no longer available'); + } + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('meta[name=keywords]').attr('content') || 'Untitled', + cover: loadedCheerio('.cover,.decorate-img').attr('src') || defaultCover, + }; + + let moreNovelInfo = this.loadedNovelCache.find(n => n.path === novelPath); + + if (!moreNovelInfo) { + moreNovelInfo = (await this.searchNovels(novel.name, 1)).find( + novel => novel.path === novelPath, + ); + } + if (moreNovelInfo) { + novel.genres = moreNovelInfo.genres; + novel.author = moreNovelInfo.author; + novel.status = + moreNovelInfo.status === '1' + ? NovelStatus.Completed + : NovelStatus.Ongoing; + const summary = moreNovelInfo.summary.replace(//gi, '\n'); + novel.summary = parseHTML(summary).text().trim(); + } + + const chapters: Plugin.ChapterItem[] = []; + const bookId = loadedCheerio('#bookId').attr('value'); + if (bookId && !translate) { + const params = new URLSearchParams(); + params.append('bookId', bookId); + params.append('curr', '1'); + params.append('limit', '42121'); + + const url = `${this.site}book/queryIndexList?` + params.toString(); + const res = await fetchApi(url); + const resJson: ApiChapter = await res.json(); + + resJson?.data?.list?.forEach(chapter => + chapters.push({ + name: chapter.indexName, + path: novelPath + '/' + chapter.indexNum, + releaseTime: chapter.createTime, + }), + ); + } + + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const url = this.site + chapterPath; + const result = await fetchApi(url).then(res => res.text()); + + const loadedCheerio = parseHTML(result); + loadedCheerio('#showReading script,ins').remove(); + const chapterText = loadedCheerio('#showReading').html(); + if (!chapterText) { + return loadedCheerio('#translate <').html() || ''; + } + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const params = new URLSearchParams(); + + params.append('curr', `${pageNo}`); + params.append('limit', '10'); + params.append('keyword', `${searchTerm}`); + + const jsonUrl = `${this.site}book/searchByPageInShelf?` + params.toString(); + const response = await fetchApi(jsonUrl); + const json: ApiResponse = await response.json(); + + return this.parseNovels(json.data.list); + } +} + +export default new NovelHi(); + +type CachedNovel = Plugin.NovelItem & { + summary: string; + genres: string; + author: string; + status: string; +}; + +type NovelData = { + id: string; + bookName: string; + picUrl: string; + simpleName: string; + authorName: string; + bookDesc: string; + bookStatus: string; + lastIndexName: string; + genres: { + genreId: string; + genreName: string; + }[]; +}; + +type ChapterData = { + id: string; + bookId: string; + indexNum: string; + indexName: string; + createTime: string; +}; + +type ApiResponse = { + code: string; + msg: string; + data: { + pageNum: string; + pageSize: string; + total: string; + list: NovelData[]; + }; +}; + +type ApiChapter = { + code: string; + msg: string; + data: { + pageNum: string; + pageSize: string; + total: string; + list: ChapterData[]; + }; +}; diff --git a/public/static/src/en/novelhi/icon.png b/public/static/src/en/novelhi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4eb688b69cfb209672c1f4a546403a0996598784 GIT binary patch literal 3108 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 literal 0 HcmV?d00001