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",
"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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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🔄');
}
+2
View File
@@ -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>,