053aa91b02
Publish Plugins / Publish Plugins (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
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.1';
|
||
|
||
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) {
|
||
// 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<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();
|