From 5a995b55b871f140c96c62a4ff3a33b96da76a80 Mon Sep 17 00:00:00 2001 From: AinsOoalGon86 Date: Fri, 27 Mar 2026 13:54:21 +0200 Subject: [PATCH 1/4] BakaInUa Plugin Update The plugin was completely rewritten because version 2.0.0 didn't work at all. --- plugins/ukrainian/bakainua.ts | 450 +++++++++++++++++++--------------- 1 file changed, 252 insertions(+), 198 deletions(-) diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index c6833ae77..daef8e009 100644 --- a/plugins/ukrainian/bakainua.ts +++ b/plugins/ukrainian/bakainua.ts @@ -1,198 +1,252 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; -import { fetchApi } from '@libs/fetch'; -import { Plugin } from '@/types/plugin'; -import { NovelStatus } from '@libs/novelStatus'; -import { Filters, FilterTypes } from '@libs/filterInputs'; - -class BakaInUa implements Plugin.PluginBase { - id = 'bakainua'; - name = 'BakaInUA'; - icon = 'src/uk/bakainua/icon.png'; - site = 'https://baka.in.ua'; - version = '2.0.0'; - - async popularNovels( - pageNo: number, - { - filters, - showLatestNovels, - }: Plugin.PopularNovelsOptions, - ): Promise { - const fictionIds: string[] = []; - - const url: URL = new URL(this.site + '/fictions/alphabetical'); - - if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); - if (showLatestNovels || (filters && filters.only_new.value)) - url.searchParams.append('only_new', '1'); - if (filters) { - if (filters.longreads.value) url.searchParams.append('longreads', '1'); - if (filters.finished.value) url.searchParams.append('finished', '1'); - if (filters.genre.value !== '') - url.searchParams.append('genre', filters.genre.value); - } - - const result = await fetchApi(url.toString()); - const body = await result.text(); - const $ = parseHTML(body); - - // get the ids of the popular novels - $('div#fiction-list-page > div > div > div > img').each((index, elem) => { - const fictionId = $(elem).attr('data-fiction-picker-id-param'); - if (fictionId) { - fictionIds.push(fictionId); - } - }); - - // fetch the details of the popular novels asynchronously - const requests: Promise[] = fictionIds.map(async id => { - const res = await fetchApi(`${this.site}/fictions/${id}/details`, { - headers: { - accept: 'text/vnd.turbo-stream.html', - }, - }); - const $ = parseHTML(await res.text()); - const elem = $('a').first(); - - return { - name: $('h3').text().trim(), - path: elem.attr('href') || '', - cover: this.site + $(elem).find('img').attr('src'), - }; - }); - - return await Promise.all(requests); - } - - async parseNovel(novelUrl: string): Promise { - const result = await fetchApi(this.site + '/' + novelUrl); - const body = await result.text(); - const $ = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelUrl, - name: $('main div > h1').text().trim(), - author: $('button#fictions-author-search').text().trim(), - cover: this.site + $('main div > img').attr('src'), - summary: $('main div > h3').first().parent().find('div').text().trim(), - genres: $('h4:contains("Жанри")') - .last() - .siblings() - .last() - .find('span') - .map((_, el) => $(el).text().trim()) - .get() - .join(', '), - }; - - switch ($('div:contains("Статус")').last().siblings().text().trim()) { - case 'Видаєт.': - novel.status = NovelStatus.Ongoing; - break; - case 'Заверш.': - novel.status = NovelStatus.Completed; - break; - case 'Покину.': - novel.status = NovelStatus.OnHiatus; - break; - default: - novel.status = NovelStatus.Unknown; - break; - } - - const chapters: Plugin.ChapterItem[] = []; - $('li.group a').each((index, elem) => { - const chapter: Plugin.ChapterItem = { - name: $(elem).find('span').eq(1).text().trim(), - path: $(elem).attr('href') || '', - chapterNumber: parseInt($(elem).find('span').eq(0).text().trim()), - releaseTime: $(elem).find('span').eq(2).text().trim(), - }; - chapters.push(chapter); - }); - - novel.chapters = chapters.reverse(); - return novel; - } - - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(this.site + '/' + chapterUrl); - const body = await result.text(); - const $ = parseHTML(body); - return $('#user-content').html() || ''; - } - - async searchNovels(searchTerm: string): Promise { - const novels: Plugin.NovelItem[] = []; - - const result = await fetchApi( - this.site + '/search?search%5B%5D=' + searchTerm + '&only_fictions=true', - ); - const body = await result.text(); - const $ = parseHTML(body); - $('ul > section').each((index, elem) => { - novels.push({ - path: $(elem).find('a').first().attr('href') || '', - name: $(elem).find('a > h2').first().text().trim(), - cover: this.site + $(elem).find('img').first().attr('src'), - }); - }); - return novels; - } - - filters = { - genre: { - type: FilterTypes.Picker, - label: 'Жанр', - value: '', - options: [ - { label: 'Всі жанри', value: '' }, - { label: 'BL', value: '19' }, - { label: 'GL', value: '20' }, - { label: 'Авторське', value: '32' }, - { label: 'Бойовик', value: '2' }, - { label: 'Вуся', value: '16' }, - { label: 'Гарем', value: '5' }, - { label: 'Детектив', value: '22' }, - { label: 'Драма', value: '12' }, - { label: 'Жахи', value: '10' }, - { label: 'Ісекай', value: '13' }, - { label: 'Історичне', value: '15' }, - { label: 'Комедія', value: '11' }, - { label: 'ЛГБТ', value: '3' }, - { label: 'Містика', value: '18' }, - { label: 'Омегаверс', value: '30' }, - { label: 'Повсякденність', value: '17' }, - { label: 'Пригоди', value: '7' }, - { label: 'Психологія', value: '28' }, - { label: 'Романтика', value: '1' }, - { label: 'Спорт', value: '9' }, - { label: 'Сюаньхвань', value: '27' }, - { label: 'Сянься', value: '26' }, - { label: 'Трагедія', value: '24' }, - { label: 'Трилер', value: '21' }, - { label: 'Фантастика', value: '8' }, - { label: 'Фанфік', value: '23' }, - { label: 'Фентезі', value: '4' }, - { label: 'Школа', value: '6' }, - ], - }, - only_new: { - type: FilterTypes.Switch, - label: 'Новинки', - value: false, - }, - longreads: { - type: FilterTypes.Switch, - label: 'Довгочити', - value: false, - }, - finished: { - type: FilterTypes.Switch, - label: 'Завершене', - value: false, - }, - } satisfies Filters; -} - -export default new BakaInUa(); +import { CheerioAPI, load as parseHTML } from 'cheerio'; + import { fetchApi } from '@libs/fetch'; + import { Plugin } from '@/types/plugin'; + import { NovelStatus } from '@libs/novelStatus'; + import { Filters, FilterTypes } from '@libs/filterInputs'; + + class BakaInUa implements Plugin.PluginBase { + id = 'bakainua'; + name = 'BakaInUA'; + icon = 'src/uk/bakainua/icon.png'; + site = 'https://baka.in.ua'; + version = '3.1.2'; + + async popularNovels( + pageNo: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const fictionIds: string[] = []; + const url = new URL(this.site + '/fictions/alphabetical'); + + if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); + if (showLatestNovels || (filters && filters.only_new.value)) + url.searchParams.append('only_new', '1'); + if (filters) { + if (filters.longreads.value) url.searchParams.append('longreads', '1'); + if (filters.finished.value) url.searchParams.append('finished', '1'); + if (filters.genre.value !== '') + url.searchParams.append('genre', filters.genre.value); + } + + const result = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + $('[data-fiction-picker-id-param]').each((_, elem) => { + const id = $(elem).attr('data-fiction-picker-id-param'); + if (id) fictionIds.push(id); + }); + + const requests = fictionIds.map(async id => { + try { + const res = await fetchApi(`${this.site}/fictions/${id}/details`, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + const detailHtml = await res.text(); + const $d = parseHTML(detailHtml); + const link = $d('a').first(); + + return { + name: $d('h3').text().trim(), + path: link.attr('href')?.replace(this.site + '/', '') || '', + cover: this.site + link.find('img').attr('src'), + }; + } catch (e) { + return null; + } + }); + + const novels = await Promise.all(requests); + return novels.filter((n): n is Plugin.NovelItem => n !== null); + } + +async parseNovel(novelUrl: string): Promise { + // 1. Спочатку відкриваємо сторінку новели + const result = await fetchApi(this.site + '/' + novelUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // 2. Збираємо доступні переклади + const translators = $('turbo-frame#alternative-tabs form') + .map((_, form) => { + const name = $(form).find('button span').first().text().trim(); + const ids = $(form) + .find('input[name="translator[]"]') + .map((_, input) => $(input).attr('value') || '') + .get(); + return { name, ids }; + }) + .get(); + + // 3. Вибір перекладу (за замовчуванням перший) + const selected = translators[0]; + + // 4. Будуємо URL з параметрами translator[] + const url = new URL(this.site + '/' + novelUrl); + if (selected?.ids?.length) { + selected.ids.forEach(id => url.searchParams.append('translator[]', id)); + } + + // 5. Завантажуємо сторінку вже з вибраним перекладом + const translatedRes = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const translatedBody = await translatedRes.text(); + const $$ = parseHTML(translatedBody); + + // 6. Основні дані новели + const coverSrc = $$('img.w-32.h-48').first().attr('src') || ''; + + const novel: Plugin.SourceNovel = { + path: novelUrl, + name: $$('h1').first().text().trim(), + author: selected?.name || 'Невідомо', + cover: coverSrc.startsWith('http') ? coverSrc : this.site + coverSrc, + summary: $$('h3:contains("Опис"), h2:contains("Опис")') + .parent() + .find('div.text-justify, .prose') + .text() + .trim(), + genres: $$('h4:contains("Жанри")') + .next() + .find('span') + .map((_, el) => $$(el).text().trim()) + .get() + .join(', '), + }; + + // 7. Статус + const statusText = $$('h4:contains("Статус")').next().text().trim(); + if (statusText.includes('Видається')) novel.status = NovelStatus.Ongoing; + else if (statusText.includes('Завершено')) novel.status = NovelStatus.Completed; + else if (statusText.includes('Покинуто')) novel.status = NovelStatus.OnHiatus; + else novel.status = NovelStatus.Unknown; + + // 8. Глави + const chapters: Plugin.ChapterItem[] = []; + $$('li.group a[href*="/chapters/"]').each((_, elem) => { + const href = $$(elem).attr('href') || ''; + chapters.push({ + name: $$(elem).find('span').eq(1).text().trim() || 'Розділ', + path: href.replace(this.site + '/', ''), + chapterNumber: + parseFloat($$(elem).find('span').eq(0).text().replace(',', '.')) || 0, + releaseTime: $$(elem).find('span').eq(2).text().trim(), + }); + }); + + novel.chapters = chapters.reverse(); + + return novel; +} + + + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(this.site + '/' + chapterUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose + let content = $('.trix-content, .prose, article, #chapter-content').first(); + + // Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo) + if (!content.text().trim()) { + const hiddenData = $('[data-chapter-content-value]').attr('data-chapter-content-value'); + if (hiddenData) return hiddenData; + } + + content.find('script, style, button, form, .ads, .social-share').remove(); + + let chapterHtml = content.html(); + + // Останній шанс: пошук тексту в JSON всередині скриптів через Regex + if (!chapterHtml || chapterHtml.trim().length < 100) { + const match = body.match(/"content\\":\\"(.*?)\\"/); + if (match && match[1]) { + chapterHtml = match[1] + .replace(/\\n/g, '
') + .replace(/\\"/g, '"') + .replace(/\\u003c/g, '<') + .replace(/\\u003e/g, '>'); + } + } + + return chapterHtml || 'Контент не знайдено. Можливо, потрібна авторизація на сайті.'; + } + + async searchNovels(searchTerm: string): Promise { + const result = await fetchApi( + `${this.site}/search?search[]=${encodeURIComponent(searchTerm)}&only_fictions=true`, + { headers: { 'user-agent': 'Mozilla/5.0' } }, + ); + + const body = await result.text(); + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('section.flex.flex-col').each((_, elem) => { + const link = $(elem).find('a').first(); + novels.push({ + path: link.attr('href')?.replace(this.site + '/', '') || '', + name: $(elem).find('h2').text().trim(), + cover: this.site + $(elem).find('img').attr('src'), + }); + }); + + return novels; + } + +filters = { + genre: { + type: FilterTypes.Picker, + label: 'Жанр', + value: '', + options: [ + { label: 'Всі жанри', value: '' }, + { label: 'BL', value: '19' }, + { label: 'GL', value: '20' }, + { label: 'Авторське', value: '32' }, + { label: 'Бойовик', value: '2' }, + { label: 'Вуся', value: '16' }, + { label: 'Гарем', value: '5' }, + { label: 'Детектив', value: '22' }, + { label: 'Драма', value: '12' }, + { label: 'Жахи', value: '10' }, + { label: 'Ісекай', value: '13' }, + { label: 'Історичне', value: '15' }, + { label: 'Комедія', value: '11' }, + { label: 'ЛГБТ', value: '3' }, + { label: 'Містика', value: '18' }, + { label: 'Омегаверс', value: '30' }, + { label: 'Повсякденність', value: '17' }, + { label: 'Пригоди', value: '7' }, + { label: 'Психологія', value: '28' }, + { label: 'Романтика', value: '1' }, + { label: 'Спорт', value: '9' }, + { label: 'Сюаньхвань', value: '27' }, + { label: 'Сянься', value: '26' }, + { label: 'Трагедія', value: '24' }, + { label: 'Трилер', value: '21' }, + { label: 'Фантастика', value: '8' }, + { label: 'Фанфік', value: '23' }, + { label: 'Фентезі', value: '4' }, + { label: 'Школа', value: '6' }, + ], + }, + only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false }, + longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false }, + finished: { type: FilterTypes.Switch, label: 'Завершене', value: false }, +} satisfies Filters; + } + + export default new BakaInUa(); \ No newline at end of file From f314e15de8782a2e858f65e89ed1012e17b515cc Mon Sep 17 00:00:00 2001 From: AinsOoalGon86 Date: Fri, 27 Mar 2026 20:10:37 +0200 Subject: [PATCH 2/4] test --- plugins/ukrainian/bakainua.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index daef8e009..cfecb7d1f 100644 --- a/plugins/ukrainian/bakainua.ts +++ b/plugins/ukrainian/bakainua.ts @@ -9,7 +9,9 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; name = 'BakaInUA'; icon = 'src/uk/bakainua/icon.png'; site = 'https://baka.in.ua'; - version = '3.1.2'; + version = '3.1.4'; + +// Comment for test async popularNovels( pageNo: number, From bad2324c3647812f62562af99f50d674b6e590a0 Mon Sep 17 00:00:00 2001 From: AinsOoalGon86 Date: Fri, 27 Mar 2026 21:39:58 +0200 Subject: [PATCH 3/4] 3.1.5 Baka --- plugins/ukrainian/bakainua.ts | 527 ++++++++++++++++++---------------- 1 file changed, 273 insertions(+), 254 deletions(-) diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index cfecb7d1f..65e892deb 100644 --- a/plugins/ukrainian/bakainua.ts +++ b/plugins/ukrainian/bakainua.ts @@ -1,254 +1,273 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; - import { fetchApi } from '@libs/fetch'; - import { Plugin } from '@/types/plugin'; - import { NovelStatus } from '@libs/novelStatus'; - import { Filters, FilterTypes } from '@libs/filterInputs'; - - class BakaInUa implements Plugin.PluginBase { - id = 'bakainua'; - name = 'BakaInUA'; - icon = 'src/uk/bakainua/icon.png'; - site = 'https://baka.in.ua'; - version = '3.1.4'; - -// Comment for test - - async popularNovels( - pageNo: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, - ): Promise { - const fictionIds: string[] = []; - const url = new URL(this.site + '/fictions/alphabetical'); - - if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); - if (showLatestNovels || (filters && filters.only_new.value)) - url.searchParams.append('only_new', '1'); - if (filters) { - if (filters.longreads.value) url.searchParams.append('longreads', '1'); - if (filters.finished.value) url.searchParams.append('finished', '1'); - if (filters.genre.value !== '') - url.searchParams.append('genre', filters.genre.value); - } - - const result = await fetchApi(url.toString(), { - headers: { 'user-agent': 'Mozilla/5.0' }, - }); - - const body = await result.text(); - const $ = parseHTML(body); - - $('[data-fiction-picker-id-param]').each((_, elem) => { - const id = $(elem).attr('data-fiction-picker-id-param'); - if (id) fictionIds.push(id); - }); - - const requests = fictionIds.map(async id => { - try { - const res = await fetchApi(`${this.site}/fictions/${id}/details`, { - headers: { 'user-agent': 'Mozilla/5.0' }, - }); - const detailHtml = await res.text(); - const $d = parseHTML(detailHtml); - const link = $d('a').first(); - - return { - name: $d('h3').text().trim(), - path: link.attr('href')?.replace(this.site + '/', '') || '', - cover: this.site + link.find('img').attr('src'), - }; - } catch (e) { - return null; - } - }); - - const novels = await Promise.all(requests); - return novels.filter((n): n is Plugin.NovelItem => n !== null); - } - -async parseNovel(novelUrl: string): Promise { - // 1. Спочатку відкриваємо сторінку новели - const result = await fetchApi(this.site + '/' + novelUrl, { - headers: { 'user-agent': 'Mozilla/5.0' }, - }); - - const body = await result.text(); - const $ = parseHTML(body); - - // 2. Збираємо доступні переклади - const translators = $('turbo-frame#alternative-tabs form') - .map((_, form) => { - const name = $(form).find('button span').first().text().trim(); - const ids = $(form) - .find('input[name="translator[]"]') - .map((_, input) => $(input).attr('value') || '') - .get(); - return { name, ids }; - }) - .get(); - - // 3. Вибір перекладу (за замовчуванням перший) - const selected = translators[0]; - - // 4. Будуємо URL з параметрами translator[] - const url = new URL(this.site + '/' + novelUrl); - if (selected?.ids?.length) { - selected.ids.forEach(id => url.searchParams.append('translator[]', id)); - } - - // 5. Завантажуємо сторінку вже з вибраним перекладом - const translatedRes = await fetchApi(url.toString(), { - headers: { 'user-agent': 'Mozilla/5.0' }, - }); - - const translatedBody = await translatedRes.text(); - const $$ = parseHTML(translatedBody); - - // 6. Основні дані новели - const coverSrc = $$('img.w-32.h-48').first().attr('src') || ''; - - const novel: Plugin.SourceNovel = { - path: novelUrl, - name: $$('h1').first().text().trim(), - author: selected?.name || 'Невідомо', - cover: coverSrc.startsWith('http') ? coverSrc : this.site + coverSrc, - summary: $$('h3:contains("Опис"), h2:contains("Опис")') - .parent() - .find('div.text-justify, .prose') - .text() - .trim(), - genres: $$('h4:contains("Жанри")') - .next() - .find('span') - .map((_, el) => $$(el).text().trim()) - .get() - .join(', '), - }; - - // 7. Статус - const statusText = $$('h4:contains("Статус")').next().text().trim(); - if (statusText.includes('Видається')) novel.status = NovelStatus.Ongoing; - else if (statusText.includes('Завершено')) novel.status = NovelStatus.Completed; - else if (statusText.includes('Покинуто')) novel.status = NovelStatus.OnHiatus; - else novel.status = NovelStatus.Unknown; - - // 8. Глави - const chapters: Plugin.ChapterItem[] = []; - $$('li.group a[href*="/chapters/"]').each((_, elem) => { - const href = $$(elem).attr('href') || ''; - chapters.push({ - name: $$(elem).find('span').eq(1).text().trim() || 'Розділ', - path: href.replace(this.site + '/', ''), - chapterNumber: - parseFloat($$(elem).find('span').eq(0).text().replace(',', '.')) || 0, - releaseTime: $$(elem).find('span').eq(2).text().trim(), - }); - }); - - novel.chapters = chapters.reverse(); - - return novel; -} - - - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(this.site + '/' + chapterUrl, { - headers: { 'user-agent': 'Mozilla/5.0' }, - }); - - const body = await result.text(); - const $ = parseHTML(body); - - // Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose - let content = $('.trix-content, .prose, article, #chapter-content').first(); - - // Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo) - if (!content.text().trim()) { - const hiddenData = $('[data-chapter-content-value]').attr('data-chapter-content-value'); - if (hiddenData) return hiddenData; - } - - content.find('script, style, button, form, .ads, .social-share').remove(); - - let chapterHtml = content.html(); - - // Останній шанс: пошук тексту в JSON всередині скриптів через Regex - if (!chapterHtml || chapterHtml.trim().length < 100) { - const match = body.match(/"content\\":\\"(.*?)\\"/); - if (match && match[1]) { - chapterHtml = match[1] - .replace(/\\n/g, '
') - .replace(/\\"/g, '"') - .replace(/\\u003c/g, '<') - .replace(/\\u003e/g, '>'); - } - } - - return chapterHtml || 'Контент не знайдено. Можливо, потрібна авторизація на сайті.'; - } - - async searchNovels(searchTerm: string): Promise { - const result = await fetchApi( - `${this.site}/search?search[]=${encodeURIComponent(searchTerm)}&only_fictions=true`, - { headers: { 'user-agent': 'Mozilla/5.0' } }, - ); - - const body = await result.text(); - const $ = parseHTML(body); - const novels: Plugin.NovelItem[] = []; - - $('section.flex.flex-col').each((_, elem) => { - const link = $(elem).find('a').first(); - novels.push({ - path: link.attr('href')?.replace(this.site + '/', '') || '', - name: $(elem).find('h2').text().trim(), - cover: this.site + $(elem).find('img').attr('src'), - }); - }); - - return novels; - } - -filters = { - genre: { - type: FilterTypes.Picker, - label: 'Жанр', - value: '', - options: [ - { label: 'Всі жанри', value: '' }, - { label: 'BL', value: '19' }, - { label: 'GL', value: '20' }, - { label: 'Авторське', value: '32' }, - { label: 'Бойовик', value: '2' }, - { label: 'Вуся', value: '16' }, - { label: 'Гарем', value: '5' }, - { label: 'Детектив', value: '22' }, - { label: 'Драма', value: '12' }, - { label: 'Жахи', value: '10' }, - { label: 'Ісекай', value: '13' }, - { label: 'Історичне', value: '15' }, - { label: 'Комедія', value: '11' }, - { label: 'ЛГБТ', value: '3' }, - { label: 'Містика', value: '18' }, - { label: 'Омегаверс', value: '30' }, - { label: 'Повсякденність', value: '17' }, - { label: 'Пригоди', value: '7' }, - { label: 'Психологія', value: '28' }, - { label: 'Романтика', value: '1' }, - { label: 'Спорт', value: '9' }, - { label: 'Сюаньхвань', value: '27' }, - { label: 'Сянься', value: '26' }, - { label: 'Трагедія', value: '24' }, - { label: 'Трилер', value: '21' }, - { label: 'Фантастика', value: '8' }, - { label: 'Фанфік', value: '23' }, - { label: 'Фентезі', value: '4' }, - { label: 'Школа', value: '6' }, - ], - }, - only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false }, - longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false }, - finished: { type: FilterTypes.Switch, label: 'Завершене', value: false }, -} satisfies Filters; - } - - export default new BakaInUa(); \ No newline at end of file +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class BakaInUa implements Plugin.PluginBase { + id = 'bakainua'; + name = 'BakaInUA'; + icon = 'src/uk/bakainua/icon.png'; + site = 'https://baka.in.ua'; + version = '3.1.5'; + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, + ): Promise { + const fictionIds: string[] = []; + const url = new URL(this.site + '/fictions/alphabetical'); + + if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); + if (showLatestNovels || (filters && filters.only_new.value)) + url.searchParams.append('only_new', '1'); + if (filters) { + if (filters.longreads.value) url.searchParams.append('longreads', '1'); + if (filters.finished.value) url.searchParams.append('finished', '1'); + if (filters.genre.value !== '') + url.searchParams.append('genre', filters.genre.value); + } + + const result = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + $('[data-fiction-picker-id-param]').each((_, elem) => { + const id = $(elem).attr('data-fiction-picker-id-param'); + if (id) fictionIds.push(id); + }); + + const requests = fictionIds.map(async id => { + try { + const res = await fetchApi(`${this.site}/fictions/${id}/details`, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + const detailHtml = await res.text(); + const $d = parseHTML(detailHtml); + const link = $d('a').first(); + + return { + name: $d('h3').text().trim(), + path: link.attr('href')?.replace(this.site + '/', '') || '', + cover: this.site + link.find('img').attr('src'), + }; + } catch (e) { + return null; + } + }); + + const novels = await Promise.all(requests); + return novels.filter((n): n is Plugin.NovelItem => n !== null); + } + + async parseNovel(novelUrl: string): Promise { + // 1. Спочатку відкриваємо сторінку новели + const result = await fetchApi(this.site + '/' + novelUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // 2. Збираємо доступні переклади + const translators = $('turbo-frame#alternative-tabs form') + .map((_, form) => { + const name = $(form).find('button span').first().text().trim(); + const ids = $(form) + .find('input[name="translator[]"]') + .map((_, input) => $(input).attr('value') || '') + .get(); + return { name, ids }; + }) + .get(); + + // 3. Вибір перекладу (за замовчуванням перший) + const selected = translators[0]; + + // 4. Будуємо URL з параметрами translator[] + const url = new URL(this.site + '/' + novelUrl); + if (selected?.ids?.length) { + selected.ids.forEach(id => url.searchParams.append('translator[]', id)); + } + + // 5. Завантажуємо сторінку вже з вибраним перекладом + const translatedRes = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const translatedBody = await translatedRes.text(); + const $$ = parseHTML(translatedBody); + + // 6. Основні дані новели + const coverSrc = $$('img.w-32.h-48').first().attr('src') || ''; + + const novel: Plugin.SourceNovel = { + path: novelUrl, + name: $$('h1').first().text().trim(), + author: selected?.name || 'Невідомо', + cover: coverSrc.startsWith('http') ? coverSrc : this.site + coverSrc, + summary: $$('h3:contains("Опис"), h2:contains("Опис")') + .parent() + .find('div.text-justify, .prose') + .text() + .trim(), + genres: $$('h4:contains("Жанри")') + .next() + .find('span') + .map((_, el) => $$(el).text().trim()) + .get() + .join(', '), + }; + + // 7. Статус + const statusText = $$('h4:contains("Статус")').next().text().trim(); + if (statusText.includes('Видається')) novel.status = NovelStatus.Ongoing; + else if (statusText.includes('Завершено')) + novel.status = NovelStatus.Completed; + else if (statusText.includes('Покинуто')) + novel.status = NovelStatus.OnHiatus; + else novel.status = NovelStatus.Unknown; + + // 8. Глави + const chapters: Plugin.ChapterItem[] = []; + $$('li.group a[href*="/chapters/"]').each((_, elem) => { + const href = $$(elem).attr('href') || ''; + chapters.push({ + name: $$(elem).find('span').eq(1).text().trim() || 'Розділ', + path: href.replace(this.site + '/', ''), + chapterNumber: + parseFloat($$(elem).find('span').eq(0).text().replace(',', '.')) || 0, + releaseTime: $$(elem).find('span').eq(2).text().trim(), + }); + }); + + novel.chapters = chapters.reverse(); + + return novel; + } + + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(this.site + '/' + chapterUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose + let content = $('.trix-content, .prose, article, #chapter-content').first(); + + // Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo) + if (!content.text().trim()) { + const hiddenData = $('[data-chapter-content-value]').attr( + 'data-chapter-content-value', + ); + if (hiddenData) return hiddenData; + } + + content.find('script, style, button, form, .ads, .social-share').remove(); + + let chapterHtml = content.html(); + + // Останній шанс: пошук тексту в JSON всередині скриптів через Regex + if (!chapterHtml || chapterHtml.trim().length < 100) { + const match = body.match(/"content\\":\\"(.*?)\\"/); + if (match && match[1]) { + chapterHtml = match[1] + .replace(/\\n/g, '
') + .replace(/\\"/g, '"') + .replace(/\\u003c/g, '<') + .replace(/\\u003e/g, '>'); + } + } + + return ( + chapterHtml || + 'Контент не знайдено. Можливо, потрібна авторизація на сайті.' + ); + } + + async searchNovels(searchTerm: string): Promise { + const url = `${this.site}/search?filter=fiction&search[]=${encodeURIComponent(searchTerm)}`; + + const result = await fetchApi(url, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('turbo-frame#fictions-section a[href^="/fictions/"]').each((_, elem) => { + const link = $(elem); + + const href = link.attr('href') || ''; + if (!href) return; + + // витягуємо id + const id = href.replace(/^\/fictions\//, '').replace(/^\/+/, ''); + + const name = link.find('h3').first().text().trim(); + + const img = link.closest('.group').find('img').attr('src'); + + novels.push({ + path: `/fictions/${id}`, // гарантуємо правильний формат + name, + cover: img ? this.site + img : '', + }); + }); + + return novels; + } + + filters = { + genre: { + type: FilterTypes.Picker, + label: 'Жанр', + value: '', + options: [ + { label: 'Всі жанри', value: '' }, + { label: 'BL', value: '19' }, + { label: 'GL', value: '20' }, + { label: 'Авторське', value: '32' }, + { label: 'Бойовик', value: '2' }, + { label: 'Вуся', value: '16' }, + { label: 'Гарем', value: '5' }, + { label: 'Детектив', value: '22' }, + { label: 'Драма', value: '12' }, + { label: 'Жахи', value: '10' }, + { label: 'Ісекай', value: '13' }, + { label: 'Історичне', value: '15' }, + { label: 'Комедія', value: '11' }, + { label: 'ЛГБТ', value: '3' }, + { label: 'Містика', value: '18' }, + { label: 'Омегаверс', value: '30' }, + { label: 'Повсякденність', value: '17' }, + { label: 'Пригоди', value: '7' }, + { label: 'Психологія', value: '28' }, + { label: 'Романтика', value: '1' }, + { label: 'Спорт', value: '9' }, + { label: 'Сюаньхвань', value: '27' }, + { label: 'Сянься', value: '26' }, + { label: 'Трагедія', value: '24' }, + { label: 'Трилер', value: '21' }, + { label: 'Фантастика', value: '8' }, + { label: 'Фанфік', value: '23' }, + { label: 'Фентезі', value: '4' }, + { label: 'Школа', value: '6' }, + ], + }, + only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false }, + longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false }, + finished: { type: FilterTypes.Switch, label: 'Завершене', value: false }, + } satisfies Filters; +} + +export default new BakaInUa(); From 92a2c14dbb27f0d3bf157fea978bf948ddadb0c9 Mon Sep 17 00:00:00 2001 From: AinsOoalGon86 Date: Sat, 28 Mar 2026 12:28:24 +0200 Subject: [PATCH 4/4] 3.1.6 Baka --- plugins/ukrainian/bakainua.ts | 40 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index 65e892deb..d71757450 100644 --- a/plugins/ukrainian/bakainua.ts +++ b/plugins/ukrainian/bakainua.ts @@ -9,7 +9,7 @@ class BakaInUa implements Plugin.PluginBase { name = 'BakaInUA'; icon = 'src/uk/bakainua/icon.png'; site = 'https://baka.in.ua'; - version = '3.1.5'; + version = '3.1.6'; async popularNovels( pageNo: number, @@ -104,35 +104,43 @@ class BakaInUa implements Plugin.PluginBase { const translatedBody = await translatedRes.text(); const $$ = parseHTML(translatedBody); - // 6. Основні дані новели - const coverSrc = $$('img.w-32.h-48').first().attr('src') || ''; + let cover = $$('meta[property="og:image"]').attr('content') || ''; + if (cover && !cover.startsWith('http')) { + cover = this.site + cover; + } const novel: Plugin.SourceNovel = { path: novelUrl, name: $$('h1').first().text().trim(), - author: selected?.name || 'Невідомо', - cover: coverSrc.startsWith('http') ? coverSrc : this.site + coverSrc, - summary: $$('h3:contains("Опис"), h2:contains("Опис")') - .parent() - .find('div.text-justify, .prose') + author: $$('#fictions-author-search').text().trim() || 'Невідомо', + artist: $$('#fictions-author-search').text().trim() || 'Невідомо', + cover, + summary: $$('div.whitespace-pre-line') + .first() .text() + .replace(/\s+/g, ' ') .trim(), - genres: $$('h4:contains("Жанри")') - .next() - .find('span') + genres: $$('div.flex.flex-wrap.gap-2 span') .map((_, el) => $$(el).text().trim()) .get() .join(', '), }; // 7. Статус - const statusText = $$('h4:contains("Статус")').next().text().trim(); - if (statusText.includes('Видається')) novel.status = NovelStatus.Ongoing; - else if (statusText.includes('Завершено')) + const statusText = $$('div.text-sm:contains("Статус")') // Знаходимо підпис "Статус" + .prev('div.text-2xl') // Беремо попередній div з класом text-2xl + .text() + .trim(); + + if (statusText.includes('Заверш')) { novel.status = NovelStatus.Completed; - else if (statusText.includes('Покинуто')) + } else if (statusText.includes('Видаєт')) { + novel.status = NovelStatus.Ongoing; + } else if (statusText.includes('Покину')) { novel.status = NovelStatus.OnHiatus; - else novel.status = NovelStatus.Unknown; + } else { + novel.status = NovelStatus.Unknown; + } // 8. Глави const chapters: Plugin.ChapterItem[] = [];