Feat: Plugin storages (#904)

* update

* update

* Update ranobelib.ts

* test

* test
This commit is contained in:
Rider21
2024-05-28 11:53:56 +03:00
committed by GitHub
parent 020930ed0a
commit df0c4b0a81
8 changed files with 708 additions and 203 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 760 B

+108
View File
@@ -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();
+55 -16
View File
@@ -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
View File
@@ -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",
+116 -44
View File
@@ -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();
headers: { const book: responseBook = await fetchApi(
Authorization: token, 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 = { 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(
headers: { this.apiUrl + 'work/' + workID + '/content',
Authorization: token, {
headers: {
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)), }
); 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; 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;
+388 -122
View File
@@ -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"]') if (data.authors?.length) {
.text() novel.author = data.authors[0].name;
.trim() }
.replace(/[\n\r]+/g, ', ') if (data.artists?.length) {
.replace(/ /g, ''); novel.artist = data.artists[0].name;
}
loadedCheerio( const genres = [data.genres || [], data.tags || []]
'div.media-info-list > div[class="media-info-list__item"]', .flat()
).each(function () { .map(genres => genres?.name)
let name = loadedCheerio(this) .filter(genres => genres);
.find('div[class="media-info-list__title"]') if (genres.length) {
.text(); novel.genres = genres.join(', ');
}
if (name === 'Автор') { const branch_id: { [key: number]: string } = {};
novel.author = loadedCheerio(this) if (data.teams.length) {
.find('div[class="media-info-list__value"]') data.teams.forEach(
.text() ({ name, details }) => (branch_id[details?.branch_id || '0'] = name),
.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') const chaptersJSON: { data: DataChapter[] } = await fetchApi(
.attr('href') this.apiSite + novelPath + '/chapters',
?.replace?.(/[^0-9]/g, ''); {
headers: this.user?.token,
const chaptersRaw = body.match(/window\.__CHAPTERS__ = (\[.*?\]);/); },
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { ).then(res => res.json());
const chaptersJson: resChapters[] = JSON.parse(chaptersRaw[1]);
if (!chaptersJson?.length) return novel;
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,
loadedCheerio(element).removeAttr('data-src'); { 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[]> { 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[] = [];
novels.push({ if (result.data instanceof Array) {
name: novel.rus_name || novel.name, result.data.forEach(novel =>
cover: novel?.cover?.default || '', novels.push({
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; 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;
} }
+37 -20
View File
@@ -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;
} }
fs.writeFileSync(icon, image); const imageSize = sizeOf(image);
console.log(' ', name.padEnd(26), `(${id})`, '\r✅'); 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 { } else {
console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄'); console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄');
} }
+2
View File
@@ -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>,