60d95fb8d2
* add ImageRequestInit & fix Hako * remove multisrc fetchFile * remove plugins fetchFile * update docs and template file
275 lines
9.6 KiB
TypeScript
275 lines
9.6 KiB
TypeScript
import { CheerioAPI, load } from 'cheerio';
|
||
import { fetchApi, fetchFile } from '@libs/fetch';
|
||
import { Plugin } from '@typings/plugin';
|
||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||
import { defaultCover } from '@libs/defaultCover';
|
||
import { NovelStatus } from '@libs/novelStatus';
|
||
import dayjs from 'dayjs';
|
||
|
||
class PhenixScansTradPlugin implements Plugin.PluginBase {
|
||
id = 'phenixscans';
|
||
name = 'PhenixScans';
|
||
icon = 'src/fr/phenixscans/icon.png';
|
||
site = 'https://phenixscans.fr';
|
||
version = '1.0.0';
|
||
|
||
async getCheerio(url: string): Promise<CheerioAPI> {
|
||
const r = await fetchApi(url);
|
||
const body = await r.text();
|
||
const $ = load(body);
|
||
return $;
|
||
}
|
||
|
||
async popularNovels(
|
||
pageNo: number,
|
||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||
): Promise<Plugin.NovelItem[]> {
|
||
const novels: Plugin.NovelItem[] = [];
|
||
let novel: Plugin.NovelItem;
|
||
|
||
let filter = '';
|
||
for (const key in filters) {
|
||
if (typeof filters[key].value === 'object') {
|
||
for (const value of filters[key].value as string[]) {
|
||
filter += `&genre%5B%5D=${value}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
const order = showLatestNovels ? 'update' : 'popular';
|
||
const url = `${this.site}/manga/?page=${pageNo}${filter}&status=&type=novel&order=${order}`;
|
||
|
||
let $ = await this.getCheerio(url);
|
||
$('div div div div a').each((i, elem) => {
|
||
const novelName = $(elem).attr('title')?.trim();
|
||
const novelUrl = $(elem).attr('href');
|
||
const novelCover = $(elem).find('div img').attr('src') || defaultCover;
|
||
|
||
if (novelUrl && novelName) {
|
||
novel = {
|
||
name: novelName,
|
||
cover: novelCover,
|
||
path: novelUrl.replace(this.site, ''),
|
||
};
|
||
novels.push(novel);
|
||
}
|
||
});
|
||
return novels;
|
||
}
|
||
|
||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||
const novel: Plugin.SourceNovel = {
|
||
path: novelPath,
|
||
name: 'Sans titre',
|
||
};
|
||
|
||
let $ = await this.getCheerio(this.site + novelPath);
|
||
|
||
novel.name = $('h1[itemprop=name]').text().replace('– Novel', '').trim();
|
||
novel.cover =
|
||
$('div[itemprop=image] img').first().attr('src') || defaultCover;
|
||
novel.author = $('.fmed b:contains(Auteur)+span').text().trim();
|
||
novel.genres = $('.mgen a')
|
||
.map(function () {
|
||
return $(this).text();
|
||
})
|
||
.get()
|
||
.join(', ');
|
||
novel.summary = $('.entry-content[itemprop=description]').text().trim();
|
||
novel.status = this.getStatus(
|
||
$('.tsinfo .imptdt:contains(Statut)').text().replace('Statut', '').trim(),
|
||
);
|
||
|
||
let chapters: Plugin.ChapterItem[] = [];
|
||
$('ul li:has(div.chbox):has(div.eph-num)').each((i, elem) => {
|
||
const chapterName = $(elem).find('a .chapternum').text().trim();
|
||
const chapterUrl = $(elem).find('a').attr('href');
|
||
const releaseDate = this.parseDate($(elem).find('a .chapterdate').text());
|
||
if (chapterUrl && chapterUrl.includes(this.site) && chapterName) {
|
||
chapters.push({
|
||
name: chapterName,
|
||
path: chapterUrl.replace(this.site, ''),
|
||
releaseTime: releaseDate,
|
||
});
|
||
}
|
||
});
|
||
novel.chapters = chapters;
|
||
return novel;
|
||
}
|
||
|
||
parseDate(date: string): string {
|
||
const monthMapping: { [key: string]: string } = {
|
||
janvier: 'January',
|
||
fevrier: 'February',
|
||
mars: 'March',
|
||
avril: 'April',
|
||
mai: 'May',
|
||
juin: 'June',
|
||
juillet: 'July',
|
||
aout: 'August',
|
||
septembre: 'September',
|
||
octobre: 'October',
|
||
novembre: 'November',
|
||
decembre: 'December',
|
||
};
|
||
const [month, day, year] = date.split(' ');
|
||
return dayjs(
|
||
`${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`,
|
||
'D MMMM YYYY',
|
||
).format('YYYY-MM-DD');
|
||
}
|
||
|
||
getStatus(status: string) {
|
||
const lowerCaseStatus = status.toLowerCase();
|
||
const ongoing = ['en cours', 'en cours de publication'];
|
||
const onhiatus = ['en pause', 'en attente'];
|
||
const completed = ['complété', 'fini', 'achevé', 'terminé'];
|
||
const cancelled = ['abandonné'];
|
||
|
||
if (ongoing.includes(lowerCaseStatus)) {
|
||
return NovelStatus.Ongoing;
|
||
} else if (onhiatus.includes(lowerCaseStatus)) {
|
||
return NovelStatus.OnHiatus;
|
||
} else if (completed.includes(lowerCaseStatus)) {
|
||
return NovelStatus.Completed;
|
||
} else if (cancelled.includes(lowerCaseStatus)) {
|
||
return NovelStatus.Cancelled;
|
||
}
|
||
return NovelStatus.Unknown;
|
||
}
|
||
|
||
async parseChapter(chapterPath: string): Promise<string> {
|
||
const $ = await this.getCheerio(this.site + chapterPath);
|
||
return $('#readerarea').html() || '';
|
||
}
|
||
|
||
async searchNovels(
|
||
searchTerm: string,
|
||
pageNo: number,
|
||
): Promise<Plugin.NovelItem[]> {
|
||
if (pageNo !== 1) return [];
|
||
|
||
let popularNovels = this.popularNovels(1, {
|
||
showLatestNovels: true,
|
||
filters: undefined,
|
||
});
|
||
|
||
let novels = (await popularNovels).filter(novel =>
|
||
novel.name
|
||
.toLowerCase()
|
||
.normalize('NFD')
|
||
.replace(/[\u0300-\u036f]/g, '')
|
||
.trim()
|
||
.includes(
|
||
searchTerm
|
||
.toLowerCase()
|
||
.normalize('NFD')
|
||
.replace(/[\u0300-\u036f]/g, '')
|
||
.trim(),
|
||
),
|
||
);
|
||
|
||
return novels;
|
||
}
|
||
|
||
filters = {
|
||
genre: {
|
||
type: FilterTypes.CheckboxGroup,
|
||
label: 'Genre',
|
||
value: [],
|
||
options: [
|
||
{ label: 'Action', value: 'action' },
|
||
{
|
||
label: 'Action Adventure Fantaisie Psychologique',
|
||
value: 'action-adventure-fantaisie-psychologique',
|
||
},
|
||
{
|
||
label: 'Action Arts martiaux Aventure Fantastique Surnaturel',
|
||
value: 'action-arts-martiaux-aventure-fantastique-surnaturel',
|
||
},
|
||
{
|
||
label: 'Action Aventure Fantastique',
|
||
value: 'action-aventure-fantastique',
|
||
},
|
||
{ label: 'Action Drame Shônen', value: 'action-drame-shonen' },
|
||
{ label: 'Adult', value: 'adult' },
|
||
{ label: 'Adventure', value: 'adventure' },
|
||
{ label: 'amitié', value: 'amitie' },
|
||
{ label: 'amour', value: 'amour' },
|
||
{ label: 'Art-martiaux', value: 'art-martiaux' },
|
||
{ label: 'Arts-martiaux', value: 'arts-martiaux' },
|
||
{ label: 'Aventure', value: 'aventure' },
|
||
{ label: 'Combat', value: 'combat' },
|
||
{ label: 'Comedie', value: 'comedie' },
|
||
{ label: 'Comedy', value: 'comedy' },
|
||
{ label: 'Demons', value: 'demons' },
|
||
{ label: 'Dragon', value: 'dragon' },
|
||
{ label: 'Drama', value: 'drama' },
|
||
{ label: 'Drame', value: 'drame' },
|
||
{ label: 'Ecchi', value: 'ecchi' },
|
||
{ label: 'Fantaisie', value: 'fantaisie' },
|
||
{ label: 'fantastique', value: 'fantastique' },
|
||
{ label: 'Fantasy', value: 'fantasy' },
|
||
{ label: 'Ghosts', value: 'ghosts' },
|
||
{ label: 'Harem', value: 'harem' },
|
||
{ label: 'Historical', value: 'historical' },
|
||
{ label: 'Historique', value: 'historique' },
|
||
{ label: 'Horreur', value: 'horreur' },
|
||
{ label: 'Horror', value: 'horror' },
|
||
{ label: 'Isekai', value: 'isekai' },
|
||
{ label: 'Josei', value: 'josei' },
|
||
{ label: 'longstrip art', value: 'longstrip-art' },
|
||
{ label: 'Magic', value: 'magic' },
|
||
{ label: 'Magie', value: 'magie' },
|
||
{ label: 'Male Protagonist', value: 'male-protagonist' },
|
||
{ label: 'manga', value: 'manga' },
|
||
{ label: 'Manhwa', value: 'manhwa' },
|
||
{ label: 'Manhwa Player', value: 'manhwa-player' },
|
||
{ label: 'Manwha', value: 'manwha' },
|
||
{ label: 'Martial Arts', value: 'martial-arts' },
|
||
{ label: 'Mature', value: 'mature' },
|
||
{ label: 'Monstre', value: 'monstre' },
|
||
{ label: 'Murim', value: 'murim' },
|
||
{ label: 'mystère', value: 'mystere' },
|
||
{ label: 'Mystery', value: 'mystery' },
|
||
{ label: 'Necromancien', value: 'necromancien' },
|
||
{ label: 'necromencer', value: 'necromencer' },
|
||
{ label: 'Novel', value: 'novel' },
|
||
{ label: 'One Shot', value: 'one-shot' },
|
||
{ label: 'Over Power MC', value: 'over-power-mc' },
|
||
{ label: 'Partenaire', value: 'partenaire' },
|
||
{ label: 'Player', value: 'player' },
|
||
{ label: 'Player Manhwa', value: 'player-manhwa' },
|
||
{ label: 'Portail', value: 'portail' },
|
||
{ label: 'Psychological', value: 'psychological' },
|
||
{ label: 'Psychologique', value: 'psychologique' },
|
||
{ label: 'regresseur', value: 'regresseur' },
|
||
{ label: 'régression', value: 'regression' },
|
||
{ label: 'Réincarnation', value: 'reincarnation' },
|
||
{ label: 'Returner', value: 'returner' },
|
||
{ label: 'Romance', value: 'romance' },
|
||
{ label: 'School Life', value: 'school-life' },
|
||
{ label: 'Sci-fi', value: 'sci-fi' },
|
||
{ label: 'Seinen', value: 'seinen' },
|
||
{ label: 'Shôjo', value: 'shojo' },
|
||
{ label: 'Shônen', value: 'shonen' },
|
||
{ label: 'Shotacon', value: 'shotacon' },
|
||
{ label: 'Shoujo', value: 'shoujo' },
|
||
{ label: 'Shounen', value: 'shounen' },
|
||
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||
{ label: 'Slide of Life', value: 'slide-of-life' },
|
||
{ label: 'Smut', value: 'smut' },
|
||
{ label: 'Sports', value: 'sports' },
|
||
{ label: 'Supernatural', value: 'supernatural' },
|
||
{ label: 'Surnaturel', value: 'surnaturel' },
|
||
{ label: 'Système', value: 'systeme' },
|
||
{ label: 'Tragédie', value: 'tragedie' },
|
||
{ label: 'Tragedy', value: 'tragedy' },
|
||
{ label: 'Webtoons', value: 'webtoons' },
|
||
],
|
||
},
|
||
} satisfies Filters;
|
||
}
|
||
|
||
export default new PhenixScansTradPlugin();
|