Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions plugins/english/novelhi.ts
Original file line number Diff line number Diff line change
@@ -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<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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(/<br\s*\/?>/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<string> {
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<Plugin.NovelItem[]> {
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[];
};
};
Binary file added public/static/src/en/novelhi/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading