diff --git a/icons/src/fr/phenixscans/icon.png b/icons/src/fr/phenixscans/icon.png new file mode 100644 index 0000000..878d68a Binary files /dev/null and b/icons/src/fr/phenixscans/icon.png differ diff --git a/plugins/french/phenixscans.ts b/plugins/french/phenixscans.ts new file mode 100644 index 0000000..f9068a6 --- /dev/null +++ b/plugins/french/phenixscans.ts @@ -0,0 +1,281 @@ +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 { + const r = await fetchApi(url); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise { + 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 { + 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 { + const $ = await this.getCheerio(this.site + chapterPath); + return $('#readerarea').html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + 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; + } + + async fetchImage(url: string): Promise { + const headers = new Headers({ + 'Referer': `${this.site}`, + }); + return await fetchFile(url, { headers }); + } + + 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();