import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { defaultCover } from '@libs/defaultCover'; import { NovelStatus } from '@libs/novelStatus'; // WeTriedTLS runs HeanCMS; everything comes from its JSON API. const API = 'https://api.wetriedtls.com'; type Series = { id: number; title: string; series_slug: string; thumbnail?: string; description?: string; author?: string; studio?: string; status?: string; alternative_names?: string; tags?: { name: string }[]; }; type Chapter = { chapter_name: string; chapter_title?: string | null; chapter_slug: string; index?: string; price: number; created_at?: string; }; type Page = { data: T[]; meta: { last_page: number } }; const STATUS: Record = { Ongoing: NovelStatus.Ongoing, Completed: NovelStatus.Completed, Hiatus: NovelStatus.OnHiatus, Dropped: NovelStatus.Cancelled, }; class WeTriedTLS implements Plugin.PluginBase { id = 'wetriedtls'; name = 'WeTriedTLS'; icon = 'src/en/wetriedtls/icon.png'; site = 'https://wetriedtls.com'; version = '1.0.1'; async api(path: string): Promise { const res = await fetchApi(API + path, { headers: { Referer: this.site } }); if (!res.ok) throw new Error(`WeTriedTLS API ${res.status} for ${path}`); return res.json(); } toItem = (s: Series): Plugin.NovelItem => ({ name: s.title, path: s.series_slug, cover: s.thumbnail || defaultCover, }); async query(params: string, pageNo: number): Promise { const res = await this.api>( `/query?series_type=Novel&perPage=20&page=${pageNo}&${params}`, ); return res.data.map(this.toItem); } popularNovels( pageNo: number, { showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { const order = showLatestNovels ? 'latest' : 'total_views'; return this.query(`orderBy=${order}&order=desc&query_string=`, pageNo); } searchNovels(searchTerm: string, pageNo: number) { // The API matches literally and titles use curly apostrophes, so search // with the longest apostrophe-free run of words. const query = searchTerm .split(/\S*['’]\S*/) .map(part => part.trim()) .reduce((a, b) => (b.length > a.length ? b : a), ''); return this.query( `query_string=${encodeURIComponent(query || searchTerm)}`, pageNo, ); } async parseNovel(novelPath: string): Promise { const s = await this.api(`/series/${novelPath}`); const chapters: Plugin.ChapterItem[] = []; for (let page = 1, last = 1; page <= last; page++) { const res = await this.api>( `/chapters/${s.id}?page=${page}&perPage=1000`, ); last = res.meta.last_page; for (const c of res.data) { const title = c.chapter_title ? `${c.chapter_name} - ${c.chapter_title}` : c.chapter_name; chapters.push({ // Paid chapters stay paid: they are labelled and parseChapter shows a notice. name: c.price > 0 ? `🔒 ${title}` : title, path: `${novelPath}/${c.chapter_slug}`, releaseTime: c.created_at, chapterNumber: c.index ? Number(c.index) : undefined, }); } } chapters.reverse(); // API lists newest first return { path: novelPath, name: s.title, cover: s.thumbnail || defaultCover, author: s.author, artist: s.studio || undefined, summary: s.description?.replace(/<[^>]+>/g, '').trim(), genres: s.tags?.map(t => t.name).join(', '), status: (s.status && STATUS[s.status]) || NovelStatus.Unknown, chapters, }; } async parseChapter(chapterPath: string): Promise { const res = await this.api<{ chapter: { chapter_content?: string; price: number }; }>(`/chapter/${chapterPath}`); const content = res.chapter.chapter_content; if (!content) { return res.chapter.price > 0 ? `

This chapter is paid on WeTriedTLS. Unlock it at ${this.site} to read it.

` : '

Chapter content is unavailable.

'; } return content; } resolveUrl = (path: string) => `${this.site}/series/${path}`; } export default new WeTriedTLS();