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