Most of the Plugin/Novel type definitions accessed using the Plugin namespace imported via
import { Plugin } from '@/types/plugin';PluginBase is a base class for all plugins.
class ExamplePlugin implements Plugin.PluginBase {}| Field | Required | Description |
|---|---|---|
| id | yes | Plugin ID |
| name | yes | Plugin Name |
| icon | yes | Plugin Icon |
| site | yes | Plugin site link |
| version | yes | Plugin version |
| imageRequestInit | no | Plugin Image Request Init |
| filters | no | Filter definition object |
| pluginSettings | no | Plugin settings object |
| webStorageUtilized | no | Flag for plugins that need localStorage/sessionStorage |
| customJS | no | Path to a custom JS file bundled with the plugin |
| customCSS | no | Path to a custom CSS file bundled with the plugin |
| popularNovels(page, options) | yes | Novel list getter |
| parseNovel(path) | yes | Novel info and chapter list getter |
| parseChapter(path) | yes | Chapter text getter |
| searchNovels(searchTerm, page) | yes | Novel searching getter |
| resolveUrl(path, isNovel) | no | Helper that turns a novel/chapter path into a full URL |
Unique ID of your plugin
class ExamplePlugin implements Plugin.PluginBase {
...
id = 'templateID';
...
}The name of your plugin that is shown in-app
class ExamplePlugin implements Plugin.PluginBase {
...
name = 'template Plugin';
...
}The path to your plugin's icon, relative to public/static (do not include the
public/static prefix itself). The file must actually live at
public/static/<icon> in this repo.
class ExamplePlugin implements Plugin.PluginBase {
...
icon = 'src/en/templateplugin/icon.png';
...
}Warning
Icons should be 96x96px
The url to the plugin's site
class ExamplePlugin implements Plugin.PluginBase {
...
site = 'https://example.com';
...
}Version of your plugin formatted according to semver2.0 spec i.e. <major>.<minor>.<patch>
Where
patchincrements on small fixes that fix the plugin (like site changed a selector, filter had a typo etc.)minorincrements on fixes that improve the plugin (like adding/removing filters, adding search options etc.)majorincrements on fixes that fix the major issues with the plugin (like changing site link)
class ExamplePlugin implements Plugin.PluginBase {
...
version = '1.0.0';
...
}The init for request to obtain images
Used if images failed to load due to site's protection
class ExamplePlugin implements Plugin.PluginBase {
...
imageRequestInit: Plugin.ImageRequestInit = {
headers: {
Referer: 'https://example.com',
},
};
...
}Optional flag that tells the app your plugin needs access to localStorage/sessionStorage
(see Other libraries). Leave it unset if your plugin only uses storage for
plugin settings.
class ExamplePlugin implements Plugin.PluginBase {
...
webStorageUtilized = true;
...
}Path to a custom JavaScript file, relative to public/static (same convention as
icon). Used by some multi-source templates to run extra JS against the parsed
page (e.g. stripping a site's injected copyright notice).
class ExamplePlugin implements Plugin.PluginBase {
...
customJS = 'src/en/templateplugin/customJS.js';
...
}Path to a custom CSS file, relative to public/static (same convention as
icon), applied when rendering the chapter/novel page in-app.
class ExamplePlugin implements Plugin.PluginBase {
...
customCSS = 'src/en/templateplugin/customCSS.css';
...
}A Filter definition object that holds filters used in the popularNovels function
class ExamplePlugin implements Plugin.PluginBase {
...
filters = {
order: {
label: 'Order',
options: [
{ label: 'Popular', value: '' },
{ label: 'Newest', value: 'newest' },
],
type: FilterTypes.Picker,
value: '',
},
status: {
label: 'Status',
options: [
{ label: 'All', value: '' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Hiatus', value: 'hiatus' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
value: '',
},
} satisfies Filters;
...
}Function that is used to get the (filtered) list of novels from the front page of the site
async popularNovels(
page: number,
options: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]>See Using cheerio for more information on how to parse HTML documents
pagecurrent page to fetchoptionsPopularNovelsOptions
NovelItem[] An array of filtered main-page NovelItems
class ExamplePlugin implements Plugin.PluginBase {
...
async popularNovels(
page: number,
options: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
if (options.filters.example.value === 'test') {
novels.push({
name: 'Novel1',
path: '/novel1',
cover: defaultCover,
});
}
return novels;
}
}This type is used for getting the options of the popularNovels function
-
showLatestNovels: booleanflag set when opened with theLatestbutton -
filters: FilterToValues<typeof filters>object containing all selected filter values. More about Filters
Function that is used to get the information about a particular novel and the list of its chapters
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel>See Using cheerio for more information on how to parse HTML documents
novelPathvalue from NovelItem::path
SourceNovel Novel information and chapter list as SourceNovel object
Caution
SourceNovel::path should be the same value as NovelItem::path provided as parameter!
class ExamplePlugin implements Plugin.PluginBase {
...
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: 'test',
artist: 'none',
author: 'none',
cover: defaultCover,
genres: 'Isekai, Neverland',
status: NovelStatus.Completed,
summary: '',
};
const chapters: Plugin.ChapterItem[] = [];
const chapter: Plugin.ChapterItem = {
name: '',
path: '',
releaseTime: '',
chapterNumber: 0,
};
chapters.push(chapter);
novel.chapters = chapters;
return novel;
}
...
}Function that is used to get the text content of a particular chapter
async parseChapter(chapterPath: string): Promise<string>See Using cheerio for more information on how to parse HTML documents
chapterPathvalue from ChapterItem::path
string HTML content of the chapter
class ExamplePlugin implements Plugin.PluginBase {
...
async parseChapter(chapterPath: string): Promise<string> {
return '<h1>No chapter here</h1>';
}
...
}Function that is used to find novels in the source
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]>See Using cheerio for more information on how to parse HTML documents
searchTermthe search termpageNosearch page number
NovelItem[] An array of found NovelItems
class ExamplePlugin implements Plugin.PluginBase {
...
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
return novels;
}
...
}Optional helper that turns a novel or chapter path into a full, requestable URL. It isn't
required by the interface, but most plugins define one to avoid repeating
this.site + '/...' string concatenation in every function.
resolveUrl?(path: string, isNovel?: boolean): string;class ExamplePlugin implements Plugin.PluginBase {
...
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? '/novel/' : '/chapter/') + path;
}It is an object representing information on how to store/access the novel
| Field | type | Required | Description |
|---|---|---|---|
path |
string |
yes | The relative path to the novel |
name |
string |
yes | The name of the novel shown in the library |
cover |
string |
no | URL to novel's cover |
You can use the default Cover not available cover by importing
import { defaultCover } from '@libs/defaultCover';SourceNovel extends NovelItem, so path, name, and cover behave the same way
here as they do there.
| Field | Type | Required | Description |
|---|---|---|---|
| path | string |
yes | Must match the NovelItem::path passed into parseNovel |
| name | string |
yes | The novel's title |
| cover | string |
no | URL to the novel's cover |
| genres | string |
no | Comma-separated genre list, e.g. "Action,Fantasy,Romance" |
| summary | string |
no | The novel's synopsis/description |
| author | string |
no | |
| artist | string |
no | |
| status | NovelStatus or string |
no | See NovelStatus for the standard values |
| rating | number |
no | Rating out of 5, as a float |
| chapters | ChapterItem[] |
no | The novel's chapter list |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string |
yes | |
| path | string |
yes | |
| releaseTime | string |
no | "YYYY-MM-DD" or an ISO date string |
| chapterNumber | number |
no | |
| page | string |
no | Only used for novels without pages (see SourcePage/PagePlugin) |
| scanlator | string or string[] |
no | Name(s) of the scanlation/translation group(s) |
Filters and FilterTypes are not in the Plugin namespace and are from @libs/filterInputs file:
import { FilterTypes, Filters } from '@libs/filterInputs';There are 2 main objects when using filters:
- Filter definition object
- FilterValues object
This is the user-defined object that defines strictly what filters are available in the "filter" menu in app. Every property of this object is a different filter. The key of the object is the name that will be used to reference this filter's value in the FilterValues object
filters = {
order: {<FilterProperties>},
} satisfies Filters;
// accessible in popularNovels as
options.filters.order;Caution
Do not forget to add satisfies Filters after the Filter definition object!
| Name | Type | Required | Description |
|---|---|---|---|
| label | string |
yes | in-app label |
| type | FilterTypes |
yes | type of the filter |
| value | check types | yes | Default value for this filter and the starting filter state in-app |
| options | check types | in some types | The options available in the given type |
filters = {
genre: {
type: FilterTypes.CheckboxGroup,
label: 'Genres',
value: [],
options: [
{ label: 'Isekai', value: 'isekai' },
{ label: 'Romance', value: 'romans' },
],
},
} satisfies Filters;Types of filters supported
| FilterType | Description | value |
options |
|---|---|---|---|
Picker |
A spinner for choosing one of the choices provided in options |
string the picked value |
Picker options |
TextInput |
A filter allowing a free text input | string written value |
N/A |
Switch |
A boolean switch | boolean state of the switch |
N/A |
CheckboxGroup |
A grouping of checkboxes | string[] array containing selected values |
CheckboxGroup options |
ExcludableCheckboxGroup |
A grouping of checkboxes where each one can be marked as included or excluded (e.g. "must have this genre" vs. "must not have this genre") | ExcludableCheckboxGroupValue object | CheckboxGroup options |
options: [
{
label: 'default', // in-app label
value: '', // in-code value
},
{
label: 'Value ABC',
value: 'abc',
},
];options: [
{
label: 'Value ABC', // in-app label
value: 'abc', // in-code value
},
{
label: 'Value DEF',
value: 'def',
},
];It is an object used inside of popularNovels that contains selected values for all filters defined in the Filter definition object.
The keys of the filter values correspond to Filter definition keys
// Filter definition object
filters = { abc: {} } satisfies Filters;
// then
options.filters; // FilterValues
options.filters.abc; // FilterValue for abc filterProperties of FilterValue:
type: FilterTypetype of the filtervaluevalue dependent on FilterTypes
options.filters.abc.value; // value of the filter
options.filters.abc.type; // type of the filter{
include?: string[]; // values of the checkboxes marked as included
exclude?: string[]; // values of the checkboxes marked as excluded
}Plugin settings allow plugins to define user-configurable options that are displayed in the app's settings UI. These settings are persistent and can be accessed within the plugin code.
A user-defined object that defines configurable settings for the plugin. Each property of this object is a different setting that will be displayed in the app's settings UI.
pluginSettings = {
settingKey: {
value: '',
label: 'Setting Label',
type: 'Text', // optional, defaults to 'Text'
},
};| Name | Type | Required | Description |
|---|---|---|---|
| value | string |
yes | Default value for this setting |
| label | string |
yes | Display label shown in the app's settings UI |
| type | string |
no | Type of the setting UI component (see below) |
Currently, two setting types are supported:
| Type | Description | UI Component | Default Value Type |
|---|---|---|---|
Switch |
A boolean toggle switch | SwitchItem | boolean |
Text |
A text input field (default if type is omitted) | TextInput | string |
Note
If type is not specified, the setting defaults to Text type and will be rendered as a TextInput.
Settings values are stored and can be accessed using the storage utility:
import { storage } from '@libs/storage';
// Get a setting value
const settingValue = storage.get('settingKey');
// Set a setting value
storage.set('settingKey', 'newValue');class ExamplePlugin implements Plugin.PluginBase {
...
hideLocked = storage.get('hideLocked');
pluginSettings = {
hideLocked: {
value: '',
label: 'Hide locked chapters',
type: 'Switch',
},
};
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
// Use the setting value
if (this.hideLocked) {
// Filter out locked chapters
}
...
}
...
}class ExamplePlugin implements Plugin.PluginBase {
...
site = storage.get('url');
email = storage.get('email');
password = storage.get('password');
pluginSettings = {
url: {
value: '',
label: 'URL',
// type: 'Text' is optional
},
email: {
value: '',
label: 'Email',
type: 'Text',
},
password: {
value: '',
label: 'Password',
// type defaults to 'Text' if omitted
},
};
async makeRequest(url: string): Promise<string> {
return await fetchApi(url, {
headers: {
Authorization: `Basic ${btoa(this.email + ':' + this.password)}`,
Referer: this.site,
},
}).then(res => res.text());
}
...
}NovelStatus is an enum of the standard values used for SourceNovel::status. Using
it (instead of a raw string) is what lets the app group/filter novels by status consistently
across plugins.
import { NovelStatus } from '@libs/novelStatus';| Member | Value |
|---|---|
Unknown |
'Unknown' |
Ongoing |
'Ongoing' |
Completed |
'Completed' |
Licensed |
'Licensed' |
PublishingFinished |
'Publishing Finished' |
Cancelled |
'Cancelled' |
OnHiatus |
'On Hiatus' |
STUB |
'STUB' |
Inactive |
'Inactive' |
status isn't restricted to these values (it accepts any string), but prefer a NovelStatus
member whenever the source's status maps onto one — free-text values won't be recognized by the
app's status filter.
Most sites are scraped by fetching the page HTML and parsing it with Cheerio, a jQuery-like API for traversing/selecting elements server-side.
import { load as parseHTML } from 'cheerio';A typical popularNovels implementation fetches a listing page, loads it into Cheerio, and maps
each matching element to a NovelItem:
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const body = await fetchApi(`${this.site}/novels?page=${page}`).then(res =>
res.text(),
);
const $ = parseHTML(body);
$('li.novel-item').each((i, el) => {
const name = $(el).find('.title').text().trim();
const path = $(el).find('a').attr('href')?.replace(this.site, '');
const cover = $(el).find('img').attr('src');
if (!path) return;
novels.push({ name, path, cover });
});
return novels;
}Notes:
$(el)re-scopes a selector to a single element found by.each(); without it you'd search the whole document again for every item.pathshould be relative (stripthis.site/the domain) — see NovelItem::path.- Prefer
.attr('href')/.attr('src')over.text()for links and images, and always guard forundefinedsince a selector can fail to match if the site changes its markup.
See the Cheerio API docs for the full set of selectors/methods
(.find(), .first(), .eq(), .attr(), .text(), .html(), etc.), and look at existing
plugins under plugins/** for real examples.
Plugins can't use the browser/Node fetch directly — use the wrappers from @libs/fetch instead,
which handle plugin-specific request setup (proxying, headers, etc.):
import { fetchApi, fetchText, fetchProto } from '@libs/fetch';declare function fetchApi(url: string, init?: FetchInit): Promise<Response>;The general-purpose fetcher. Returns a standard Response, so use .text(), .json(), etc. on
the result, the same way you would with the native fetch.
const res = await fetchApi(this.resolveUrl(novelPath));
const body = await res.text();declare function fetchText(
url: string,
init?: FetchInit,
encoding?: string,
): Promise<string>;A shortcut for fetchApi(...).then(res => res.text()), with an optional encoding for sites that
don't serve UTF-8 (e.g. fetchText(url, undefined, 'gbk') for some Chinese-language sites).
declare function fetchProto(
protoInit: ProtoRequestInit,
url: string,
init?: FetchInit,
): Promise<unknown>;For sites whose API responds with Protocol Buffers instead of JSON/HTML.
type ProtoRequestInit = {
proto: string; // the .proto schema source
requestType: string; // message type to encode the request as
requestData?: any; // request payload, encoded as `requestType`
responseType: string; // message type to decode the response as
};This is an advanced/uncommon case — only reach for it if the site's API is proto-based, which you
can usually tell from binary (non-JSON) response bodies on an application/x-protobuf-style
content type.
The init object accepted by all three functions above:
type FetchInit = {
headers?: Record<string, string> | Headers;
method?: string;
body?: FormData | string;
[key: string]: string | Record<string, string> | FormData | Headers | undefined;
};It mirrors the standard fetch init object
(headers, method, body) — set headers like Referer/Authorization/Cookie under
headers, not as top-level keys.
A few smaller helpers are available for less common cases. You generally won't need these unless your target site requires them.
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
declare function isUrlAbsolute(url: string): boolean;Useful when a site mixes absolute and relative URLs in the same listing (e.g. some cover images are full URLs, others are paths) and you need to normalize them before returning a NovelItem/SourceNovel.
import { storage, localStorage, sessionStorage } from '@libs/storage';storage is the same persistent key-value store used for plugin settings —
you can also use it directly for things like caching a session cookie or an auth token between
requests. localStorage/sessionStorage are separate, lower-level stores for plugins that need
that exact browser-style API (for example, reusing scraping code shared with a web target). If
your plugin uses either of them, set webStorageUtilized to
true on the plugin so the app knows to provide that access.
import { gcm } from '@libs/aes';
import { utf8ToBytes, bytesToUtf8 } from '@libs/utils';For sites that encrypt their API responses with AES-GCM (uncommon, but seen on a handful of
sources). gcm(key, nonce, AAD?) returns a Cipher with encrypt/decrypt methods operating on
Uint8Array; utf8ToBytes/bytesToUtf8 convert between that and plain strings.
const cipher = gcm(keyBytes, nonceBytes);
const plaintext = bytesToUtf8(cipher.decrypt(ciphertextBytes));This is an advanced case — only needed if you've confirmed the site is actually encrypting its payloads, not just minifying/obfuscating them.