fix(fictioneer): migrate Illusia to the generator and add per-source overrides (#2567)
This commit is contained in:
@@ -65,5 +65,25 @@
|
||||
"versionIncrements": 1,
|
||||
"browsePage": "stories"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "illusia",
|
||||
"sourceSite": "https://illusia.com.br",
|
||||
"sourceName": "Illusia",
|
||||
"options": {
|
||||
"browsePage": "historias",
|
||||
"lang": "Portuguese",
|
||||
"trimTrailingSlash": true,
|
||||
"selectors": {
|
||||
"browseCard": "#list-of-stories > li.illusia-card",
|
||||
"searchCard": "#search-result-list > li.illusia-card",
|
||||
"cardTitle": "h2.illusia-card__title > a",
|
||||
"cardCover": "img.illusia-card__cover-img",
|
||||
"novelTitle": "h1.illusia-single-story__title",
|
||||
"novelAuthor": "span.illusia-single-story__credits-primary a.author",
|
||||
"novelCover": "img.illusia-single-story__cover-img",
|
||||
"novelSummary": "div.illusia-single-story__description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,10 +4,35 @@ import { Plugin } from '@/types/plugin';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { Filters } from '@libs/filterInputs';
|
||||
|
||||
type FictioneerSelectors = {
|
||||
browseCard: string;
|
||||
searchCard: string;
|
||||
cardTitle: string;
|
||||
cardCover: string;
|
||||
novelTitle: string;
|
||||
novelAuthor: string;
|
||||
novelCover: string;
|
||||
novelSummary: string;
|
||||
};
|
||||
|
||||
const defaultSelectors: FictioneerSelectors = {
|
||||
browseCard:
|
||||
'#featured-list > li > div > div, #list-of-stories > li > div > div',
|
||||
searchCard: '#search-result-list > li > div > div',
|
||||
cardTitle: 'h3 > a',
|
||||
cardCover: 'a.cell-img:has(img)',
|
||||
novelTitle: 'h1.story__identity-title',
|
||||
novelAuthor: 'div.story__identity-meta',
|
||||
novelCover: 'figure.story__thumbnail > a',
|
||||
novelSummary: 'section.story__summary',
|
||||
};
|
||||
|
||||
type FictioneerOptions = {
|
||||
browsePage: string;
|
||||
lang?: string;
|
||||
versionIncrements?: number;
|
||||
trimTrailingSlash?: boolean;
|
||||
selectors?: Partial<FictioneerSelectors>;
|
||||
};
|
||||
|
||||
export type FictioneerMetadata = {
|
||||
@@ -24,6 +49,7 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
site: string;
|
||||
version: string;
|
||||
options: FictioneerOptions;
|
||||
selectors: FictioneerSelectors;
|
||||
filters: Filters | undefined = undefined;
|
||||
|
||||
constructor(metadata: FictioneerMetadata) {
|
||||
@@ -32,8 +58,14 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
this.icon = `multisrc/fictioneer/${metadata.id.toLowerCase()}/icon.png`;
|
||||
this.site = metadata.sourceSite;
|
||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||
this.version = `1.1.${0 + versionIncrements}`;
|
||||
this.version = `1.2.${0 + versionIncrements}`;
|
||||
this.options = metadata.options;
|
||||
this.selectors = { ...defaultSelectors, ...metadata.options?.selectors };
|
||||
}
|
||||
|
||||
private toPath(url: string): string {
|
||||
const path = new URL(url, this.site).pathname.substring(1);
|
||||
return this.options.trimTrailingSlash ? path.replace(/\/$/, '') : path;
|
||||
}
|
||||
|
||||
private parseNovels(
|
||||
@@ -43,16 +75,19 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
return loadedCheerio(selector)
|
||||
.map((i, el) => {
|
||||
const element = loadedCheerio(el);
|
||||
const novelName = element.find('h3 > a').text();
|
||||
const novelCover = element.find('a.cell-img:has(img)').attr('href');
|
||||
const novelUrl = element.find('h3 > a').attr('href');
|
||||
const title = element.find(this.selectors.cardTitle);
|
||||
const novelName = title.text();
|
||||
const cover = element.find(this.selectors.cardCover);
|
||||
const novelCover =
|
||||
cover.attr('data-src') || cover.attr('src') || cover.attr('href');
|
||||
const novelUrl = title.attr('href');
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
return {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
path: new URL(novelUrl, this.site).pathname.substring(1),
|
||||
path: this.toPath(novelUrl),
|
||||
};
|
||||
})
|
||||
.toArray();
|
||||
@@ -60,11 +95,20 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
// {
|
||||
// showLatestNovels,
|
||||
// filters,
|
||||
// }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
if (showLatestNovels) {
|
||||
// Latest updates come from a search results page, so use searchCard.
|
||||
const req = await fetchApi(
|
||||
this.site +
|
||||
`/${pageNo === 1 ? '' : 'page/' + pageNo + '/'}?s=&post_type=fcn_story&orderby=modified&order=desc`,
|
||||
);
|
||||
const body = await req.text();
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
|
||||
return this.parseNovels(loadedCheerio, this.selectors.searchCard);
|
||||
}
|
||||
|
||||
const req = await fetchApi(
|
||||
this.site +
|
||||
'/' +
|
||||
@@ -75,10 +119,7 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
const body = await req.text();
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
|
||||
return this.parseNovels(
|
||||
loadedCheerio,
|
||||
'#featured-list > li > div > div, #list-of-stories > li > div > div',
|
||||
);
|
||||
return this.parseNovels(loadedCheerio, this.selectors.browseCard);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
@@ -88,52 +129,78 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio('h1.story__identity-title').text(),
|
||||
name: loadedCheerio(this.selectors.novelTitle).text(),
|
||||
};
|
||||
|
||||
// novel.artist = '';
|
||||
novel.author = loadedCheerio('div.story__identity-meta')
|
||||
.text()
|
||||
.split('|')[0]
|
||||
.replace('Author: ', '')
|
||||
.replace('by ', '')
|
||||
.trim();
|
||||
novel.cover = loadedCheerio('figure.story__thumbnail > a').attr('href');
|
||||
const author = loadedCheerio(this.selectors.novelAuthor).text();
|
||||
// The default selector matches the "Author: X | ..." meta line, while an
|
||||
// override is expected to match the author name itself.
|
||||
novel.author = this.options.selectors?.novelAuthor
|
||||
? author.trim()
|
||||
: author.split('|')[0].replace('Author: ', '').replace('by ', '').trim();
|
||||
const cover = loadedCheerio(this.selectors.novelCover);
|
||||
novel.cover =
|
||||
cover.attr('data-src') || cover.attr('src') || cover.attr('href');
|
||||
novel.genres = loadedCheerio('div.tag-group > a, section.tag-group > a')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
loadedCheerio('section.story__summary .related-stories-block').remove();
|
||||
novel.summary = loadedCheerio('section.story__summary').text();
|
||||
const summary = loadedCheerio(this.selectors.novelSummary);
|
||||
summary.find('.related-stories-block, section.small-card-block').remove();
|
||||
summary.find('p').after('\n\n');
|
||||
summary.find('br').after('\n');
|
||||
novel.summary = summary
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
novel.chapters = loadedCheerio('li.chapter-group__list-item._publish')
|
||||
.filter((i, el) => !el.attribs['class'].includes('_password'))
|
||||
.filter(
|
||||
(i, el) =>
|
||||
!loadedCheerio(el)
|
||||
.find('i')
|
||||
.first()!
|
||||
.attr('class')!
|
||||
.includes('fa-lock'),
|
||||
!(loadedCheerio(el).find('i').first().attr('class') || '').includes(
|
||||
'fa-lock',
|
||||
),
|
||||
)
|
||||
.map((i, el) => {
|
||||
const chapterName = loadedCheerio(el).find('a').text();
|
||||
const chapterUrl = loadedCheerio(el).find('a').attr('href');
|
||||
|
||||
if (!chapterUrl) return;
|
||||
return {
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: chapterName,
|
||||
path: new URL(chapterUrl, this.site).pathname.substring(1),
|
||||
path: this.toPath(chapterUrl),
|
||||
};
|
||||
const chapterNumber = chapterName.match(
|
||||
/^(?:cap[íi]tulo|chapter|ch\.?)\s*(\d+(?:\.\d+)?)/i,
|
||||
);
|
||||
if (chapterNumber) chapter.chapterNumber = Number(chapterNumber[1]);
|
||||
return chapter;
|
||||
})
|
||||
.toArray();
|
||||
|
||||
const status = loadedCheerio('span.story__status').text().trim();
|
||||
if (status === 'Ongoing') novel.status = NovelStatus.Ongoing;
|
||||
if (status === 'Completed') novel.status = NovelStatus.Completed;
|
||||
if (status === 'Cancelled') novel.status = NovelStatus.Cancelled;
|
||||
if (status === 'Hiatus') novel.status = NovelStatus.OnHiatus;
|
||||
// The status class ("_" + lowercase Fictioneer status) is
|
||||
// language-independent; the English text is a fallback.
|
||||
const statusElement = loadedCheerio('span.story__status');
|
||||
const status = statusElement.text().trim();
|
||||
if (statusElement.hasClass('_ongoing') || status === 'Ongoing')
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
if (
|
||||
statusElement.hasClass('_completed') ||
|
||||
statusElement.hasClass('_oneshot') ||
|
||||
status === 'Completed'
|
||||
)
|
||||
novel.status = NovelStatus.Completed;
|
||||
if (
|
||||
statusElement.hasClass('_canceled') ||
|
||||
status === 'Canceled' ||
|
||||
status === 'Cancelled'
|
||||
)
|
||||
novel.status = NovelStatus.Cancelled;
|
||||
if (statusElement.hasClass('_hiatus') || status === 'Hiatus')
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
|
||||
return novel;
|
||||
}
|
||||
@@ -146,6 +213,10 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
|
||||
// chapterTransformJs HERE
|
||||
|
||||
loadedCheerio('section#chapter-content')
|
||||
.find('script, style, iframe')
|
||||
.remove();
|
||||
|
||||
return loadedCheerio('section#chapter-content > div').html() || '';
|
||||
}
|
||||
|
||||
@@ -160,12 +231,9 @@ export class FictioneerPlugin implements Plugin.PluginBase {
|
||||
const body = await req.text();
|
||||
const loadedCheerio = loadCheerio(body);
|
||||
|
||||
return this.parseNovels(
|
||||
loadedCheerio,
|
||||
'#search-result-list > li > div > div',
|
||||
);
|
||||
return this.parseNovels(loadedCheerio, this.selectors.searchCard);
|
||||
}
|
||||
|
||||
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
// this.site + '/' + path + '/';
|
||||
resolveUrl = (path: string) =>
|
||||
this.site.replace(/\/+$/, '') + '/' + path.replace(/^\/+|\/+$/g, '') + '/';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user