Files
lnreader-plugins/scripts/multisrc/lightnovelwp/template.ts
T
Justin COLLON f10dd1ad10 LightNovelWP: fixs for bacalighnovel (#1116)
* LightNovelWP: fix bacalighnovel

* LightNovelWP: fix novel without covers & weird names in popNovels and search (html entities)
2024-08-06 23:25:14 +07:00

416 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 } 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.${3 + versionIncrements}`;
this.options = metadata.options ?? ({} as LightNovelWPOptions);
this.filters = metadata.filters satisfies Filters;
}
getHostname(url: string): string {
url = url.split('/')[2];
const 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[] {
html = load(html).html(); // fix "'" beeing replaced by "&#8217;" (html entities)
const novels: Plugin.NovelItem[] = [];
const articles = html.match(/<article([\s\S]*?)<\/article>/g) || [];
articles.forEach(article => {
const [, novelUrl, novelName] =
article.match(/<a href="(.*?)".*title="(.*?)"/) || [];
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')) {
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'] && attribs['class'].includes('eplister')) {
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> {
let data = await this.safeFecth(this.site + chapterPath, false);
if (this.options?.customJs) {
try {
const $ = load(data);
// CustomJS HERE
data = $.html();
} catch (error) {
console.error('Error executing customJs:', error);
throw error;
}
}
return (
data
.match(/<div.*class="epcontent ([\s\S]*?)<div.*class="bottomnav"/g)?.[0]
.match(/<p.*>([\s\S]*?)<\/p>/g)
?.join('\n') || ''
);
}
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);
}
}
function extractChapterNumber(data: string, tempChapter: Plugin.ChapterItem) {
const tempChapterNumber = data.match(/(\d+)$/);
if (tempChapterNumber && tempChapterNumber[0]) {
tempChapter.chapterNumber = parseInt(tempChapterNumber[0]);
}
}