Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 166 additions & 83 deletions plugins/ukrainian/bakainua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,8 +19,7 @@ class BakaInUa implements Plugin.PluginBase {
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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))
Expand All @@ -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<Plugin.NovelItem>[] = 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<Plugin.SourceNovel> {
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<string> {
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, '<br>')
.replace(/\\"/g, '"')
.replace(/\\u003c/g, '<')
.replace(/\\u003e/g, '>');
}
}

return (
chapterHtml ||
'Контент не знайдено. Можливо, потрібна авторизація на сайті.'
);
}

async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
Loading