Files
masi 053aa91b02
Publish Plugins / Publish Plugins (push) Has been cancelled
feat: find-novel search across all sources; fix WeTriedTLS apostrophe search
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:31:47 -03:00

141 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();