From 9a45cb5987dbc34a0a8f62cba7bd5c93c8d20644 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Sat, 17 Oct 2026 20:03:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(russian):=20add=20=D0=A0=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B1=D1=8D=D0=9C=D0=B8=D1=80=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/russian/ranobemir.ts | 174 ++++++++++++++++++++++++ public/static/src/ru/ranobemir/icon.png | Bin 0 -> 10318 bytes 2 files changed, 174 insertions(+) create mode 100644 plugins/russian/ranobemir.ts create mode 100644 public/static/src/ru/ranobemir/icon.png diff --git a/plugins/russian/ranobemir.ts b/plugins/russian/ranobemir.ts new file mode 100644 index 000000000..72cb59d7c --- /dev/null +++ b/plugins/russian/ranobemir.ts @@ -0,0 +1,174 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; + +const headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', +}; + +class RanobeMir implements Plugin.PluginBase { + id = 'ranobemir'; + name = 'РанобэМир'; + icon = 'src/ru/ranobemir/icon.png'; + site = 'https://ranobemir.com'; + version = '1.0.0'; + + imageRequestInit: Plugin.ImageRequestInit = { headers }; + + private async request(url: string) { + const response = await fetchApi(url, { headers }); + if (!response.ok) { + throw Object.assign(new Error(`РанобэМир: HTTP ${response.status}.`), { + status: response.status, + }); + } + return response.text(); + } + + private parseList(html: string): Plugin.NovelItem[] { + const $ = parseHTML(html); + const novels: Plugin.NovelItem[] = []; + + $('a[data-card-link-type="title"]').each((_, el) => { + const link = $(el); + const path = link.attr('href'); + if (!path) return; + novels.push({ + name: link.text().trim(), + path, + cover: + link + .closest('section') + .find('a[data-card-link-type="poster"] img') + .attr('src') || defaultCover, + }); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise { + const sort = showLatestNovels + ? 'last_chapter_id' + : filters?.sort?.value || 'views'; + const html = await this.request( + `${this.site}/novel?sort=${sort}&page=${pageNo}`, + ); + return this.parseList(html); + } + + async parseNovel(novelPath: string): Promise { + // `?toc` is the no-JS fallback of "Показать все главы" and lists every chapter. + const html = await this.request(`${this.site}${novelPath}?toc`); + const $ = parseHTML(html); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $('h1').first().text().trim() || 'Untitled', + cover: $('meta[property="og:image"]').attr('content') || defaultCover, + }; + + const ldJson = $('script[type="application/ld+json"]').first().html(); + if (ldJson) { + try { + const book = JSON.parse(ldJson)['@graph']?.[0]; + if (book?.genre?.length) novel.genres = book.genre.join(', '); + } catch { + // genres are optional + } + } + + const summary = $('h2:contains("О романе")').next('div'); + summary.find('br').replaceWith('\n'); + summary.find('div').before('\n'); + novel.summary = summary + .text() + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + .join('\n\n'); + + const statusLabel = $('a[href^="/novel?status"]').first().text().trim(); + novel.status = + { + 'Выпускается': NovelStatus.Ongoing, + 'Выпущено': NovelStatus.Completed, + 'Заброшено': NovelStatus.Cancelled, + }[statusLabel] || NovelStatus.Unknown; + + const chapters: Plugin.ChapterItem[] = []; + $('#chapters_list li.list-row').each((_, el) => { + const row = $(el); + const link = row.find('a[href]').first(); + const path = link.attr('href'); + const releaseTime = row.find('time').attr('datetime'); + // Rows without a date are "Прогноз" placeholders for unpublished chapters. + if (!path || !releaseTime) return; + const name = link.text().trim(); + chapters.push({ + name, + path, + releaseTime, + chapterNumber: Number(name.match(/Глава\s+(\d+(?:\.\d+)?)/)?.[1]) || 0, + }); + }); + // The site lists the newest chapter first. + novel.chapters = chapters.reverse(); + chapters.forEach((chapter, index) => { + if (!chapter.chapterNumber) chapter.chapterNumber = index + 1; + }); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const html = await this.request(this.site + chapterPath); + const $ = parseHTML(html); + const article = $('article[data-chapter-url]'); + // The collapsible «Ранее в ...» recap precedes the real chapter text. + article.find('details.collapse').remove(); + return article + .find('p') + .map((_, el) => $.html(el)) + .get() + .join(''); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const html = await this.request( + `${this.site}/novel?q=${encodeURIComponent(searchTerm)}&page=${pageNo}`, + ); + return this.parseList(html); + } + + resolveUrl = (path: string) => this.site + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'views', + options: [ + { label: 'По просмотрам', value: 'views' }, + { label: 'По дате обновления', value: 'last_chapter_id' }, + { label: 'По дате добавления', value: 'published_at' }, + { label: 'По названию', value: 'name' }, + { label: 'По рейтингу', value: 'rating' }, + { label: 'По подпискам', value: 'subscriptions' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RanobeMir(); diff --git a/public/static/src/ru/ranobemir/icon.png b/public/static/src/ru/ranobemir/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b09ff1b95f653abb1bc222232722b5d647999a15 GIT binary patch literal 10318 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

5_t;)7{a%hv@}sP# z1}|2YZAFz*bo(bx23*v7i?ECe!J=-Wn4u`UMYOYz=?h)lmB{=K%h{MZn2ep(hl7Qh zfPZxE^AwBhEd>6S>*Lmpg@v{93KI|9Seu{!Xl>g*m*fo}0-6%Lt)E$2D=mhO2Q4P| zAEYg`pr%?)T0ehLHu&;kWL5>^G73Ch@uKjG@KK$;>6KQV14SgaR8U}Kh{bW}I(&rM{ zBH;c=B3x-2{4g0e<+eDRJZAqWtS9)QF?^km9D=!qhJgD1GeiA`n78sixkMzUw4uc< z-yYddyjpLqqyR(=myd=j(M)c%#T4`jbQkOnSS&2Jl>EtaU?!0L}F=riwfcSS*Cv*P0}B% z`e06#u=q}v6E>*VY{YVo5Yh{w>R!lG5Ez#T5(mg7B0@;D0^J4d^z}}@N=r$`@ff~8 z5!_DM5-DA=mQMz@~Q3XTrvkxUReHU@QIq5W#SRKXT?$40otKM$|w?9=VoXtXg9 zzyh7n(Ut6-H&yjgoy*$WZ#GU&V5f@AYY^OF$QA+hGA(2oQKX3Jm$+J<>8rK6`mMR0 z9V-%2358havp){32L-C;SXgMufP|!5Y0waUaL^V9e3|KkuW!zecE4&^)j}2%Q&} Date: Sat, 17 Oct 2026 20:30:17 +0800 Subject: [PATCH 2/2] fix(russian/ranobemir): sanitize chapter html --- plugins/russian/ranobemir.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/russian/ranobemir.ts b/plugins/russian/ranobemir.ts index 72cb59d7c..b5d2800b5 100644 --- a/plugins/russian/ranobemir.ts +++ b/plugins/russian/ranobemir.ts @@ -17,7 +17,7 @@ class RanobeMir implements Plugin.PluginBase { name = 'РанобэМир'; icon = 'src/ru/ranobemir/icon.png'; site = 'https://ranobemir.com'; - version = '1.0.0'; + version = '1.0.1'; imageRequestInit: Plugin.ImageRequestInit = { headers }; @@ -135,6 +135,31 @@ class RanobeMir implements Plugin.PluginBase { const article = $('article[data-chapter-url]'); // The collapsible «Ранее в ...» recap precedes the real chapter text. article.find('details.collapse').remove(); + article.find('script, style, noscript').remove(); + + // The app renders this HTML as is, so drop event handlers and URLs that + // would run script. + article.find('*').each((_, el) => { + const node = $(el); + for (const name of Object.keys(el.attribs)) { + if (/^on/i.test(name)) { + node.removeAttr(name); + } else if (name === 'href' || name === 'src') { + const value = el.attribs[name] + .split('') + .filter(c => c.charCodeAt(0) > 32) + .join('') + .toLowerCase(); + if ( + /^(javascript|vbscript):/.test(value) || + (value.startsWith('data:') && !value.startsWith('data:image/')) + ) { + node.removeAttr(name); + } + } + } + }); + return article .find('p') .map((_, el) => $.html(el))