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 000000000..ca5d77e86 Binary files /dev/null and b/public/static/src/en/bronstl/icon.png differ