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",
|
"dayjs": "^1.11.10",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"htmlparser2": "^9.1.0",
|
"htmlparser2": "^9.1.0",
|
||||||
|
"image-size": "^1.1.1",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"protobufjs": "^7.2.6",
|
"protobufjs": "^7.2.6",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"terser": "^5.30.4",
|
"terser": "^5.31.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
@@ -2229,15 +2230,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/image-size": {
|
"node_modules/image-size": {
|
||||||
"version": "0.5.5",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||||
"optional": true,
|
"dependencies": {
|
||||||
|
"queue": "6.0.2"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"image-size": "bin/image-size.js"
|
"image-size": "bin/image-size.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=16.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
@@ -2508,6 +2511,18 @@
|
|||||||
"source-map": "~0.6.0"
|
"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": {
|
"node_modules/less/node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -3434,6 +3449,14 @@
|
|||||||
"node": ">=0.4.x"
|
"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": {
|
"node_modules/queue-lit": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
||||||
@@ -4098,9 +4121,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.30.4",
|
"version": "5.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
||||||
"integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==",
|
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
@@ -6290,10 +6313,12 @@
|
|||||||
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
||||||
},
|
},
|
||||||
"image-size": {
|
"image-size": {
|
||||||
"version": "0.5.5",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||||
"optional": true
|
"requires": {
|
||||||
|
"queue": "6.0.2"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inflight": {
|
"inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -6486,6 +6511,12 @@
|
|||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
|
||||||
"integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="
|
"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": {
|
"queue-lit": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz",
|
||||||
@@ -7669,9 +7708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser": {
|
"terser": {
|
||||||
"version": "5.30.4",
|
"version": "5.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
||||||
"integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==",
|
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
|
|||||||
+2
-1
@@ -26,12 +26,13 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"htmlparser2": "^9.1.0",
|
"htmlparser2": "^9.1.0",
|
||||||
|
"image-size": "^1.1.1",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"protobufjs": "^7.2.6",
|
"protobufjs": "^7.2.6",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"terser": "^5.30.4",
|
"terser": "^5.31.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|||||||
+114
-42
@@ -4,23 +4,22 @@ import { defaultCover } from '@libs/defaultCover';
|
|||||||
import { fetchApi, fetchFile } from '@libs/fetch';
|
import { fetchApi, fetchFile } from '@libs/fetch';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
|
import { storage } from '@libs/storage';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const apiUrl = 'https://api.author.today/';
|
|
||||||
const token = 'Bearer guest';
|
|
||||||
|
|
||||||
class AuthorToday implements Plugin.PluginBase {
|
class AuthorToday implements Plugin.PluginBase {
|
||||||
id = 'AT';
|
id = 'AT';
|
||||||
name = 'Автор Тудей';
|
name = 'Автор Тудей';
|
||||||
icon = 'src/ru/authortoday/icon.png';
|
icon = 'src/ru/authortoday/icon.png';
|
||||||
site = 'https://author.today';
|
site = 'https://author.today';
|
||||||
version = '1.0.0';
|
apiUrl = 'https://api.author.today/v1/';
|
||||||
|
version = '1.1.0';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = apiUrl + 'v1/catalog/search?page=' + pageNo;
|
let url = this.apiUrl + 'catalog/search?page=' + pageNo;
|
||||||
if (filters?.genre?.value) {
|
if (filters?.genre?.value) {
|
||||||
url += '&genre=' + filters.genre.value;
|
url += '&genre=' + filters.genre.value;
|
||||||
}
|
}
|
||||||
@@ -35,19 +34,18 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
url += '&access=' + (filters?.access?.value || 'any');
|
url += '&access=' + (filters?.access?.value || 'any');
|
||||||
url += '&promo=' + (filters?.promo?.value || 'hide');
|
url += '&promo=' + (filters?.promo?.value || 'hide');
|
||||||
|
|
||||||
const result = await fetchApi(url, {
|
const result: response = await fetchApi(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token,
|
Authorization: 'Bearer guest',
|
||||||
},
|
},
|
||||||
});
|
}).then(res => res.json());
|
||||||
const json = (await result.json()) as response;
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
if (json.code === 'NotFound') {
|
if (result.code === 'NotFound') {
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
json?.searchResults?.forEach(novel =>
|
result?.searchResults?.forEach(novel =>
|
||||||
novels.push({
|
novels.push({
|
||||||
name: novel.title,
|
name: novel.title,
|
||||||
cover: novel.coverUrl
|
cover: novel.coverUrl
|
||||||
@@ -61,13 +59,16 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(workID: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(workID: string): Promise<Plugin.SourceNovel> {
|
||||||
const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
|
if (!this.user) this.user = await this.getUser();
|
||||||
|
const book: responseBook = await fetchApi(
|
||||||
|
this.apiUrl + 'work/' + workID + '/details',
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token,
|
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
|
||||||
const book = (await result.json()) as responseBook;
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: workID,
|
path: workID,
|
||||||
name: book.title,
|
name: book.title,
|
||||||
@@ -91,15 +92,16 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
novel.summary += 'Примечания автора:\n' + book.authorNotes;
|
novel.summary += 'Примечания автора:\n' + book.authorNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
|
const chaptersJSON: ChaptersEntity[] = await fetchApi(
|
||||||
|
this.apiUrl + 'work/' + workID + '/content',
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token,
|
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
|
||||||
const chaptersJSON = (await chaptersRaw.json()) as ChaptersEntity[];
|
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
|
||||||
chaptersJSON.forEach((chapter, chapterIndex) => {
|
chaptersJSON.forEach((chapter, chapterIndex) => {
|
||||||
if (chapter.isAvailable && !chapter.isDraft) {
|
if (chapter.isAvailable && !chapter.isDraft) {
|
||||||
chapters.push({
|
chapters.push({
|
||||||
@@ -119,37 +121,30 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const [workID, chapterID] = chapterPath.split('/');
|
const [workID, chapterID] = chapterPath.split('/');
|
||||||
const result = await fetchApi(
|
if (!this.user) this.user = await this.getUser();
|
||||||
apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`,
|
const result: encryptedСhapter = await fetchApi(
|
||||||
|
this.apiUrl + `work/${workID}/chapter/${chapterID}/text`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token,
|
Authorization: 'Bearer ' + this.user?.token || 'guest',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
).then(res => res.json());
|
||||||
const json = (await result.json()) as encryptedСhapter;
|
|
||||||
|
|
||||||
if (json.code) {
|
if (result.code) {
|
||||||
return json.code + '\n' + json?.message;
|
return result.code + '\n' + result?.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = json.key.split('').reverse().join('') + '@_@';
|
const chapterText = decrypt(result.text, result.key, this.user?.userId);
|
||||||
let text = '';
|
if (chapterText.includes('<img')) {
|
||||||
|
return chapterText.replace(/src="(.*?)"/g, (match, url) => {
|
||||||
for (let i = 0; i < json.text.length; i++) {
|
if (!url.startsWith('http')) {
|
||||||
text += String.fromCharCode(
|
return `src="${this.site}${url}"`;
|
||||||
json.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return `src="${url}"`;
|
||||||
});
|
});
|
||||||
const chapterText = loadedCheerio.html();
|
}
|
||||||
|
|
||||||
return chapterText;
|
return chapterText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +179,40 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
isNovel ? this.site + '/work/' + path : this.site + '/reader/' + path;
|
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 = {
|
filters = {
|
||||||
sort: {
|
sort: {
|
||||||
label: 'Сортировка',
|
label: 'Сортировка',
|
||||||
@@ -333,6 +362,48 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
|
|
||||||
export default new AuthorToday();
|
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 {
|
interface response {
|
||||||
searchResults?: SearchResultsEntity[] | null;
|
searchResults?: SearchResultsEntity[] | null;
|
||||||
realTotalCount: number;
|
realTotalCount: number;
|
||||||
@@ -400,6 +471,7 @@ interface Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface responseBook {
|
interface responseBook {
|
||||||
|
message?: string;
|
||||||
chapters?: ChaptersEntity[] | null;
|
chapters?: ChaptersEntity[] | null;
|
||||||
allowDownloads: boolean;
|
allowDownloads: boolean;
|
||||||
downloadErrorCode: string;
|
downloadErrorCode: string;
|
||||||
|
|||||||
+384
-118
@@ -1,16 +1,26 @@
|
|||||||
import { Plugin } from '@typings/plugin';
|
import { Plugin } from '@typings/plugin';
|
||||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||||
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { fetchApi, fetchFile } from '@libs/fetch';
|
import { fetchApi, fetchFile } from '@libs/fetch';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
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 {
|
class RLIB implements Plugin.PluginBase {
|
||||||
id = 'RLIB';
|
id = 'RLIB';
|
||||||
name = 'RanobeLib (OLD)';
|
name = 'RanobeLib';
|
||||||
site = 'https://old.ranobelib.me/old/';
|
site = 'https://ranobelib.me';
|
||||||
version = '1.1.0';
|
apiSite = 'https://api.lib.social/api/manga/';
|
||||||
|
version = '2.0.0';
|
||||||
icon = 'src/ru/ranobelib/icon.png';
|
icon = 'src/ru/ranobelib/icon.png';
|
||||||
ui: string | undefined = undefined;
|
webStorageUtilized = true;
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -19,7 +29,7 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.site + 'manga-list?page=' + pageNo;
|
let url = this.apiSite + '?site_id[0]=3&page=' + pageNo;
|
||||||
url +=
|
url +=
|
||||||
'&sort_by=' +
|
'&sort_by=' +
|
||||||
(showLatestNovels
|
(showLatestNovels
|
||||||
@@ -65,138 +75,196 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fetchApi(url).then(res => res.text());
|
const result: TopLevel = await fetchApi(url, {
|
||||||
const loadedCheerio = parseHTML(result);
|
headers: this.user?.token,
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
if (result.data instanceof Array) {
|
||||||
this.ui = loadedCheerio('a.header-right-menu__item')
|
result.data.forEach(novel =>
|
||||||
.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 => {
|
|
||||||
novels.push({
|
novels.push({
|
||||||
name: novel.rus_name,
|
name: novel.rus_name || novel.eng_name || novel.name,
|
||||||
cover: novel.cover.default,
|
cover: novel.cover?.default || defaultCover,
|
||||||
path: novel.slug_url,
|
path: novel.slug_url || novel.id + '--' + novel.slug,
|
||||||
});
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
|
const { data }: { data: DataClass } = await fetchApi(
|
||||||
res.text(),
|
this.apiSite +
|
||||||
);
|
novelPath +
|
||||||
const loadedCheerio = parseHTML(body);
|
'?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 = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
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, '');
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.ui = loadedCheerio('a.header-right-menu__item')
|
if (data.authors?.length) {
|
||||||
.attr('href')
|
novel.author = data.authors[0].name;
|
||||||
?.replace?.(/[^0-9]/g, '');
|
}
|
||||||
|
if (data.artists?.length) {
|
||||||
|
novel.artist = data.artists[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
const chaptersRaw = body.match(/window\.__CHAPTERS__ = (\[.*?\]);/);
|
const genres = [data.genres || [], data.tags || []]
|
||||||
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
|
.flat()
|
||||||
const chaptersJson: resChapters[] = JSON.parse(chaptersRaw[1]);
|
.map(genres => genres?.name)
|
||||||
|
.filter(genres => genres);
|
||||||
|
if (genres.length) {
|
||||||
|
novel.genres = genres.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
if (!chaptersJson?.length) return novel;
|
const branch_id: { [key: number]: string } = {};
|
||||||
|
if (data.teams.length) {
|
||||||
|
data.teams.forEach(
|
||||||
|
({ name, details }) => (branch_id[details?.branch_id || '0'] = name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
chaptersJson.forEach(chapter =>
|
|
||||||
|
chaptersJSON.data.forEach(chapter =>
|
||||||
chapters.push({
|
chapters.push({
|
||||||
name:
|
name:
|
||||||
'Том ' +
|
'Том ' +
|
||||||
chapter.volume +
|
chapter.volume +
|
||||||
' Глава ' +
|
' Глава ' +
|
||||||
chapter.number +
|
chapter.number +
|
||||||
(chapter.name ? ' ' + chapter.name.trim() : ''),
|
(chapter.name ? ' ' + chapter.name : ''),
|
||||||
path: novelPath + '/v' + chapter.volume + '/c' + chapter.number,
|
path:
|
||||||
chapterNumber: chapter.index + 1,
|
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;
|
novel.chapters = chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
return novel;
|
return novel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const result = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
|
const [slug, volume, number, branch_id] = chapterPath.split('/');
|
||||||
res.text(),
|
let chapterText = '';
|
||||||
);
|
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(result);
|
if (slug && volume && number) {
|
||||||
loadedCheerio('.reader-container img').each((index, element) => {
|
const result: { data: DataClass } = await fetchApi(
|
||||||
const src =
|
this.apiSite +
|
||||||
loadedCheerio(element).attr('data-src') ||
|
slug +
|
||||||
loadedCheerio(element).attr('src');
|
'/chapter?' +
|
||||||
if (!src?.startsWith('http')) {
|
(branch_id ? 'branch_id=' + branch_id + '&' : '') +
|
||||||
loadedCheerio(element).attr('src', this.site + src);
|
'number=' +
|
||||||
} else {
|
number +
|
||||||
loadedCheerio(element).attr('src', src);
|
'&volume=' +
|
||||||
|
volume,
|
||||||
|
{ headers: this.user?.token },
|
||||||
|
).then(res => res.json());
|
||||||
|
chapterText =
|
||||||
|
result?.data?.content?.type == 'doc'
|
||||||
|
? jsonToHtml(result.data.content.content)
|
||||||
|
: result?.data?.content;
|
||||||
}
|
}
|
||||||
loadedCheerio(element).removeAttr('data-src');
|
|
||||||
});
|
|
||||||
|
|
||||||
const chapterText = loadedCheerio('.reader-container').html();
|
return chapterText;
|
||||||
return chapterText || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||||
const result = await fetchApi(this.site + 'api/manga?q=' + searchTerm);
|
const url = this.apiSite + '?site_id[0]=3&q=' + searchTerm;
|
||||||
const { data }: { data: resNovels[] } = await result.json();
|
const result: TopLevel = await fetchApi(url, {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
headers: this.user?.token,
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
data.forEach(novel =>
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
if (result.data instanceof Array) {
|
||||||
|
result.data.forEach(novel =>
|
||||||
novels.push({
|
novels.push({
|
||||||
name: novel.rus_name || novel.name,
|
name: novel.rus_name || novel.eng_name || novel.name,
|
||||||
cover: novel?.cover?.default || '',
|
cover: novel.cover?.default || defaultCover,
|
||||||
path: novel.slug_url,
|
path: novel.slug_url || novel.id + '--' + novel.slug,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchImage = fetchFile;
|
fetchImage = fetchFile;
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
resolveUrl = (path: string, isNovel?: boolean) => {
|
||||||
this.site + path + (this.ui ? '?ui=' + this.ui : '');
|
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 = {
|
filters = {
|
||||||
sort_by: {
|
sort_by: {
|
||||||
@@ -410,41 +478,230 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
|
|
||||||
export default new RLIB();
|
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;
|
id: number;
|
||||||
name: string;
|
username: string;
|
||||||
rus_name: string;
|
avatar: Cover;
|
||||||
eng_name: string;
|
last_online_at: string;
|
||||||
slug: string;
|
metadata: Metadata;
|
||||||
slug_url: string;
|
|
||||||
cover: Cover;
|
|
||||||
ageRestriction: AgeRestrictionOrTypeOrStatus;
|
|
||||||
site: number;
|
|
||||||
type: AgeRestrictionOrTypeOrStatus;
|
|
||||||
rating: Rating;
|
|
||||||
is_licensed: boolean;
|
|
||||||
model: string;
|
|
||||||
status: AgeRestrictionOrTypeOrStatus;
|
|
||||||
releaseDateString: string;
|
|
||||||
}
|
}
|
||||||
interface Cover {
|
interface Metadata {
|
||||||
filename: string;
|
auth_domains: string;
|
||||||
thumbnail: string;
|
|
||||||
default: string;
|
|
||||||
}
|
}
|
||||||
interface AgeRestrictionOrTypeOrStatus {
|
|
||||||
|
interface TopLevel {
|
||||||
|
data: DataClass | DataClass[];
|
||||||
|
links?: Links;
|
||||||
|
meta?: Meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgeRestriction {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
interface Rating {
|
|
||||||
average: string;
|
interface Branch {
|
||||||
averageFormated: string;
|
id: number;
|
||||||
votes: number;
|
branch_id: null;
|
||||||
votesFormated: string;
|
created_at: string;
|
||||||
user: number;
|
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;
|
id: number;
|
||||||
index: number;
|
index: number;
|
||||||
item_number: number;
|
item_number: number;
|
||||||
@@ -452,4 +709,13 @@ interface resChapters {
|
|||||||
number: string;
|
number: string;
|
||||||
number_secondary: string;
|
number_secondary: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
branches_count: number;
|
||||||
|
branches: Branch[];
|
||||||
|
}
|
||||||
|
interface BranchesEntity {
|
||||||
|
id: number;
|
||||||
|
branch_id?: number;
|
||||||
|
created_at: string;
|
||||||
|
teams?: BranchTeam[];
|
||||||
|
user: User;
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-18
@@ -1,12 +1,12 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { Plugin } from '@typings/plugin';
|
|
||||||
import { languages } from '@libs/languages';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import sizeOf from 'image-size';
|
||||||
|
|
||||||
const root = path.join(__dirname, '..');
|
const root = path.join(__dirname, '..');
|
||||||
const size = 96;
|
const size = 96;
|
||||||
|
const minSize = 16;
|
||||||
|
|
||||||
const skip: string[] = [
|
const skip = new Set([
|
||||||
//custom icons
|
//custom icons
|
||||||
'FWK.US',
|
'FWK.US',
|
||||||
'ReN',
|
'ReN',
|
||||||
@@ -23,19 +23,14 @@ const skip: string[] = [
|
|||||||
'sektenovel',
|
'sektenovel',
|
||||||
'sonicmtl',
|
'sonicmtl',
|
||||||
'translatinotaku',
|
'translatinotaku',
|
||||||
|
'warriorlegendtrad',
|
||||||
'wuxiaworld.site',
|
'wuxiaworld.site',
|
||||||
|
]);
|
||||||
|
|
||||||
//low quality
|
const used = new Set([
|
||||||
'BLN',
|
path.join(root, 'icons', 'coverNotAvailable.webp'),
|
||||||
'NO.net',
|
path.join(root, 'icons', 'siteNotAvailable.png'),
|
||||||
'novelbookid',
|
]);
|
||||||
'novelhall',
|
|
||||||
'olaoe',
|
|
||||||
];
|
|
||||||
|
|
||||||
const used = new Set();
|
|
||||||
used.add(path.join(root, 'icons', 'coverNotAvailable.webp'));
|
|
||||||
used.add(path.join(root, 'icons', 'siteNotAvailable.png'));
|
|
||||||
|
|
||||||
const notAvailableImage = fs.readFileSync(
|
const notAvailableImage = fs.readFileSync(
|
||||||
path.join(root, 'icons', 'siteNotAvailable.png'),
|
path.join(root, 'icons', 'siteNotAvailable.png'),
|
||||||
@@ -54,6 +49,8 @@ const notAvailableImage = fs.readFileSync(
|
|||||||
let language;
|
let language;
|
||||||
for (let plugin in plugins) {
|
for (let plugin in plugins) {
|
||||||
const { id, name, site, iconUrl, lang } = plugins[plugin];
|
const { id, name, site, iconUrl, lang } = plugins[plugin];
|
||||||
|
const icon = iconUrl && 'icons/' + iconUrl.split('icons/')[1];
|
||||||
|
|
||||||
if (language !== lang) {
|
if (language !== lang) {
|
||||||
language = lang;
|
language = lang;
|
||||||
console.log(
|
console.log(
|
||||||
@@ -62,12 +59,10 @@ const notAvailableImage = fs.readFileSync(
|
|||||||
.padEnd(30, '='),
|
.padEnd(30, '='),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let icon;
|
|
||||||
if (iconUrl) icon = 'icons/' + iconUrl.split('icons/')[1];
|
|
||||||
try {
|
try {
|
||||||
if (icon) used.add(path.join(root, icon));
|
if (icon) used.add(path.join(root, icon));
|
||||||
|
if (!skip.has(id) && icon && site) {
|
||||||
if (!skip.includes(id) && icon && site) {
|
|
||||||
const image = await fetch(
|
const image = await fetch(
|
||||||
`https://www.google.com/s2/favicons?domain=${site}&sz=${size}&type=png`,
|
`https://www.google.com/s2/favicons?domain=${site}&sz=${size}&type=png`,
|
||||||
)
|
)
|
||||||
@@ -85,8 +80,30 @@ const notAvailableImage = fs.readFileSync(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
fs.writeFileSync(icon, image);
|
||||||
console.log(' ', name.padEnd(26), `(${id})`, '\r✅');
|
console.log(' ', name.padEnd(26), `(${id})`, '\r✅');
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
' ',
|
||||||
|
name.padEnd(26),
|
||||||
|
`(${id})`.padEnd(20),
|
||||||
|
'Low quality',
|
||||||
|
'\r🔄',
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄');
|
console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export namespace Plugin {
|
|||||||
site: string;
|
site: string;
|
||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
version: string;
|
version: string;
|
||||||
|
//flag indicates whether access to LocalStorage, SesesionStorage is required.
|
||||||
|
webStorageUtilized?: boolean;
|
||||||
popularNovels(
|
popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
options: PopularNovelsOptions<Filters>,
|
options: PopularNovelsOptions<Filters>,
|
||||||
|
|||||||
Reference in New Issue
Block a user