Skip to content

Commit eb992eb

Browse files
authored
feat: Add Witch Cult Translations (#2115)
* feat: add Witch Cult Translation (WCT) plugin * perf: avoid doing unnecessary requests in Witch Cult Translations (EN) * chore: change summary of WCT novel
1 parent 1774bd8 commit eb992eb

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

plugins/english/wct.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { CheerioAPI, load as parseHTML } from 'cheerio';
2+
import { fetchApi } from '@libs/fetch';
3+
import { NovelStatus } from '@libs/novelStatus';
4+
import { Plugin } from '@/types/plugin';
5+
6+
class WitchCultTranslations implements Plugin.PluginBase {
7+
id = 'witchculttranslations';
8+
name = 'Witch Cult Translations';
9+
site = 'https://witchculttranslation.com';
10+
icon = 'src/en/wct/icon.png';
11+
version = '1.0.0';
12+
13+
private cachedNovel: Plugin.NovelItem | null = null;
14+
15+
private async novel(): Promise<Plugin.NovelItem> {
16+
if (this.cachedNovel !== null) {
17+
return this.cachedNovel;
18+
}
19+
20+
const result = await fetchApi(this.site);
21+
const body = await result.text();
22+
const loadedCheerio = parseHTML(body);
23+
24+
const latestArcCover = loadedCheerio('.entry-content h1 img')
25+
.last()
26+
.attr('src');
27+
28+
this.cachedNovel = {
29+
name: 'Re:Zero kara Hajimeru Isekai Seikatsu',
30+
path: '/table-of-content',
31+
cover: latestArcCover,
32+
};
33+
34+
return this.cachedNovel;
35+
}
36+
37+
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
38+
const novels = [];
39+
if (pageNo === 1) {
40+
novels.push(await this.novel());
41+
}
42+
43+
return novels;
44+
}
45+
46+
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
47+
const novels = [await this.novel()];
48+
49+
const q = this.normalize(searchTerm);
50+
51+
return novels.filter(({ name }) => this.normalize(name).includes(q));
52+
}
53+
54+
private normalize(str: string) {
55+
return str.toLowerCase().replace(/[^a-z0-9]/g, '');
56+
}
57+
58+
async parseChapter(chapterPath: string): Promise<string> {
59+
const result = await fetchApi(this.site + chapterPath);
60+
const body = await result.text();
61+
const loadedCheerio = parseHTML(body);
62+
63+
const title = loadedCheerio('h1.entry-title').text().trim();
64+
const content = loadedCheerio('.entry-content').first();
65+
content
66+
.find('#patreon-snippet, .sharedaddy, .jp-relatedposts, #jp-post-flair')
67+
.remove();
68+
69+
return `<h1>${title}</h1>${content.html() || ''}`;
70+
}
71+
72+
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
73+
const [body, novel] = await Promise.all([
74+
fetchApi(this.site + novelPath).then(result => result.text()),
75+
this.novel(),
76+
]);
77+
78+
const loadedCheerio = parseHTML(body);
79+
80+
return {
81+
...novel,
82+
author: 'Tappei Nagatsuki',
83+
chapters: this.parseChaptersFromTOC(loadedCheerio),
84+
status: NovelStatus.Ongoing,
85+
summary:
86+
'Fan translation of the Re:Zero web novel (Arc 5 onwards).\n\nSuddenly, Natsuki Subaru, a shut-in student, is summoned to another world on his way home from the convenience store. A completely ordinary person with no knowledge, skills, combat abilities, or communication skills, he\'s thrown into this other world without any cheat bonuses and must desperately try to survive. The only blessing he receives is the painful ability to "return by death," which allows him to rewind time after dying! In this other world where he has no one to rely on, how many times will he die, and what will he ultimately gain?',
87+
};
88+
}
89+
90+
private parseChaptersFromTOC(
91+
loadedCheerio: CheerioAPI,
92+
): Plugin.ChapterItem[] {
93+
const chapters: Plugin.ChapterItem[] = [];
94+
let currentArc = 0;
95+
let chapterNumber = 0;
96+
97+
const children = loadedCheerio('.entry-content')
98+
.first()
99+
.children()
100+
.toArray();
101+
102+
for (const el of children) {
103+
if (el.type !== 'tag') continue;
104+
const tag = el.tagName.toLowerCase();
105+
106+
if (tag === 'h1' || tag === 'h2') {
107+
const text = loadedCheerio(el).text().trim();
108+
const arcMatch = text.match(/^Arc\s+(\d+)/i);
109+
if (arcMatch) {
110+
currentArc = parseInt(arcMatch[1], 10);
111+
continue;
112+
}
113+
if (/^Side Content/i.test(text)) {
114+
break;
115+
}
116+
continue;
117+
}
118+
119+
if (tag !== 'ul' || currentArc < 5) continue;
120+
121+
loadedCheerio(el)
122+
.find('li > a')
123+
.each((_, a) => {
124+
const href = loadedCheerio(a).attr('href');
125+
if (!href) return;
126+
127+
const onSite =
128+
/^https?:\/\/(?:www\.)?witchculttranslation\.com\//i.test(href);
129+
if (!onSite) return;
130+
131+
const name = loadedCheerio(a).text().trim();
132+
if (!name) return;
133+
134+
const path = `/${href
135+
.replace(/^https?:\/\/(?:www\.)?witchculttranslation\.com\//i, '')
136+
.replace(/^\/+/, '')}`;
137+
138+
const dateMatch = path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\//);
139+
const releaseTime = dateMatch
140+
? `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`
141+
: null;
142+
143+
chapterNumber += 1;
144+
chapters.push({
145+
name: `Arc ${currentArc}, ${name}`,
146+
path,
147+
releaseTime,
148+
chapterNumber,
149+
});
150+
});
151+
}
152+
153+
return chapters;
154+
}
155+
}
156+
157+
export default new WitchCultTranslations();

public/static/src/en/wct/icon.png

7.89 KB
Loading

0 commit comments

Comments
 (0)