Files
lnreader-plugins/scripts/multisrc/lightnovelwp/template.ts
T
Rider21 c7f6a9b5ee Update(template): lightnovel (#956)
* ippotranslations, cpunovel

* fix

* test

* [ranobelib] heading -> h2 (fixes LNReader/lnreader-plugins#960)

* icon
2024-06-03 19:42:47 +07:00

418 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { load } from 'cheerio';
import { Parser } from 'htmlparser2';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from '@libs/defaultCover';
import { Filters } from '@libs/filterInputs';
interface LightNovelWPOptions {
reverseChapters?: boolean;
lang?: string;
versionIncrements?: number;
seriesPath?: string;
customJs?: string;
}
export interface LightNovelWPMetadata {
id: string;
sourceSite: string;
sourceName: string;
options?: LightNovelWPOptions;
filters?: any;
}
class LightNovelWPPlugin implements Plugin.PluginBase {
id: string;
name: string;
icon: string;
site: string;
version: string;
options?: LightNovelWPOptions;
filters?: Filters;
constructor(metadata: LightNovelWPMetadata) {
this.id = metadata.id;
this.name = metadata.sourceName;
this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite;
const versionIncrements = metadata.options?.versionIncrements || 0;
this.version = `1.1.${versionIncrements}`;
this.options = metadata.options ?? ({} as LightNovelWPOptions);
this.filters = metadata.filters satisfies Filters;
}
getHostname(url: string): string {
url = url.split('/')[2];
let url_parts = url.split('.');
url_parts.pop(); // remove TLD
return url_parts.join('.');
}
async safeFecth(url: string, search: boolean): Promise<string> {
const r = await fetchApi(url);
if (!r.ok && search != true)
throw new Error(
'Could not reach site (' + r.status + ') try to open in webview.',
);
const data = await r.text();
const title = data.match(/<title>(.*?)<\/title>/)?.[1]?.trim();
if (
this.getHostname(url) != this.getHostname(r.url) ||
(title &&
(title == 'Bot Verification' ||
title == 'You are being redirected...' ||
title == 'Un instant...' ||
title == 'Just a moment...' ||
title == 'Redirecting...'))
)
throw new Error('Captcha error, please open in webview');
return data;
}
parseNovels(html: string): Plugin.NovelItem[] {
const novels: Plugin.NovelItem[] = [];
const articles = html.match(/<article([\s\S]*?)<\/article>/g) || [];
articles.forEach(article => {
const novelName = article.match(/<a.*title="(.*?)"/)?.[1];
const novelUrl = article.match(/<a href="(.*?)"/)?.[1];
if (novelName && novelUrl) {
const novelCover =
article.match(/<img.*src="(.*?)"(?:\sdata-src="(.*?)")?.*\/>/) || [];
novels.push({
name: novelName,
cover: novelCover[2] || novelCover[1] || defaultCover,
path: novelUrl.replace(this.site, ''),
});
}
});
return novels;
}
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const seriesPath = this.options?.seriesPath ?? 'series/';
let url = this.site + seriesPath + '?page=' + pageNo;
if (!filters) filters = this.filters || {};
if (showLatestNovels) url += '&order=latest';
for (const key in filters) {
if (typeof filters[key].value === 'object')
for (const value of filters[key].value as string[])
url += `&${key}=${value}`;
else if (filters[key].value) url += `&${key}=${filters[key].value}`;
}
const html = await this.safeFecth(url, false);
return this.parseNovels(html);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const baseURL = this.site;
const html = await this.safeFecth(baseURL + novelPath, false);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
genres: '',
summary: '',
author: '',
artist: '',
status: '',
chapters: [] as Plugin.ChapterItem[],
};
let isParsingGenres = false;
let isReadingGenre = false;
let isReadingSummary = false;
let isParsingInfo = false;
let isReadingInfo = false;
let isReadingAuthor = false;
let isReadingArtist = false;
let isReadingStatus = false;
let isParsingChapterList = false;
let isReadingChapter = false;
let isReadingChapterInfo = 0;
let isPaidChapter = false;
const chapters: Plugin.ChapterItem[] = [];
let tempChapter = {} as Plugin.ChapterItem;
const parser = new Parser({
onopentag(name, attribs) {
// name and cover
if (
!novel.cover &&
attribs['class']?.includes('ts-post-image' || 'wp-post-image')
) {
novel.name = attribs['title'];
novel.cover = attribs['data-src'] || attribs['src'] || defaultCover;
} // genres
else if (
attribs['class'] === 'genxed' ||
attribs['class'] === 'sertogenre'
) {
isParsingGenres = true;
} else if (isParsingGenres && name === 'a') {
isReadingGenre = true;
} // summary
else if (
name === 'div' &&
(attribs['class'] === 'entry-content' ||
attribs['itemprop'] === 'description')
) {
isReadingSummary = true;
} // author and status
else if (attribs['class'] === 'spe' || attribs['class'] === 'serl') {
isParsingInfo = true;
} else if (isParsingInfo && name === 'span') {
isReadingInfo = true;
} else if (name === 'div' && attribs['class'] === 'sertostat') {
isParsingInfo = true;
isReadingInfo = true;
isReadingStatus = true;
}
// chapters
else if (attribs['class'] === 'eplister eplisterfull') {
isParsingChapterList = true;
} else if (isParsingChapterList && name === 'li') {
isReadingChapter = true;
} else if (isReadingChapter) {
if (name === 'a') {
tempChapter.path = attribs['href'].replace(baseURL, '').trim();
} else if (attribs['class'] === 'epl-num') {
isReadingChapterInfo = 1;
} else if (attribs['class'] === 'epl-title') {
isReadingChapterInfo = 2;
} else if (attribs['class'] === 'epl-date') {
isReadingChapterInfo = 3;
} else if (attribs['class'] === 'epl-price') {
isReadingChapterInfo = 4;
}
}
},
ontext(data) {
// genres
if (isParsingGenres) {
if (isReadingGenre) {
novel.genres += data + ', ';
}
} // summary
else if (isReadingSummary) {
novel.summary += data.trim();
} // author and status
else if (isParsingInfo) {
if (isReadingInfo) {
const detailName = data.toLowerCase().replace(':', '').trim();
if (isReadingAuthor) {
novel.author += data || 'Unknown';
} else if (isReadingArtist) {
novel.artist += data || 'Unknown';
} else if (isReadingStatus) {
switch (detailName) {
case 'مكتملة':
case 'completed':
case 'complété':
case 'completo':
case 'completado':
case 'tamamlandı':
novel.status = NovelStatus.Completed;
break;
case 'مستمرة':
case 'ongoing':
case 'en cours':
case 'em andamento':
case 'en progreso':
case 'devam ediyor':
novel.status = NovelStatus.Ongoing;
break;
case 'متوقفة':
case 'hiatus':
case 'en pause':
case 'hiato':
case 'pausa':
case 'pausado':
case 'duraklatıldı':
novel.status = NovelStatus.OnHiatus;
break;
default:
novel.status = NovelStatus.Unknown;
break;
}
}
switch (detailName) {
case 'الكاتب':
case 'author':
case 'auteur':
case 'autor':
case 'yazar':
isReadingAuthor = true;
break;
case 'الحالة':
case 'status':
case 'statut':
case 'estado':
case 'durum':
isReadingStatus = true;
break;
case 'الفنان':
case 'artist':
case 'artiste':
case 'artista':
case 'çizer':
isReadingArtist = true;
break;
}
}
} // chapters
else if (isParsingChapterList) {
if (isReadingChapter) {
if (isReadingChapterInfo === 1) {
extractChapterNumber(data, tempChapter);
} else if (isReadingChapterInfo === 2) {
tempChapter.name =
data
.match(
RegExp(
`^${novel.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(.+)`,
),
)?.[1]
?.trim() || data.trim();
if (!tempChapter.chapterNumber) {
extractChapterNumber(data, tempChapter);
}
} else if (isReadingChapterInfo === 3) {
tempChapter.releaseTime = data; //new Date(data).toISOString();
} else if (isReadingChapterInfo === 4) {
const detailName = data.toLowerCase().trim();
switch (detailName) {
case 'free':
case 'gratuit':
case 'مجاني':
case 'livre':
case '':
isPaidChapter = false;
break;
default:
isPaidChapter = true;
break;
}
}
}
}
},
onclosetag(name) {
// genres
if (isParsingGenres) {
if (isReadingGenre) {
isReadingGenre = false; // stop reading genre
} else {
isParsingGenres = false; // stop parsing genres
novel.genres = novel.genres?.slice(0, -2); // remove trailing comma
}
} // summary
else if (isReadingSummary) {
if (name === 'br') {
novel.summary += '\n';
} else if (name === 'div') {
isReadingSummary = false;
}
} // author and status
else if (isParsingInfo) {
if (isReadingInfo) {
if (name === 'span') {
isReadingInfo = false;
if (isReadingAuthor && novel.author) {
isReadingAuthor = false;
} else if (isReadingArtist && novel.artist) {
isReadingArtist = false;
} else if (isReadingStatus && novel.status !== '') {
isReadingStatus = false;
}
}
} else if (name === 'div') {
isParsingInfo = false;
novel.author = novel.author?.trim();
novel.artist = novel.artist?.trim();
}
} // chapters
else if (isParsingChapterList) {
if (isReadingChapter) {
if (isReadingChapterInfo === 1) {
isReadingChapterInfo = 0;
} else if (isReadingChapterInfo === 2) {
isReadingChapterInfo = 0;
} else if (isReadingChapterInfo === 3) {
isReadingChapterInfo = 0;
} else if (isReadingChapterInfo === 4) {
isReadingChapterInfo = 0;
} else if (name === 'li') {
isReadingChapter = false;
if (!tempChapter.chapterNumber) tempChapter.chapterNumber = 0;
if (!isPaidChapter) chapters.push(tempChapter);
tempChapter = {} as Plugin.ChapterItem;
}
} else if (name === 'ul') {
isParsingChapterList = false;
}
}
},
});
parser.write(html);
parser.end();
if (chapters.length) {
if (this.options?.reverseChapters) chapters.reverse();
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const data = await this.safeFecth(this.site + chapterPath, false);
const chapterText = data.match(
/<div.*class="epcontent ([\s\S]*?)<div.*class="bottomnav"/g,
)?.[0];
if (this.options?.customJs) {
try {
const $ = load(chapterText || data);
// CustomJS HERE
return $.html();
} catch (error) {
console.error('Error executing customJs:', error);
throw error;
}
}
return chapterText || '';
}
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = this.site + 'page/' + page + '/?s=' + searchTerm;
const html = await this.safeFecth(url, true);
return this.parseNovels(html);
}
fetchImage = fetchFile;
}
function extractChapterNumber(data: string, tempChapter: Plugin.ChapterItem) {
const tempChapterNumber = data.match(/(\d+)$/);
if (tempChapterNumber && tempChapterNumber[0]) {
tempChapter.chapterNumber = parseInt(tempChapterNumber[0]);
}
}