From 53c01d4ccb067bab8d77ed747c268acaeabf6fd1 Mon Sep 17 00:00:00 2001 From: Gourab0002 Date: Sun, 25 Oct 2026 18:42:53 +0530 Subject: [PATCH] feat(english/bronstl): add BronsTL Uses the site's public Laravel JSON API (/api/books, /api/books/{slug}/chapters) for listing, search, novel details and chapters. Co-Authored-By: Grok Bot (AI agent) --- plugins/english/bronstl.ts | 157 ++++++++++++++++++++++++++ public/static/src/en/bronstl/icon.png | Bin 0 -> 1685 bytes 2 files changed, 157 insertions(+) create mode 100644 plugins/english/bronstl.ts create mode 100644 public/static/src/en/bronstl/icon.png diff --git a/plugins/english/bronstl.ts b/plugins/english/bronstl.ts new file mode 100644 index 000000000..e87df50d3 --- /dev/null +++ b/plugins/english/bronstl.ts @@ -0,0 +1,157 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +type Paginated = { + current_page: number; + data: T[]; + next_page_url: string | null; +}; + +type Book = { + title: string; + slug: string; + description: string | null; + author: string | null; + illustrator: string | null; + cover_image: string | null; + status?: { name: string } | null; + tags?: { name?: string }[]; +}; + +type Chapter = { + chapter_title: string; + chapter_slug: string; + chapter_weight: number; + body?: string | null; + created_at: string | null; +}; + +class BronsTLPlugin implements Plugin.PluginBase { + id = 'bronstl'; + name = 'BronsTL'; + icon = 'src/en/bronstl/icon.png'; + site = 'https://bronstl.com'; + version = '1.0.0'; + + private headers = { + 'Accept': 'application/json', + 'Referer': 'https://bronstl.com/', + 'X-Requested-With': 'XMLHttpRequest', + }; + + private async api(path: string): Promise { + const url = path.startsWith('http') ? path : this.site + '/api/' + path; + const r = await fetchApi(url, { headers: this.headers }); + if (!r.ok) { + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + } + return (await r.json()) as T; + } + + private async fetchAll(path: string): Promise { + const items: T[] = []; + let next: string | null = path; + let guard = 0; + while (next && guard++ < 100) { + const page: Paginated = await this.api>(next); + items.push(...page.data); + next = page.next_page_url; + } + return items; + } + + private toItem(book: Book): Plugin.NovelItem { + return { + name: book.title, + path: 'books/' + book.slug, + cover: book.cover_image || defaultCover, + }; + } + + async popularNovels(pageNo: number): Promise { + if (pageNo > 1) return []; + const books = await this.fetchAll('books'); + return books.map(b => this.toItem(b)); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + const term = searchTerm.toLowerCase().trim(); + const books = await this.fetchAll('books'); + return books + .filter( + b => + b.title.toLowerCase().includes(term) || + (b.author || '').toLowerCase().includes(term), + ) + .map(b => this.toItem(b)); + } + + async parseNovel(novelPath: string): Promise { + const slug = novelPath.replace(/^\/?books\//, ''); + const book = await this.api('books/' + slug); + + let status: string = NovelStatus.Unknown; + switch (book.status?.name) { + case 'ongoing': + status = NovelStatus.Ongoing; + break; + case 'finished': + status = NovelStatus.Completed; + break; + case 'dropped': + status = NovelStatus.Cancelled; + break; + } + + const chapters = ( + await this.fetchAll('books/' + slug + '/chapters') + ) + .sort((a, b) => a.chapter_weight - b.chapter_weight) + .map((c, i) => ({ + name: c.chapter_title, + path: 'books/' + slug + '/chapters/' + c.chapter_slug, + releaseTime: c.created_at, + chapterNumber: i + 1, + })); + + return { + path: novelPath, + name: book.title, + cover: book.cover_image || defaultCover, + author: book.author || undefined, + artist: book.illustrator || undefined, + summary: book.description || undefined, + genres: (book.tags || []) + .map(t => t.name) + .filter(Boolean) + .join(', '), + status, + chapters, + }; + } + + async parseChapter(chapterPath: string): Promise { + const chapter = await this.api(chapterPath.replace(/^\//, '')); + const $ = parseHTML(chapter.body || ''); + $('img').each((_, el) => { + const src = $(el).attr('src'); + if (src && src.startsWith('/')) $(el).attr('src', this.site + src); + }); + return $('body').html() || ''; + } + + resolveUrl(path: string): string { + return this.site + '/' + path.replace(/^\//, ''); + } +} + +export default new BronsTLPlugin(); diff --git a/public/static/src/en/bronstl/icon.png b/public/static/src/en/bronstl/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ca5d77e86359f5ddc43d98484f29700f06f212ba GIT binary patch literal 1685 zcmV;G25R|{$Io}8=qLk_a3R-6WfLOD05c8k!1l2%q5&6Atp;hmu6`0C*isryprPQN!3-5daVxyg1 zZmY^db^xP9L_y4e*f_r&Gt_j8S48A1xZ4Cx!c@MSELKW2g}Vk6X%v%e0)oM+1mkD$+?L5@G8%yBPfPMD`sxtXob)jIe+L-71`*s;Zjl zGw1T)yZCc;=^PQmnWL!qmv!EMmK3htYCh^PD zHQ_LO_8ruqDmlI~0O08!-I+XowC%Oh?+>}GreS<%0Kk}0!)?pY{`I%ZN*czO26XG% z*|z-r`HLE4zV!Rf zfTPDMSigCj?X^Mud%LWrVSHu4(PI@%E1FA9O-2 zI7RXH-K^id&9?FZCXFBM$v(hyQxQ(kA{3%@=SEt!d`!2LIDFLv*T! zG-C|Vj4?nn#sJM212kg{(2OxaGsXbT7y~q84A6`*Kr_Yw&3JA_EY*EPxj8v>@79^Z zk;9lWq0sj27%$_ijafYly0LckvW!|2_Ei&f?06;9issn9Dd|;wWdOh*k-rB8< zvu94%l3&P|O;9R*KutKzx=qE}@(cOSfMoB8B|}$!AzvAg>>aVj5DrI}zi5f8pM;F> z0~-5|SSo}<4^z5hqbu9nJ!gPdm^yJRi{`)McpP6fLFr`UmTjEBaLMsF#()IWgu{GS zdcg5G#(*Ta(HR>9l5pmn^ZA900ZF)U!TJ0`#(*TakzdFdkc9j^=QAuC1Cr3O!xN6j zFa{)H@BruY3mF3v5DWwuI>`C_LdJjuOrB8a%I*paxaD}145LLT#G6H~oZxuN0z8>k zTo*3q7rJTz6>dbz1T^FqI%fetxE0OIv~V`MKRquP2ry+rA$xah z(2?A!EC?J1P6PG{mwkqZ@(U@Tr1mJoSws)=G`L5Zi1e%d9`HF19_D7SWhKPEh{eu5 ze^0BN?4lgF=P*xzEx=AKa7{p z$bR5?;3wyFWd*B=HaoVvyppa5h{$Q6H_;(#Rrh!oXtcIpz>2`54+l@H7Y zrUTwj%B%+k90FDWn?=O_IHx)^+JHL|lxhYH23`W51-bx_01u`5tPJNr;3{yIXa{=` fC=rn}sXXL=!8y2xvH}Sq00000NkvXXu0mjfD&7;! literal 0 HcmV?d00001