From e299175828d7590182349aaf15780c7b13ab46a5 Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:56:19 +0300 Subject: [PATCH 1/5] update --- libs/storage.ts | 134 +++++++ plugins/russian/authortoday.ts | 149 +++++--- plugins/russian/ranobelib.ts | 630 +++++++++++++++++++-------------- types/plugin.ts | 2 + 4 files changed, 612 insertions(+), 303 deletions(-) create mode 100644 libs/storage.ts diff --git a/libs/storage.ts b/libs/storage.ts new file mode 100644 index 000000000..db3210b3d --- /dev/null +++ b/libs/storage.ts @@ -0,0 +1,134 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Represents a storage system with methods for setting, getting, and deleting key-value pairs. + */ +class Storage { + private db: Record< + string, + Record + >; + + /** + * Initializes a new instance of the Storage class. + */ + constructor() { + this.db = {}; + } + + /** + * Sets a key-value pair in the storage. + * + * @param pluginID - The ID of the plugin. + * @param key - The key to set. + * @param value - The value to set. + * @param expires - Optional. The expiration date for the key-value pair. + */ + set( + pluginID: string, + key: string, + value: any, + expires?: Date | number, + ): void { + if (!this.db[pluginID]) this.db[pluginID] = {}; + this.db[pluginID][key] = { + created: new Date(), + value, + expires: expires instanceof Date ? expires.getTime() : expires, + }; + } + + /** + * Gets the value associated with a key from the storage. + * + * @param pluginID - The ID of the plugin. + * @param key - The key to retrieve. + * @param raw - Optional. If true, returns the raw storage item object. + * @returns The value associated with the key or undefined if not found or expired. + */ + get(pluginID: string, key: string, raw?: boolean): any { + const item = this.db[pluginID]?.[key]; + if (item?.expires && Date.now() > item.expires) { + this.delete(pluginID, key); + return undefined; + } + return raw ? item : item?.value; + } + + /** + * Gets all keys associated with a plugin from the storage. + * + * @param pluginID - The ID of the plugin. + * @returns An array of keys associated with the plugin. + */ + getAllKeys(pluginID: string): string[] { + return Object.keys(this.db[pluginID] || {}); + } + + /** + * Deletes a key from the storage. + * + * @param pluginID - The ID of the plugin. + * @param key - The key to delete. + */ + delete(pluginID: string, key: string): void { + delete this.db[pluginID]?.[key]; + } + + /** + * Clears all keys associated with a plugin from the storage. + * + * @param pluginID - The ID of the plugin. + */ + clearAll(pluginID: string): void { + delete this.db[pluginID]; + } +} + +// 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]: string | undefined; +} + +/** + * Represents a simplified version of the browser's localStorage. + */ +class LocalStorage { + db: Record; + + /** + * Initializes a new instance of the LocalStorage class. + */ + constructor() { + this.db = {}; + } + + /** + * Gets the storage object associated with a plugin ID. + * + * @param pluginID - The ID of the plugin. + * @returns The storage object associated with the plugin ID. + */ + get(pluginID: string): StorageObject | undefined { + return this.db[pluginID] || {}; + } +} + +// Export singleton instances of LocalStorage and sessionStorage +export const localStorage = new LocalStorage(); +export const sessionStorage = new LocalStorage(); diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index c12eba0e7..e7c760ac4 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -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.0.1'; async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, ): Promise { - 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 { - 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,38 +121,42 @@ class AuthorToday implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { 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('') + '@_@'; + const key = + result.key.split('').reverse().join('') + + '@_@' + + (this.user?.userId || ''); let text = ''; - for (let i = 0; i < json.text.length; i++) { + for (let i = 0; i < result.text.length; i++) { text += String.fromCharCode( - json.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length)), + result.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); - } - }); - const chapterText = loadedCheerio.html(); - return chapterText; + if (text.includes(' { + if (!url.startsWith('http')) { + return `src="${this.site}${url}"`; + } + return `src="${url}"`; + }); + } + + return text; } async searchNovels( @@ -184,6 +190,41 @@ 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(this.id, '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(this.id, '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( + this.id, + '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 +374,31 @@ class AuthorToday implements Plugin.PluginBase { export default new AuthorToday(); +interface authorization { + userId: number; + token: string; + issued: string; + expires: string; +} + +export 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 +466,7 @@ interface Duration { } interface responseBook { + message?: string; chapters?: ChaptersEntity[] | null; allowDownloads: boolean; downloadErrorCode: string; diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 56e47b869..da55fb832 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -1,8 +1,9 @@ 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 } = { @@ -16,9 +17,10 @@ class RLIB implements Plugin.PluginBase { id = 'RLIB'; name = 'RanobeLib'; site = 'https://ranobelib.me'; - version = '1.0.0'; + 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, @@ -27,233 +29,257 @@ class RLIB implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + '/manga-list?sort='; - url += showLatestNovels - ? 'last_chapter_at' - : filters?.sort?.value || 'rate'; - url += '&dir=' + (filters?.order?.value || 'desc'); - url += '&chapters[min]=' + (filters?.require_chapters?.value ? '1' : '0'); - - Object.entries(filters || {}).forEach(([type, { value }]: any) => { - if (value instanceof Array && value.length) { - url += '&' + type + '[]=' + value.join('&' + type + '[]='); + let url = this.apiSite + '?site_id[0]=3&page=' + pageNo; + url += + '&sort_by=' + + (showLatestNovels + ? 'last_chapter_at' + : filters?.sort_by?.value || 'rating_score'); + url += '&sort_type=' + (filters?.sort_type?.value || 'desc'); + + if (filters?.require_chapters?.value) { + url += '&chapters[min]=1'; + } + if (filters?.types?.value?.length) { + url += '&types[]=' + filters.types.value.join('&types[]='); + } + if (filters?.scanlateStatus?.value?.length) { + url += + '&scanlateStatus[]=' + + filters.scanlateStatus.value.join('&scanlateStatus[]='); + } + if (filters?.manga_status?.value?.length) { + url += + '&manga_status[]=' + + filters.manga_status.value.join('&manga_status[]='); + } + + if (filters?.genres) { + if (filters.genres.value?.include?.length) { + url += '&genres[]=' + filters.genres.value.include.join('&genres[]='); } - if (value?.include instanceof Array && value.include.length) { + if (filters.genres.value?.exclude?.length) { url += - '&' + - type + - '[include][]=' + - value.include.join('&' + type + '[include][]='); + '&genres_exclude[]=' + + filters.genres.value.exclude.join('&genres_exclude[]='); + } + } + if (filters?.tags) { + if (filters.tags.value?.include?.length) { + url += '&tags[]=' + filters.tags.value.include.join('&tags[]='); } - if (value?.exclude instanceof Array && value.exclude.length) { + if (filters.tags.value?.exclude?.length) { url += - '&' + - type + - '[exclude][]=' + - value.exclude.join('&' + type + '[exclude][]='); + '&tags_exclude[]=' + + filters.tags.value.exclude.join('&tags_exclude[]='); } - }); - - url += '&page=' + pageNo; + } - const result = await fetchApi(url).then(res => res.text()); - const loadedCheerio = parseHTML(result); - this.ui = loadedCheerio('a.header-right-menu__item') - .attr('href') - ?.replace?.(/[^0-9]/g, ''); + const result: TopLevel = await fetchApi(url, { + headers: this.user?.token, + }).then(res => res.json()); const novels: Plugin.NovelItem[] = []; - loadedCheerio('.media-card-wrap').each((index, element) => { - const name = loadedCheerio(element).find('.media-card__title').text(); - const cover = loadedCheerio(element) - .find('a.media-card') - .attr('data-src'); - const url = loadedCheerio(element).find('a.media-card').attr('href'); - if (!url) return; - novels.push({ name, cover, path: url.replace(this.site, '') }); - }); - + 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; } async parseNovel(novelPath: string): Promise { - 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(); - - novel.genres = loadedCheerio('div[class="media-tags"]') - .text() - .trim() - .replace(/[\n\r]+/g, ', ') - .replace(/ /g, ''); - - loadedCheerio( - 'div[class="media-info-list paper"] > [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') - .attr('href') - ?.replace?.(/[^0-9]/g, ''); + if (data.status?.id) { + novel.status = statusKey[data.status.id] || NovelStatus.Unknown; + } - const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/); - if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { - const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]); + if (data.authors?.length) { + novel.author = data.authors[0].name; + } + if (data.artists?.length) { + novel.artist = data.artists[0].name; + } - if (!novel.name) { - novel.name = - chaptersJson.manga.rusName || - chaptersJson.manga.engName || - chaptersJson.manga.name; - } - novel.status = - statusKey[chaptersJson.manga.status] || NovelStatus.Unknown; - this.ui = chaptersJson?.user?.id; - - if (!chaptersJson.chapters.list?.length) return novel; - const totalChapters = chaptersJson.chapters.list.length; - - const customPage: { [key: number]: string } = {}; - const customOrder: { [key: number]: number } = {}; - if ( - chaptersJson.chapters.branches?.length && - chaptersJson.chapters.branches.length > 1 - ) { - //if the novel is being translated by more than one team - chaptersJson.chapters.branches.forEach(({ teams, id }) => { - if (teams?.length) { - customPage[id || 0] = - teams.find(team => team.is_active)?.name || teams[0].name; - } - }); - //fixes the chapter's position. - chaptersJson.chapters.list.forEach(chapter => { - chapter.index = customOrder[chapter.branch_id || 0] || 1; - customOrder[chapter.branch_id || 0] = - (customOrder[chapter.branch_id || 0] || 1) + 1; - }); - } + const genres = [data.genres || [], data.tags || []] + .flat() + .map(genres => genres?.name) + .filter(genres => genres); + if (genres.length) { + novel.genres = genres.join(', '); + } + + 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: DataClass[] } = await fetchApi( + this.apiSite + novelPath + '/chapters', + { + headers: this.user?.token, + }, + ).then(res => res.json()); + + if (chaptersJSON.data.length) { const chapters: Plugin.ChapterItem[] = []; - chaptersJson.chapters.list.forEach((chapter, chapterIndex) => + + chaptersJSON.data.forEach((chapter: any) => chapters.push({ name: 'Том ' + - chapter.chapter_volume + + chapter.volume + ' Глава ' + - chapter.chapter_number + - (chapter.chapter_name ? ' ' + chapter.chapter_name.trim() : ''), + chapter.number + + (chapter.name ? ' ' + chapter.name : ''), path: novelPath + - '/v' + - chapter.chapter_volume + - '/c' + - chapter.chapter_number + - '?bid=' + - (chapter.branch_id || ''), - releaseTime: dayjs(chapter.chapter_created_at).format('LLL'), - chapterNumber: - customOrder[chapter.branch_id || 0] - (chapter.index || 0) || - totalChapters - chapterIndex, - page: customPage[chapter.branch_id || 0] || 'Основной перевод', + '/' + + 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 = - chaptersJson.chapters.branches?.length && - chaptersJson.chapters.branches.length > 1 - ? chapters.sort((a, b) => { - if ((a.page || 0) > (b.page || 0)) return 1; - if ((a.page || 0) < (b.page || 0)) return -1; - return (a.chapterNumber || 0) - (b.chapterNumber || 0); - }) - : chapters.reverse(); + + novel.chapters = chapters; } + return novel; } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.resolveUrl(chapterPath)).then(res => - res.text(), - ); - - 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'); - }); + const [slug, volume, number, branch_id] = chapterPath.split('/'); + let chapterText = ''; - const chapterText = loadedCheerio('.reader-container').html(); - return chapterText || ''; + 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 || ''; + } + return chapterText; } async searchNovels(searchTerm: string): Promise { - const result = await fetchApi( - this.site + '/search?q=' + searchTerm + '&type=manga', - ); - const body = (await result.json()) as Manga[]; - 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()); - body.forEach(novel => - novels.push({ - name: novel.rus_name || novel.name, - cover: novel.coverImage, - path: '/' + novel.slug, - }), - ); + 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 ? (isNovel ? '?' : '&') + '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(this.id, 'user'); + if (user) { + return { token: { Authorization: 'Bearer ' + user?.token }, ui: user.id }; + } + const dataRaw = localStorage.get(this.id)?.auth; + if (!dataRaw) { + return {}; + } + + const data = JSON.parse(dataRaw) as authorization; + if (!data?.token?.access_token) return; + storage.set( + this.id, + '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: { + sort_by: { label: 'Сортировка', - value: 'rate', + value: 'rating_score', options: [ - { label: 'Рейтинг', value: 'rate' }, - { label: 'Имя', value: 'name' }, - { label: 'Просмотры', value: 'views' }, - { label: 'Дате добавления', value: 'created_at' }, + { label: 'По рейтингу', value: 'rate_avg' }, + { label: 'По популярности', value: 'rating_score' }, + { label: 'По просмотрам', value: 'views' }, + { label: 'Количеству глав', value: 'chap_count' }, { label: 'Дате обновления', value: 'last_chapter_at' }, - { label: 'Количество глав', value: 'chap_count' }, + { label: 'Дате добавления', value: 'created_at' }, + { label: 'По названию (A-Z)', value: 'name' }, + { label: 'По названию (А-Я)', value: 'rus_name' }, ], type: FilterTypes.Picker, }, - order: { + sort_type: { label: 'Порядок', value: 'desc', options: [ @@ -262,34 +288,20 @@ class RLIB implements Plugin.PluginBase { ], type: FilterTypes.Picker, }, - type: { + types: { label: 'Тип', value: [], options: [ - { label: 'Авторский', value: '14' }, - { label: 'Английский', value: '13' }, - { label: 'Китай', value: '12' }, + { label: 'Япония', value: '10' }, { label: 'Корея', value: '11' }, + { label: 'Китай', value: '12' }, + { label: 'Английский', value: '13' }, + { label: 'Авторский', value: '14' }, { label: 'Фанфик', value: '15' }, - { label: 'Япония', value: '10' }, ], type: FilterTypes.CheckboxGroup, }, - format: { - label: 'Формат выпуска', - value: { include: [], exclude: [] }, - options: [ - { label: '4-кома (Ёнкома)', value: '1' }, - { label: 'В цвете', value: '4' }, - { label: 'Веб', value: '6' }, - { label: 'Вебтун', value: '7' }, - { label: 'Додзинси', value: '3' }, - { label: 'Сборник', value: '2' }, - { label: 'Сингл', value: '5' }, - ], - type: FilterTypes.ExcludableCheckboxGroup, - }, - status: { + scanlateStatus: { label: 'Статус перевода', value: [], options: [ @@ -366,6 +378,7 @@ class RLIB implements Plugin.PluginBase { { label: 'Ужасы', value: '67' }, { label: 'Фантастика', value: '68' }, { label: 'Фэнтези', value: '69' }, + { label: 'Хентай', value: '84' }, { label: 'Школа', value: '70' }, { label: 'Эротика', value: '71' }, { label: 'Этти', value: '72' }, @@ -379,7 +392,7 @@ class RLIB implements Plugin.PluginBase { value: { include: [], exclude: [] }, options: [ { label: 'Авантюристы', value: '328' }, - { label: 'Антигерой', value: '176' }, + { label: 'Антигерой', value: '175' }, { label: 'Бессмертные', value: '333' }, { label: 'Боги', value: '218' }, { label: 'Борьба за власть', value: '309' }, @@ -462,85 +475,178 @@ class RLIB implements Plugin.PluginBase { export default new RLIB(); -interface responseBook { - hasStickyPermission: boolean; - bookmark?: null; - auth: boolean; - comments_version: string; - manga: Manga; - chapters: Chapters; - user?: User; +interface authorization { + token: Token; + auth: Auth; + timestamp: number; } -interface Manga { +interface Token { + token_type: string; + expires_in: number; + access_token: string; + refresh_token: string; + timestamp: number; +} +interface Auth { + id: number; + username: string; + avatar: Cover; + last_online_at: string; + metadata: Metadata; +} +interface Metadata { + auth_domains: string; +} + +interface TopLevel { + data: DataClass | DataClass[]; + links?: Links; + meta?: Meta; +} + +interface AgeRestriction { + id: number; + label: string; +} + +interface Branch { + id: number; + branch_id: null; + created_at: string; + teams: BranchTeam[]; + user: User; +} + +interface BranchTeam { id: number; - name: string; - rusName?: string; - rus_name?: string; - engName?: string; slug: string; - status: number; - chapters_count: number; - altNames?: string[] | null; - coverImage?: string; - href?: string; + slug_url: string; + model: string; + name: string; + cover: Cover; } -interface Chapters { - list?: ListEntity[]; - teams?: TeamsEntity[]; - branches?: BranchesEntity[]; - is_paid?: string[] | null; + +interface Cover { + filename: null | string; + thumbnail: string; + default: string; } -interface ListEntity { - index?: number; //crutch - chapter_id: number; - chapter_slug: string; - chapter_name: string; - chapter_number: string; - chapter_volume: number; - chapter_moderated: number; - chapter_user_id: number; - chapter_expired_at: string; - chapter_scanlator_id: number; - chapter_created_at: string; - status?: null; - price: number; - branch_id?: number; + +interface User { username: string; + id: number; +} + +interface Rating { + average: string; + votes: number; + votesFormated: string; } -interface TeamsEntity { + +interface DataClass { + id: number; name: string; - alt_name: string; - cover: string; + rus_name?: string; + eng_name?: string; slug: string; - id: number; - branch_id: number; - sale: number; - href: string; - pivot: Pivot; -} -interface Pivot { - manga_id: number; - team_id: number; + slug_url?: string; + cover?: Cover; + ageRestriction?: AgeRestriction; + site?: number; + type: AgeRestriction | 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?: string; + attachments?: Attachment[]; } -interface BranchesEntity { + +interface Artist { id: number; - manga_id: number; + slug: string; + slug_url: string; + model: string; name: string; - teams?: TeamsEntity1[] | null; + 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 TeamsEntity1 { + +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; - cover: string; - branch_id: number; - is_active: number; + slug_url: string; + model: string; + name: string; + cover: Cover; + details?: Details; + vk?: string; + discord?: null; } -interface User { - id: string; - avatar: string; - access: boolean; - isAdmin: boolean; - paid: boolean; + +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; } diff --git a/types/plugin.ts b/types/plugin.ts index a440e8daf..797e4c346 100644 --- a/types/plugin.ts +++ b/types/plugin.ts @@ -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, From e2057d41f3a63633fabcccd2c5bb579f63ab7c7a Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:07:53 +0300 Subject: [PATCH 2/5] update --- icons/src/en/ellotl/icon.png | Bin 590 -> 609 bytes package.json | 1 + plugins/russian/ranobelib.ts | 21 ++++++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/icons/src/en/ellotl/icon.png b/icons/src/en/ellotl/icon.png index f6a00c135e152dc2eb5d139bd4c7ecb1e1b0773f..93b22401c8bc971747212e7374cfa4d2727ec88d 100644 GIT binary patch delta 584 zcmX@d@{nbMO1*u6Pl&5~fe!;1+-rr<=;Y%LhK*SaXQ~)(w=lS9GW3KnCW;uw;kb60--$^ERhmXlF$`s92UbBn1Cf3%Ne{A6n2WMA;?*m}mQ z=EPPORX2V08 z_p^0@U&nhz)r|rno*x^p91(9iR=@jpf=bwZuK=kn-|rgcH1-~|XLT2@X9>4-bS&pS zu)cZufxh1%pY|U(`F)X!qno)YoA8!DUk~*6o!coW9I%)*BktdhSQhcx`Tkde)$}VK zHZ&#KzS+3!;S#F{Ijb*e&5Yp;VN`BSHGk{OkiF!7T)`Pl3sx1!f)x{lBdq_he3+ln zV&tGbbK`bjp)EaivlKL>{Px>D=&CQvXxz0;?$IM>K@rQ-(vAxfWZDvy{&i$GMt#&^ z=3Q)>_JFHt=`wlAFHh~7_V0B4%$i!lt*Y3O$E3<~F?UVJf-S3;PZ4FYV9gge(tm~J z-l8MU2W=Q1Mi~1$Sj;#pEYLU2nDu4*K@ Twtri5K*__?)z4*}Q$iB}cNO*R delta 565 zcmV-50?Pg21kMDIB!4qdOjJePg#gQX0CGhDx^MuHRsfw|0I+HR;D-PN0ss;V0M>v2 zW<3Be8~{To0DemVQZWD^696zOE;j%G0oF-GK~!ko?U>z;gdhw?LFA`^;QK%AqO;iu z*71Umi%mEtVF1xEhD#If&0v+jrn0&v$ z%IF>;WBUb~TwYkR>nQAtz?H$zVhAT-e=}O}A0t5KKrjNS*NO!k!SRoevH$l`ZW-)MOWvi$2=0B+*?0j3eCr4l@Xaq$!0r#m3B diff --git a/package.json b/package.json index cc6276123..4b67f748c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "json": "tsc && node ./.js/scripts/json_plugins.js", "generate": "ts-node ./scripts/multisrc/generate.ts", "clearMultisrc": "find . -type f -wholename \"*plugins*\\[*\\]*.[t,j]s\" -delete", + "icon": "tsc && node ./.js/scripts/icon_reloading.js", "less": "npx less ./test_web/static/css/index.less ./test_web/static/css/index.css", "host-linux": "chmod +x ./host.sh && ./host.sh", "host-windows": "powershell ./host.ps1", diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 03e815453..0e025a35f 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -133,7 +133,7 @@ class RLIB implements Plugin.PluginBase { ); } - const chaptersJSON: { data: DataClass[] } = await fetchApi( + const chaptersJSON: { data: DataChapter[] } = await fetchApi( this.apiSite + novelPath + '/chapters', { headers: this.user?.token, @@ -650,3 +650,22 @@ interface Meta { seed?: string; country?: string; } + +interface DataChapter { + id: number; + index: number; + item_number: number; + volume: string; + 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; +} \ No newline at end of file From 9424099a880690de02a3b454435d75222df8f991 Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:54:32 +0300 Subject: [PATCH 3/5] Update ranobelib.ts --- plugins/russian/ranobelib.ts | 63 ++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 0e025a35f..5ea590eee 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -187,8 +187,12 @@ class RLIB implements Plugin.PluginBase { volume, { headers: this.user?.token }, ).then(res => res.json()); - chapterText = result?.data?.content || ''; + chapterText = + result?.data?.content?.type == 'doc' + ? jsonToHtml(result.data.content.content) + : result?.data?.content; } + return chapterText; } @@ -238,7 +242,7 @@ class RLIB implements Plugin.PluginBase { getUser = () => { const user = storage.get(this.id, 'user'); if (user) { - return { token: { Authorization: 'Bearer ' + user?.token }, ui: user.id }; + return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id }; } const dataRaw = localStorage.get(this.id)?.auth; if (!dataRaw) { @@ -475,6 +479,53 @@ class RLIB implements Plugin.PluginBase { export default new RLIB(); +function jsonToHtml(json: HTML[], html: string = '') { + json.forEach(element => { + switch (element.type) { + case 'hardBreak': + html += '
'; + break; + case 'horizontalRule': + html += '
'; + break; + case 'image': + if (element.attrs) { + const attrs = Object.entries(element.attrs) + .filter(attr => attr?.[1]) + .map(attr => `${attr[0]}="${attr[1]}"`); + html += ''; + } + break; + case 'paragraph': + html += + '

' + + (element.content ? jsonToHtml(element.content) : '
') + + '

'; + 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; @@ -553,7 +604,7 @@ interface DataClass { cover?: Cover; ageRestriction?: AgeRestriction; site?: number; - type: AgeRestriction | string; + type: string; summary?: string; is_licensed?: boolean; teams: DataTeam[]; @@ -573,7 +624,7 @@ interface DataClass { created_at?: string; moderated?: AgeRestriction; likes_count?: number; - content?: string; + content?: any; attachments?: Attachment[]; } @@ -666,6 +717,6 @@ interface BranchesEntity { id: number; branch_id?: number; created_at: string; - teams?: (BranchTeam)[]; + teams?: BranchTeam[]; user: User; -} \ No newline at end of file +} From 4c11e9669e71fca31efb8ac9d7d101e205de3f1c Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Fri, 10 May 2024 17:46:15 +0300 Subject: [PATCH 4/5] test --- libs/storage.ts | 78 ++++++++++++---------------------- package-lock.json | 14 +++--- package.json | 2 +- plugins/russian/authortoday.ts | 5 +-- plugins/russian/ranobelib.ts | 5 +-- 5 files changed, 38 insertions(+), 66 deletions(-) diff --git a/libs/storage.ts b/libs/storage.ts index db3210b3d..9cd73bcbc 100644 --- a/libs/storage.ts +++ b/libs/storage.ts @@ -1,14 +1,8 @@ import fs from 'fs'; import path from 'path'; -/** - * Represents a storage system with methods for setting, getting, and deleting key-value pairs. - */ class Storage { - private db: Record< - string, - Record - >; + private db: Record; /** * Initializes a new instance of the Storage class. @@ -18,21 +12,14 @@ class Storage { } /** - * Sets a key-value pair in the storage. + * Sets a key-value pair in storage. * - * @param pluginID - The ID of the plugin. - * @param key - The key to set. - * @param value - The value to set. - * @param expires - Optional. The expiration date for the key-value pair. + * @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( - pluginID: string, - key: string, - value: any, - expires?: Date | number, - ): void { - if (!this.db[pluginID]) this.db[pluginID] = {}; - this.db[pluginID][key] = { + set(key: string, value: any, expires?: Date | number): void { + this.db[key] = { created: new Date(), value, expires: expires instanceof Date ? expires.getTime() : expires, @@ -40,30 +27,28 @@ class Storage { } /** - * Gets the value associated with a key from the storage. + * Retrieves the value for a given key from storage. * - * @param pluginID - The ID of the plugin. - * @param key - The key to retrieve. - * @param raw - Optional. If true, returns the raw storage item object. - * @returns The value associated with the key or undefined if not found or expired. + * @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(pluginID: string, key: string, raw?: boolean): any { - const item = this.db[pluginID]?.[key]; + get(key: string, raw?: boolean): any { + const item = this.db[key]; if (item?.expires && Date.now() > item.expires) { - this.delete(pluginID, key); + this.delete(key); return undefined; } return raw ? item : item?.value; } /** - * Gets all keys associated with a plugin from the storage. + * Retrieves all keys set by the `set` method. * - * @param pluginID - The ID of the plugin. - * @returns An array of keys associated with the plugin. + * @returns {string[]} An array of keys. */ - getAllKeys(pluginID: string): string[] { - return Object.keys(this.db[pluginID] || {}); + getAllKeys(): string[] { + return Object.keys(this.db); } /** @@ -72,17 +57,15 @@ class Storage { * @param pluginID - The ID of the plugin. * @param key - The key to delete. */ - delete(pluginID: string, key: string): void { - delete this.db[pluginID]?.[key]; + delete(key: string): void { + delete this.db[key]; } /** - * Clears all keys associated with a plugin from the storage. - * - * @param pluginID - The ID of the plugin. + * Clears all stored items from storage. */ clearAll(pluginID: string): void { - delete this.db[pluginID]; + this.db = {}; } } @@ -102,30 +85,21 @@ 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]: string | undefined; + [key: string]: any; } /** * Represents a simplified version of the browser's localStorage. */ class LocalStorage { - db: Record; + private db: StorageObject; - /** - * Initializes a new instance of the LocalStorage class. - */ constructor() { this.db = {}; } - /** - * Gets the storage object associated with a plugin ID. - * - * @param pluginID - The ID of the plugin. - * @returns The storage object associated with the plugin ID. - */ - get(pluginID: string): StorageObject | undefined { - return this.db[pluginID] || {}; + get(): StorageObject | undefined { + return this.db; } } diff --git a/package-lock.json b/package-lock.json index 3fe437a39..47dda36d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "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", @@ -4098,9 +4098,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", @@ -7669,9 +7669,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", diff --git a/package.json b/package.json index 4b67f748c..dcfd97f0a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "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", diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index e7c760ac4..7d49367a3 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -192,7 +192,7 @@ class AuthorToday implements Plugin.PluginBase { user: authorization | undefined; getUser = async () => { - let user = storage.get(this.id, 'user') || { userId: '', token: 'guest' }; + let user = storage.get('user') || { userId: '', token: 'guest' }; if (user && user.userId && user.token) { const currentUser: currentUser = await fetchApi( this.apiUrl + 'account/current-user', @@ -204,7 +204,7 @@ class AuthorToday implements Plugin.PluginBase { ).then(res => res.json()); if (currentUser?.id && !currentUser.isDisabled) return user; - storage.delete(this.id, 'user'); + storage.delete('user'); user = { userId: '', token: 'guest' }; } @@ -217,7 +217,6 @@ class AuthorToday implements Plugin.PluginBase { user = { userId: loginUser.userId, token: loginUser.token }; storage.set( - this.id, 'user', user, //for some reason they're ending an hour early. new Date(loginUser.expires).getTime() - 1 * 60 * 60 * 1000, diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 5ea590eee..921c71b1a 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -240,11 +240,11 @@ class RLIB implements Plugin.PluginBase { }; getUser = () => { - const user = storage.get(this.id, 'user'); + const user = storage.get('user'); if (user) { return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id }; } - const dataRaw = localStorage.get(this.id)?.auth; + const dataRaw = localStorage.get()?.auth; if (!dataRaw) { return {}; } @@ -252,7 +252,6 @@ class RLIB implements Plugin.PluginBase { const data = JSON.parse(dataRaw) as authorization; if (!data?.token?.access_token) return; storage.set( - this.id, 'user', { id: data.auth.id, From 9792ac350ffb8f9f19beeb387350af7ed9d3d931 Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Wed, 22 May 2024 22:16:21 +0300 Subject: [PATCH 5/5] test --- icons/src/ru/ranobelib/icon.png | Bin 823 -> 760 bytes package-lock.json | 57 +++++++++++++++++++++++++++----- package.json | 2 +- plugins/russian/authortoday.ts | 40 ++++++++++++---------- scripts/icon_reloading.ts | 57 +++++++++++++++++++++----------- 5 files changed, 109 insertions(+), 47 deletions(-) diff --git a/icons/src/ru/ranobelib/icon.png b/icons/src/ru/ranobelib/icon.png index 158c3800f8941b6a4bad2d4dcd6d8d67553b64e1..2bce5a1a35ecb4bc562be032b7987be765d465a8 100644 GIT binary patch delta 719 zcmV;=0x6n?DIz2;GS6eA8Gebp5+S}X!00?h!bY5U&XMbpHjgFCq@?_rt00JXP zL_t(&f$f>wa)K}rK!aBVL;L~*Tz-!+R!JOJ$)%;669}64IDYnh;ng8XI0@ z1m1LJ#LO0Keu)I=NYij$$q0}+AI0$Vy+p8+6Cg8zi+>2WD59~m)E0>l2?#5rfB*_k zwjN{iy|jH7h9IKqHX4+YE*Di$_nBI@fXVi;lJoD z9%ubj?tiyR=CE50*`D!f?)4wjs@th2z*knNd=|h2hy*k;;5q?LfDf!x8xbJer=c6$ zBtQXnX~{0Wu%fe*;PcVo5p-_PLaHi2)^VKh z>*T?Y;FvaZWkKw?QxdErcnKwVJAs$EbK8On%t%G{+VBHVV9sZPo6i=YU}UuNP=a?6 z2&*W;m_Xb@o73EXlO`L8Yjjlbe;CO63WY)e$~OX-GP^@z>S6!@002ovPDHLkV1l=( BLU;fG delta 783 zcmV+q1MvL#1-Ax}HGd%?B^@$G3VzD~o&O4Z!5|$T8XhL}^z?XnefRhG7#kxmF*o1e z;JUiJNJ&pVKt}+V_ZUiV0G06U?Cl>YG7xO58#PE079Na^kQ+Kx5o4MgF+r!PtCp9W zHaS2NTZ|tW6=h~?($myfT3?=^qW}pN000fc!^RU*eSm_6P=8ZbCn_-zhux+C00Lr3 zL_t(&f$f>wcA_v8KtmDHh~Z`di?xv!q+^x;|GSE4h(gp{ShLnl_G2E-_9Qunm;j*B zXf)i6*eD5~SZopn@!ywYHfwo7vNUazg&lp!&eF&l+b?VuZwdq^JH9!zKxc3zFzp@| zDt1vLFr!HiKY!A-Oa%7iYubZsd|DtMObe6>;4O*;m3vK9ffNZzdIc~n{Tnb|)BB@y870R#XYNb&6k5r06JEJML5u%8qZLIRMiqs57z z2ZB&+ijI7|)zIgYzC}(qWRZPNLHEB#XPZLk?%M{t=M;3ID!E*pD0gjb#|MGW0#h)b z)yxeFT*=12-;2DhbOLt;u_GV1R(^b N00>D%PDHLkV1l&$SW5r^ diff --git a/package-lock.json b/package-lock.json index 47dda36d6..555cc1c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "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", @@ -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", @@ -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", diff --git a/package.json b/package.json index 827a07a66..89deff820 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "json": "tsc && node ./.js/scripts/json_plugins.js", "generate": "ts-node ./scripts/multisrc/generate.ts", "clearMultisrc": "find . -type f -wholename \"*plugins*\\[*\\]*.[t,j]s\" -delete", - "icon": "tsc && node ./.js/scripts/icon_reloading.js", "less": "npx less ./test_web/static/css/index.less ./test_web/static/css/index.css", "host-linux": "chmod +x ./host.sh && ./host.sh", "host-windows": "powershell ./host.ps1", @@ -27,6 +26,7 @@ "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", diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index 7d49367a3..45d50d725 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -13,7 +13,7 @@ class AuthorToday implements Plugin.PluginBase { icon = 'src/ru/authortoday/icon.png'; site = 'https://author.today'; apiUrl = 'https://api.author.today/v1/'; - version = '1.0.1'; + version = '1.1.0'; async popularNovels( pageNo: number, @@ -135,20 +135,9 @@ class AuthorToday implements Plugin.PluginBase { return result.code + '\n' + result?.message; } - const key = - result.key.split('').reverse().join('') + - '@_@' + - (this.user?.userId || ''); - let text = ''; - - for (let i = 0; i < result.text.length; i++) { - text += String.fromCharCode( - result.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length)), - ); - } - - if (text.includes(' { + const chapterText = decrypt(result.text, result.key, this.user?.userId); + if (chapterText.includes(' { if (!url.startsWith('http')) { return `src="${this.site}${url}"`; } @@ -156,7 +145,7 @@ class AuthorToday implements Plugin.PluginBase { }); } - return text; + return chapterText; } async searchNovels( @@ -373,6 +362,23 @@ 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; @@ -380,7 +386,7 @@ interface authorization { expires: string; } -export interface currentUser { +interface currentUser { id: number; userName: string; fio: string; diff --git a/scripts/icon_reloading.ts b/scripts/icon_reloading.ts index e10664a9f..7248c9654 100644 --- a/scripts/icon_reloading.ts +++ b/scripts/icon_reloading.ts @@ -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🔄'); }