Files
lnreader-plugins/plugins/chinese/linovelib.ts
T
ConstasJ 6e2e51f3dd fix: Bug Fixes and Feature Improvements to Linovelib Plugin (#1984)
* fix(linovelib): Searching not work

* ci: fix branch cannot be correctly identified in forked environment

* fix: BRANCH cannot be identified

* fix(linovelib): correctly get cover image in redirect case of searching

* fix: why we need a prefix slash here? remove it

* chore(linovelib): update linovelib plugin version

* fix: before LNReader puts its default UA to chrome, forcely set LNReader plugin's UA to Chrome's UA

* refactor: intgrates FetchInit into plugin

* feat: add chapter name transform

* feat(linovelib): add decryptor for shuffle scramble of original html

* feat(linovelib): use external server to extract shuffle coefficients

* fix(linovelib): extract author name more precisely

* fix(linovelib): set Referer header to bypass anti-leeching/hotlinking of image source

Also, use LocalStorage to store coefficients and chapter.js version

* refactor: delete FetchInit definition in this plugin

* fix(linovelib): replace localStorage with storage utility for better data management

* fix(linovelib): update storage import path to use the correct library

* feat(linovelib): Implement chapter number

* refactor(linovelib): as mobile site equipped with strong cloudflare challenge, move most of parsing logic to LDS server

* fix(linovelib): update API calls to use query parameters instead of POST requests for novel, chapter, and search endpoints

* fix(linovelib): add caching mechanism for novel and chapter parsing to reduce API calls

* refactor(linovelib): refactor server URL handling to use class property for API requests

* fix: apply changes pointed by review

* docs(linovelib): make some comments
2026-02-05 23:42:31 +05:30

139 lines
4.4 KiB
TypeScript

import { load as parseHTML } from 'cheerio';
import { fetchText } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@/types/plugin';
import { storage } from '@libs/storage';
class Linovelib implements Plugin.PluginBase {
id = 'linovelib';
name = 'Linovelib';
icon = 'src/cn/linovelib/icon.png';
site = 'https://www.bilinovel.com';
version = '1.2.0';
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
method: 'GET',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0',
'Referer': 'https://www.linovelib.com',
'Accept':
'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
},
};
webStorageUtilized = true;
// The URL of the custom LDS (Linovelib Descramble Server) URL. Due to complex de-scrambling logic, an external LDS is required.
pluginSettings = {
host: {
value: 'http://example.com',
label: 'Custom LDS Host',
type: 'Text',
},
};
serverUrl = storage.get('host') || 'http://localhost:5301';
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const rank = showLatestNovels ? 'lastupdate' : filters.rank.value;
const url = `${this.site}/top/${rank}/${pageNo}.html`;
const body = await fetchText(url);
if (body === '') throw Error('无法获取小说列表,请检查网络');
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.module-rank-booklist .book-layout').each((i, el) => {
const url = loadedCheerio(el).attr('href');
const novelName = loadedCheerio(el).find('.book-title').text();
const novelCover = loadedCheerio(el)
.find('div.book-cover > img')
.attr('data-src')
?.replace('/https', 'https');
if (!url) return;
const novel = {
name: novelName,
cover: novelCover,
path: url,
};
novels.push(novel);
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
// move major logic to LDS
const res = await fetchText(
`${this.serverUrl}/api/novel?path=${novelPath}`,
);
const novel = JSON.parse(res) as Plugin.SourceNovel;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
// move major logic to LDS
const lastFetchChapterTime =
storage.get('lastFetchChapterTime_' + chapterPath) || 0;
if (Date.now() - lastFetchChapterTime < 10000) {
return storage.get('chapterContent_' + chapterPath) || '';
}
const res = await fetchText(
`${this.serverUrl}/api/chapter?path=${chapterPath}`,
);
const resObj = JSON.parse(res);
storage.set('lastFetchChapterTime_' + chapterPath, Date.now());
storage.set('chapterContent_' + chapterPath, resObj.content);
return resObj.content;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
// move major logic to LDS
const lastSearchTime = storage.get('lastSearchTime_' + this.id) || 0;
if (Date.now() - lastSearchTime < 5000) {
return [];
}
const res = await fetchText(
`${this.serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`,
);
const novelsData = JSON.parse(res).results as Plugin.NovelItem[];
storage.set('lastSearchTime_' + this.id, Date.now());
return novelsData;
}
filters = {
rank: {
label: '排行榜',
value: 'monthvisit',
options: [
{ label: '月点击榜', value: 'monthvisit' },
{ label: '周点击榜', value: 'weekvisit' },
{ label: '月推荐榜', value: 'monthvote' },
{ label: '周推荐榜', value: 'weekvote' },
{ label: '月鲜花榜', value: 'monthflower' },
{ label: '周鲜花榜', value: 'weekflower' },
{ label: '月鸡蛋榜', value: 'monthegg' },
{ label: '周鸡蛋榜', value: 'weekegg' },
{ label: '最近更新', value: 'lastupdate' },
{ label: '最新入库', value: 'postdate' },
{ label: '收藏榜', value: 'goodnum' },
{ label: '新书榜', value: 'newhot' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new Linovelib();