diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index c6833ae77..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 = '2.0.0'; + version = '3.1.6'; async popularNovels( pageNo: number, @@ -19,8 +19,7 @@ class BakaInUa implements Plugin.PluginBase { }: Plugin.PopularNovelsOptions, ): Promise { const fictionIds: string[] = []; - - const url: URL = new URL(this.site + '/fictions/alphabetical'); + const url = new URL(this.site + '/fictions/alphabetical'); if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); if (showLatestNovels || (filters && filters.only_new.value)) @@ -32,111 +31,207 @@ class BakaInUa implements Plugin.PluginBase { url.searchParams.append('genre', filters.genre.value); } - const result = await fetchApi(url.toString()); + const result = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + 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); - } + $('[data-fiction-picker-id-param]').each((_, elem) => { + const id = $(elem).attr('data-fiction-picker-id-param'); + if (id) fictionIds.push(id); }); - // 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'), - }; + 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; + } }); - return await Promise.all(requests); + const novels = await Promise.all(requests); + return novels.filter((n): n is Plugin.NovelItem => n !== null); } async parseNovel(novelUrl: string): Promise { - const result = await fetchApi(this.site + '/' + novelUrl); + // 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); + + let cover = $$('meta[property="og:image"]').attr('content') || ''; + if (cover && !cover.startsWith('http')) { + cover = this.site + cover; + } + 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()) + name: $$('h1').first().text().trim(), + 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: $$('div.flex.flex-wrap.gap-2 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; + // 7. Статус + 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('Видаєт')) { + novel.status = NovelStatus.Ongoing; + } else if (statusText.includes('Покину')) { + novel.status = NovelStatus.OnHiatus; + } else { + novel.status = NovelStatus.Unknown; } + // 8. Глави 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); + $$('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); + const result = await fetchApi(this.site + '/' + chapterUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + const body = await result.text(); const $ = parseHTML(body); - return $('#user-content').html() || ''; + + // 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 novels: Plugin.NovelItem[] = []; + const url = `${this.site}/search?filter=fiction&search[]=${encodeURIComponent(searchTerm)}`; + + const result = await fetchApi(url, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); - 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) => { + 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: $(elem).find('a').first().attr('href') || '', - name: $(elem).find('a > h2').first().text().trim(), - cover: this.site + $(elem).find('img').first().attr('src'), + path: `/fictions/${id}`, // гарантуємо правильний формат + name, + cover: img ? this.site + img : '', }); }); + return novels; } @@ -177,21 +272,9 @@ class BakaInUa implements Plugin.PluginBase { { 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, - }, + only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false }, + longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false }, + finished: { type: FilterTypes.Switch, label: 'Завершене', value: false }, } satisfies Filters; }