fix: Illusia Bug Fixes (#2095)

- Fixed the 'Popular' and 'Latest' novel lists.

- Removed chapter.reverse() to align with the source's natural chapter order.

- Improved novel details extraction (author, genres, and status).

- Optimized chapter parsing by bypassing CSS pseudo-selectors for better performance.

- Injected User-Agent header to prevent basic bot blocking.
This commit is contained in:
Samuel
2026-04-06 23:35:59 -03:00
committed by GitHub
parent 7aef598dcd
commit 429e979535
+240 -81
View File
@@ -3,165 +3,324 @@ import { Plugin } from '@/types/plugin';
import { Filters } from '@libs/filterInputs'; import { Filters } from '@libs/filterInputs';
import { load as loadCheerio } from 'cheerio'; import { load as loadCheerio } from 'cheerio';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from '@libs/defaultCover';
class Illusia implements Plugin.PluginBase { class Illusia implements Plugin.PluginBase {
id = 'illusia'; id = 'illusia';
name = 'Illusia'; name = 'Illusia';
icon = 'src/pt-br/illusia/icon.png'; icon = 'src/pt-br/illusia/icon.png';
site = 'https://illusia.com.br'; site = 'https://illusia.com.br';
version = '1.0.0'; version = '1.0.1';
filters: Filters | undefined = undefined; filters: Filters | undefined = undefined;
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
};
async popularNovels( async popularNovels(
pageNo: number, pageNo: number,
{ { showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
// Usando o sistema de busca nativo para evitar o Erro 500 das rotas customizadas const orderBy = showLatestNovels ? 'modified' : 'comment_count';
const orderby = showLatestNovels ? 'modified' : 'views'; const pagePath = pageNo === 1 ? '' : `page/${pageNo}/`;
const url = `${this.site}/${pageNo === 1 ? '' : 'page/' + pageNo + '/'}?s=&post_type=fcn_story&orderby=${orderby}&order=desc`; const url = `${this.site}/${pagePath}?s=&post_type=fcn_story&sentence=0&orderby=${orderBy}&order=desc&age_rating=Any&story_status=Any&miw=0&maw=0&genres=&fandoms=&characters=&tags=&warnings=&authors=&ex_genres=&ex_fandoms=&ex_characters=&ex_tags=&ex_warnings=&ex_authors=`;
const req = await fetchApi(url); const req = await fetchApi(url, { headers: this.headers });
const body = await req.text(); const body = await req.text();
const loadedCheerio = loadCheerio(body); const loadedCheerio = loadCheerio(body);
return loadedCheerio('#search-result-list > li > div > div') const novels = loadedCheerio(
'#search-result-list > li, article.story, article.post, .card, .story-card, .ranking-item, ul.ranking-list li, .bsx, .book-item, .fcn-story',
)
.map((i, el) => { .map((i, el) => {
const item = loadedCheerio(el); const item = loadedCheerio(el);
const titleEl = item
.find(
'.card__title a, h2 a, h3 a, h4 a, .card-title a, .story-title a, .story__title a, .ranking-title a, .entry-title a, .tt',
)
.first();
const novelName = titleEl.text().trim();
const novelUrl =
titleEl.attr('href') || item.find('a').first().attr('href');
const novelName = item.find('h3 > a').text().trim(); let novelCover =
const novelUrl = item.find('h3 > a').attr('href'); item.find('img').attr('data-src') ||
const novelCover = item.find('img').attr('data-lazy-src') ||
item.find('a.cell-img img').attr('src') || item.find('img').attr('src') ||
item.find('a.cell-img').attr('href'); item.find('.ranking-cover, .story-cover, .img-cover').attr('data-bg');
if (!novelCover) {
const bgElement = item.find('[style*="url("]');
const styleAttr = bgElement.length
? bgElement.attr('style')
: item.attr('style');
if (styleAttr) {
const match = styleAttr.match(/url\(['"]?([^'"]+)['"]?\)/i);
if (match) novelCover = match[1];
}
}
if (!novelName || !novelUrl) return null; if (!novelName || !novelUrl) return null;
if (novelCover && novelCover.startsWith('/')) {
novelCover = this.site + novelCover;
}
return { return {
name: novelName, name: novelName,
cover: novelCover, cover: novelCover || defaultCover,
path: novelUrl.replace(this.site + '/', '').replace(/\/$/, ''), path: novelUrl
}; .replace(this.site, '')
.replace(/^\//, '')
.replace(/\/$/, ''),
} as Plugin.NovelItem;
}) })
.toArray() .toArray()
.filter(novel => novel !== null) as Plugin.NovelItem[]; .filter(novel => novel !== null) as Plugin.NovelItem[];
const uniqueNovels = Array.from(
new Map(novels.map(item => [item.path, item])).values(),
);
return uniqueNovels;
} }
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const req = await fetchApi(`${this.site}/${novelPath}/`); const req = await fetchApi(`${this.site}/${novelPath}/`, {
headers: this.headers,
});
const body = await req.text(); const body = await req.text();
const loadedCheerio = loadCheerio(body); const loadedCheerio = loadCheerio(body);
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: loadedCheerio('h1.story__identity-title').text().trim(), name: loadedCheerio('h1.story__identity-title, h1.post-title')
.text()
.trim(),
}; };
novel.author = loadedCheerio('div.story__identity-meta') let author =
.text() loadedCheerio(
.split('|')[0] 'span.custom-story-info a.author, a[href*="/author/"], a[rel="author"]',
.replace('Author: ', '') )
.replace('by ', '') .first()
.trim(); .text()
.trim() ||
loadedCheerio(
'.story__author, .story-author, .author-name, .post-author, [class*="__author"]',
)
.first()
.text()
.trim();
if (!author) {
const metaText = loadedCheerio(
'.story__identity-meta, .story-meta, .custom-story-info',
)
.text()
.trim();
if (metaText) {
author = metaText
.split('|')[0]
.replace(/^(Autor[a]?|Por|Author|by)[\s:]*/i, '')
.trim();
}
}
novel.author = author || 'Desconhecido';
novel.cover = novel.cover =
loadedCheerio('figure.story__thumbnail img').attr('data-src') ||
loadedCheerio('figure.story__thumbnail img').attr('src') || loadedCheerio('figure.story__thumbnail img').attr('src') ||
loadedCheerio('figure.story__thumbnail > a').attr('href'); loadedCheerio('.story__thumbnail img').attr('data-src') ||
loadedCheerio('.story__thumbnail img').attr('src') ||
loadedCheerio('figure.story__thumbnail > a').attr('href') ||
defaultCover;
novel.genres = loadedCheerio('div.tag-group > a, section.tag-group > a') novel.genres = loadedCheerio(
'div.tag-group > a, section.tag-group > a, .genres a',
)
.map((i, el) => loadedCheerio(el).text().trim()) .map((i, el) => loadedCheerio(el).text().trim())
.toArray() .toArray()
.join(','); .join(',');
novel.summary = loadedCheerio('section.story__summary').text().trim(); let summaryHtml =
loadedCheerio(
'section.story__summary, div.story__summary, .summary',
).html() || '';
summaryHtml = summaryHtml
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<\/div>/gi, '\n');
novel.summary = loadCheerio(summaryHtml)
.text()
.trim()
.replace(/\n{3,}/g, '\n\n');
let chapterElements = loadedCheerio('li.chapter-group__list-item'); const chapterElements = loadedCheerio(
'li.chapter-group__list-item, ul.chapter-list li, .chapters li, .chapter-item',
if (chapterElements.length === 0) { );
chapterElements = loadedCheerio(
'ul.chapter-list li, .chapters li, .chapter-item',
);
}
novel.chapters = chapterElements novel.chapters = chapterElements
.filter((i, el) => {
const className = el.attribs['class'] || '';
return !className.includes('_password');
})
.filter(
(i, el) =>
!loadedCheerio(el)
.find('i')
.first()
?.attr('class')
?.includes('fa-lock'),
)
.map((i, el) => { .map((i, el) => {
const aTag = loadedCheerio(el).find('a').first(); const item = loadedCheerio(el);
const chapterName = aTag.text().trim();
const chapterUrl = aTag
.attr('href')
?.replace(this.site + '/', '')
.replace(/\/$/, '');
return { const aTag = item.find('a').first();
const chapterName = aTag.text().trim();
const chapterUrl = aTag.attr('href');
if (!chapterUrl) return null;
const chapterNumberMatch =
chapterName.match(/(?:cap[íi]tulo|cap\.?|ch\.?)\s*(\d+(\.\d+)?)/i) ||
chapterName.match(/^(\d+(\.\d+)?)/);
const chapterNumber = chapterNumberMatch
? Number(chapterNumberMatch[1])
: undefined;
const chapter: Plugin.ChapterItem = {
name: chapterName, name: chapterName,
path: chapterUrl || '', path: chapterUrl
.replace(this.site, '')
.replace(/^\//, '')
.replace(/\/$/, ''),
}; };
if (chapterNumber !== undefined) {
chapter.chapterNumber = chapterNumber;
}
return chapter;
}) })
.toArray() .toArray()
.filter(chapter => chapter.path !== ''); .filter(chapter => chapter !== null) as Plugin.ChapterItem[];
novel.chapters.reverse(); const metaBlockText =
loadedCheerio('div.story__identity-meta, .story-meta').text() || '';
const metaParts = metaBlockText.split('|').map(p => p.trim());
const status = loadedCheerio('span.story__status').text().trim(); let statusText = loadedCheerio('span.story__status')
if (status === 'Ongoing') novel.status = NovelStatus.Ongoing; .text()
if (status === 'Completed') novel.status = NovelStatus.Completed; .trim()
if (status === 'Cancelled') novel.status = NovelStatus.Cancelled; .toLowerCase();
if (status === 'Hiatus') novel.status = NovelStatus.OnHiatus; if (!statusText && metaParts.length > 1) {
statusText = metaBlockText.toLowerCase();
}
if (
statusText.includes('ongoing') ||
statusText.includes('andamento') ||
statusText.includes('lançando') ||
statusText.includes('ativa')
)
novel.status = NovelStatus.Ongoing;
else if (
statusText.includes('completed') ||
statusText.includes('completo')
)
novel.status = NovelStatus.Completed;
else if (
statusText.includes('cancelled') ||
statusText.includes('cancelado') ||
statusText.includes('dropado')
)
novel.status = NovelStatus.Cancelled;
else if (
statusText.includes('hiatus') ||
statusText.includes('hiato') ||
statusText.includes('pausado')
)
novel.status = NovelStatus.OnHiatus;
else novel.status = NovelStatus.Unknown;
return novel; return novel;
} }
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
// Você mencionou usar node-fetch para o capítulo. const req = await fetchApi(`${this.site}/${chapterPath}/`, {
// Se o fetchApi estiver travando aqui, você pode trocá-lo pelo fetch do NodeJS (neste caso eu mantive a base padrão para testarmos). headers: this.headers,
const req = await fetchApi(`${this.site}/${chapterPath}/`); });
const body = await req.text(); const body = await req.text();
const loadedCheerio = loadCheerio(body); const loadedCheerio = loadCheerio(body);
return loadedCheerio('section#chapter-content > div').html() || ''; const chapterContent = loadedCheerio(
'section#chapter-content > div, div.chapter-content',
);
chapterContent
.find(
'script, style, iframe, .patreon-popup, .fcn-notice, .fictioneer-notice, div.card',
)
.remove();
return chapterContent.html() || '';
} }
async searchNovels( async searchNovels(
searchTerm: string, searchTerm: string,
pageNo: number, pageNo: number,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const req = await fetchApi( const pagePath = pageNo === 1 ? '' : `page/${pageNo}/`;
`${this.site}/${pageNo === 1 ? '' : 'page/' + pageNo + '/'}?s=${encodeURIComponent(searchTerm)}&post_type=fcn_story`,
); const url = `${this.site}/${pagePath}?s=${encodeURIComponent(searchTerm)}&post_type=fcn_story&sentence=0&orderby=relevance&order=desc&age_rating=Any&story_status=Any&miw=0&maw=0&genres=&fandoms=&characters=&tags=&warnings=&authors=&ex_genres=&ex_fandoms=&ex_characters=&ex_tags=&ex_warnings=&ex_authors=`;
const req = await fetchApi(url, { headers: this.headers });
const body = await req.text(); const body = await req.text();
const loadedCheerio = loadCheerio(body); const loadedCheerio = loadCheerio(body);
return loadedCheerio('#search-result-list > li > div > div') const novels = loadedCheerio(
'#search-result-list > li, article.story, article.post, .card, .story-card, .ranking-item, ul.ranking-list li, .bsx, .book-item, .fcn-story',
)
.map((i, el) => { .map((i, el) => {
const novelName = loadedCheerio(el).find('h3 > a').text().trim(); const item = loadedCheerio(el);
const novelCover = const titleEl = item
loadedCheerio(el).find('a.cell-img img').attr('src') || .find(
loadedCheerio(el).find('a.cell-img').attr('href'); '.card__title a, h2 a, h3 a, h4 a, .card-title a, .story-title a, .story__title a, .ranking-title a, .entry-title a, .tt',
const novelUrl = loadedCheerio(el).find('h3 > a').attr('href'); )
.first();
const novelName = titleEl.text().trim();
const novelUrl =
titleEl.attr('href') || item.find('a').first().attr('href');
let novelCover =
item.find('img').attr('data-src') ||
item.find('img').attr('data-lazy-src') ||
item.find('img').attr('src') ||
item.find('.ranking-cover, .story-cover, .img-cover').attr('data-bg');
if (!novelCover) {
const bgElement = item.find('[style*="url("]');
const styleAttr = bgElement.length
? bgElement.attr('style')
: item.attr('style');
if (styleAttr) {
const match = styleAttr.match(/url\(['"]?([^'"]+)['"]?\)/i);
if (match) novelCover = match[1];
}
}
if (!novelName || !novelUrl) return null;
if (novelCover && novelCover.startsWith('/')) {
novelCover = this.site + novelCover;
}
return { return {
name: novelName, name: novelName,
cover: novelCover, cover: novelCover || defaultCover,
path: path: novelUrl
novelUrl?.replace(this.site + '/', '')?.replace(/\/$/, '') || '', .replace(this.site, '')
}; .replace(/^\//, '')
.replace(/\/$/, ''),
} as Plugin.NovelItem;
}) })
.toArray(); .toArray()
.filter(novel => novel !== null) as Plugin.NovelItem[];
const uniqueNovels = Array.from(
new Map(novels.map(item => [item.path, item])).values(),
);
return uniqueNovels;
} }
resolveUrl = (path: string, isNovel?: boolean) => `${this.site}/${path}/`; resolveUrl = (path: string, isNovel?: boolean) => `${this.site}/${path}/`;