fix: linter issues
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
|
||||
@@ -8,11 +8,11 @@ class NovelsOnline implements Plugin.PluginBase {
|
||||
name = 'novelsOnline';
|
||||
site = 'https://novelsonline.org';
|
||||
icon = 'src/en/novelsonline/icon.png';
|
||||
version = '1.0.1';
|
||||
version = '1.0.2';
|
||||
|
||||
async safeFetch(
|
||||
url: string,
|
||||
init: any | undefined = undefined,
|
||||
init: FetchInit | undefined = undefined,
|
||||
): Promise<CheerioAPI> {
|
||||
const r = await fetchApi(url, init);
|
||||
if (!r.ok)
|
||||
@@ -131,12 +131,14 @@ class NovelsOnline implements Plugin.PluginBase {
|
||||
.join(', ');
|
||||
break;
|
||||
case 'Artist(s)':
|
||||
const artist = detail
|
||||
.find('li')
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(', ');
|
||||
if (artist && artist != 'N/A') novel.artist = artist;
|
||||
{
|
||||
const artist = detail
|
||||
.find('li')
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(', ');
|
||||
if (artist && artist != 'N/A') novel.artist = artist;
|
||||
}
|
||||
break;
|
||||
case 'Status':
|
||||
novel.status = detail.text().trim();
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import { CheerioAPI, load } from 'cheerio';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { fetchApi, fetchText } from '@libs/fetch';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import dayjs, { ManipulateType } from 'dayjs';
|
||||
|
||||
type NovelJSON = {
|
||||
success: boolean;
|
||||
data: {
|
||||
posts: {
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
permalink: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
type ChapterJSON = {
|
||||
success: boolean;
|
||||
data: {
|
||||
title: string;
|
||||
url: string;
|
||||
slug: string;
|
||||
date: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
id = 'storyseedling';
|
||||
name = 'StorySeedling';
|
||||
icon = 'src/en/storyseedling/icon.png';
|
||||
site = 'https://storyseedling.com/';
|
||||
version = '1.0.4';
|
||||
version = '1.0.6';
|
||||
nonce: string | undefined;
|
||||
|
||||
async getCheerio(url: string, search: boolean): Promise<CheerioAPI> {
|
||||
@@ -22,8 +44,40 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
return $;
|
||||
}
|
||||
|
||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
private parseAgoDate(date: string | undefined) {
|
||||
//parseMadaraDate
|
||||
const parsed = dayjs(date);
|
||||
if (date && parsed.isValid()) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
|
||||
const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || [];
|
||||
const decade = time?.includes('decade'); // dayjs no support, but just in case
|
||||
const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1);
|
||||
const unit = (decade ? 'year' : time) as ManipulateType;
|
||||
|
||||
const validUnits = [
|
||||
'millisecond', // waow
|
||||
'second',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year',
|
||||
];
|
||||
|
||||
if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dayjs().subtract(amount, unit).toISOString();
|
||||
}
|
||||
|
||||
private async getNovels(
|
||||
pageNo: number,
|
||||
searchTerm = '',
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const body = await fetchApi(this.site + 'browse').then(r => r.text());
|
||||
const loadedCheerio = load(body);
|
||||
|
||||
@@ -33,31 +87,32 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
.replace("')", '') as string;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('search', '');
|
||||
data.append('search', searchTerm);
|
||||
data.append('orderBy', 'recent');
|
||||
data.append('curpage', pageNo.toString());
|
||||
data.append('post', postValue);
|
||||
data.append('action', 'fetch_browse');
|
||||
|
||||
const response: any = await fetchApi(this.site + 'ajax', {
|
||||
const response = (await fetchApi(this.site + 'ajax', {
|
||||
body: data,
|
||||
method: 'POST',
|
||||
}).then(res => res.json());
|
||||
}).then(res => res.json())) as NovelJSON;
|
||||
|
||||
response.data.posts.forEach((element: any) =>
|
||||
novels.push({
|
||||
name: element.title,
|
||||
cover: element.thumbnail,
|
||||
path: element.permalink.replace(this.site, ''),
|
||||
}),
|
||||
);
|
||||
const novels: Plugin.NovelItem[] = response.data.posts.map(element => ({
|
||||
name: element.title,
|
||||
cover: element.thumbnail,
|
||||
path: element.permalink.replace(this.site, ''),
|
||||
}));
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
return this.getNovels(pageNo);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const $ = await this.getCheerio(this.site + novelPath, false);
|
||||
const baseUrl = this.site;
|
||||
|
||||
const novel: Partial<Plugin.SourceNovel> = {
|
||||
path: novelPath,
|
||||
@@ -67,7 +122,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
const coverUrl = $('img[x-ref="art"].w-full.rounded.shadow-md').attr('src');
|
||||
|
||||
if (coverUrl) {
|
||||
novel.cover = new URL(coverUrl, baseUrl).href;
|
||||
novel.cover = new URL(coverUrl, this.site).href;
|
||||
}
|
||||
|
||||
const genres: string[] = [];
|
||||
@@ -122,7 +177,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
const chaptersUrl = `${this.site}${chapterListing}`;
|
||||
const refererUrl = `${this.site}${novel.path}`;
|
||||
|
||||
let results = await fetchApi(chaptersUrl, {
|
||||
const results: ChapterJSON = await fetchApi(chaptersUrl, {
|
||||
method: 'POST',
|
||||
referrer: refererUrl,
|
||||
referrerPolicy: 'origin',
|
||||
@@ -136,20 +191,18 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
);
|
||||
|
||||
if (results.data) {
|
||||
results = results.data;
|
||||
results.forEach(function (chap: any) {
|
||||
results.data.forEach(chap => {
|
||||
if (chap.url == null) {
|
||||
return;
|
||||
}
|
||||
const name = chap.title;
|
||||
const url = chap.url as string;
|
||||
const releaseTime = chap.date;
|
||||
const chapterNumber = chap.slug;
|
||||
|
||||
chapters.push({
|
||||
name: name,
|
||||
path: url.replace(baseUrl, ''),
|
||||
releaseTime,
|
||||
path: url.replace(this.site, ''),
|
||||
releaseTime: this.parseAgoDate(chap.date),
|
||||
chapterNumber: parseInt(chapterNumber),
|
||||
});
|
||||
});
|
||||
@@ -166,7 +219,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
const $ = await this.getCheerio(this.site + chapterPath, false);
|
||||
this.nonce = $('div.mb-4:has(h1.text-xl) > div')
|
||||
.attr('x-data')
|
||||
?.match(/loadChapter\('.+?', '(.+?)'\)/)[1];
|
||||
?.match(/loadChapter\('.+?', '(.+?)'\)/)![1];
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
@@ -200,7 +253,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
);
|
||||
}
|
||||
}
|
||||
let html = text
|
||||
const html = text
|
||||
.replace(/cls[a-f0-9]+/g, '')
|
||||
.split('')
|
||||
.map(char => {
|
||||
@@ -212,7 +265,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
: char;
|
||||
})
|
||||
.join('');
|
||||
let $ = load(html);
|
||||
const $ = load(html);
|
||||
|
||||
$('span').text((_, txt) =>
|
||||
txt.toLowerCase().includes('storyseedling') ||
|
||||
@@ -228,37 +281,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const body = await fetchApi(this.site + 'browse').then(r => r.text());
|
||||
const loadedCheerio = load(body);
|
||||
|
||||
const postValue = loadedCheerio('div[ax-load][x-data]')
|
||||
.attr('x-data')
|
||||
?.replace("browse('", '')
|
||||
.replace("')", '') as string;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('search', searchTerm);
|
||||
data.append('orderBy', 'recent');
|
||||
data.append('curpage', pageNo.toString());
|
||||
data.append('post', postValue);
|
||||
data.append('action', 'fetch_browse');
|
||||
|
||||
const response: any = await fetchApi(this.site + 'ajax', {
|
||||
body: data,
|
||||
method: 'POST',
|
||||
}).then(res => res.json());
|
||||
|
||||
response.data.posts.forEach((element: any) =>
|
||||
novels.push({
|
||||
name: element.title,
|
||||
cover: element.thumbnail,
|
||||
path: element.permalink.replace(this.site, ''),
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
return this.getNovels(pageNo, searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+74
-53
@@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover';
|
||||
class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||
id = 'archiveofourown';
|
||||
name = 'Archive Of Our Own';
|
||||
version = '1.0.3';
|
||||
version = '1.0.4';
|
||||
icon = 'src/en/ao3/icon.png';
|
||||
site = 'https://archiveofourown.org/';
|
||||
|
||||
@@ -42,57 +42,73 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
// Base URL and common parameters
|
||||
let link = `${this.site}works/search?page=${page}&work_search%5Blanguage_id%5D=en`;
|
||||
const params = new URLSearchParams({
|
||||
commit: 'Search',
|
||||
page: page.toString(),
|
||||
'work_search[language_id]': filters.language.value,
|
||||
});
|
||||
|
||||
// Apply sorting based on showLatestNovels
|
||||
if (showLatestNovels) {
|
||||
link += `&work_search%5Bsort_column%5D=revised_at&work_search%5Bsort_direction%5D=${filters.sortdir.value}`;
|
||||
} else if (filters) {
|
||||
link += `&work_search%5Bsort_column%5D=${filters.sort.value}&work_search%5Bsort_direction%5D=${filters.sortdir.value}`;
|
||||
}
|
||||
|
||||
// Apply additional filters
|
||||
if (filters) {
|
||||
// if (filters.genre.value !== '') link += `&work_search%5Bfandom_names%5D=${filters.genre.value}`;
|
||||
if (filters.completion.value !== '')
|
||||
link += `&work_search%5Bcomplete%5D=${filters.completion.value}`;
|
||||
if (filters.crossover.value !== '')
|
||||
link += `&work_search%5Bcrossover%5D=${filters.crossover.value}`;
|
||||
if (filters.categories.value.length > 0) {
|
||||
filters.categories.value.forEach((category: string) => {
|
||||
link += `&work_search%5Bcategory_ids%5D%5B%5D=${category}`;
|
||||
});
|
||||
}
|
||||
if (filters.warningsFilter.value.length > 0) {
|
||||
filters.warningsFilter.value.forEach((warning: string) => {
|
||||
link += `&work_search%5Barchive_warning_ids%5D%5B%5D=${warning}`;
|
||||
});
|
||||
}
|
||||
if (filters.singlechap.value !== '')
|
||||
link += `&work_search%5Bsingle_chapter%5D=${filters.singlechap.value}`;
|
||||
if (filters.author.value !== '')
|
||||
link += `&work_search%5Bcreators%5D=${filters.author.value}`;
|
||||
if (
|
||||
filters.dateFilter.value !== '' &&
|
||||
filters.dateIncrements.value !== ''
|
||||
) {
|
||||
link += `&work_search%5Brevised_at%5D=${filters.dateFilter.value}+${filters.dateIncrements.value}`;
|
||||
}
|
||||
if (filters.words.value !== '')
|
||||
link += `&work_search%5Bword_count%5D=${filters.words.value}`;
|
||||
if (filters.hits.value !== '')
|
||||
link += `&work_search%5Bhits%5D=${filters.hits.value}`;
|
||||
if (filters.bookmarks.value !== '')
|
||||
link += `&work_search%5Bbookmarks_count%5D=${filters.bookmarks.value}`;
|
||||
if (filters.comments.value !== '')
|
||||
link += `&work_search%5Bcomments_count%5D=${filters.comments.value}`;
|
||||
if (filters.kudos.value !== '')
|
||||
link += `&work_search%5Bkudos_count%5D=${filters.kudos.value}`;
|
||||
params.set('work_search[sort_column]', 'revised_at');
|
||||
} else {
|
||||
params.set('work_search[sort_column]', filters.sort.value);
|
||||
}
|
||||
params.set('work_search[sort_direction]', filters.sortdir.value);
|
||||
|
||||
// we could send in the entire thing without checking for blanks
|
||||
if (filters.completion.value !== '') {
|
||||
params.set('work_search[complete]', filters.completion.value);
|
||||
}
|
||||
if (filters.crossover.value !== '') {
|
||||
params.set('work_search[crossover]', filters.crossover.value);
|
||||
}
|
||||
if (filters.categories.value.length > 0) {
|
||||
filters.categories.value.forEach((category: string) => {
|
||||
params.append('work_search[category_ids][]', category);
|
||||
});
|
||||
}
|
||||
if (filters.warningsFilter.value.length > 0) {
|
||||
filters.warningsFilter.value.forEach((warning: string) => {
|
||||
params.append('work_search[archive_warning_ids][]', warning);
|
||||
});
|
||||
}
|
||||
if (filters.singlechap.value) {
|
||||
params.set('work_search[single_chapter]', '1');
|
||||
}
|
||||
if (filters.author.value !== '') {
|
||||
params.set('work_search[creators]', filters.author.value);
|
||||
}
|
||||
if (
|
||||
filters.dateFilter.value !== '' &&
|
||||
filters.dateIncrements.value !== ''
|
||||
) {
|
||||
params.set(
|
||||
'work_search[revised_at]',
|
||||
`${filters.dateFilter.value} ${filters.dateIncrements.value}`,
|
||||
);
|
||||
}
|
||||
if (filters.words.value !== '') {
|
||||
params.set('work_search[word_count]', filters.words.value);
|
||||
}
|
||||
if (filters.hits.value !== '') {
|
||||
params.set('work_search[hits]', filters.hits.value);
|
||||
}
|
||||
if (filters.bookmarks.value !== '') {
|
||||
params.set('work_search[bookmarks_count]', filters.bookmarks.value);
|
||||
}
|
||||
if (filters.comments.value !== '') {
|
||||
params.set('work_search[comments_count]', filters.comments.value);
|
||||
}
|
||||
if (filters.kudos.value !== '') {
|
||||
params.set('work_search[kudos_count]', filters.kudos.value);
|
||||
}
|
||||
|
||||
const link = `${this.site}works/search?${params.toString()}`;
|
||||
const body = await fetchApi(link).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
@@ -149,12 +165,12 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||
novel.summary = `Fandom:\n${fandom}\n\nRating:\n${rating}\n\nWarning:\n${warning}\n\nSummary:\n${summary}\n\nSeries:\n${series}\n\nRelationships:\n${relation}\n\nCharacters:\n${character}\n\nStats:\n${stats}`;
|
||||
const chapterItems: Plugin.ChapterItem[] = [];
|
||||
const longReleaseDate: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
// let match: RegExpExecArray | null;
|
||||
chapterlistload('ol.index').each((i, ele) => {
|
||||
chapterlistload(ele)
|
||||
.find('li')
|
||||
.each((i, el) => {
|
||||
const chapterNameMatch = chapterlistload(el).find('a').text().trim();
|
||||
// const chapterNameMatch = chapterlistload(el).find('a').text().trim();
|
||||
const releaseTimeText = chapterlistload(el)
|
||||
.find('span.datetime')
|
||||
.text()
|
||||
@@ -282,7 +298,13 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||
searchTerm: string,
|
||||
page: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const searchUrl = `${this.site}works/search?page=${page}&work_search%5Blanguage_id%5D=en&work_search%5Bquery%5D=${encodeURIComponent(searchTerm)}`;
|
||||
const params = new URLSearchParams({
|
||||
commit: 'Search',
|
||||
page: page.toString(),
|
||||
'work_search[language_id]': 'en',
|
||||
'work_search[query]': searchTerm,
|
||||
});
|
||||
const searchUrl = `${this.site}works/search?${params.toString()}`;
|
||||
|
||||
const result = await fetchApi(searchUrl);
|
||||
const body = await result.text();
|
||||
@@ -521,10 +543,9 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
singlechap: {
|
||||
value: '',
|
||||
value: false,
|
||||
label: 'Single Chapter Stories',
|
||||
options: [{ label: 'Single Chapter', value: '1' }],
|
||||
type: FilterTypes.Picker,
|
||||
type: FilterTypes.Switch,
|
||||
},
|
||||
author: {
|
||||
value: '',
|
||||
|
||||
@@ -9,7 +9,7 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
||||
name = 'Chrysanthemum Garden';
|
||||
icon = 'src/en/chrysanthemumgarden/icon.png';
|
||||
site = 'https://chrysanthemumgarden.com';
|
||||
version = '1.0.1';
|
||||
version = '1.0.3';
|
||||
filters: Filters | undefined = undefined;
|
||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||
|
||||
@@ -18,10 +18,10 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
// {
|
||||
// showLatestNovels,
|
||||
// filters,
|
||||
// }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const req = await fetchApi(
|
||||
this.site + (pageNo === 1 ? '/books' : '/books/page/' + pageNo) + '/',
|
||||
@@ -136,7 +136,9 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
||||
);
|
||||
}
|
||||
|
||||
async getAllNovels() {}
|
||||
async getAllNovels() {
|
||||
// linter? what is the purpose of this
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + (isNovel ? '/book/' : '/chapter/') + path;
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { storage } from '@libs/storage';
|
||||
import dayjs from 'dayjs';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
enum APIAction {
|
||||
novels = 'load_novels',
|
||||
search = 'live_novel_search',
|
||||
}
|
||||
|
||||
interface APIParams {
|
||||
type APIParams = {
|
||||
action: APIAction;
|
||||
params: Record<string, string | number>;
|
||||
}
|
||||
};
|
||||
|
||||
type ChapterJSON = {
|
||||
items: ChapterItem[];
|
||||
total: number;
|
||||
total_pages?: number;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
order?: string;
|
||||
};
|
||||
|
||||
type ChapterItem = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
id = 'crimsonscrolls';
|
||||
name = 'Crimson Scrolls';
|
||||
icon = 'src/en/crimsonscrolls/icon.png';
|
||||
site = 'https://crimsonscrolls.net';
|
||||
version = '1.0.0';
|
||||
version = '1.0.1';
|
||||
|
||||
hideLocked = storage.get('hideLocked');
|
||||
pluginSettings = {
|
||||
@@ -35,7 +51,7 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
const formData = new FormData();
|
||||
formData.append('action', query.action);
|
||||
for (const [key, value] of Object.entries(query.params))
|
||||
formData.append(key, value);
|
||||
formData.append(key, value.toString());
|
||||
|
||||
const result = await fetchApi(`${this.site}/wp-admin/admin-ajax.php`, {
|
||||
method: 'POST',
|
||||
@@ -45,17 +61,28 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
return parseHTML(result.html);
|
||||
}
|
||||
|
||||
async fetchChapters(id: number, page?: number | undefined) {
|
||||
const chapter: any[] = [];
|
||||
const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`; //page=${page}
|
||||
const data = await fetchApi(`${url}&page=${page || 1}`).then(r => r.json());
|
||||
const locked = data.items.some(e => e.locked);
|
||||
async fetchChapters(
|
||||
id: number,
|
||||
page?: number | undefined,
|
||||
): Promise<ChapterItem[]> {
|
||||
const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`;
|
||||
const data: ChapterJSON = await fetchApi(`${url}&page=${page ?? 1}`).then(
|
||||
r => r.json(),
|
||||
);
|
||||
|
||||
if (data.page < data.total_pages && !(locked && this.hideLocked))
|
||||
return data.items.concat(
|
||||
await this.fetchChapters(id, parseInt(data.page) + 1),
|
||||
);
|
||||
else return data.items;
|
||||
const items = data.items || [];
|
||||
const locked = items.some(e => e.locked);
|
||||
|
||||
if (
|
||||
data.total_pages &&
|
||||
(data.page ?? 1) < data.total_pages &&
|
||||
!(locked && this.hideLocked)
|
||||
) {
|
||||
const nextItems = await this.fetchChapters(id, (data.page ?? 0) + 1);
|
||||
return items.concat(nextItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
@@ -83,7 +110,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
.filter(e => e.length > 0)
|
||||
.join(' '),
|
||||
cover: novelCover,
|
||||
path: novelUrl.replace(this.site, '').split('/').at(2),
|
||||
path: novelUrl
|
||||
? new URL(novelUrl, this.site).pathname.substring(1)
|
||||
: defaultCover,
|
||||
};
|
||||
novels.push(novel);
|
||||
},
|
||||
@@ -94,59 +123,61 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
||||
const loadedCheerio = await this.queryAPI({
|
||||
action: APIAction.novels,
|
||||
params: { page: page as string },
|
||||
params: { page: page.toString() },
|
||||
});
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(`${this.site}/novel/${novelPath}`).then(r =>
|
||||
const result = await fetchApi(`${this.site}/${novelPath}`).then(r =>
|
||||
r.text(),
|
||||
);
|
||||
|
||||
let loadedCheerio = parseHTML(result);
|
||||
let novelInfo = loadedCheerio('#single-novel-content-wrapper');
|
||||
const loadedCheerio = parseHTML(result);
|
||||
const novelInfo = loadedCheerio('#single-novel-content-wrapper');
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: novelInfo.find('h1.chapter-title').text().trim() || 'Untitled',
|
||||
cover: novelInfo.find('.single-novel-cover > img').data('src'),
|
||||
name: novelInfo.find('h1').text().trim() ?? 'Untitled',
|
||||
cover:
|
||||
novelInfo.find('img:first').data('src')?.toString() ?? defaultCover,
|
||||
summary: novelInfo.find('#synopsis-full').text().trim(),
|
||||
author: novelInfo
|
||||
.find('.single-novel-meta strong')
|
||||
.filter(
|
||||
(i, el) =>
|
||||
loadedCheerio(el).text().toLowerCase().search('author') >= 0,
|
||||
)[0]
|
||||
.next.data.trim(),
|
||||
author: novelInfo.find('strong:first').next().text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.genres = novelInfo
|
||||
.find('.single-novel-meta strong')
|
||||
.filter(
|
||||
(i, el) => loadedCheerio(el).text().toLowerCase().search('genre') >= 0,
|
||||
)[0]
|
||||
.next.data.split(',')
|
||||
.map(e => e.trim())
|
||||
.find('.cs-genre-chip')
|
||||
.map((_, el) => loadedCheerio(el).text().trim())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
novel.status = 'Unknown';
|
||||
const id = loadedCheerio('#chapter-list').data('novel');
|
||||
const chapters = await this.fetchChapters(id);
|
||||
const rawStatus = novelInfo.find('.cs-nsb-badge').text().trim();
|
||||
const map: Record<string, string> = {
|
||||
ongoing: NovelStatus.Ongoing,
|
||||
hiatus: NovelStatus.OnHiatus,
|
||||
dropped: NovelStatus.Cancelled,
|
||||
cancelled: NovelStatus.Cancelled,
|
||||
completed: NovelStatus.Completed,
|
||||
};
|
||||
novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown;
|
||||
|
||||
novel.chapters = [];
|
||||
for (const idx in chapters) {
|
||||
if (!(chapters[idx].locked && this.hideLocked)) {
|
||||
novel.chapters.push({
|
||||
name: chapters[idx].locked
|
||||
? `🔒 ${chapters[idx].title}`
|
||||
: chapters[idx].title,
|
||||
path: chapters[idx].url.replace(this.site, '').split('/').at(2),
|
||||
chapterNumber: parseInt(idx) + 1,
|
||||
const id = loadedCheerio('#chapter-list').data('novel');
|
||||
const chapters = await this.fetchChapters(Number(id));
|
||||
|
||||
const novelChapters: Plugin.ChapterItem[] = [];
|
||||
chapters.forEach((chapter, index) => {
|
||||
if (!(chapter.locked && this.hideLocked)) {
|
||||
novelChapters.push({
|
||||
name: chapter.locked ? `🔒 ${chapter.title}` : chapter.title,
|
||||
path: chapter.url
|
||||
? new URL(chapter.url, this.site).pathname.split('/')[2]
|
||||
: '',
|
||||
chapterNumber: index + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
}
|
||||
@@ -176,8 +207,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + '/novel/' + path;
|
||||
// not sure purpose of this, commented out
|
||||
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
// this.site + '/novel/' + path;
|
||||
}
|
||||
|
||||
export default new CrimsonScrollsPlugin();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Filters, FilterTypes, FilterValueWithType } from '@libs/filterInputs';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
|
||||
//TODO: this looks similar to fictioneer source? maybe use multisrc someday
|
||||
// TODO: change layoput, layout has updated
|
||||
|
||||
class DDLPlugin implements Plugin.PluginBase {
|
||||
id = 'DDL.com';
|
||||
@@ -291,10 +291,7 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()),
|
||||
)
|
||||
.map(([, , path]) => path);
|
||||
return await this.asyncMap(
|
||||
await foundNovels,
|
||||
this.grabCachedNovel.bind(this),
|
||||
);
|
||||
return await this.asyncMap(foundNovels, this.grabCachedNovel.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
|
||||
|
||||
link += `?page=${pageNo}`;
|
||||
|
||||
const body = await fetchApi(link).then(res => res.text());
|
||||
const body = await fetchApi(link).then((res: Response) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
@@ -110,7 +110,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
|
||||
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = this.site + novelPath + '?page=' + page;
|
||||
const body = await fetchApi(url).then(res => res.text());
|
||||
const body = await fetchApi(url).then((res: Response) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapters = this.parseChapters(loadedCheerio);
|
||||
return { chapters };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Node } from 'domhandler';
|
||||
import { load as loadCheerio } from 'cheerio';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { storage } from '@libs/storage';
|
||||
@@ -13,6 +12,10 @@ type APINovel = {
|
||||
description: string;
|
||||
status: string;
|
||||
genres: { name: string }[];
|
||||
user?: {
|
||||
username?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type APIChapter = {
|
||||
@@ -35,12 +38,44 @@ type ChapterInfo = {
|
||||
chapterNumber: number;
|
||||
};
|
||||
|
||||
type Chapter = {
|
||||
type: string;
|
||||
content: {
|
||||
type: string;
|
||||
attrs?: Attrs;
|
||||
content: {
|
||||
type: string;
|
||||
text?: string;
|
||||
marks?: {
|
||||
type: string;
|
||||
attrs?: Attrs;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type Attrs = {
|
||||
textAlign?: string;
|
||||
href?: string;
|
||||
level?: number;
|
||||
};
|
||||
|
||||
type Nodes = {
|
||||
type: string;
|
||||
nodes?: {
|
||||
type: string;
|
||||
// couldnt find the sveltekit so disable eslint here
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any[];
|
||||
}[];
|
||||
};
|
||||
|
||||
class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
id = 'fenrir';
|
||||
name = 'Fenrir Realm';
|
||||
icon = 'src/en/fenrirrealm/icon.png';
|
||||
site = 'https://fenrirealm.com';
|
||||
version = '1.0.13';
|
||||
version = '1.1.0';
|
||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||
|
||||
hideLocked = storage.get('hideLocked');
|
||||
@@ -62,14 +97,16 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
// let sort = "updated";
|
||||
let sort = filters.sort.value;
|
||||
if (showLatestNovels) sort = 'latest';
|
||||
const genresFilter = filters.genres.value
|
||||
.map(g => '&genres%5B%5D=' + g)
|
||||
.join('');
|
||||
const params = new URLSearchParams({
|
||||
page: pageNo.toString(),
|
||||
per_page: '20',
|
||||
status: filters.status.value,
|
||||
order: showLatestNovels ? 'latest' : filters.sort.value,
|
||||
});
|
||||
filters.genres.value.forEach(g => params.append('genres[]', g));
|
||||
|
||||
const res = await fetchApi(
|
||||
`${this.site}/api/series/filter?page=${pageNo}&per_page=20&status=${filters.status.value}&order=${sort}${genresFilter}`,
|
||||
`${this.site}/api/series/filter?${params.toString()}`,
|
||||
).then(r =>
|
||||
r.json().catch(() => {
|
||||
throw new Error(
|
||||
@@ -90,7 +127,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
|
||||
if (!apiRes.ok) {
|
||||
const slugMatch = novelPath.match(/^\d+-(.+)$/);
|
||||
let searchSlug = slugMatch ? slugMatch[1] : novelPath;
|
||||
const searchSlug = slugMatch ? slugMatch[1] : novelPath;
|
||||
apiRes = await fetchApi(
|
||||
`${this.site}/api/new/v2/series/${searchSlug}/chapters`,
|
||||
{},
|
||||
@@ -120,7 +157,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
}
|
||||
}
|
||||
|
||||
const seriesData = await fetchApi(
|
||||
const seriesData: APINovel = await fetchApi(
|
||||
`${this.site}/api/new/v2/series/${cleanNovelPath}`,
|
||||
).then(r => r.json());
|
||||
const summaryCheerio = loadCheerio(seriesData.description || '');
|
||||
@@ -131,7 +168,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
summary:
|
||||
summaryCheerio('p').length > 0
|
||||
? summaryCheerio('p')
|
||||
.map((i, el) => loadCheerio(el).text())
|
||||
.map((_, el) => loadCheerio(el).text())
|
||||
.get()
|
||||
.join('\n\n')
|
||||
: summaryCheerio.text() || '',
|
||||
@@ -139,7 +176,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
cover: seriesData.cover
|
||||
? this.site + '/' + seriesData.cover
|
||||
: defaultCover,
|
||||
genres: (seriesData.genres || []).map((g: any) => g.name).join(','),
|
||||
genres: (seriesData.genres || []).map(g => g.name).join(','),
|
||||
status: seriesData.status || 'Unknown',
|
||||
};
|
||||
|
||||
@@ -184,14 +221,14 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
const content = json.content;
|
||||
|
||||
if (content) {
|
||||
const parsedContent = JSON.parse(content);
|
||||
const parsedContent: Chapter = JSON.parse(content);
|
||||
if (parsedContent.type === 'doc') {
|
||||
return parsedContent.content
|
||||
.map((node: any) => {
|
||||
.map(node => {
|
||||
if (node.type === 'paragraph') {
|
||||
const innerHtml =
|
||||
node.content
|
||||
?.map((c: any) => {
|
||||
?.map(c => {
|
||||
if (c.type === 'text') {
|
||||
let text = c.text;
|
||||
if (c.marks) {
|
||||
@@ -216,8 +253,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
}
|
||||
if (node.type === 'heading') {
|
||||
const level = node.attrs?.level || 1;
|
||||
const innerHtml =
|
||||
node.content?.map((c: any) => c.text).join('') || '';
|
||||
const innerHtml = node.content?.map(c => c.text).join('') || '';
|
||||
return `<h${level}>${innerHtml}</h${level}>`;
|
||||
}
|
||||
return '';
|
||||
@@ -235,7 +271,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
|
||||
let chapterText = loadedCheerio('div.content-area p')
|
||||
.map((i, el) => `<p>${loadCheerio(el).html()}</p>`)
|
||||
.map((_, el) => `<p>${loadCheerio(el).html()}</p>`)
|
||||
.get()
|
||||
.join('\n');
|
||||
|
||||
@@ -247,24 +283,24 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
try {
|
||||
const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`;
|
||||
const jsonRes = await fetchApi(jsonUrl);
|
||||
const json = await jsonRes.json();
|
||||
const json: Nodes = await jsonRes.json();
|
||||
|
||||
const nodes = json.nodes;
|
||||
const data = nodes?.find((n: any) => n.type === 'data')?.data;
|
||||
const data = nodes?.find(n => n.type === 'data')?.data;
|
||||
if (data) {
|
||||
const contentStr = data.find(
|
||||
(d: any) => typeof d === 'string' && d.includes('{"type":"doc"'),
|
||||
d => typeof d === 'string' && d.includes('{"type":"doc"'),
|
||||
);
|
||||
|
||||
if (contentStr) {
|
||||
const contentJson = JSON.parse(contentStr);
|
||||
const contentJson: Chapter = JSON.parse(contentStr);
|
||||
if (contentJson.type === 'doc') {
|
||||
chapterText = contentJson.content
|
||||
.map((node: any) => {
|
||||
.map(node => {
|
||||
if (node.type === 'paragraph') {
|
||||
const innerHtml =
|
||||
node.content
|
||||
?.map((c: any) => {
|
||||
?.map(c => {
|
||||
if (c.type === 'text') {
|
||||
let text = c.text;
|
||||
if (c.marks) {
|
||||
@@ -330,8 +366,8 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||
};
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + '/series/' + path.split('~~')[0];
|
||||
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
// this.site + '/series/' + path.split('~~')[0];
|
||||
|
||||
filters = {
|
||||
status: {
|
||||
|
||||
@@ -15,7 +15,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
// filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
return await this.getPage(
|
||||
@@ -23,7 +23,8 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
);
|
||||
}
|
||||
|
||||
async getData(url: string) {
|
||||
// TODO: Fix URLs, current URL leads to 404
|
||||
async getData<T>(url: string): Promise<Response<T>> {
|
||||
return await fetchApi(this.site + '/api/__api_party/fictionzone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -41,20 +42,25 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
async getPage(url: string) {
|
||||
const data = await this.getData(url);
|
||||
async getPage(url: string): Promise<Plugin.NovelItem[]> {
|
||||
const data = await this.getData<{ novels: NovelItem[] }>(url);
|
||||
|
||||
return data.data.novels.map((n: any) => ({
|
||||
return data.data.novels.map(n => ({
|
||||
name: n.title,
|
||||
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${n.image}.webp`,
|
||||
path: `novel/${n.slug}`,
|
||||
}));
|
||||
}
|
||||
|
||||
async getChapterPage(id: string, novelPath: string) {
|
||||
const data = await this.getData('/platform/chapter-lists?novel_id=' + id);
|
||||
async getChapterPage(
|
||||
id: string,
|
||||
novelPath: string,
|
||||
): Promise<Plugin.ChapterItem[]> {
|
||||
const data = await this.getData<{ chapters: ChapterItem[] }>(
|
||||
'/platform/chapter-lists?novel_id=' + id,
|
||||
);
|
||||
|
||||
return data.data.chapters.map((n: any) => ({
|
||||
return data.data.chapters.map(n => ({
|
||||
name: n.title,
|
||||
number: n.chapter_number,
|
||||
date: n.published_date
|
||||
@@ -66,7 +72,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novelSlug = novelPath.replace('novel/', '');
|
||||
const data = await this.getData(
|
||||
const data = await this.getData<NovelDetails>(
|
||||
`/platform/novel-details?slug=${novelSlug}`,
|
||||
);
|
||||
|
||||
@@ -75,8 +81,8 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
name: data.data.title,
|
||||
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`,
|
||||
genres: [
|
||||
...data.data.genres.map((g: any) => g.name),
|
||||
...data.data.tags.map((g: any) => g.name),
|
||||
...data.data.genres.map(g => g.name),
|
||||
...data.data.tags.map(g => g.name),
|
||||
].join(','),
|
||||
status:
|
||||
data.data.status == 1
|
||||
@@ -85,7 +91,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
? NovelStatus.Completed
|
||||
: NovelStatus.Unknown,
|
||||
author:
|
||||
data.data.contributors.filter((c: any) => c.role == 'author')[0]
|
||||
data.data.contributors.filter(c => c.role == 'author')[0]
|
||||
?.display_name || '',
|
||||
summary: data.data.synopsis,
|
||||
chapters: await this.getChapterPage(data.data.id, novelPath),
|
||||
@@ -93,8 +99,10 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const data = await this.getData(chapterPath.split('|')[1]);
|
||||
return '<p>' + data.data.content.replaceAll('\n', '</p><p>') + '</p>';
|
||||
const data = await this.getData<{ content: string }>(
|
||||
chapterPath.split('|')[1],
|
||||
);
|
||||
return '<p>' + data.data.content.split('\n').join('</p><p>') + '</p>';
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
@@ -106,8 +114,36 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
||||
);
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + '/' + path.split('|')[0];
|
||||
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
// this.site + '/' + path.split('|')[0];
|
||||
}
|
||||
|
||||
export default new FictionZonePlugin();
|
||||
|
||||
type Response<T> = {
|
||||
data: T;
|
||||
};
|
||||
|
||||
type NovelItem = {
|
||||
title: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
type ChapterItem = {
|
||||
title: string;
|
||||
chapter_number: number;
|
||||
published_date: string;
|
||||
chapter_id: string;
|
||||
};
|
||||
|
||||
type NovelDetails = {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
genres: { name: string }[];
|
||||
tags: { name: string }[];
|
||||
status: number;
|
||||
contributors: { role: string; display_name: string }[];
|
||||
synopsis: string;
|
||||
};
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Plugin } from '@/types/plugin';
|
||||
import { Parser } from 'htmlparser2';
|
||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
class Foxteller implements Plugin.PluginBase {
|
||||
id = 'foxteller';
|
||||
name = 'Foxteller';
|
||||
site = 'https://www.foxteller.com';
|
||||
version = '1.0.2';
|
||||
version = '1.0.3';
|
||||
icon = 'src/en/foxteller/icon.png';
|
||||
|
||||
async safeFecth(url: string, init: any = {}): Promise<string> {
|
||||
async safeFecth(url: string, init?: FetchInit): Promise<string> {
|
||||
const r = await fetchApi(url, init);
|
||||
if (!r.ok)
|
||||
throw new Error(
|
||||
@@ -221,8 +221,8 @@ class Foxteller implements Plugin.PluginBase {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Referer: this.resolveUrl(chapterPath),
|
||||
},
|
||||
Referer: this.resolveUrl(chapterPath),
|
||||
body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }),
|
||||
}).then(res => res.json());
|
||||
|
||||
@@ -259,8 +259,8 @@ class Foxteller implements Plugin.PluginBase {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Referer: this.site,
|
||||
},
|
||||
Referer: this.site,
|
||||
body: JSON.stringify({ query: searchTerm }),
|
||||
});
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
+138
-74
@@ -1,9 +1,10 @@
|
||||
import { CheerioAPI, load } from 'cheerio';
|
||||
import { load } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { storage } from '@libs/storage';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
|
||||
class Genesis implements Plugin.PluginBase {
|
||||
id = 'genesistudio';
|
||||
@@ -12,7 +13,7 @@ class Genesis implements Plugin.PluginBase {
|
||||
customCSS = 'src/en/genesis/customCSS.css';
|
||||
site = 'https://genesistudio.com';
|
||||
api = 'https://api.genesistudio.com';
|
||||
version = '2.0.0';
|
||||
version = '2.0.1';
|
||||
|
||||
hideLocked = storage.get('hideLocked');
|
||||
pluginSettings = {
|
||||
@@ -23,14 +24,27 @@ class Genesis implements Plugin.PluginBase {
|
||||
},
|
||||
};
|
||||
|
||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
||||
imageRequestInit?: Plugin.ImageRequestInit = {
|
||||
headers: {
|
||||
'referrer': this.site,
|
||||
},
|
||||
};
|
||||
|
||||
async parseNovelJSON(json: any[]): Promise<Plugin.SourceNovel[]> {
|
||||
return json.map((novel: any) => ({
|
||||
async parseNovelJSON(): Promise<Plugin.SourceNovel[]> {
|
||||
// Thought about caching this,
|
||||
// but not sure what would happen if a new novel were to be
|
||||
// added to the library, so, fetch everytime it is
|
||||
//
|
||||
// fields param literally gives you the JSON you want
|
||||
// maybe TODO: add filtering
|
||||
const params = new URLSearchParams({
|
||||
status: 'published',
|
||||
fields: '["id","novel_title","cover","abbreviation"]',
|
||||
limit: '-1',
|
||||
});
|
||||
const link = `${this.site}/api/directus/novels?${params.toString()}`;
|
||||
const json: NovelJSON[] = await fetchApi(link).then(r => r.json());
|
||||
return json.map(novel => ({
|
||||
name: novel.novel_title,
|
||||
path: `/novels/${novel.abbreviation}`.trim(),
|
||||
cover: `${this.api}/storage/v1/object/public/directus/${novel.cover}.png`,
|
||||
@@ -40,11 +54,19 @@ class Genesis implements Plugin.PluginBase {
|
||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
// There is only one page of results, and no known page function, so do not try
|
||||
if (pageNo !== 1) return [];
|
||||
// Only 14 results, no use in sorting or status
|
||||
// Also all novels are Ongoing with no Completed, can't test status filter
|
||||
const link = `${this.site}/api/directus/novels?status=published&fields=["cover","novel_title","cover","abbreviation"]&limit=-1`;
|
||||
const json = await fetchApi(link).then(r => r.json());
|
||||
return this.parseNovelJSON(json);
|
||||
return this.parseNovelJSON();
|
||||
}
|
||||
|
||||
async getCoverUrl(coverId: string): Promise<string> {
|
||||
// genesis doesn't actually use jpegs but just in case
|
||||
const ext = await fetchApi(`${this.site}/api/directus-file/${coverId}`)
|
||||
.then(res => res.json())
|
||||
.then(data =>
|
||||
data.type ? data.type.split('/')[1].replace('jpeg', 'jpg') : 'png',
|
||||
)
|
||||
.catch(() => 'png');
|
||||
|
||||
return `${this.api}/storage/v1/object/public/directus/${coverId}.${ext}`;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
@@ -53,13 +75,20 @@ class Genesis implements Plugin.PluginBase {
|
||||
|
||||
// Fetch the novel's data in JSON format
|
||||
const raw = await fetchApi(url);
|
||||
const json = await raw.json();
|
||||
const json: NovelJSON = await raw.json();
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
name: json.novel_title,
|
||||
path: novelPath,
|
||||
summary: json.synopsis,
|
||||
author: json.author,
|
||||
cover: json.cover ? await this.getCoverUrl(json.cover) : defaultCover,
|
||||
genres: json.genres
|
||||
?.map(g => g.genres_id?.label)
|
||||
.filter(l => l)
|
||||
.join(','),
|
||||
};
|
||||
|
||||
// Initialize the novel object with default values
|
||||
const parse = await this.parseNovelJSON([json]);
|
||||
const novel: Plugin.SourceNovel = parse[0];
|
||||
novel.summary = json.synopsis;
|
||||
novel.author = json.author;
|
||||
const map: Record<string, string> = {
|
||||
ongoing: NovelStatus.Ongoing,
|
||||
hiatus: NovelStatus.OnHiatus,
|
||||
@@ -68,21 +97,8 @@ class Genesis implements Plugin.PluginBase {
|
||||
completed: NovelStatus.Completed,
|
||||
unknown: NovelStatus.Unknown,
|
||||
};
|
||||
novel.status = map[json.serialization.toLowerCase()] ?? NovelStatus.Unknown;
|
||||
if (json.cover) {
|
||||
const url = `${this.site}/api/directus-file/${json.cover}`;
|
||||
const imgJson = await (await fetchApi(url)).json();
|
||||
console.log(imgJson.type);
|
||||
novel.cover = `${this.api}/storage/v1/object/public/directus/${json.cover}.png`;
|
||||
if (imgJson.type == 'image/gif') {
|
||||
novel.cover = novel.cover?.replace('.png', '.gif');
|
||||
} else if (imgJson.type !== 'image/png') {
|
||||
novel.cover = novel.cover?.replace(
|
||||
'.png',
|
||||
'.' + imgJson.type.toString().split('/')[1],
|
||||
);
|
||||
}
|
||||
}
|
||||
novel.status =
|
||||
map[json.serialization?.toLowerCase() || ''] ?? NovelStatus.Unknown;
|
||||
|
||||
// Parse the chapters if available and assign them to the novel object
|
||||
novel.chapters = await this.extractChapters(json.id);
|
||||
@@ -91,12 +107,12 @@ class Genesis implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
// Helper function to extract and format chapters
|
||||
async extractChapters(id: string): Plugin.ChapterItem[] {
|
||||
async extractChapters(id: string): Promise<Plugin.ChapterItem[]> {
|
||||
const url = `${this.site}/api/novels-chapter/${id}`;
|
||||
|
||||
// Fetch the chapter data in JSON format
|
||||
const raw = await fetchApi(url);
|
||||
const json = await raw.json();
|
||||
const json: ChapterJSON = await raw.json();
|
||||
|
||||
// Format each chapter and add only valid ones
|
||||
const chapters = json.data.chapters
|
||||
@@ -116,7 +132,7 @@ class Genesis implements Plugin.PluginBase {
|
||||
chapterNumber: Number(chapterNum),
|
||||
};
|
||||
})
|
||||
.filter(chapter => chapter !== null) as Plugin.ChapterItem[];
|
||||
.filter(chapter => chapter !== null);
|
||||
|
||||
return chapters;
|
||||
}
|
||||
@@ -131,21 +147,17 @@ class Genesis implements Plugin.PluginBase {
|
||||
let external_api;
|
||||
let apikey;
|
||||
|
||||
let URLs = [];
|
||||
const URLs: string[] = [];
|
||||
let code;
|
||||
|
||||
// Find URL with API Key
|
||||
const srcs = $('head')
|
||||
.find('script')
|
||||
.map(function () {
|
||||
const src = $(this).attr('src');
|
||||
if (src in URLs) {
|
||||
return null;
|
||||
}
|
||||
$('head script[src]').each((_, el) => {
|
||||
const src = $(el).attr('src')!;
|
||||
if (!URLs.includes(src)) {
|
||||
URLs.push(src);
|
||||
})
|
||||
.toArray();
|
||||
for (let src of URLs) {
|
||||
}
|
||||
});
|
||||
|
||||
for (const src of URLs) {
|
||||
const script = await fetchApi(`${this.site}${src}`);
|
||||
const raw = await script.text();
|
||||
if (raw.includes('sb_publishable')) {
|
||||
@@ -176,9 +188,14 @@ class Genesis implements Plugin.PluginBase {
|
||||
}
|
||||
}
|
||||
|
||||
const path = `${external_api}/rest/v1/chapters?select=id,chapter_title,chapter_number,chapter_content,status,novel&id=eq.${id}&status=eq.released`;
|
||||
const path = `${external_api}/rest/v1/chapters`;
|
||||
const search = new URLSearchParams({
|
||||
select: 'id,chapter_title,chapter_number,chapter_content,status,novel',
|
||||
id: `eq.${id}`,
|
||||
status: 'eq.released',
|
||||
});
|
||||
|
||||
const chQuery = await fetchApi(path, {
|
||||
const chQuery = await fetchApi(`${path}?${search}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
// Cookie: 'csrftoken=' + csrftoken,
|
||||
@@ -197,12 +214,21 @@ class Genesis implements Plugin.PluginBase {
|
||||
pageNo: number,
|
||||
): Promise<Plugin.SourceNovel[]> {
|
||||
if (pageNo !== 1) return [];
|
||||
// TODO: Figure out how to search
|
||||
const url = `${this.site}/api/novels/search?title=${encodeURIComponent(searchTerm)}`;
|
||||
const json = await fetchApi(url).then(r => r.json());
|
||||
return this.parseNovelJSON(json);
|
||||
// Since only 26 novels, fetch all the novels
|
||||
// then filter out the novels which match the criteria
|
||||
const novels = await this.parseNovelJSON();
|
||||
const query = this.normalize(searchTerm);
|
||||
|
||||
return novels.filter(novel => this.normalize(novel.name).includes(query));
|
||||
}
|
||||
|
||||
// grabbed from Witch Cult Translations
|
||||
private normalize(str: string) {
|
||||
return str.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
// due to the low amount of novels, using filters kinda overkill
|
||||
// unless we apply filters to cached results
|
||||
filters = {
|
||||
sort: {
|
||||
label: 'Sort Results By',
|
||||
@@ -213,32 +239,43 @@ class Genesis implements Plugin.PluginBase {
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
storyStatus: {
|
||||
label: 'Status',
|
||||
value: 'All',
|
||||
options: [
|
||||
{ label: 'All', value: 'All' },
|
||||
{ label: 'Ongoing', value: 'Ongoing' },
|
||||
{ label: 'Completed', value: 'Completed' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
// storyStatus: {
|
||||
// label: 'Status',
|
||||
// value: 'All',
|
||||
// options: [
|
||||
// { label: 'All', value: 'All' },
|
||||
// { label: 'Ongoing', value: 'Ongoing' },
|
||||
// { label: 'Completed', value: 'Completed' },
|
||||
// ],
|
||||
// type: FilterTypes.Picker,
|
||||
// },
|
||||
genres: {
|
||||
label: 'Genres',
|
||||
value: [],
|
||||
options: [
|
||||
{ label: 'Action', value: 'Action' },
|
||||
{ label: 'Comedy', value: 'Comedy' },
|
||||
{ label: 'Drama', value: 'Drama' },
|
||||
{ label: 'Fantasy', value: 'Fantasy' },
|
||||
{ label: 'Harem', value: 'Harem' },
|
||||
{ label: 'Martial Arts', value: 'Martial Arts' },
|
||||
{ label: 'Modern', value: 'Modern' },
|
||||
{ label: 'Mystery', value: 'Mystery' },
|
||||
{ label: 'Psychological', value: 'Psychological' },
|
||||
{ label: 'Romance', value: 'Romance' },
|
||||
{ label: 'Slice of life', value: 'Slice of Life' },
|
||||
{ label: 'Tragedy', value: 'Tragedy' },
|
||||
{ 'label': 'Academy', 'value': '21' },
|
||||
{ 'label': 'Action', 'value': '1' },
|
||||
{ 'label': 'Adventure', 'value': '15' },
|
||||
{ 'label': 'Calm Protagonist', 'value': '22' },
|
||||
{ 'label': 'Comedy', 'value': '2' },
|
||||
{ 'label': 'Cultivation', 'value': '25' },
|
||||
{ 'label': 'Drama', 'value': '3' },
|
||||
{ 'label': 'Fantasy', 'value': '5' },
|
||||
{ 'label': 'Harem', 'value': '11' },
|
||||
{ 'label': 'Idol', 'value': '20' },
|
||||
{ 'label': 'Martial Arts', 'value': '6' },
|
||||
{ 'label': 'Modern', 'value': '4' },
|
||||
{ 'label': 'Modern Fantasy', 'value': '27' },
|
||||
{ 'label': 'Mystery', 'value': '8' },
|
||||
{ 'label': 'Psychological', 'value': '10' },
|
||||
{ 'label': 'Romance', 'value': '9' },
|
||||
{ 'label': 'School Life', 'value': '13' },
|
||||
{ 'label': 'Sci-fi', 'value': '24' },
|
||||
{ 'label': 'Slice of Life', 'value': '7' },
|
||||
{ 'label': 'Supernatural', 'value': '14' },
|
||||
{ 'label': 'Tragedy', 'value': '12' },
|
||||
{ 'label': 'Transmigration', 'value': '23' },
|
||||
{ 'label': 'Yandere', 'value': '26' },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
@@ -246,3 +283,30 @@ class Genesis implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
export default new Genesis();
|
||||
|
||||
type NovelJSON = {
|
||||
id: string;
|
||||
novel_title: string;
|
||||
abbreviation: string;
|
||||
cover: string;
|
||||
synopsis?: string;
|
||||
author?: string;
|
||||
serialization?: string;
|
||||
genres?: {
|
||||
genres_id?: {
|
||||
id?: number;
|
||||
label?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
type ChapterJSON = {
|
||||
data: {
|
||||
chapters: {
|
||||
id: string;
|
||||
chapter_number: number;
|
||||
chapter_title: string;
|
||||
isUnlocked: boolean;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,8 +8,8 @@ class IndraTranslations implements Plugin.PluginBase {
|
||||
id = 'indratranslations';
|
||||
name = 'Indra Translations';
|
||||
site = 'https://indratranslations.com';
|
||||
version = '1.2.0';
|
||||
// icon = 'src/en/indratranslations/icon.png';
|
||||
version = '1.2.1';
|
||||
icon = 'src/en/indratranslations/icon.png';
|
||||
// customCSS = 'src/en/indratranslations/customCSS.css';
|
||||
// (optional) Add these files to the repo and uncomment the lines above if you want an icon/custom CSS.
|
||||
|
||||
@@ -101,12 +101,12 @@ class IndraTranslations implements Plugin.PluginBase {
|
||||
$(el).find('a[href*="/series/"]').first() ||
|
||||
$(el).find('.tab-thumb a[href*="/series/"]').first();
|
||||
|
||||
const href = (a as any).attr?.('href') || '';
|
||||
const href = a.attr?.('href') || '';
|
||||
const title =
|
||||
this.clean($(el).find('.post-title a').text()) ||
|
||||
this.clean($(el).find('.tab-summary .post-title a').text()) ||
|
||||
this.clean((a as any).attr?.('title')) ||
|
||||
this.clean((a as any).text?.());
|
||||
this.clean(a.attr?.('title')) ||
|
||||
this.clean(a.text?.());
|
||||
|
||||
const img =
|
||||
$(el).find('img').attr('data-src') ||
|
||||
|
||||
@@ -18,7 +18,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
// showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
@@ -40,7 +40,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
throw new Error('Failed to load novels, try opening in webview.');
|
||||
}
|
||||
|
||||
return data.stories.map((novel: any) => {
|
||||
return data.stories.map((novel: InkittNovel) => {
|
||||
return {
|
||||
name: novel.title,
|
||||
path: this.getPath(novel),
|
||||
@@ -52,7 +52,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
});
|
||||
}
|
||||
|
||||
getPath(novel: any) {
|
||||
getPath(novel: InkittNovel) {
|
||||
return (novel.category_one || novel.genres[0]) + '/' + novel.id;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
novel.author = loadedCheerio('dl > dd > a.author-link').text();
|
||||
|
||||
novel.genres = loadedCheerio('dd.genres > a')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.map((_, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(', ');
|
||||
const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd')
|
||||
@@ -81,12 +81,12 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
const apiReq = await fetchApi(
|
||||
this.site + `/api/stories/${novelPath.split('/')[1]}`,
|
||||
);
|
||||
const apiData = await apiReq.json();
|
||||
const apiData = (await apiReq.json()) as InkittApi;
|
||||
novel.cover = apiData.vertical_cover.url;
|
||||
|
||||
novel.summary = loadedCheerio('p.story-summary').text();
|
||||
|
||||
novel.chapters = apiData.chapters.map((c: any) => {
|
||||
novel.chapters = apiData.chapters.map((c: InkittChapter) => {
|
||||
return {
|
||||
name: c.name,
|
||||
path: novelPath + '/chapters/' + c.chapter_number,
|
||||
@@ -119,7 +119,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
throw new Error('Failed to search novels, try opening in webview.');
|
||||
}
|
||||
|
||||
return data.stories.map((novel: any) => {
|
||||
return data.stories.map((novel: InkittNovel) => {
|
||||
return {
|
||||
name: novel.title,
|
||||
path: this.getPath(novel),
|
||||
@@ -131,8 +131,8 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
});
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + '/stories/' + path;
|
||||
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
// this.site + '/stories/' + path;
|
||||
|
||||
filters = {
|
||||
genres: {
|
||||
@@ -160,3 +160,31 @@ class InkittPlugin implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
export default new InkittPlugin();
|
||||
|
||||
// Typings inferred from usage, no actual investigation done
|
||||
// TODO: change layout
|
||||
type InkittNovel = {
|
||||
id: number;
|
||||
title: string;
|
||||
category_one?: string;
|
||||
genres: string[];
|
||||
cover: {
|
||||
url: string;
|
||||
};
|
||||
vertical_cover: {
|
||||
url: string;
|
||||
iphone: string;
|
||||
};
|
||||
};
|
||||
|
||||
type InkittChapter = {
|
||||
name: string;
|
||||
chapter_number: number;
|
||||
};
|
||||
|
||||
type InkittApi = {
|
||||
vertical_cover: {
|
||||
url: string;
|
||||
};
|
||||
chapters: InkittChapter[];
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
name = 'iNovelTranslation';
|
||||
icon = 'src/en/inoveltranslation/icon.png';
|
||||
site = 'https://inoveltranslation.com';
|
||||
version = '1.0.1';
|
||||
version = '1.0.2';
|
||||
filters: Filters | undefined = undefined;
|
||||
|
||||
pluginSettings = {
|
||||
@@ -22,7 +22,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
},
|
||||
};
|
||||
|
||||
// Optimized stealth headers to mirror a real browser environment
|
||||
private readonly HEADERS = {
|
||||
'Accept':
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
@@ -33,20 +32,16 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
};
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/api/novels?limit=50&page=${pageNo}`;
|
||||
const result = await fetchApi(url, { headers: this.HEADERS }).then(r =>
|
||||
r.json(),
|
||||
);
|
||||
const result: ApiResponse<NovelData> = await fetchApi(url, {
|
||||
headers: this.HEADERS,
|
||||
}).then(r => r.json());
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
if (result.docs) {
|
||||
result.docs.forEach((doc: any) => {
|
||||
result.docs.forEach(doc => {
|
||||
novels.push({
|
||||
name: doc.title,
|
||||
path: `/novels/${doc.id}`,
|
||||
@@ -61,27 +56,24 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const id = novelPath.split('/').pop();
|
||||
const novelUrl = `${this.site}/api/novels/${id}?depth=1`;
|
||||
const novelData = await fetchApi(novelUrl, { headers: this.HEADERS }).then(
|
||||
r => r.json(),
|
||||
);
|
||||
|
||||
const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`;
|
||||
const chaptersData = await fetchApi(chaptersUrl, {
|
||||
const novelData: NovelData = await fetchApi(novelUrl, {
|
||||
headers: this.HEADERS,
|
||||
}).then(r => r.json());
|
||||
|
||||
const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`;
|
||||
const chaptersData: ApiResponse<ChapterData> = await fetchApi(chaptersUrl, {
|
||||
headers: this.HEADERS,
|
||||
}).then(r => r.json());
|
||||
|
||||
// Extract status
|
||||
const status =
|
||||
novelData.publication === 'completed'
|
||||
? NovelStatus.Completed
|
||||
: NovelStatus.Ongoing;
|
||||
|
||||
// Extract genres (tags)
|
||||
const genres = novelData.tags
|
||||
? novelData.tags.map((tag: any) => tag.name).join(', ')
|
||||
? novelData.tags.map(tag => tag.name).join(', ')
|
||||
: '';
|
||||
|
||||
// Process summary (Lexical JSON from API 'sypnosis') - use Plain Text for App Synopsis
|
||||
let summary = '';
|
||||
if (novelData.sypnosis && novelData.sypnosis.root) {
|
||||
summary = this.lexicalToText(novelData.sypnosis.root);
|
||||
@@ -103,7 +95,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
const hideLocked = storage.get('hideLocked');
|
||||
|
||||
if (chaptersData.docs) {
|
||||
chaptersData.docs.forEach((doc: any) => {
|
||||
chaptersData.docs.forEach(doc => {
|
||||
const isLocked = doc.tier !== null;
|
||||
if (isLocked && hideLocked) {
|
||||
return;
|
||||
@@ -121,7 +113,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure chapters are sorted numerically
|
||||
novel.chapters = chapters.sort(
|
||||
(a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0),
|
||||
);
|
||||
@@ -129,7 +120,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
// Artificial delay to prevent aggressive rate limiting
|
||||
await new Promise(res => setTimeout(res, 1500));
|
||||
|
||||
const rscHeader = { ...this.HEADERS, rsc: '1' };
|
||||
@@ -139,8 +129,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
response = await fetchApi(this.site + chapterPath, {
|
||||
headers: rscHeader,
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error(`Network error: ${e.message}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Network error: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
@@ -155,7 +145,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
throw new Error('Server returned empty data.');
|
||||
}
|
||||
|
||||
// 1. Proactive Cloudflare Detection
|
||||
if (
|
||||
rscText.includes('cf-browser-verification') ||
|
||||
rscText.includes('cf-challenge') ||
|
||||
@@ -192,7 +181,12 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
let startIndex = rscText.lastIndexOf('{', sigIndex);
|
||||
|
||||
// Check for "content" or "root" before to find the start of the relevant object
|
||||
const contextKeys = ['"content"', '\\"content\\"', '"root"', '\\"root\\"'];
|
||||
const contextKeys = [
|
||||
'"content"',
|
||||
'\\"content\\"',
|
||||
'"root"',
|
||||
'\\"root\\"',
|
||||
];
|
||||
for (const key of contextKeys) {
|
||||
const keyIndex = rscText.lastIndexOf(key, sigIndex);
|
||||
if (keyIndex !== -1 && keyIndex > startIndex - 50) {
|
||||
@@ -236,8 +230,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
|
||||
if (jsonStr) {
|
||||
try {
|
||||
// Attempt to parse directly (if it's unescaped RSC)
|
||||
let safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(safeJson);
|
||||
@@ -246,18 +240,22 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
const cleanJson = jsonStr
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||
parsedData = JSON.parse(cleanJson);
|
||||
}
|
||||
|
||||
let lexicalRoot = parsedData.root || parsedData.content?.root || parsedData;
|
||||
const lexicalRoot =
|
||||
parsedData.root || parsedData.content?.root || parsedData;
|
||||
if (lexicalRoot && lexicalRoot.children) {
|
||||
return this.lexicalToHtml(lexicalRoot);
|
||||
}
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
// Fallback to regex text extraction if JSON parsing fails
|
||||
let fallbackHtml = '';
|
||||
const textMatches = jsonStr.match(/\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g);
|
||||
const textMatches = jsonStr.match(
|
||||
/\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g,
|
||||
);
|
||||
if (textMatches && textMatches.length > 0) {
|
||||
textMatches.forEach(m => {
|
||||
let text = m.match(/: ?"?(.*?)"?$/)?.[1] || '';
|
||||
@@ -296,11 +294,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively converts Lexical JSON nodes to HTML strings.
|
||||
* Suitable for Chapter Content.
|
||||
*/
|
||||
private lexicalToHtml(node: any): string {
|
||||
private lexicalToHtml(node: LexicalNode): string {
|
||||
let html = '';
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
@@ -308,8 +302,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
html += `<p>${this.lexicalToHtml(child)}</p>`;
|
||||
} else if (child.type === 'text') {
|
||||
let text = child.text || '';
|
||||
if (child.format & 1) text = `<b>${text}</b>`;
|
||||
if (child.format & 2) text = `<i>${text}</i>`;
|
||||
if (child.format && child.format & 1) text = `<b>${text}</b>`;
|
||||
if (child.format && child.format & 2) text = `<i>${text}</i>`;
|
||||
html += text;
|
||||
} else if (child.type === 'list') {
|
||||
const tag = child.listType === 'number' ? 'ol' : 'ul';
|
||||
@@ -327,11 +321,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively converts Lexical JSON nodes to Plain Text with newlines.
|
||||
* Suitable for Novel Synopsis in the app.
|
||||
*/
|
||||
private lexicalToText(node: any): string {
|
||||
private lexicalToText(node: LexicalNode): string {
|
||||
let textOut = '';
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
@@ -356,14 +346,14 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
const url = `${this.site}/api/novels?where[title][contains]=${encodeURIComponent(
|
||||
searchTerm,
|
||||
)}&limit=50&page=${pageNo}`;
|
||||
const result = await fetchApi(url, { headers: this.HEADERS }).then(r =>
|
||||
r.json(),
|
||||
);
|
||||
const result: ApiResponse<NovelData> = await fetchApi(url, {
|
||||
headers: this.HEADERS,
|
||||
}).then(r => r.json());
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
if (result.docs) {
|
||||
result.docs.forEach((doc: any) => {
|
||||
result.docs.forEach(doc => {
|
||||
novels.push({
|
||||
name: doc.title,
|
||||
path: `/novels/${doc.id}`,
|
||||
@@ -374,8 +364,43 @@ class INovelTranslation implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) => this.site + path;
|
||||
}
|
||||
|
||||
export default new INovelTranslation();
|
||||
|
||||
type LexicalNode = {
|
||||
type: string;
|
||||
text?: string;
|
||||
children?: LexicalNode[];
|
||||
format?: number;
|
||||
listType?: string;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
type NovelData = {
|
||||
id: string;
|
||||
title: string;
|
||||
cover?: {
|
||||
url: string;
|
||||
};
|
||||
author?: {
|
||||
name: string;
|
||||
};
|
||||
publication?: string;
|
||||
tags?: { name: string }[];
|
||||
sypnosis?: {
|
||||
root: LexicalNode;
|
||||
};
|
||||
};
|
||||
|
||||
type ChapterData = {
|
||||
id: string;
|
||||
title?: string;
|
||||
chapter: number;
|
||||
tier: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ApiResponse<T> = {
|
||||
docs: T[];
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ class LeafStudio implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
// { filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + 'novels';
|
||||
if (page > 1) {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Filters } from '@libs/filterInputs';
|
||||
import { load as loadCheerio } from 'cheerio';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
// import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
|
||||
// import { storage, localStorage, sessionStorage } from '@libs/storage';
|
||||
// import { encode, decode } from 'urlencode';
|
||||
// import dayjs from 'dayjs';
|
||||
import { Parser } from 'htmlparser2';
|
||||
// import { Parser } from 'htmlparser2';
|
||||
|
||||
class LNTPlugin implements Plugin.PluginBase {
|
||||
id = 'lightnoveltranslations';
|
||||
@@ -26,7 +26,7 @@ class LNTPlugin implements Plugin.PluginBase {
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
// filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + 'read/';
|
||||
@@ -193,11 +193,11 @@ class LNTPlugin implements Plugin.PluginBase {
|
||||
novels.push(tempNovel);
|
||||
});
|
||||
|
||||
type SearchEntry = {
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
permalink: string;
|
||||
};
|
||||
// type SearchEntry = {
|
||||
// title: string;
|
||||
// thumbnail: string;
|
||||
// permalink: string;
|
||||
// };
|
||||
return novels;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class LnMTLPlugin implements Plugin.PagePlugin {
|
||||
name = 'LnMTL';
|
||||
icon = 'src/en/lnmtl/icon.png';
|
||||
site = 'https://lnmtl.com/';
|
||||
version = '2.1.0';
|
||||
version = '2.1.1';
|
||||
|
||||
async sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -126,10 +126,12 @@ class LnMTLPlugin implements Plugin.PagePlugin {
|
||||
ontext(data) {
|
||||
switch (state) {
|
||||
case ParsingState.InScript:
|
||||
const volume = JSON.parse(
|
||||
data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '',
|
||||
);
|
||||
novel.totalPages = volume.length;
|
||||
{
|
||||
const volume = JSON.parse(
|
||||
data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '',
|
||||
);
|
||||
novel.totalPages = volume.length;
|
||||
}
|
||||
break;
|
||||
case ParsingState.InDescription:
|
||||
summaryParts.push(data.trim());
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { Parser } from 'htmlparser2';
|
||||
|
||||
//has to be here cus this scoping moment
|
||||
// im just gonna disable eslint here instead of reading lol
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const parserData: {
|
||||
inTag?: string;
|
||||
depthRatingWrapper?: number;
|
||||
@@ -45,33 +47,29 @@ class MVLEMPYRPlugin implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<Filters>,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const data = await this.getAllNovels();
|
||||
const filtered = data.filter(novel => {
|
||||
// @ts-ignore
|
||||
for (const genre of filters.genre.value.exclude) {
|
||||
for (const genre of filters.genre.value.exclude || []) {
|
||||
if (novel.genres.includes(genre)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
for (const genre of filters.genre.value.include) {
|
||||
for (const genre of filters.genre.value.include || []) {
|
||||
if (!novel.genres.includes(genre)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
for (const tag of filters.tag.value.exclude) {
|
||||
for (const tag of filters.tag.value.exclude || []) {
|
||||
if (novel.tags.includes(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
for (const tag of filters.tag.value.include) {
|
||||
for (const tag of filters.tag.value.include || []) {
|
||||
if (!novel.tags.includes(tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { storage } from '@libs/storage';
|
||||
class NovelFire implements Plugin.PluginBase {
|
||||
id = 'novelfire';
|
||||
name = 'Novel Fire';
|
||||
version = '1.4.1';
|
||||
version = '1.4.2';
|
||||
icon = 'src/en/novelfire/icon.png';
|
||||
site = 'https://novelfire.net/';
|
||||
webStorageUtilized = true;
|
||||
@@ -294,7 +294,7 @@ class NovelFire implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
novel.genres = $('.categories .property-item')
|
||||
.map((i, el) => $(el).text())
|
||||
.map((_, el) => $(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
@@ -356,7 +356,11 @@ class NovelFire implements Plugin.PluginBase {
|
||||
|
||||
if (post_id && !isNaN(Number(post_id))) {
|
||||
try {
|
||||
const chapters = await this.getAllChapters(novelPath, post_id, page);
|
||||
const chapters = await this.getAllChapters(
|
||||
novelPath,
|
||||
String(post_id),
|
||||
page,
|
||||
);
|
||||
return { chapters };
|
||||
} catch (e) {
|
||||
// Fallback to scraping if AJAX fails
|
||||
@@ -373,7 +377,7 @@ class NovelFire implements Plugin.PluginBase {
|
||||
const loadedCheerio = load(body);
|
||||
|
||||
const chapters = loadedCheerio('.chapter-list li')
|
||||
.map((index, ele) => {
|
||||
.map((_, ele) => {
|
||||
const chapterName =
|
||||
loadedCheerio(ele).find('a').attr('title') || 'No Title Found';
|
||||
const chapterPath = loadedCheerio(ele).find('a').attr('href');
|
||||
|
||||
+90
-31
@@ -3,13 +3,14 @@ import { Plugin } from '@/types/plugin';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { Filters, FilterTypes } from '@/types/filters';
|
||||
|
||||
class NovelHi implements Plugin.PluginBase {
|
||||
id = 'novelhi';
|
||||
name = 'NovelHi';
|
||||
icon = 'src/en/novelhi/icon.png';
|
||||
site = 'https://novelhi.com/';
|
||||
version = '1.0.0';
|
||||
version = '1.1.0';
|
||||
|
||||
// flag indicates whether access to LocalStorage, SesesionStorage is required.
|
||||
webStorageUtilized?: boolean;
|
||||
@@ -17,8 +18,25 @@ class NovelHi implements Plugin.PluginBase {
|
||||
// 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 => ({
|
||||
private async getNovels(
|
||||
pageNo: number,
|
||||
keyword = '',
|
||||
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||
): Promise<CachedNovel[]> {
|
||||
const params = new URLSearchParams({
|
||||
curr: pageNo.toString(),
|
||||
limit: '10',
|
||||
keyword,
|
||||
...(filters?.genres.value && { 'bookGenres[]': filters.genres.value }),
|
||||
...(filters?.order.value && { bookStatus: filters.order.value }),
|
||||
...(filters?.time.value && { updatePeriod: filters.time.value }),
|
||||
});
|
||||
|
||||
const url = `${this.site}book/searchByPageInShelf?${params}`;
|
||||
const response = await fetchApi(url);
|
||||
const json: ApiResponse = await response.json();
|
||||
|
||||
const novels: CachedNovel[] = json.data.list.map(item => ({
|
||||
name: item.bookName,
|
||||
path: `s/${item.simpleName}`,
|
||||
cover: item.picUrl || defaultCover,
|
||||
@@ -28,30 +46,19 @@ class NovelHi implements Plugin.PluginBase {
|
||||
genres: item.genres.map(g => g.genreName).join(', '),
|
||||
}));
|
||||
|
||||
// Manage cache size
|
||||
this.loadedNovelCache.push(...ret);
|
||||
this.loadedNovelCache.push(...novels);
|
||||
if (this.loadedNovelCache.length > 100) {
|
||||
this.loadedNovelCache = this.loadedNovelCache.slice(-100);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return novels;
|
||||
}
|
||||
|
||||
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);
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<CachedNovel[]> {
|
||||
return this.getNovels(pageNo, '', filters);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
@@ -130,19 +137,71 @@ class NovelHi implements Plugin.PluginBase {
|
||||
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);
|
||||
): Promise<CachedNovel[]> {
|
||||
return this.getNovels(pageNo, searchTerm);
|
||||
}
|
||||
|
||||
filters = {
|
||||
genres: {
|
||||
label: 'Genres',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Action', value: 'action' },
|
||||
{ label: 'Adventure', value: 'adventure' },
|
||||
{ label: 'Comedy', value: 'comedy' },
|
||||
{ label: 'Light Novel', value: 'light-novel' },
|
||||
{ label: 'Fanfiction', value: 'fanfiction' },
|
||||
{ label: 'Fantasy', value: 'fantasy' },
|
||||
{ label: 'Game', value: 'game' },
|
||||
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||
{ label: 'Harem', value: 'harem' },
|
||||
{ label: 'Historical', value: 'historical' },
|
||||
{ label: 'Horror', value: 'horror' },
|
||||
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||
{ label: 'Mature', value: 'mature' },
|
||||
{ label: 'Mecha', value: 'mecha' },
|
||||
{ label: 'Military', value: 'military' },
|
||||
{ label: 'Mystery', value: 'mystery' },
|
||||
{ label: 'Romance', value: 'romance' },
|
||||
{ label: 'School Life', value: 'school-life' },
|
||||
{ label: 'Sci-fi', value: 'sci-fi' },
|
||||
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||||
{ label: 'Sports', value: 'sports' },
|
||||
{ label: 'Supernatural', value: 'supernatural' },
|
||||
{ label: 'Tragedy', value: 'tragedy' },
|
||||
{ label: 'Urban Life', value: 'urban-life' },
|
||||
{ label: 'Wuxia', value: 'wuxia' },
|
||||
{ label: 'Xianxia', value: 'xianxia' },
|
||||
{ label: 'Xuanhuan', value: 'xuanhuan' },
|
||||
{ label: 'Yaoi', value: 'yaoi' },
|
||||
{ label: 'Yuri', value: 'yuri' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
order: {
|
||||
label: 'Status',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Ongoing', value: '0' },
|
||||
{ label: 'Completed', value: '1' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
time: {
|
||||
label: 'Update Period',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: '3 Days', value: '3' },
|
||||
{ label: '7 Days', value: '7' },
|
||||
{ label: '15 Days', value: '15' },
|
||||
{ label: '30 Days', value: '30' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelHi();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { storage } from '@libs/storage';
|
||||
class Novelight implements Plugin.PagePlugin {
|
||||
id = 'novelight';
|
||||
name = 'Novelight';
|
||||
version = '1.1.4';
|
||||
version = '1.1.5';
|
||||
icon = 'src/en/novelight/icon.png';
|
||||
site = 'https://novelight.net/';
|
||||
|
||||
@@ -184,7 +184,9 @@ class Novelight implements Plugin.PagePlugin {
|
||||
parseHTML(ele)('.date').text().trim(),
|
||||
'DD.MM.YYYY',
|
||||
).toISOString();
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
// linter happy
|
||||
}
|
||||
|
||||
const chapterName = isLocked ? '🔒 ' + title : title;
|
||||
let chapterUrl = ele.attribs.href;
|
||||
|
||||
@@ -342,23 +342,26 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
chapterContent = loadedCheerio('.content').html()!;
|
||||
break;
|
||||
}
|
||||
// Last edited in 0.9.0 by Batorian - 19/03/2025
|
||||
// Last edited in 0.9.0 by Batorian - 19/03/2025,
|
||||
// remove any typing in 0.9.9 by K1ngfish3r - 04/05/2026, remove this comment if no issues
|
||||
case 'genesistudio': {
|
||||
const url = `${chapterPath}/__data.json?x-sveltekit-invalidated=001`;
|
||||
try {
|
||||
// Fetch the chapter's data in JSON format
|
||||
const json = await fetchApi(url).then(r => r.json());
|
||||
const json = (await fetchApi(url).then(r => r.json())) as {
|
||||
nodes: { type: string; data: Record<string, unknown> }[];
|
||||
};
|
||||
const nodes = json.nodes;
|
||||
const data = nodes
|
||||
.filter((node: { type: string }) => node.type === 'data')
|
||||
.map((node: { data: any }) => node.data)[0];
|
||||
.filter(node => node.type === 'data')
|
||||
.map(node => node.data)[0];
|
||||
// Look for chapter container with required fields
|
||||
const contentKey = 'content';
|
||||
const notesKey = 'notes';
|
||||
const footnotesKey = 'footnotes';
|
||||
// Iterate over each property in data to find chapter containers
|
||||
for (const key in data) {
|
||||
const mapping = data[key];
|
||||
const mapping = data[key] as Record<string, number>;
|
||||
// Check container for keys that match the required fields
|
||||
if (
|
||||
mapping &&
|
||||
@@ -368,9 +371,11 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
footnotesKey in mapping
|
||||
) {
|
||||
// Retrieve the chapter's content, notes, and footnotes using the mapping.
|
||||
const content = data[mapping[contentKey]];
|
||||
const notes = data[mapping[notesKey]];
|
||||
const footnotes = data[mapping[footnotesKey]];
|
||||
const content = data[String(mapping[contentKey])] as string;
|
||||
const notes = data[String(mapping[notesKey])] as string;
|
||||
const footnotes = data[String(mapping[footnotesKey])] as
|
||||
| string
|
||||
| undefined;
|
||||
// Combine the parts with appropriate formatting
|
||||
chapterText =
|
||||
content +
|
||||
|
||||
+12
-12
@@ -6,7 +6,7 @@ import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
class PawRead implements Plugin.PluginBase {
|
||||
id = 'pawread';
|
||||
name = 'PawRead';
|
||||
version = '2.1.0';
|
||||
version = '2.1.1';
|
||||
icon = 'src/en/pawread/icon.png';
|
||||
site = 'https://m.pawread.com/';
|
||||
|
||||
@@ -171,13 +171,15 @@ class PawRead implements Plugin.PluginBase {
|
||||
break;
|
||||
case ParsingState.ChapterTime:
|
||||
if (text?.includes('Advanced')) return;
|
||||
const releaseDate = text.split('.').map(x => Number(x));
|
||||
if (releaseDate.length === 3) {
|
||||
tempChapter.releaseTime = new Date(
|
||||
releaseDate[0],
|
||||
releaseDate[1] - 1,
|
||||
releaseDate[2],
|
||||
).toISOString();
|
||||
{
|
||||
const releaseDate = text.split('.').map(x => Number(x));
|
||||
if (releaseDate.length === 3) {
|
||||
tempChapter.releaseTime = new Date(
|
||||
releaseDate[0],
|
||||
releaseDate[1] - 1,
|
||||
releaseDate[2],
|
||||
).toISOString();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -251,9 +253,7 @@ class PawRead implements Plugin.PluginBase {
|
||||
let state: ParsingState = ParsingState.Idle;
|
||||
const chapterHtml: string[] = [];
|
||||
|
||||
type EscapeChar = '&' | '<' | '>' | '"' | "'" | ' ';
|
||||
const escapeRegex = /[&<>"' ]/g;
|
||||
const escapeMap: Record<EscapeChar, string> = {
|
||||
const escapeMap: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
@@ -262,7 +262,7 @@ class PawRead implements Plugin.PluginBase {
|
||||
' ': ' ',
|
||||
};
|
||||
const escapeHtml = (text: string): string =>
|
||||
text.replace(escapeRegex, char => escapeMap[char as EscapeChar]);
|
||||
text.replace(/[&<>"'\xA0]/g, char => escapeMap[char]);
|
||||
|
||||
const parser = new Parser({
|
||||
onopentag(name, attribs) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Plugin } from '@/types/plugin';
|
||||
import { Filters } from '@libs/filterInputs';
|
||||
import { load as loadCheerio } from 'cheerio';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelItem } from '../../test_web/static/js';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
type FuzzySearchOptions = {
|
||||
@@ -164,7 +163,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
name = 'Re:Library';
|
||||
icon = 'src/en/relibrary/icon.png';
|
||||
site = 'https://re-library.com';
|
||||
version = '1.0.2';
|
||||
version = '1.0.3';
|
||||
imageRequestInit: Plugin.ImageRequestInit = {
|
||||
headers: {
|
||||
Referer: 'https://re-library.com/',
|
||||
@@ -183,7 +182,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
loadedCheerio('.entry-content > ol > li').each((_i, el) => {
|
||||
const novel: Partial<NovelItem> = {};
|
||||
const novel: Partial<Plugin.NovelItem> = {};
|
||||
novel.name = loadedCheerio(el).find('h3 > a').text();
|
||||
novel.path = loadedCheerio(el)
|
||||
.find('table > tbody > tr > td > a')
|
||||
@@ -197,9 +196,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
.find('table > tbody > tr > td > a > img')
|
||||
.attr('src') ||
|
||||
defaultCover;
|
||||
if (novel.path.startsWith(this.site)) {
|
||||
novel.path = novel.path.slice(this.site.length);
|
||||
}
|
||||
novel.path = new URL(novel.path, this.site).pathname;
|
||||
novels.push(novel as Plugin.NovelItem);
|
||||
});
|
||||
return novels;
|
||||
@@ -224,9 +221,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
.find('.entry-content > table > tbody > tr > td > a >img')
|
||||
.attr('src') ||
|
||||
defaultCover;
|
||||
if (novel.path.startsWith(this.site)) {
|
||||
novel.path = novel.path.slice(this.site.length);
|
||||
}
|
||||
novel.path = new URL(novel.path, this.site).pathname;
|
||||
novels.push(novel as Plugin.NovelItem);
|
||||
});
|
||||
return novels;
|
||||
@@ -257,7 +252,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
// synopis: .entry-content > div.su-box > div.su-box-content
|
||||
// chapters: .entry-content > div.su-accordion <then> li.page_item[]
|
||||
|
||||
const result = await fetchApi(`${this.site}/${novelPath}`);
|
||||
const result = await fetchApi(`${this.site}${novelPath}`);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
@@ -330,18 +325,11 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
.find('li > a')
|
||||
.each((_i2, chap_el) => {
|
||||
chapter_idx += 1;
|
||||
let chap_path = loadedCheerio(chap_el).attr('href')?.trim();
|
||||
if (
|
||||
loadedCheerio(chap_el).text() === undefined ||
|
||||
chap_path === undefined
|
||||
)
|
||||
return;
|
||||
if (chap_path.startsWith(this.site)) {
|
||||
chap_path = chap_path.slice(this.site.length);
|
||||
}
|
||||
const chap_path = loadedCheerio(chap_el).attr('href')?.trim();
|
||||
if (loadedCheerio(chap_el).text() === undefined || !chap_path) return;
|
||||
chapters.push({
|
||||
name: loadedCheerio(chap_el).text(),
|
||||
path: chap_path,
|
||||
path: new URL(chap_path, this.site).pathname,
|
||||
chapterNumber: chapter_idx,
|
||||
// we KNOW that we can't get the released time (at least without any additional fetches), so set it to null purposfully
|
||||
releaseTime: null,
|
||||
@@ -355,7 +343,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
// parse chapter text here
|
||||
const result = await fetchApi(`${this.site}/${chapterPath}`);
|
||||
const result = await fetchApi(`${this.site}${chapterPath}`);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
@@ -408,10 +396,11 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
||||
|
||||
loadedCheerio('article article').each((_i, el) => {
|
||||
const e = loadedCheerio(el);
|
||||
if (e.find('a').attr('href') && e.find('a').text()) {
|
||||
const href = e.find('a').attr('href');
|
||||
if (href && e.find('a').text()) {
|
||||
novels.push({
|
||||
name: e.find('h4').text(),
|
||||
path: e.find('a').attr('href')?.replace(this.site, '') || '',
|
||||
path: new URL(href, this.site).pathname,
|
||||
cover:
|
||||
e.find('img').attr('data-cfsrc') ||
|
||||
e.find('img').attr('src') ||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { storage } from '@libs/storage';
|
||||
class RoyalRoad implements Plugin.PluginBase {
|
||||
id = 'royalroad';
|
||||
name = 'Royal Road';
|
||||
version = '2.3.0';
|
||||
version = '2.3.1';
|
||||
icon = 'src/en/royalroad/icon.png';
|
||||
site = 'https://www.royalroad.com/';
|
||||
|
||||
@@ -415,18 +415,20 @@ class RoyalRoad implements Plugin.PluginBase {
|
||||
depth--;
|
||||
return;
|
||||
case ParsingState.InNote:
|
||||
const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`;
|
||||
const notesHtml = notesHtmlParts.join('').trim();
|
||||
const fullNote = `<div class="${noteClass}">${notesHtml}</div>`;
|
||||
if (isBeforeChapter) {
|
||||
beforeNotesParts.push(fullNote);
|
||||
} else {
|
||||
afterNotesParts.push(fullNote);
|
||||
{
|
||||
const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`;
|
||||
const notesHtml = notesHtmlParts.join('').trim();
|
||||
const fullNote = `<div class="${noteClass}">${notesHtml}</div>`;
|
||||
if (isBeforeChapter) {
|
||||
beforeNotesParts.push(fullNote);
|
||||
} else {
|
||||
afterNotesParts.push(fullNote);
|
||||
}
|
||||
notesHtmlParts.length = 0;
|
||||
state = ParsingState.Idle;
|
||||
stateDepth = 0;
|
||||
depth--;
|
||||
}
|
||||
notesHtmlParts.length = 0;
|
||||
state = ParsingState.Idle;
|
||||
stateDepth = 0;
|
||||
depth--;
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
|
||||
+46
-50
@@ -4,26 +4,24 @@ import { defaultCover } from '@libs/defaultCover';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import dayjs from 'dayjs';
|
||||
import dayjs, { ManipulateType } from 'dayjs';
|
||||
|
||||
class VyNovel implements Plugin.PluginBase {
|
||||
id = 'vynovel';
|
||||
name = 'VyNovel';
|
||||
site = 'https://vynovel.com';
|
||||
version = '1.0.0';
|
||||
version = '1.0.1';
|
||||
icon = 'src/en/vynovel/icon.png';
|
||||
|
||||
async fetchNovels(
|
||||
page: number,
|
||||
{
|
||||
filters,
|
||||
showLatestNovels,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
showLatestNovels?: boolean,
|
||||
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||
searchTerm?: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const data = new URLSearchParams({
|
||||
sort: showLatestNovels ? 'updated_at' : filters?.sort?.value || 'viewed',
|
||||
page,
|
||||
page: page.toString(),
|
||||
});
|
||||
if (searchTerm) data.append('q', searchTerm);
|
||||
|
||||
@@ -33,7 +31,7 @@ class VyNovel implements Plugin.PluginBase {
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="comic-item"] > a').each((index, element) => {
|
||||
loadedCheerio('div[class="comic-item"] > a').each((_, element) => {
|
||||
const name = loadedCheerio(element)
|
||||
.find('div[class="comic-title"]')
|
||||
.text()
|
||||
@@ -52,17 +50,18 @@ class VyNovel implements Plugin.PluginBase {
|
||||
return novels;
|
||||
}
|
||||
|
||||
popularNovels = this.fetchNovels;
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
async popularNovels(
|
||||
page: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const defaultOptions: any = {
|
||||
filters: undefined,
|
||||
showLatestNovels: false,
|
||||
};
|
||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
) {
|
||||
return this.fetchNovels(page, showLatestNovels, filters);
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, page: number) {
|
||||
return this.fetchNovels(page, false, undefined, searchTerm);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
@@ -102,7 +101,7 @@ class VyNovel implements Plugin.PluginBase {
|
||||
chapters.push({
|
||||
name,
|
||||
path: novelPath + '/' + id,
|
||||
releaseTime: this.parseDate(releaseDate?.trim()),
|
||||
releaseTime: this.parseAgoDate(releaseDate),
|
||||
chapterNumber: totalChapters - chapterIndex,
|
||||
});
|
||||
},
|
||||
@@ -122,38 +121,35 @@ class VyNovel implements Plugin.PluginBase {
|
||||
return chapterText || '';
|
||||
}
|
||||
|
||||
parseDate = (date = '') => {
|
||||
if (!date) return null;
|
||||
if (date.includes('ago')) {
|
||||
const [value, type] = date.split(' ');
|
||||
if (!value || !type) return null;
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'minutes': {
|
||||
const minutes = parseInt(value, 10);
|
||||
date = Date.now() - minutes * 60 * 1000;
|
||||
break;
|
||||
}
|
||||
case 'hour':
|
||||
case 'hours': {
|
||||
const hours = parseInt(value, 10);
|
||||
date = Date.now() - hours * 60 * 60 * 1000;
|
||||
break;
|
||||
}
|
||||
case 'day':
|
||||
case 'days': {
|
||||
const days = parseInt(value, 10);
|
||||
date = Date.now() - days * 24 * 60 * 60 * 1000;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log(date);
|
||||
date = undefined;
|
||||
}
|
||||
return dayjs(date).format('LLL');
|
||||
private parseAgoDate(date: string | undefined) {
|
||||
//parseMadaraDate
|
||||
const parsed = dayjs(date);
|
||||
if (date && parsed.isValid()) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || [];
|
||||
const decade = time?.includes('decade'); // dayjs no support, but just in case
|
||||
const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1);
|
||||
const unit = (decade ? 'year' : time) as ManipulateType;
|
||||
|
||||
const validUnits = [
|
||||
'millisecond', // waow
|
||||
'second',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year',
|
||||
];
|
||||
|
||||
if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dayjs().subtract(amount, unit).toISOString();
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + (isNovel ? '/novel/' : '/read/') + path;
|
||||
|
||||
+26
-35
@@ -1,14 +1,14 @@
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||
import { gcm } from '@libs/aes';
|
||||
|
||||
class WTRLAB implements Plugin.PluginBase {
|
||||
id = 'WTRLAB';
|
||||
name = 'WTR-LAB';
|
||||
site = 'https://wtr-lab.com/';
|
||||
version = '1.1.0';
|
||||
version = '1.1.2';
|
||||
icon = 'src/en/wtrlab/icon.png';
|
||||
sourceLang = 'en/';
|
||||
|
||||
@@ -116,14 +116,14 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
const seenIds = new Set();
|
||||
|
||||
const novels: Plugin.NovelItem[] = json.pageProps.series
|
||||
.filter((novel: any) => {
|
||||
.filter((novel: Datum) => {
|
||||
if (seenIds.has(novel.raw_id)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(novel.raw_id);
|
||||
return true;
|
||||
})
|
||||
.map((novel: any) => ({
|
||||
.map((novel: Datum) => ({
|
||||
name: novel.data.title,
|
||||
cover: novel.data.image,
|
||||
path: `${this.sourceLang}serie-${novel.raw_id}/${novel.slug}`,
|
||||
@@ -409,7 +409,7 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
}
|
||||
}
|
||||
|
||||
async getKey($: any): Promise<string> {
|
||||
async getKey($: CheerioAPI): Promise<string> {
|
||||
// Fetch the novel's data in JSON format
|
||||
const searchKey = 'TextEncoder().encode("';
|
||||
|
||||
@@ -426,7 +426,7 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
URLs.push(src);
|
||||
}
|
||||
|
||||
for (let src of URLs) {
|
||||
for (const src of URLs) {
|
||||
const script = await fetchApi(`${this.site}${src}`);
|
||||
const raw = await script.text();
|
||||
index = raw.indexOf(searchKey);
|
||||
@@ -446,7 +446,7 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
async translate(data: string[]): Promise<string[]> {
|
||||
const contained = data.map((line, i) => `<a i=${i}>${line}</a>`);
|
||||
|
||||
let translated: any = await fetchApi(
|
||||
const response = await fetchApi(
|
||||
'https://translate-pa.googleapis.com/v1/translateHtml',
|
||||
{
|
||||
'credentials': 'omit',
|
||||
@@ -457,11 +457,11 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520',
|
||||
},
|
||||
'referrer': 'https://wtr-lab.com/',
|
||||
'body': `[[${JSON.stringify(contained)},\"zh-CN\",\"en\"],\"te_lib\"]`,
|
||||
'body': `[[${JSON.stringify(contained)},"zh-CN","en"],"te_lib"]`,
|
||||
'method': 'POST',
|
||||
},
|
||||
);
|
||||
translated = await translated.json();
|
||||
const translated = await response.json();
|
||||
const out = translated && translated[0] ? translated[0] : [];
|
||||
return out as string[];
|
||||
}
|
||||
@@ -536,15 +536,8 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
let chapterContent = parsedJson.data.data.body;
|
||||
let chapterGlossary = {} as JSON;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
parsedJson.data.data,
|
||||
'glossary_data',
|
||||
)
|
||||
) {
|
||||
chapterGlossary = parsedJson.data.data.glossary_data;
|
||||
}
|
||||
const chapterGlossary: ChapterContent['glossary_data'] | undefined =
|
||||
parsedJson?.data?.data?.glossary_data;
|
||||
|
||||
let htmlString = '';
|
||||
|
||||
@@ -571,19 +564,14 @@ class WTRLAB implements Plugin.PluginBase {
|
||||
htmlString += `<p style="color:darkred;">${eLog}</p>`;
|
||||
}
|
||||
|
||||
let dictionary = [];
|
||||
if (Object.prototype.hasOwnProperty.call(chapterGlossary, 'terms')) {
|
||||
dictionary = Object.fromEntries(
|
||||
chapterGlossary.terms.map((definition, index) => [
|
||||
`※${index}⛬`,
|
||||
definition[0],
|
||||
]),
|
||||
);
|
||||
}
|
||||
const dictionary = chapterGlossary?.terms?.map(t => t[0]) || [];
|
||||
|
||||
for (let text of chapterContent) {
|
||||
if (Object.keys(dictionary).length > 0) {
|
||||
text = text.replaceAll(/※[0-9]+⛬/g, m => dictionary[m]);
|
||||
if (dictionary.length > 0) {
|
||||
text = text.replaceAll(
|
||||
/(?:wtr-lab\s+)?※([0-9]+)[⛬〓]/g,
|
||||
(m: string, index: string) => dictionary[parseInt(index)] || m,
|
||||
);
|
||||
}
|
||||
htmlString += `<p>${text}</p>`;
|
||||
}
|
||||
@@ -1716,18 +1704,21 @@ type ApiChapter = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type GlossaryTerm = {
|
||||
index: number;
|
||||
english: string;
|
||||
chinese: string;
|
||||
symbol: string;
|
||||
};
|
||||
// type GlossaryTerm = {
|
||||
// index: number;
|
||||
// english: string;
|
||||
// chinese: string;
|
||||
// symbol: string;
|
||||
// };
|
||||
type ChapterData = {
|
||||
data: ChapterContent;
|
||||
};
|
||||
type ChapterContent = {
|
||||
title: string;
|
||||
body: string;
|
||||
glossary_data?: {
|
||||
terms: string[][];
|
||||
};
|
||||
};
|
||||
|
||||
type SerieData = {
|
||||
|
||||
Reference in New Issue
Block a user