fix: Fiction Zone (#1935)
* Fictionzone progress * Fix fixtion zone * Fix novel covers * Fix image attempt 2 * Fix image attempt 3 * api for novel parse * Maybe fix novel parsing * Maybe fix novel parsing 2 * Maybe fix novel parsing 3 * Debug * Maybe fix novel parsing 4
This commit is contained in:
+67
-155
@@ -1,23 +1,15 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { load as loadCheerio } from 'cheerio';
|
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
class FictionZonePlugin implements Plugin.PagePlugin {
|
class FictionZonePlugin implements Plugin.PluginBase {
|
||||||
id = 'fictionzone';
|
id = 'fictionzone';
|
||||||
name = 'Fiction Zone';
|
name = 'Fiction Zone';
|
||||||
icon = 'src/en/fictionzone/icon.png';
|
icon = 'src/en/fictionzone/icon.png';
|
||||||
site = 'https://fictionzone.net';
|
site = 'https://fictionzone.net';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
filters: Filters | undefined = undefined;
|
filters: Filters | undefined = undefined;
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
|
||||||
|
|
||||||
//flag indicates whether access to LocalStorage, SesesionStorage is required.
|
|
||||||
webStorageUtilized?: boolean;
|
|
||||||
|
|
||||||
cachedNovelIds = new Map<string, string>();
|
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -26,129 +18,83 @@ class FictionZonePlugin implements Plugin.PagePlugin {
|
|||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
return await this.getPage(this.site + '/library?page=' + pageNo);
|
return await this.getPage(
|
||||||
}
|
`/platform/browse?page=${pageNo}&page_size=20&sort_by=${showLatestNovels ? 'created_at' : 'bookmark_count'}&sort_order=desc&include_genres=true`,
|
||||||
|
|
||||||
async getPage(url: string) {
|
|
||||||
const req = await fetchApi(url);
|
|
||||||
const body = await req.text();
|
|
||||||
const loadedCheerio = loadCheerio(body);
|
|
||||||
|
|
||||||
return loadedCheerio('div.novel-card')
|
|
||||||
.map((i, el) => {
|
|
||||||
const novelName = loadedCheerio(el).find('a > div.title > h1').text();
|
|
||||||
const novelCover = loadedCheerio(el).find('img').attr('src');
|
|
||||||
const novelUrl = loadedCheerio(el).find('a').attr('href');
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: novelName,
|
|
||||||
cover: novelCover,
|
|
||||||
path: novelUrl!.replace(/^\//, '').replace(/\/$/, ''),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseNovel(
|
|
||||||
novelPath: string,
|
|
||||||
): Promise<Plugin.SourceNovel & { totalPages: number }> {
|
|
||||||
const req = await fetchApi(this.site + '/' + novelPath);
|
|
||||||
const body = await req.text();
|
|
||||||
const loadedCheerio = loadCheerio(body);
|
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel & { totalPages: number } = {
|
|
||||||
path: novelPath,
|
|
||||||
name: loadedCheerio('div.novel-title > h1').text(),
|
|
||||||
totalPages: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// novel.artist = '';
|
|
||||||
novel.author = loadedCheerio('div.novel-author > content').text();
|
|
||||||
novel.cover = loadedCheerio('div.novel-img > img').attr('src');
|
|
||||||
novel.genres = [
|
|
||||||
...loadedCheerio('div.genres > .items > span')
|
|
||||||
.map((i, el) => loadedCheerio(el).text())
|
|
||||||
.toArray(),
|
|
||||||
...loadedCheerio('div.tags > .items > a')
|
|
||||||
.map((i, el) => loadedCheerio(el).text())
|
|
||||||
.toArray(),
|
|
||||||
].join(',');
|
|
||||||
const status = loadedCheerio('div.novel-status > div.content')
|
|
||||||
.text()
|
|
||||||
.trim();
|
|
||||||
if (status === 'Ongoing') novel.status = NovelStatus.Ongoing;
|
|
||||||
novel.summary = loadedCheerio('#synopsis > div.content').text();
|
|
||||||
|
|
||||||
const nuxtData = loadedCheerio('script#__NUXT_DATA__').html();
|
|
||||||
const parsed = JSON.parse(nuxtData!);
|
|
||||||
let last = null;
|
|
||||||
for (const a of parsed) {
|
|
||||||
if (typeof a === 'string' && a.startsWith('novel_covers/')) break;
|
|
||||||
last = a;
|
|
||||||
}
|
|
||||||
this.cachedNovelIds.set(novelPath, last.toString());
|
|
||||||
// @ts-ignore
|
|
||||||
novel.chapters = loadedCheerio(
|
|
||||||
'div.chapters > div.list-wrapper > div.items > a.chapter',
|
|
||||||
)
|
|
||||||
.map((i, el) => {
|
|
||||||
const chapterName = loadedCheerio(el).find('span.chapter-title').text();
|
|
||||||
const chapterUrl = loadedCheerio(el)
|
|
||||||
.attr('href')
|
|
||||||
?.replace(/^\//, '')
|
|
||||||
.replace(/\/$/, '');
|
|
||||||
const uploadTime = this.parseAgoDate(
|
|
||||||
loadedCheerio(el).find('span.update-date').text(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: chapterName,
|
|
||||||
releaseTime: uploadTime,
|
|
||||||
path: chapterUrl?.replace(/^\//, '').replace(/\/$/, ''),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.toArray()
|
|
||||||
.filter(chap => !!chap.path);
|
|
||||||
novel.totalPages = parseInt(
|
|
||||||
loadedCheerio('div.chapters ul.el-pager > li:last-child').text(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return novel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
async getData(url: string) {
|
||||||
let id = this.cachedNovelIds.get(novelPath);
|
return await fetchApi(this.site + '/api/__api_party/fictionzone', {
|
||||||
if (!id) {
|
|
||||||
await this.parseNovel(novelPath);
|
|
||||||
id = this.cachedNovelIds.get(novelPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetchApi(this.site + '/api/__api_party/api-v1', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
'path': '/chapter/all/' + id,
|
'path': url,
|
||||||
'query': { 'page': parseInt(page) },
|
'headers': [
|
||||||
'headers': { 'content-type': 'application/json' },
|
['content-type', 'application/json'],
|
||||||
'method': 'get',
|
['x-request-time', new Date().toISOString()],
|
||||||
|
],
|
||||||
|
'method': 'GET',
|
||||||
}),
|
}),
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPage(url: string) {
|
||||||
|
const data = await this.getData(url);
|
||||||
|
|
||||||
|
return data.data.novels.map((n: any) => ({
|
||||||
|
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);
|
||||||
|
|
||||||
|
return data.data.chapters.map((n: any) => ({
|
||||||
|
name: n.title,
|
||||||
|
number: n.chapter_number,
|
||||||
|
date: n.published_date
|
||||||
|
? new Date(n.published_date).toISOString()
|
||||||
|
: undefined,
|
||||||
|
path: `${novelPath}/${n.chapter_id}|/platform/chapter-content?novel_id=${id}&chapter_id=${n.chapter_id}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
|
const novelSlug = novelPath.replace('novel/', '');
|
||||||
|
const data = await this.getData(
|
||||||
|
`/platform/novel-details?slug=${novelSlug}`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chapters: data._data.map((c: any) => ({
|
path: novelPath,
|
||||||
name: c.title,
|
name: data.data.title,
|
||||||
releaseTime: new Date(c.created_at).toISOString(),
|
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`,
|
||||||
path: novelPath + '/' + c.slug,
|
genres: [
|
||||||
})),
|
...data.data.genres.map((g: any) => g.name),
|
||||||
|
...data.data.tags.map((g: any) => g.name),
|
||||||
|
].join(','),
|
||||||
|
status:
|
||||||
|
data.data.status == 1
|
||||||
|
? NovelStatus.Ongoing
|
||||||
|
: data.data.status == 0
|
||||||
|
? NovelStatus.Completed
|
||||||
|
: NovelStatus.Unknown,
|
||||||
|
author:
|
||||||
|
data.data.contributors.filter((c: any) => c.role == 'author')[0]
|
||||||
|
?.display_name || '',
|
||||||
|
summary: data.data.synopsis,
|
||||||
|
chapters: await this.getChapterPage(data.data.id, novelPath),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const req = await fetchApi(this.site + '/' + chapterPath);
|
const data = await this.getData(chapterPath.split('|')[1]);
|
||||||
const body = await req.text();
|
return '<p>' + data.data.content.replaceAll('\n', '</p><p>') + '</p>';
|
||||||
const loadedCheerio = loadCheerio(body);
|
|
||||||
return loadedCheerio('div.chapter-content').html() || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
@@ -156,46 +102,12 @@ class FictionZonePlugin implements Plugin.PagePlugin {
|
|||||||
pageNo: number,
|
pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
return await this.getPage(
|
return await this.getPage(
|
||||||
this.site +
|
`/platform/browse?search=${encodeURIComponent(searchTerm)}&page=${pageNo}&page_size=20&search_in_synopsis=true&sort_by=bookmark_count&sort_order=desc&include_genres=true`,
|
||||||
'/library?query=' +
|
|
||||||
encodeURIComponent(searchTerm) +
|
|
||||||
'&page=' +
|
|
||||||
pageNo +
|
|
||||||
'&sort=views-all',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseAgoDate(date: string | undefined) {
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
//parseMadaraDate
|
this.site + '/' + path.split('|')[0];
|
||||||
if (date?.includes('ago')) {
|
|
||||||
const dayJSDate = dayjs(new Date()); // today
|
|
||||||
const timeAgo = date.match(/\d+/)?.[0] || '';
|
|
||||||
const timeAgoInt = parseInt(timeAgo, 10);
|
|
||||||
|
|
||||||
if (!timeAgo) return null; // there is no number!
|
|
||||||
|
|
||||||
if (date.includes('hours ago') || date.includes('hour ago')) {
|
|
||||||
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.includes('days ago') || date.includes('day ago')) {
|
|
||||||
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.includes('months ago') || date.includes('month ago')) {
|
|
||||||
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.includes('years ago') || date.includes('year ago')) {
|
|
||||||
dayJSDate.subtract(timeAgoInt, 'years'); // go back N years
|
|
||||||
}
|
|
||||||
|
|
||||||
return dayJSDate.toISOString();
|
|
||||||
}
|
|
||||||
return null; // there is no "ago" so give up
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) => this.site + '/' + path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FictionZonePlugin();
|
export default new FictionZonePlugin();
|
||||||
|
|||||||
Reference in New Issue
Block a user