Skip to content
Merged
Show file tree
Hide file tree
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
20 changes: 20 additions & 0 deletions plugins/multisrc/fictioneer/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
]
150 changes: 109 additions & 41 deletions plugins/multisrc/fictioneer/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -43,28 +75,40 @@ 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();
}

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 +
'/' +
Expand All @@ -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> {
Expand All @@ -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;
}
Expand All @@ -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() || '';
}

Expand All @@ -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, '') + '/';
Comment thread
nullrender marked this conversation as resolved.
}
Loading
Loading