From eb992eb988e75afb9dc56717e21cb388e124e27a Mon Sep 17 00:00:00 2001 From: Aran Leite Date: Mon, 13 Apr 2026 22:28:51 -0300 Subject: [PATCH] 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 --- plugins/english/wct.ts | 157 ++++++++++++++++++++++++++++++ public/static/src/en/wct/icon.png | Bin 0 -> 8079 bytes 2 files changed, 157 insertions(+) create mode 100644 plugins/english/wct.ts create mode 100644 public/static/src/en/wct/icon.png diff --git a/plugins/english/wct.ts b/plugins/english/wct.ts new file mode 100644 index 0000000..b390666 --- /dev/null +++ b/plugins/english/wct.ts @@ -0,0 +1,157 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; + +class WitchCultTranslations implements Plugin.PluginBase { + id = 'witchculttranslations'; + name = 'Witch Cult Translations'; + site = 'https://witchculttranslation.com'; + icon = 'src/en/wct/icon.png'; + version = '1.0.0'; + + private cachedNovel: Plugin.NovelItem | null = null; + + private async novel(): Promise { + if (this.cachedNovel !== null) { + return this.cachedNovel; + } + + const result = await fetchApi(this.site); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const latestArcCover = loadedCheerio('.entry-content h1 img') + .last() + .attr('src'); + + this.cachedNovel = { + name: 'Re:Zero kara Hajimeru Isekai Seikatsu', + path: '/table-of-content', + cover: latestArcCover, + }; + + return this.cachedNovel; + } + + async popularNovels(pageNo: number): Promise { + const novels = []; + if (pageNo === 1) { + novels.push(await this.novel()); + } + + return novels; + } + + async searchNovels(searchTerm: string): Promise { + const novels = [await this.novel()]; + + const q = this.normalize(searchTerm); + + return novels.filter(({ name }) => this.normalize(name).includes(q)); + } + + private normalize(str: string) { + return str.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const title = loadedCheerio('h1.entry-title').text().trim(); + const content = loadedCheerio('.entry-content').first(); + content + .find('#patreon-snippet, .sharedaddy, .jp-relatedposts, #jp-post-flair') + .remove(); + + return `

${title}

${content.html() || ''}`; + } + + async parseNovel(novelPath: string): Promise { + const [body, novel] = await Promise.all([ + fetchApi(this.site + novelPath).then(result => result.text()), + this.novel(), + ]); + + const loadedCheerio = parseHTML(body); + + return { + ...novel, + author: 'Tappei Nagatsuki', + chapters: this.parseChaptersFromTOC(loadedCheerio), + status: NovelStatus.Ongoing, + summary: + '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?', + }; + } + + private parseChaptersFromTOC( + loadedCheerio: CheerioAPI, + ): Plugin.ChapterItem[] { + const chapters: Plugin.ChapterItem[] = []; + let currentArc = 0; + let chapterNumber = 0; + + const children = loadedCheerio('.entry-content') + .first() + .children() + .toArray(); + + for (const el of children) { + if (el.type !== 'tag') continue; + const tag = el.tagName.toLowerCase(); + + if (tag === 'h1' || tag === 'h2') { + const text = loadedCheerio(el).text().trim(); + const arcMatch = text.match(/^Arc\s+(\d+)/i); + if (arcMatch) { + currentArc = parseInt(arcMatch[1], 10); + continue; + } + if (/^Side Content/i.test(text)) { + break; + } + continue; + } + + if (tag !== 'ul' || currentArc < 5) continue; + + loadedCheerio(el) + .find('li > a') + .each((_, a) => { + const href = loadedCheerio(a).attr('href'); + if (!href) return; + + const onSite = + /^https?:\/\/(?:www\.)?witchculttranslation\.com\//i.test(href); + if (!onSite) return; + + const name = loadedCheerio(a).text().trim(); + if (!name) return; + + const path = `/${href + .replace(/^https?:\/\/(?:www\.)?witchculttranslation\.com\//i, '') + .replace(/^\/+/, '')}`; + + const dateMatch = path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\//); + const releaseTime = dateMatch + ? `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}` + : null; + + chapterNumber += 1; + chapters.push({ + name: `Arc ${currentArc}, ${name}`, + path, + releaseTime, + chapterNumber, + }); + }); + } + + return chapters; + } +} + +export default new WitchCultTranslations(); diff --git a/public/static/src/en/wct/icon.png b/public/static/src/en/wct/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3d9ceb7af1dba99af69f18e4644bda1349c03b GIT binary patch literal 8079 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