diff --git a/package.json b/package.json index ba6a7d9..74ee664 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "host-windows": "powershell ./host.ps1", "prettier": "prettier --write \"./**/*.{js,ts}\"", "prepare": "husky install", - "icons": "npm run clearMultisrc && npm run generate && npm run json && ts-node ./scripts/icon_reloading.ts" + "icons": "npm run clearMultisrc && npm run generate && npx tsc -m node16 && npm run json && node ./scripts/icon_reloading.js" }, "author": "LNReader", "license": "MIT", diff --git a/public/icons/multisrc/madara/olaoe/icon.png b/public/icons/multisrc/madara/olaoe/icon.png index 6ac7d5d..2fec8c1 100644 Binary files a/public/icons/multisrc/madara/olaoe/icon.png and b/public/icons/multisrc/madara/olaoe/icon.png differ diff --git a/public/icons/src/fr/kisswood/icon.png b/public/icons/src/fr/kisswood/icon.png index 6258470..05da80c 100644 Binary files a/public/icons/src/fr/kisswood/icon.png and b/public/icons/src/fr/kisswood/icon.png differ diff --git a/public/icons/src/ru/ruvers/icon.png b/public/icons/src/ru/ruvers/icon.png new file mode 100644 index 0000000..7f1de8a Binary files /dev/null and b/public/icons/src/ru/ruvers/icon.png differ diff --git a/scripts/icon_reloading.ts b/scripts/icon_reloading.js similarity index 82% rename from scripts/icon_reloading.ts rename to scripts/icon_reloading.js index 7248c96..4fa2dc6 100644 --- a/scripts/icon_reloading.ts +++ b/scripts/icon_reloading.js @@ -2,7 +2,6 @@ import * as fs from 'fs'; import * as path from 'path'; import sizeOf from 'image-size'; -const root = path.join(__dirname, '..'); const size = 96; const minSize = 16; @@ -28,19 +27,19 @@ const skip = new Set([ ]); const used = new Set([ - path.join(root, 'icons', 'coverNotAvailable.webp'), - path.join(root, 'icons', 'siteNotAvailable.png'), + path.join('public', 'icons', 'coverNotAvailable.webp'), + path.join('public', 'icons', 'siteNotAvailable.png'), ]); const notAvailableImage = fs.readFileSync( - path.join(root, 'icons', 'siteNotAvailable.png'), + path.join('public', 'icons', 'siteNotAvailable.png'), ); (async () => { console.log('Loading plugins.json ⌛'); - const plugin_path = path.join(root, '.dist', 'plugins.json'); + const plugin_path = path.join('.dist', 'plugins.json'); if (!fs.existsSync(plugin_path)) { - console.log('❌', plugin_path, 'not found (run "json_plugins.ts" first)'); + console.log('❌', plugin_path, 'not found (run "json_plugins.js" first)'); return; } const plugins = JSON.parse(fs.readFileSync(plugin_path, 'utf-8')); @@ -49,7 +48,7 @@ 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]; + const icon = iconUrl && 'public/icons/' + iconUrl.split('icons/')[1]; if (language !== lang) { language = lang; @@ -61,7 +60,7 @@ const notAvailableImage = fs.readFileSync( } try { - if (icon) used.add(path.join(root, icon)); + if (icon) used.add(path.join(icon)); if (!skip.has(id) && icon && site) { const image = await fetch( `https://www.google.com/s2/favicons?domain=${site}&sz=${size}&type=png`, @@ -84,7 +83,7 @@ const notAvailableImage = fs.readFileSync( const exist = fs.existsSync(icon); if (!exist) { - const dir = icon.match(/^.*[\\\/]/)[0] as string; + const dir = icon.match(/^.*[\\\/]/)[0]; fs.mkdirSync(dir, { recursive: true }); } @@ -121,7 +120,7 @@ const notAvailableImage = fs.readFileSync( } console.log('\nDeleting unused icons ⌛'); - fileList(path.join(root, 'icons')).forEach(path => { + fileList(path.join('public', 'icons')).forEach(path => { if (!used.has(path)) { console.log('🗑️', path); fs.rmSync(path, { force: true }); @@ -130,8 +129,8 @@ const notAvailableImage = fs.readFileSync( console.log('\nDone ✅'); })(); -function fileList(dir: string): string[] { - return fs.readdirSync(dir).reduce((list: string[], file: string) => { +function fileList(dir) { + return fs.readdirSync(dir).reduce((list, file) => { const name = path.join(dir, file); const isDir = fs.statSync(name).isDirectory(); return list.concat(isDir ? fileList(name) : [name]); diff --git a/src/plugins/russian/ruvers.ts b/src/plugins/russian/ruvers.ts new file mode 100644 index 0000000..28154da --- /dev/null +++ b/src/plugins/russian/ruvers.ts @@ -0,0 +1,182 @@ +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 dayjs from 'dayjs'; + +class RV implements Plugin.PluginBase { + id = 'RV'; + name = 'Ruvers'; + site = 'https://ruvers.ru/'; + version = '1.0.0'; + icon = 'src/ru/ruvers/icon.png'; + + async fetchNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + searchTerm?: string, + ): Promise { + let url = this.site + 'api/books?page=' + page; + url += '&sort=' + (filters?.sort?.value || '-rating'); + + if (searchTerm) url += '&search=' + searchTerm; + + const { data }: { data: book[] } = await fetchApi(url).then(res => + res.json(), + ); + const novels: Plugin.NovelItem[] = []; + + data.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.images[0] ? this.site + novel.images[0] : defaultCover, + path: novel.slug, + }), + ); + + return novels; + } + + popularNovels = this.fetchNovels; + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const defaultOptions: any = { filters: {} }; + return this.fetchNovels(page, defaultOptions, searchTerm); + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('div.name > h1').text().trim(), + cover: loadedCheerio('.slider_prods_single > img').attr('src'), + summary: loadedCheerio('.book_description').text().trim(), + genres: loadedCheerio('.genres > a') + .map((index, element) => loadedCheerio(element).text()?.trim()) + .get() + .join(','), + status: loadedCheerio('.status_row > div:nth-child(1) > a') + .text() + .includes('В работе') + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + const bookId = loadedCheerio('comments-list').attr('commentable-id'); + + const chaptersJSON: { data: chapters[] } = await fetchApi( + this.site + 'api/books/' + bookId + '/chapters/all', + ).then(res => res.json()); + + if (chaptersJSON.data.length) { + const chapters: Plugin.ChapterItem[] = []; + chaptersJSON.data.forEach((chapter, chapterIndex) => { + if ( + chapter.is_published && + (chapter.is_free || chapter.purchased_by_user) + ) { + chapters.push({ + name: 'Глава ' + chapter.number + ' ' + (chapter.name || ''), + path: novelPath + '/' + chapter.id, + //releaseTime: dayjs(chapter.created_at).format('LLL'), + chapterNumber: chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const encrypted = body.match( + /(mobile-books|books)-chapters-text-component.*:text='"(.*?)"'/s, + )?.[2]; + if (!encrypted) throw new Error('No chapter found'); + + return unicodeToUtf8(encrypted); + } + + filters = { + sort: { + label: 'Сортировка', + value: '-rating', + options: [ + { label: 'По названию', value: 'name' }, + { label: 'По дате добавления', value: '-created_at' }, + { label: 'По рейтингу', value: '-rating' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RV(); + +interface book { + id: number; + name: string; + original_name: string; + description: string; + publishing_house?: string | null; + country: string; + original_author: string; + images: string[]; + price?: number | null; + is_verified: boolean; + rating: number; + type: string; + release_year: number; + tags?: string[] | null; + isbn?: null; + alternative_title?: string | null; + count_chapters: number; + chapter_open_days?: number | null; + age_category_id: number; + status_id: number; + writer_id: number; + created_at: string; + updated_at: string; + slug: string; + chapters_count: number; +} + +interface chapters { + id: number; + name: string; + number: string; + price?: string | null; + open_by_timer: boolean; + order: number; + opened_at?: string | null; + rating?: null; + book_id: number; + deleted_at?: string | null; + created_at: string; + updated_at: string; + is_published: boolean; + first_published_at: string; + full_name: string; + is_free: boolean; + is_new: boolean; + purchased_by_user: boolean; + book_slug: string; +} + +function unicodeToUtf8(unicode: string) { + return unicode.replace(/\\u([\d\w]{4})/gi, (match, hex) => { + return String.fromCharCode(parseInt(hex, 16)); + }); +}