feat(english): add WeTriedTLS plugin
Publish Plugins / Publish Plugins (push) Has been cancelled

HeanCMS JSON API source. Paid chapters are labelled and show an unlock
notice instead of content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
masi
2026-09-25 12:26:34 -03:00
parent c8aad99606
commit a504a48e6e
2 changed files with 131 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
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<T> = { data: T[]; meta: { last_page: number } };
const STATUS: Record<string, string> = {
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.0';
async api<T>(path: string): Promise<T> {
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<Plugin.NovelItem[]> {
const res = await this.api<Page<Series>>(
`/query?series_type=Novel&perPage=20&page=${pageNo}&${params}`,
);
return res.data.map(this.toItem);
}
popularNovels(
pageNo: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const order = showLatestNovels ? 'latest' : 'total_views';
return this.query(`orderBy=${order}&order=desc&query_string=`, pageNo);
}
searchNovels(searchTerm: string, pageNo: number) {
return this.query(`query_string=${encodeURIComponent(searchTerm)}`, pageNo);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const s = await this.api<Series>(`/series/${novelPath}`);
const chapters: Plugin.ChapterItem[] = [];
for (let page = 1, last = 1; page <= last; page++) {
const res = await this.api<Page<Chapter>>(
`/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<string> {
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
? `<p>This chapter is paid on WeTriedTLS. Unlock it at <a href="${this.site}/series/${chapterPath}">${this.site}</a> to read it.</p>`
: '<p>Chapter content is unavailable.</p>';
}
return content;
}
resolveUrl = (path: string) => `${this.site}/series/${path}`;
}
export default new WeTriedTLS();
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB