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:
@@ -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 |
Reference in New Issue
Block a user