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
246 changes: 246 additions & 0 deletions plugins/french/rezerowebnovelfr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { CheerioAPI, load } from 'cheerio';
import { fetchApi } from '@libs/fetch';
import { Plugin } from '@/types/plugin';
import { NovelStatus } from '@libs/novelStatus';

const NOVEL_METADATA: Record<string, { name: string; summary: string }> = {
'/histoire-principale/': {
name: 'Re:Zero - Histoire Principale',
summary: 'Histoire principale de Re:Zero traduite en français.',
},
'/histoires-annexes/': {
name: 'Re:Zero - Histoires Annexes',
summary:
'Histoires annexes (Side Stories) de Re:Zero traduites en français.',
},
'/if-stories/': {
name: 'Re:Zero - IF Stories',
summary:
'Histoires alternatives (IF Stories) de Re:Zero traduites en français.',
},
};

const NOVEL_COVER =
'https://rezerowebnovelfr.wordpress.com/wp-content/uploads/2021/03/sans-titre-1.png';

function toRoman(num: number): string {
const romanMap: [number, string][] = [
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I'],
];
let roman = '';
let n = num;
for (const [value, symbol] of romanMap) {
while (n >= value) {
roman += symbol;
n -= value;
}
}
return roman;
}

function getArcRomanPrefix(matchStr: string): string {
const num = parseInt(matchStr, 10);
if (isNaN(num)) {
return matchStr.toUpperCase();
}
return toRoman(num);
}

class ReZeroWebNovelFrPlugin implements Plugin.PluginBase {
id = 'rezerowebnovelfr';
name = 'Re:Zero Web Novel FR';
icon = 'src/fr/rezerowebnovelfr/icon.png';
site = 'https://rezerowebnovelfr.wordpress.com';
version = '1.0.0';

async getCheerio(url: string): Promise<CheerioAPI> {
const r = await fetchApi(url, {
headers: { 'Accept-Encoding': 'deflate' },
});
const body = await r.text();
const $ = load(body);
return $;
}

async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
if (pageNo > 1) return [];

return Object.entries(NOVEL_METADATA).map(([path, meta]) => ({
name: meta.name,
cover: NOVEL_COVER,
path: path,
}));
}

async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: 'Sans titre',
};

const $ = await this.getCheerio(this.site + novelPath);
novel.name = $('h1.entry-title').text().trim();
novel.author = 'Tappei Nagatsuki';
novel.status = NovelStatus.Ongoing;

const meta = NOVEL_METADATA[novelPath];
if (meta) {
novel.name = meta.name;
novel.cover = NOVEL_COVER;
novel.summary = meta.summary;
}

const chapters: Plugin.ChapterItem[] = [];

const tryAddChapter = (
href: string | undefined,
name: string,
prefix = '',
) => {
if (!href || !href.includes(this.site)) return;
const cleanHref = href.split('?')[0];
const dateMatch = cleanHref.match(/\/(\d{4})\/(\d{2})\/(\d{2})\//);
if (dateMatch && name) {
const path = cleanHref.replace(this.site, '');
if (!chapters.some(c => c.path === path)) {
const releaseDate = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
let cleanName = name;
if (novelPath === '/if-stories/') {
cleanName = name
.replace(/^(Lire|lire|LIRE|LIRE LE)\s+/i, '')
.trim();
}
chapters.push({
name: prefix + cleanName,
path,
releaseTime: releaseDate,
});
}
}
};

if (novelPath === '/histoire-principale/') {
const arcUrls: string[] = [];
$('.entry-content .wp-block-button a').each((i, el) => {
const href = $(el).attr('href');
if (href && href.includes(this.site)) {
arcUrls.push(href);
}
});

const pages = await Promise.all(
arcUrls.map(async arcUrl => {
try {
const page$ = await this.getCheerio(arcUrl);
return { page$, arcUrl };
} catch (e) {
return null;
}
}),
);

for (const result of pages) {
if (!result) continue;
const { page$, arcUrl } = result;

// Extract arc designation from URL, e.g. "arc-viii-vincent-vollachia" -> "Arc VIII"
const arcMatch = arcUrl
.replace(this.site, '')
.match(/^\/arc-([^-/]+)/i);
const arcPrefix = arcMatch
? `Arc ${getArcRomanPrefix(arcMatch[1])} - `
: '';

page$('div.entry-content ul li a, div.entry-content ol li a').each(
(i, el) => {
const href = page$(el).attr('href');
const name = page$(el).text().trim();
tryAddChapter(href, name, arcPrefix);
},
);
}
} else if (novelPath === '/histoires-annexes/') {
// All arcs are on a single page; h2 elements mark each arc section.
let currentArcPrefix = '';
const children = $('div.entry-content').children().toArray();
for (const el of children) {
if (el.type !== 'tag') continue;
const tag = el.tagName.toLowerCase();
if (tag === 'h2') {
// Extract arc number, e.g. "Arc 1 - Memory Snow" or "ARC I" -> "Arc I - "
const h2Text = $(el).text().trim();
const arcMatch = h2Text.match(/arc\s+([ivxlcdm]+|\d+)/i);
currentArcPrefix = arcMatch
? `Arc ${getArcRomanPrefix(arcMatch[1])} - `
: `${h2Text} - `;
} else {
$(el)
.find('a')
.each((_j, a) => {
const href = $(a).attr('href');
const name = $(a).text().trim();
tryAddChapter(href, name, currentArcPrefix);
});
}
}
} else {
$('.entry-content a').each((i, el) => {
const href = $(el).attr('href');
const name = $(el).text().trim();
tryAddChapter(href, name);
});
}

novel.chapters = chapters;
return novel;
}

async parseChapter(chapterPath: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterPath);

// Remove ads, social sharing widgets, like widgets
$(
'div.entry-content .sharedaddy, div.entry-content .wpcnt, div.entry-content #jp-post-flair, div.entry-content div[id^="atatags-"]',
).remove();

const title = $('h1.entry-title').html() || '';
const chapter = $('div.entry-content').html() || '';
return title + chapter;
}

async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
if (pageNo !== 1) return [];

const popularNovels = this.popularNovels(1);

const 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;
}
}

export default new ReZeroWebNovelFrPlugin();
Loading
Loading