Files
2026-05-18 10:39:07 +05:00

191 lines
5.2 KiB
TypeScript

import { fetchApi } from '@libs/fetch';
import { Plugin } from '@/types/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { load as loadCheerio } from 'cheerio';
import { NovelStatus } from '@libs/novelStatus';
class InkittPlugin implements Plugin.PluginBase {
id = 'inkitt';
name = 'Inkitt';
icon = 'src/en/inkitt/icon.png';
site = 'https://www.inkitt.com';
version = '1.0.1';
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
//flag indicates whether access to LocalStorage, SesesionStorage is required.
webStorageUtilized?: boolean;
async popularNovels(
pageNo: number,
{
// showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let req;
if (filters?.genres?.value) {
req = await fetchApi(
this.site +
`/genre/${filters.genres.value}/${pageNo}?period=alltime&sort=popular`,
);
} else {
req = await fetchApi(
this.site + `/trending_stories?page=${pageNo}&period=alltime`,
);
}
let data;
try {
data = await req.json();
} catch (e) {
throw new Error('Failed to load novels, try opening in webview.');
}
return data.stories.map((novel: InkittNovel) => {
return {
name: novel.title,
path: this.getPath(novel),
cover:
novel.vertical_cover.url ||
novel.vertical_cover.iphone ||
novel.cover.url,
};
});
}
getPath(novel: InkittNovel) {
return (novel.category_one || novel.genres[0]) + '/' + novel.id;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const req = await fetchApi(this.site + `/stories/${novelPath}`);
const text = await req.text();
const loadedCheerio = loadCheerio(text);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h1.story-title').text(),
};
novel.author = loadedCheerio('dl > dd > a.author-link').text();
novel.genres = loadedCheerio('dd.genres > a')
.map((_, el) => loadedCheerio(el).text())
.toArray()
.join(', ');
const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd')
.text()
.trim();
if (status === 'Complete') novel.status = NovelStatus.Completed;
if (status === 'Ongoing') novel.status = NovelStatus.Ongoing;
const apiReq = await fetchApi(
this.site + `/api/stories/${novelPath.split('/')[1]}`,
);
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: InkittChapter) => {
return {
name: c.name,
path: novelPath + '/chapters/' + c.chapter_number,
chapterNumber: c.chapter_number,
};
});
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const req = await fetchApi(this.site + '/stories/' + chapterPath);
const text = await req.text();
const loadedCheerio = loadCheerio(text);
return loadedCheerio('div#chapterText').html() || '';
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const req = await fetchApi(
this.site +
`/api/2/search/title?q=${encodeURIComponent(searchTerm)}&page=${pageNo}`,
);
let data;
try {
data = await req.json();
} catch (e) {
throw new Error('Failed to search novels, try opening in webview.');
}
return data.stories.map((novel: InkittNovel) => {
return {
name: novel.title,
path: this.getPath(novel),
cover:
novel.vertical_cover.url ||
novel.vertical_cover.iphone ||
novel.cover.url,
};
});
}
// resolveUrl = (path: string, isNovel?: boolean) =>
// this.site + '/stories/' + path;
filters = {
genres: {
type: FilterTypes.Picker,
label: 'Genre',
value: '',
options: [
{ 'label': 'Sci-Fi', 'value': 'scifi' },
{ 'label': 'Fantasy', 'value': 'fantasy' },
{ 'label': 'Adventure', 'value': 'adventure' },
{ 'label': 'Mystery', 'value': 'mystery' },
{ 'label': 'Action', 'value': 'action' },
{ 'label': 'Horror', 'value': 'horror' },
{ 'label': 'Humor', 'value': 'humor' },
{ 'label': 'Erotica', 'value': 'erotica' },
{ 'label': 'Poetry', 'value': 'poetry' },
{ 'label': 'Other', 'value': 'other' },
{ 'label': 'Thriller', 'value': 'thriller' },
{ 'label': 'Romance', 'value': 'romance' },
{ 'label': 'Children', 'value': 'children' },
{ 'label': 'Drama', 'value': 'drama' },
],
},
} satisfies Filters;
}
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[];
};