Feat: Plugin storages (#904)
* update * update * Update ranobelib.ts * test * test
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 823 B After Width: | Height: | Size: 760 B |
+108
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
class Storage {
|
||||
private db: Record<string, { created: Date; value: any; expires?: number }>;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Storage class.
|
||||
*/
|
||||
constructor() {
|
||||
this.db = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a key-value pair in storage.
|
||||
*
|
||||
* @param {string} key - The key to set.
|
||||
* @param {any} value - The value to set.
|
||||
* @param {Date | number} [expires] - Optional expiry date or time in milliseconds.
|
||||
*/
|
||||
set(key: string, value: any, expires?: Date | number): void {
|
||||
this.db[key] = {
|
||||
created: new Date(),
|
||||
value,
|
||||
expires: expires instanceof Date ? expires.getTime() : expires,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value for a given key from storage.
|
||||
*
|
||||
* @param {string} key - The key to retrieve the value for.
|
||||
* @param {boolean} [raw] - Optional flag to return the raw stored item.
|
||||
* @returns {any} The stored value or undefined if key is not found.
|
||||
*/
|
||||
get(key: string, raw?: boolean): any {
|
||||
const item = this.db[key];
|
||||
if (item?.expires && Date.now() > item.expires) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return raw ? item : item?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all keys set by the `set` method.
|
||||
*
|
||||
* @returns {string[]} An array of keys.
|
||||
*/
|
||||
getAllKeys(): string[] {
|
||||
return Object.keys(this.db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a key from the storage.
|
||||
*
|
||||
* @param pluginID - The ID of the plugin.
|
||||
* @param key - The key to delete.
|
||||
*/
|
||||
delete(key: string): void {
|
||||
delete this.db[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored items from storage.
|
||||
*/
|
||||
clearAll(pluginID: string): void {
|
||||
this.db = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance of the Storage class
|
||||
export const storage = new Storage();
|
||||
|
||||
/*
|
||||
These parameters cannot be implemented in `test-web`.
|
||||
They are generated in the browser when js-scripts are executed
|
||||
Read more
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the structure of a storage object with string keys and values.
|
||||
*/
|
||||
interface StorageObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a simplified version of the browser's localStorage.
|
||||
*/
|
||||
class LocalStorage {
|
||||
private db: StorageObject;
|
||||
|
||||
constructor() {
|
||||
this.db = {};
|
||||
}
|
||||
|
||||
get(): StorageObject | undefined {
|
||||
return this.db;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instances of LocalStorage and sessionStorage
|
||||
export const localStorage = new LocalStorage();
|
||||
export const sessionStorage = new LocalStorage();
|
||||
Generated
+55
-16
@@ -16,12 +16,13 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "^4.18.2",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"image-size": "^1.1.1",
|
||||
"less": "^4.2.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"protobufjs": "^7.2.6",
|
||||
"qs": "^6.11.2",
|
||||
"sanitize-html": "^2.12.1",
|
||||
"terser": "^5.30.4",
|
||||
"terser": "^5.31.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -2229,15 +2230,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||
"optional": true,
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
@@ -2508,6 +2511,18 @@
|
||||
"source-map": "~0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/less/node_modules/image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/less/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -3434,6 +3449,14 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-lit": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
||||
@@ -4098,9 +4121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.30.4",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz",
|
||||
"integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==",
|
||||
"version": "5.31.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
||||
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
@@ -6290,10 +6313,12 @@
|
||||
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||
"optional": true
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||
"requires": {
|
||||
"queue": "6.0.2"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
@@ -6486,6 +6511,12 @@
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||
"optional": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -7168,6 +7199,14 @@
|
||||
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
|
||||
"integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="
|
||||
},
|
||||
"queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"queue-lit": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
||||
@@ -7669,9 +7708,9 @@
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.30.4",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz",
|
||||
"integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==",
|
||||
"version": "5.31.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
||||
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
|
||||
+2
-1
@@ -26,12 +26,13 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "^4.18.2",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"image-size": "^1.1.1",
|
||||
"less": "^4.2.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"protobufjs": "^7.2.6",
|
||||
"qs": "^6.11.2",
|
||||
"sanitize-html": "^2.12.1",
|
||||
"terser": "^5.30.4",
|
||||
"terser": "^5.31.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
+116
-44
@@ -4,23 +4,22 @@ import { defaultCover } from '@libs/defaultCover';
|
||||
import { fetchApi, fetchFile } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { storage } from '@libs/storage';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const apiUrl = 'https://api.author.today/';
|
||||
const token = 'Bearer guest';
|
||||
|
||||
class AuthorToday implements Plugin.PluginBase {
|
||||
id = 'AT';
|
||||
name = 'Автор Тудей';
|
||||
icon = 'src/ru/authortoday/icon.png';
|
||||
site = 'https://author.today';
|
||||
version = '1.0.0';
|
||||
apiUrl = 'https://api.author.today/v1/';
|
||||
version = '1.1.0';
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = apiUrl + 'v1/catalog/search?page=' + pageNo;
|
||||
let url = this.apiUrl + 'catalog/search?page=' + pageNo;
|
||||
if (filters?.genre?.value) {
|
||||
url += '&genre=' + filters.genre.value;
|
||||
}
|
||||
@@ -35,19 +34,18 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
url += '&access=' + (filters?.access?.value || 'any');
|
||||
url += '&promo=' + (filters?.promo?.value || 'hide');
|
||||
|
||||
const result = await fetchApi(url, {
|
||||
const result: response = await fetchApi(url, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
Authorization: 'Bearer guest',
|
||||
},
|
||||
});
|
||||
const json = (await result.json()) as response;
|
||||
}).then(res => res.json());
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
if (json.code === 'NotFound') {
|
||||
if (result.code === 'NotFound') {
|
||||
return novels;
|
||||
}
|
||||
|
||||
json?.searchResults?.forEach(novel =>
|
||||
result?.searchResults?.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel.coverUrl
|
||||
@@ -61,13 +59,16 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
async parseNovel(workID: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
if (!this.user) this.user = await this.getUser();
|
||||
const book: responseBook = await fetchApi(
|
||||
this.apiUrl + 'work/' + workID + '/details',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||
},
|
||||
},
|
||||
});
|
||||
).then(res => res.json());
|
||||
|
||||
const book = (await result.json()) as responseBook;
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: workID,
|
||||
name: book.title,
|
||||
@@ -91,15 +92,16 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
novel.summary += 'Примечания автора:\n' + book.authorNotes;
|
||||
}
|
||||
|
||||
const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
const chaptersJSON: ChaptersEntity[] = await fetchApi(
|
||||
this.apiUrl + 'work/' + workID + '/content',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||
},
|
||||
},
|
||||
});
|
||||
).then(res => res.json());
|
||||
|
||||
const chaptersJSON = (await chaptersRaw.json()) as ChaptersEntity[];
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
chaptersJSON.forEach((chapter, chapterIndex) => {
|
||||
if (chapter.isAvailable && !chapter.isDraft) {
|
||||
chapters.push({
|
||||
@@ -119,37 +121,30 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const [workID, chapterID] = chapterPath.split('/');
|
||||
const result = await fetchApi(
|
||||
apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`,
|
||||
if (!this.user) this.user = await this.getUser();
|
||||
const result: encryptedСhapter = await fetchApi(
|
||||
this.apiUrl + `work/${workID}/chapter/${chapterID}/text`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: token,
|
||||
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = (await result.json()) as encryptedСhapter;
|
||||
).then(res => res.json());
|
||||
|
||||
if (json.code) {
|
||||
return json.code + '\n' + json?.message;
|
||||
if (result.code) {
|
||||
return result.code + '\n' + result?.message;
|
||||
}
|
||||
|
||||
const key = json.key.split('').reverse().join('') + '@_@';
|
||||
let text = '';
|
||||
|
||||
for (let i = 0; i < json.text.length; i++) {
|
||||
text += String.fromCharCode(
|
||||
json.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length)),
|
||||
);
|
||||
const chapterText = decrypt(result.text, result.key, this.user?.userId);
|
||||
if (chapterText.includes('<img')) {
|
||||
return chapterText.replace(/src="(.*?)"/g, (match, url) => {
|
||||
if (!url.startsWith('http')) {
|
||||
return `src="${this.site}${url}"`;
|
||||
}
|
||||
return `src="${url}"`;
|
||||
});
|
||||
}
|
||||
|
||||
const loadedCheerio = parseHTML(text);
|
||||
loadedCheerio('img').each((index, element) => {
|
||||
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
|
||||
const src = loadedCheerio(element).attr('src');
|
||||
loadedCheerio(element).attr('src', this.site + src);
|
||||
}
|
||||
});
|
||||
const chapterText = loadedCheerio.html();
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
@@ -184,6 +179,40 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
isNovel ? this.site + '/work/' + path : this.site + '/reader/' + path;
|
||||
|
||||
user: authorization | undefined;
|
||||
getUser = async () => {
|
||||
let user = storage.get('user') || { userId: '', token: 'guest' };
|
||||
if (user && user.userId && user.token) {
|
||||
const currentUser: currentUser = await fetchApi(
|
||||
this.apiUrl + 'account/current-user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + user.token || 'guest',
|
||||
},
|
||||
},
|
||||
).then(res => res.json());
|
||||
if (currentUser?.id && !currentUser.isDisabled) return user;
|
||||
|
||||
storage.delete('user');
|
||||
user = { userId: '', token: 'guest' };
|
||||
}
|
||||
|
||||
const result = await fetchApi(this.site + '/account/bearer-token');
|
||||
if (result.url.includes('Login?ReturnUrl=')) {
|
||||
return user; //It looks like the user has lost the session
|
||||
}
|
||||
|
||||
const loginUser: authorization = await result.json();
|
||||
user = { userId: loginUser.userId, token: loginUser.token };
|
||||
|
||||
storage.set(
|
||||
'user',
|
||||
user, //for some reason they're ending an hour early.
|
||||
new Date(loginUser.expires).getTime() - 1 * 60 * 60 * 1000,
|
||||
);
|
||||
return user; //user authorized successfully
|
||||
};
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: 'Сортировка',
|
||||
@@ -333,6 +362,48 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
|
||||
export default new AuthorToday();
|
||||
|
||||
function decrypt(
|
||||
encrypt: string,
|
||||
encryptedKey: string,
|
||||
userId: number | string = '',
|
||||
) {
|
||||
const key = encryptedKey.split('').reverse().join('') + '@_@' + userId;
|
||||
let text = '';
|
||||
|
||||
for (let i = 0; i < encrypt.length; i++) {
|
||||
text += String.fromCharCode(
|
||||
encrypt.charCodeAt(i) ^ key.charCodeAt(i % key.length),
|
||||
);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
interface authorization {
|
||||
userId: number;
|
||||
token: string;
|
||||
issued: string;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
interface currentUser {
|
||||
id: number;
|
||||
userName: string;
|
||||
fio: string;
|
||||
email: string;
|
||||
avatarUrl?: null;
|
||||
backgroundUrl?: null;
|
||||
status?: null;
|
||||
hideDislike: boolean;
|
||||
hideFinished: boolean;
|
||||
isBanned: boolean;
|
||||
banReason?: null;
|
||||
banEnd?: null;
|
||||
emailConfirmed: boolean;
|
||||
isDeleted: boolean;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
interface response {
|
||||
searchResults?: SearchResultsEntity[] | null;
|
||||
realTotalCount: number;
|
||||
@@ -400,6 +471,7 @@ interface Duration {
|
||||
}
|
||||
|
||||
interface responseBook {
|
||||
message?: string;
|
||||
chapters?: ChaptersEntity[] | null;
|
||||
allowDownloads: boolean;
|
||||
downloadErrorCode: string;
|
||||
|
||||
+388
-122
@@ -1,16 +1,26 @@
|
||||
import { Plugin } from '@typings/plugin';
|
||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { fetchApi, fetchFile } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { storage, localStorage } from '@libs/storage';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const statusKey: { [key: number]: string } = {
|
||||
1: NovelStatus.Ongoing,
|
||||
2: NovelStatus.Completed,
|
||||
3: NovelStatus.OnHiatus,
|
||||
4: NovelStatus.Cancelled,
|
||||
};
|
||||
|
||||
class RLIB implements Plugin.PluginBase {
|
||||
id = 'RLIB';
|
||||
name = 'RanobeLib (OLD)';
|
||||
site = 'https://old.ranobelib.me/old/';
|
||||
version = '1.1.0';
|
||||
name = 'RanobeLib';
|
||||
site = 'https://ranobelib.me';
|
||||
apiSite = 'https://api.lib.social/api/manga/';
|
||||
version = '2.0.0';
|
||||
icon = 'src/ru/ranobelib/icon.png';
|
||||
ui: string | undefined = undefined;
|
||||
webStorageUtilized = true;
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
@@ -19,7 +29,7 @@ class RLIB implements Plugin.PluginBase {
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + 'manga-list?page=' + pageNo;
|
||||
let url = this.apiSite + '?site_id[0]=3&page=' + pageNo;
|
||||
url +=
|
||||
'&sort_by=' +
|
||||
(showLatestNovels
|
||||
@@ -65,138 +75,196 @@ class RLIB implements Plugin.PluginBase {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await fetchApi(url).then(res => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
const result: TopLevel = await fetchApi(url, {
|
||||
headers: this.user?.token,
|
||||
}).then(res => res.json());
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
this.ui = loadedCheerio('a.header-right-menu__item')
|
||||
.attr('href')
|
||||
?.replace?.(/[^0-9]/g, '');
|
||||
|
||||
const novelsRaw = result.match(/window\.__CATALOG_ITEMS__ = (\[.*?\]);/);
|
||||
if (novelsRaw instanceof Array && novelsRaw.length >= 2) {
|
||||
const novelsJson: resNovels[] = JSON.parse(novelsRaw[1]);
|
||||
novelsJson.forEach(novel => {
|
||||
if (result.data instanceof Array) {
|
||||
result.data.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.rus_name,
|
||||
cover: novel.cover.default,
|
||||
path: novel.slug_url,
|
||||
});
|
||||
});
|
||||
name: novel.rus_name || novel.eng_name || novel.name,
|
||||
cover: novel.cover?.default || defaultCover,
|
||||
path: novel.slug_url || novel.id + '--' + novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
|
||||
res.text(),
|
||||
);
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const { data }: { data: DataClass } = await fetchApi(
|
||||
this.apiSite +
|
||||
novelPath +
|
||||
'?fields[]=summary&fields[]=genres&fields[]=tags&fields[]=teams&fields[]=authors&fields[]=status_id&fields[]=artists',
|
||||
{ headers: this.user?.token },
|
||||
).then(res => res.json());
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio('.media-name__main').text()?.trim?.() || '',
|
||||
name: data.rus_name || data.name,
|
||||
cover: data.cover?.default || defaultCover,
|
||||
summary: data.summary,
|
||||
};
|
||||
novel.cover = loadedCheerio('.container_responsive img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('.media-description__text').text().trim();
|
||||
if (data.status?.id) {
|
||||
novel.status = statusKey[data.status.id] || NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
novel.genres = loadedCheerio('div[class="media-tags"]')
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/[\n\r]+/g, ', ')
|
||||
.replace(/ /g, '');
|
||||
if (data.authors?.length) {
|
||||
novel.author = data.authors[0].name;
|
||||
}
|
||||
if (data.artists?.length) {
|
||||
novel.artist = data.artists[0].name;
|
||||
}
|
||||
|
||||
loadedCheerio(
|
||||
'div.media-info-list > div[class="media-info-list__item"]',
|
||||
).each(function () {
|
||||
let name = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__title"]')
|
||||
.text();
|
||||
const genres = [data.genres || [], data.tags || []]
|
||||
.flat()
|
||||
.map(genres => genres?.name)
|
||||
.filter(genres => genres);
|
||||
if (genres.length) {
|
||||
novel.genres = genres.join(', ');
|
||||
}
|
||||
|
||||
if (name === 'Автор') {
|
||||
novel.author = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__value"]')
|
||||
.text()
|
||||
.trim();
|
||||
} else if (name === 'Художник') {
|
||||
novel.artist = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__value"]')
|
||||
.text()
|
||||
.trim();
|
||||
}
|
||||
});
|
||||
const branch_id: { [key: number]: string } = {};
|
||||
if (data.teams.length) {
|
||||
data.teams.forEach(
|
||||
({ name, details }) => (branch_id[details?.branch_id || '0'] = name),
|
||||
);
|
||||
}
|
||||
|
||||
this.ui = loadedCheerio('a.header-right-menu__item')
|
||||
.attr('href')
|
||||
?.replace?.(/[^0-9]/g, '');
|
||||
|
||||
const chaptersRaw = body.match(/window\.__CHAPTERS__ = (\[.*?\]);/);
|
||||
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
|
||||
const chaptersJson: resChapters[] = JSON.parse(chaptersRaw[1]);
|
||||
|
||||
if (!chaptersJson?.length) return novel;
|
||||
const chaptersJSON: { data: DataChapter[] } = await fetchApi(
|
||||
this.apiSite + novelPath + '/chapters',
|
||||
{
|
||||
headers: this.user?.token,
|
||||
},
|
||||
).then(res => res.json());
|
||||
|
||||
if (chaptersJSON.data.length) {
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
chaptersJson.forEach(chapter =>
|
||||
|
||||
chaptersJSON.data.forEach(chapter =>
|
||||
chapters.push({
|
||||
name:
|
||||
'Том ' +
|
||||
chapter.volume +
|
||||
' Глава ' +
|
||||
chapter.number +
|
||||
(chapter.name ? ' ' + chapter.name.trim() : ''),
|
||||
path: novelPath + '/v' + chapter.volume + '/c' + chapter.number,
|
||||
chapterNumber: chapter.index + 1,
|
||||
(chapter.name ? ' ' + chapter.name : ''),
|
||||
path:
|
||||
novelPath +
|
||||
'/' +
|
||||
chapter.volume +
|
||||
'/' +
|
||||
chapter.number +
|
||||
'/' +
|
||||
(chapter.branches[0]?.branch_id || ''),
|
||||
releaseTime: dayjs(chapter.branches[0].created_at).format('LLL'),
|
||||
chapterNumber: chapter.index,
|
||||
page: branch_id[chapter.branches[0].branch_id || '0'],
|
||||
}),
|
||||
);
|
||||
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
|
||||
res.text(),
|
||||
);
|
||||
const [slug, volume, number, branch_id] = chapterPath.split('/');
|
||||
let chapterText = '';
|
||||
|
||||
const loadedCheerio = parseHTML(result);
|
||||
loadedCheerio('.reader-container img').each((index, element) => {
|
||||
const src =
|
||||
loadedCheerio(element).attr('data-src') ||
|
||||
loadedCheerio(element).attr('src');
|
||||
if (!src?.startsWith('http')) {
|
||||
loadedCheerio(element).attr('src', this.site + src);
|
||||
} else {
|
||||
loadedCheerio(element).attr('src', src);
|
||||
}
|
||||
loadedCheerio(element).removeAttr('data-src');
|
||||
});
|
||||
if (slug && volume && number) {
|
||||
const result: { data: DataClass } = await fetchApi(
|
||||
this.apiSite +
|
||||
slug +
|
||||
'/chapter?' +
|
||||
(branch_id ? 'branch_id=' + branch_id + '&' : '') +
|
||||
'number=' +
|
||||
number +
|
||||
'&volume=' +
|
||||
volume,
|
||||
{ headers: this.user?.token },
|
||||
).then(res => res.json());
|
||||
chapterText =
|
||||
result?.data?.content?.type == 'doc'
|
||||
? jsonToHtml(result.data.content.content)
|
||||
: result?.data?.content;
|
||||
}
|
||||
|
||||
const chapterText = loadedCheerio('.reader-container').html();
|
||||
return chapterText || '';
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(this.site + 'api/manga?q=' + searchTerm);
|
||||
const { data }: { data: resNovels[] } = await result.json();
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
const url = this.apiSite + '?site_id[0]=3&q=' + searchTerm;
|
||||
const result: TopLevel = await fetchApi(url, {
|
||||
headers: this.user?.token,
|
||||
}).then(res => res.json());
|
||||
|
||||
data.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.rus_name || novel.name,
|
||||
cover: novel?.cover?.default || '',
|
||||
path: novel.slug_url,
|
||||
}),
|
||||
);
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
if (result.data instanceof Array) {
|
||||
result.data.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.rus_name || novel.eng_name || novel.name,
|
||||
cover: novel.cover?.default || defaultCover,
|
||||
path: novel.slug_url || novel.id + '--' + novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + path + (this.ui ? '?ui=' + this.ui : '');
|
||||
resolveUrl = (path: string, isNovel?: boolean) => {
|
||||
const ui = this.user?.ui ? 'ui=' + this.user.ui : '';
|
||||
|
||||
if (isNovel) return this.site + '/ru/book/' + path + (ui ? '?' + ui : '');
|
||||
|
||||
const [slug, volume, number, branch_id] = path.split('/');
|
||||
const chapterPath =
|
||||
slug +
|
||||
'/read/v' +
|
||||
volume +
|
||||
'/c' +
|
||||
number +
|
||||
(branch_id ? '?bid=' + branch_id : '');
|
||||
|
||||
return (
|
||||
this.site +
|
||||
'/ru/' +
|
||||
chapterPath +
|
||||
(ui ? (branch_id ? '&' : '?') + ui : '')
|
||||
);
|
||||
};
|
||||
|
||||
getUser = () => {
|
||||
const user = storage.get('user');
|
||||
if (user) {
|
||||
return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id };
|
||||
}
|
||||
const dataRaw = localStorage.get()?.auth;
|
||||
if (!dataRaw) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = JSON.parse(dataRaw) as authorization;
|
||||
if (!data?.token?.access_token) return;
|
||||
storage.set(
|
||||
'user',
|
||||
{
|
||||
id: data.auth.id,
|
||||
token: data.token.access_token,
|
||||
},
|
||||
data.token.timestamp + data.token.expires_in, //the token is valid for about 7 days
|
||||
);
|
||||
return {
|
||||
token: { Authorization: 'Bearer ' + data.token.access_token },
|
||||
ui: data.auth.id,
|
||||
};
|
||||
};
|
||||
user = this.getUser(); //To change the account, you need to restart the application
|
||||
|
||||
filters = {
|
||||
sort_by: {
|
||||
@@ -410,41 +478,230 @@ class RLIB implements Plugin.PluginBase {
|
||||
|
||||
export default new RLIB();
|
||||
|
||||
interface resNovels {
|
||||
function jsonToHtml(json: HTML[], html: string = '') {
|
||||
json.forEach(element => {
|
||||
switch (element.type) {
|
||||
case 'hardBreak':
|
||||
html += '<br>';
|
||||
break;
|
||||
case 'horizontalRule':
|
||||
html += '<hr>';
|
||||
break;
|
||||
case 'image':
|
||||
if (element.attrs) {
|
||||
const attrs = Object.entries(element.attrs)
|
||||
.filter(attr => attr?.[1])
|
||||
.map(attr => `${attr[0]}="${attr[1]}"`);
|
||||
html += '<img ' + attrs.join('; ') + '>';
|
||||
}
|
||||
break;
|
||||
case 'paragraph':
|
||||
html +=
|
||||
'<p>' +
|
||||
(element.content ? jsonToHtml(element.content) : '<br>') +
|
||||
'</p>';
|
||||
break;
|
||||
case 'text':
|
||||
html += element.text;
|
||||
break;
|
||||
default:
|
||||
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
|
||||
break;
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
interface HTML {
|
||||
type: string;
|
||||
content?: HTML[];
|
||||
attrs?: Attrs;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface Attrs {
|
||||
src: string;
|
||||
alt: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
interface authorization {
|
||||
token: Token;
|
||||
auth: Auth;
|
||||
timestamp: number;
|
||||
}
|
||||
interface Token {
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
timestamp: number;
|
||||
}
|
||||
interface Auth {
|
||||
id: number;
|
||||
name: string;
|
||||
rus_name: string;
|
||||
eng_name: string;
|
||||
slug: string;
|
||||
slug_url: string;
|
||||
cover: Cover;
|
||||
ageRestriction: AgeRestrictionOrTypeOrStatus;
|
||||
site: number;
|
||||
type: AgeRestrictionOrTypeOrStatus;
|
||||
rating: Rating;
|
||||
is_licensed: boolean;
|
||||
model: string;
|
||||
status: AgeRestrictionOrTypeOrStatus;
|
||||
releaseDateString: string;
|
||||
username: string;
|
||||
avatar: Cover;
|
||||
last_online_at: string;
|
||||
metadata: Metadata;
|
||||
}
|
||||
interface Cover {
|
||||
filename: string;
|
||||
thumbnail: string;
|
||||
default: string;
|
||||
interface Metadata {
|
||||
auth_domains: string;
|
||||
}
|
||||
interface AgeRestrictionOrTypeOrStatus {
|
||||
|
||||
interface TopLevel {
|
||||
data: DataClass | DataClass[];
|
||||
links?: Links;
|
||||
meta?: Meta;
|
||||
}
|
||||
|
||||
interface AgeRestriction {
|
||||
id: number;
|
||||
label: string;
|
||||
}
|
||||
interface Rating {
|
||||
average: string;
|
||||
averageFormated: string;
|
||||
votes: number;
|
||||
votesFormated: string;
|
||||
user: number;
|
||||
|
||||
interface Branch {
|
||||
id: number;
|
||||
branch_id: null;
|
||||
created_at: string;
|
||||
teams: BranchTeam[];
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface resChapters {
|
||||
interface BranchTeam {
|
||||
id: number;
|
||||
slug: string;
|
||||
slug_url: string;
|
||||
model: string;
|
||||
name: string;
|
||||
cover: Cover;
|
||||
}
|
||||
|
||||
interface Cover {
|
||||
filename: null | string;
|
||||
thumbnail: string;
|
||||
default: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Rating {
|
||||
average: string;
|
||||
votes: number;
|
||||
votesFormated: string;
|
||||
}
|
||||
|
||||
interface DataClass {
|
||||
id: number;
|
||||
name: string;
|
||||
rus_name?: string;
|
||||
eng_name?: string;
|
||||
slug: string;
|
||||
slug_url?: string;
|
||||
cover?: Cover;
|
||||
ageRestriction?: AgeRestriction;
|
||||
site?: number;
|
||||
type: string;
|
||||
summary?: string;
|
||||
is_licensed?: boolean;
|
||||
teams: DataTeam[];
|
||||
genres?: Genre[];
|
||||
tags?: Genre[];
|
||||
authors?: Artist[];
|
||||
model?: string;
|
||||
status?: AgeRestriction;
|
||||
scanlateStatus?: AgeRestriction;
|
||||
artists?: Artist[];
|
||||
releaseDateString?: string;
|
||||
volume?: string;
|
||||
number?: string;
|
||||
number_secondary?: string;
|
||||
branch_id?: null;
|
||||
manga_id?: number;
|
||||
created_at?: string;
|
||||
moderated?: AgeRestriction;
|
||||
likes_count?: number;
|
||||
content?: any;
|
||||
attachments?: Attachment[];
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
id: number;
|
||||
slug: string;
|
||||
slug_url: string;
|
||||
model: string;
|
||||
name: string;
|
||||
rus_name: null;
|
||||
alt_name: null;
|
||||
cover: Cover;
|
||||
subscription: Subscription;
|
||||
confirmed: null;
|
||||
user_id: number;
|
||||
titles_count_details: null;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
is_subscribed: boolean;
|
||||
source_type: string;
|
||||
source_id: number;
|
||||
relation: null;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
id: null;
|
||||
filename: string;
|
||||
name: string;
|
||||
extension: string;
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DataTeam {
|
||||
id: number;
|
||||
slug: string;
|
||||
slug_url: string;
|
||||
model: string;
|
||||
name: string;
|
||||
cover: Cover;
|
||||
details?: Details;
|
||||
vk?: string;
|
||||
discord?: null;
|
||||
}
|
||||
|
||||
interface Details {
|
||||
branch_id: null;
|
||||
is_active: boolean;
|
||||
subscriptions_count: null;
|
||||
}
|
||||
|
||||
interface Links {
|
||||
first: string;
|
||||
last: null;
|
||||
prev: null;
|
||||
next: string;
|
||||
}
|
||||
|
||||
interface Meta {
|
||||
current_page?: number;
|
||||
from?: number;
|
||||
path?: string;
|
||||
per_page?: number;
|
||||
to?: number;
|
||||
page?: number;
|
||||
has_next_page?: boolean;
|
||||
seed?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
interface DataChapter {
|
||||
id: number;
|
||||
index: number;
|
||||
item_number: number;
|
||||
@@ -452,4 +709,13 @@ interface resChapters {
|
||||
number: string;
|
||||
number_secondary: string;
|
||||
name: string;
|
||||
branches_count: number;
|
||||
branches: Branch[];
|
||||
}
|
||||
interface BranchesEntity {
|
||||
id: number;
|
||||
branch_id?: number;
|
||||
created_at: string;
|
||||
teams?: BranchTeam[];
|
||||
user: User;
|
||||
}
|
||||
|
||||
+37
-20
@@ -1,12 +1,12 @@
|
||||
import * as fs from 'fs';
|
||||
import { Plugin } from '@typings/plugin';
|
||||
import { languages } from '@libs/languages';
|
||||
import * as path from 'path';
|
||||
import sizeOf from 'image-size';
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const size = 96;
|
||||
const minSize = 16;
|
||||
|
||||
const skip: string[] = [
|
||||
const skip = new Set([
|
||||
//custom icons
|
||||
'FWK.US',
|
||||
'ReN',
|
||||
@@ -23,19 +23,14 @@ const skip: string[] = [
|
||||
'sektenovel',
|
||||
'sonicmtl',
|
||||
'translatinotaku',
|
||||
'warriorlegendtrad',
|
||||
'wuxiaworld.site',
|
||||
]);
|
||||
|
||||
//low quality
|
||||
'BLN',
|
||||
'NO.net',
|
||||
'novelbookid',
|
||||
'novelhall',
|
||||
'olaoe',
|
||||
];
|
||||
|
||||
const used = new Set();
|
||||
used.add(path.join(root, 'icons', 'coverNotAvailable.webp'));
|
||||
used.add(path.join(root, 'icons', 'siteNotAvailable.png'));
|
||||
const used = new Set([
|
||||
path.join(root, 'icons', 'coverNotAvailable.webp'),
|
||||
path.join(root, 'icons', 'siteNotAvailable.png'),
|
||||
]);
|
||||
|
||||
const notAvailableImage = fs.readFileSync(
|
||||
path.join(root, 'icons', 'siteNotAvailable.png'),
|
||||
@@ -54,6 +49,8 @@ const notAvailableImage = fs.readFileSync(
|
||||
let language;
|
||||
for (let plugin in plugins) {
|
||||
const { id, name, site, iconUrl, lang } = plugins[plugin];
|
||||
const icon = iconUrl && 'icons/' + iconUrl.split('icons/')[1];
|
||||
|
||||
if (language !== lang) {
|
||||
language = lang;
|
||||
console.log(
|
||||
@@ -62,12 +59,10 @@ const notAvailableImage = fs.readFileSync(
|
||||
.padEnd(30, '='),
|
||||
);
|
||||
}
|
||||
let icon;
|
||||
if (iconUrl) icon = 'icons/' + iconUrl.split('icons/')[1];
|
||||
|
||||
try {
|
||||
if (icon) used.add(path.join(root, icon));
|
||||
|
||||
if (!skip.includes(id) && icon && site) {
|
||||
if (!skip.has(id) && icon && site) {
|
||||
const image = await fetch(
|
||||
`https://www.google.com/s2/favicons?domain=${site}&sz=${size}&type=png`,
|
||||
)
|
||||
@@ -85,8 +80,30 @@ const notAvailableImage = fs.readFileSync(
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.writeFileSync(icon, image);
|
||||
console.log(' ', name.padEnd(26), `(${id})`, '\r✅');
|
||||
const imageSize = sizeOf(image);
|
||||
const exist = fs.existsSync(icon);
|
||||
|
||||
if (!exist) {
|
||||
const dir = icon.match(/^.*[\\\/]/)[0] as string;
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (
|
||||
((imageSize?.width || size) > minSize &&
|
||||
(imageSize?.height || size) > minSize) ||
|
||||
!exist
|
||||
) {
|
||||
fs.writeFileSync(icon, image);
|
||||
console.log(' ', name.padEnd(26), `(${id})`, '\r✅');
|
||||
} else {
|
||||
console.log(
|
||||
' ',
|
||||
name.padEnd(26),
|
||||
`(${id})`.padEnd(20),
|
||||
'Low quality',
|
||||
'\r🔄',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄');
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ export namespace Plugin {
|
||||
site: string;
|
||||
filters?: Filters;
|
||||
version: string;
|
||||
//flag indicates whether access to LocalStorage, SesesionStorage is required.
|
||||
webStorageUtilized?: boolean;
|
||||
popularNovels(
|
||||
pageNo: number,
|
||||
options: PopularNovelsOptions<Filters>,
|
||||
|
||||
Reference in New Issue
Block a user