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
+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;