fix: Update BakaInUa (#2089)
* BakaInUa Plugin Update The plugin was completely rewritten because version 2.0.0 didn't work at all. * test * 3.1.5 Baka * 3.1.6 Baka
This commit is contained in:
+167
-84
@@ -9,7 +9,7 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
name = 'BakaInUA';
|
name = 'BakaInUA';
|
||||||
icon = 'src/uk/bakainua/icon.png';
|
icon = 'src/uk/bakainua/icon.png';
|
||||||
site = 'https://baka.in.ua';
|
site = 'https://baka.in.ua';
|
||||||
version = '2.0.0';
|
version = '3.1.6';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -19,8 +19,7 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const fictionIds: string[] = [];
|
const fictionIds: string[] = [];
|
||||||
|
const url = new URL(this.site + '/fictions/alphabetical');
|
||||||
const url: URL = new URL(this.site + '/fictions/alphabetical');
|
|
||||||
|
|
||||||
if (pageNo > 1) url.searchParams.append('page', pageNo.toString());
|
if (pageNo > 1) url.searchParams.append('page', pageNo.toString());
|
||||||
if (showLatestNovels || (filters && filters.only_new.value))
|
if (showLatestNovels || (filters && filters.only_new.value))
|
||||||
@@ -32,111 +31,207 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
url.searchParams.append('genre', filters.genre.value);
|
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 body = await result.text();
|
||||||
const $ = parseHTML(body);
|
const $ = parseHTML(body);
|
||||||
|
|
||||||
// get the ids of the popular novels
|
$('[data-fiction-picker-id-param]').each((_, elem) => {
|
||||||
$('div#fiction-list-page > div > div > div > img').each((index, elem) => {
|
const id = $(elem).attr('data-fiction-picker-id-param');
|
||||||
const fictionId = $(elem).attr('data-fiction-picker-id-param');
|
if (id) fictionIds.push(id);
|
||||||
if (fictionId) {
|
});
|
||||||
fictionIds.push(fictionId);
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetch the details of the popular novels asynchronously
|
const novels = await Promise.all(requests);
|
||||||
const requests: Promise<Plugin.NovelItem>[] = fictionIds.map(async id => {
|
return novels.filter((n): n is Plugin.NovelItem => n !== null);
|
||||||
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<Plugin.SourceNovel> {
|
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 body = await result.text();
|
||||||
const $ = parseHTML(body);
|
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 = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelUrl,
|
path: novelUrl,
|
||||||
name: $('main div > h1').text().trim(),
|
name: $$('h1').first().text().trim(),
|
||||||
author: $('button#fictions-author-search').text().trim(),
|
author: $$('#fictions-author-search').text().trim() || 'Невідомо',
|
||||||
cover: this.site + $('main div > img').attr('src'),
|
artist: $$('#fictions-author-search').text().trim() || 'Невідомо',
|
||||||
summary: $('main div > h3').first().parent().find('div').text().trim(),
|
cover,
|
||||||
genres: $('h4:contains("Жанри")')
|
summary: $$('div.whitespace-pre-line')
|
||||||
.last()
|
.first()
|
||||||
.siblings()
|
.text()
|
||||||
.last()
|
.replace(/\s+/g, ' ')
|
||||||
.find('span')
|
.trim(),
|
||||||
.map((_, el) => $(el).text().trim())
|
genres: $$('div.flex.flex-wrap.gap-2 span')
|
||||||
|
.map((_, el) => $$(el).text().trim())
|
||||||
.get()
|
.get()
|
||||||
.join(', '),
|
.join(', '),
|
||||||
};
|
};
|
||||||
|
|
||||||
switch ($('div:contains("Статус")').last().siblings().text().trim()) {
|
// 7. Статус
|
||||||
case 'Видаєт.':
|
const statusText = $$('div.text-sm:contains("Статус")') // Знаходимо підпис "Статус"
|
||||||
novel.status = NovelStatus.Ongoing;
|
.prev('div.text-2xl') // Беремо попередній div з класом text-2xl
|
||||||
break;
|
.text()
|
||||||
case 'Заверш.':
|
.trim();
|
||||||
novel.status = NovelStatus.Completed;
|
|
||||||
break;
|
if (statusText.includes('Заверш')) {
|
||||||
case 'Покину.':
|
novel.status = NovelStatus.Completed;
|
||||||
novel.status = NovelStatus.OnHiatus;
|
} else if (statusText.includes('Видаєт')) {
|
||||||
break;
|
novel.status = NovelStatus.Ongoing;
|
||||||
default:
|
} else if (statusText.includes('Покину')) {
|
||||||
novel.status = NovelStatus.Unknown;
|
novel.status = NovelStatus.OnHiatus;
|
||||||
break;
|
} else {
|
||||||
|
novel.status = NovelStatus.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8. Глави
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
$('li.group a').each((index, elem) => {
|
$$('li.group a[href*="/chapters/"]').each((_, elem) => {
|
||||||
const chapter: Plugin.ChapterItem = {
|
const href = $$(elem).attr('href') || '';
|
||||||
name: $(elem).find('span').eq(1).text().trim(),
|
chapters.push({
|
||||||
path: $(elem).attr('href') || '',
|
name: $$(elem).find('span').eq(1).text().trim() || 'Розділ',
|
||||||
chapterNumber: parseInt($(elem).find('span').eq(0).text().trim()),
|
path: href.replace(this.site + '/', ''),
|
||||||
releaseTime: $(elem).find('span').eq(2).text().trim(),
|
chapterNumber:
|
||||||
};
|
parseFloat($$(elem).find('span').eq(0).text().replace(',', '.')) || 0,
|
||||||
chapters.push(chapter);
|
releaseTime: $$(elem).find('span').eq(2).text().trim(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
novel.chapters = chapters.reverse();
|
novel.chapters = chapters.reverse();
|
||||||
|
|
||||||
return novel;
|
return novel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterUrl: string): Promise<string> {
|
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 body = await result.text();
|
||||||
const $ = parseHTML(body);
|
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[]> {
|
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 body = await result.text();
|
||||||
const $ = parseHTML(body);
|
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({
|
novels.push({
|
||||||
path: $(elem).find('a').first().attr('href') || '',
|
path: `/fictions/${id}`, // гарантуємо правильний формат
|
||||||
name: $(elem).find('a > h2').first().text().trim(),
|
name,
|
||||||
cover: this.site + $(elem).find('img').first().attr('src'),
|
cover: img ? this.site + img : '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,21 +272,9 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
{ label: 'Школа', value: '6' },
|
{ label: 'Школа', value: '6' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
only_new: {
|
only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false },
|
||||||
type: FilterTypes.Switch,
|
longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false },
|
||||||
label: 'Новинки',
|
finished: { type: FilterTypes.Switch, label: 'Завершене', value: false },
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
longreads: {
|
|
||||||
type: FilterTypes.Switch,
|
|
||||||
label: 'Довгочити',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
finished: {
|
|
||||||
type: FilterTypes.Switch,
|
|
||||||
label: 'Завершене',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
} satisfies Filters;
|
} satisfies Filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user