refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc * feat: Overhaul Playground UI * feat: Add Filters to New UI * feat: Persist Selected Plugin * feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts [skip ci] * feat: UX Improvements * chore: Update Plugin Path in Publish Workflow * fix: Add Backward Compatibility * chore: Update Plugin Publishing Script
This commit is contained in:
committed by
GitHub
parent
252beff0bf
commit
20c2b75b01
@@ -0,0 +1,258 @@
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { Filters } from '@libs/filterInputs';
|
||||
|
||||
class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
||||
id = 'blogdoamonnovels';
|
||||
name = 'Blog do Amon Novels';
|
||||
version = '1.0.0';
|
||||
icon = 'src/pt-br/blogdoamonnovels/icon.png';
|
||||
site = 'https://www.blogdoamonnovels.com';
|
||||
|
||||
parseNovels(json: string) {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const result = JSON.parse(json);
|
||||
|
||||
if (!('entry' in result.feed)) {
|
||||
return novels;
|
||||
}
|
||||
|
||||
result.feed.entry.forEach((n: any) => {
|
||||
const novelName: string = n.title.$t;
|
||||
|
||||
const novelUrl = n.link.find(
|
||||
(t: { rel: string }) => 'alternate' == t.rel,
|
||||
).href;
|
||||
if (!novelUrl) return;
|
||||
|
||||
const coverUrl = n.media$thumbnail.url.replace('/s72-c/', '/w340/');
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: coverUrl || defaultCover,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
if (showLatestNovels) {
|
||||
return this.searchNovels('', pageNo);
|
||||
}
|
||||
|
||||
if (pageNo > 1) {
|
||||
return [];
|
||||
}
|
||||
const body = await fetchApi(this.site).then(result => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio('.PopularPosts article').each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find('h3 a').text().trim();
|
||||
const novelUrl = loadedCheerio(ele).find('h3 a').attr('href');
|
||||
const coverUrl = loadedCheerio(ele).find('img').attr('src');
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: coverUrl || defaultCover,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then(r => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio('[itemprop="name"]').text() || 'Untitled',
|
||||
cover: loadedCheerio('img[itemprop="image"]').attr('src'),
|
||||
summary: loadedCheerio('#synopsis')
|
||||
.find('br')
|
||||
.replaceWith('\n')
|
||||
.end()
|
||||
.text()
|
||||
.trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.author = loadedCheerio('#extra-info dl:contains("Autor") dd')
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.artist = loadedCheerio('#extra-info dl:contains("Artista") dd')
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.status = loadedCheerio('[data-status]').text().trim();
|
||||
|
||||
novel.genres = loadedCheerio('dt:contains("Gênero:")')
|
||||
.parent()
|
||||
.find('a')
|
||||
.map((_, ex) => loadedCheerio(ex).text().trim())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
const cat = loadedCheerio('#clwd').text().split("'")[1];
|
||||
|
||||
if (!cat) {
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio('#chapters chapter').each((idx, ele) => {
|
||||
const chapterName = loadedCheerio(ele).find('a').text().trim();
|
||||
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse().map((c, i) => ({
|
||||
...c,
|
||||
name: c.name + ` - Ch. ${i + 1}`,
|
||||
chapterNumber: i + 1,
|
||||
}));
|
||||
|
||||
if (!novel.summary) {
|
||||
const $summary = loadedCheerio('#chapters');
|
||||
$summary.find('h3').remove();
|
||||
$summary.find('div.flex').remove();
|
||||
$summary.find('div.separator').remove();
|
||||
$summary.find('#custom-hero').remove();
|
||||
$summary.find('[id=listItem]').remove();
|
||||
novel.summary = $summary.text().trim();
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
const maxResults = 150;
|
||||
let startIndex = 1;
|
||||
let length = 0;
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
do {
|
||||
const jsonUrl = `${this.site}/feeds/posts/default/-/${cat}?alt=json&start-index=${startIndex}&max-results=${maxResults}`;
|
||||
const bodyResponse = await fetchApi(jsonUrl).then(result =>
|
||||
result.text(),
|
||||
);
|
||||
|
||||
const result = JSON.parse(bodyResponse);
|
||||
|
||||
if (!('entry' in result.feed)) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.feed.entry.forEach((n: any) => {
|
||||
let chapterName: string = n.title.$t;
|
||||
|
||||
// Skip self
|
||||
if (chapterName === novel.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const chapterNumber: number = parseFloat(chapterName.split(' ', 2)[1]);
|
||||
const chapterUrl = n.link.find(
|
||||
(t: { rel: string }) => 'alternate' == t.rel,
|
||||
).href;
|
||||
const releaseTime = new Date(n.updated.$t);
|
||||
if (!chapterUrl) return;
|
||||
|
||||
if (n.content && n.content.$t) {
|
||||
try {
|
||||
const dom = parseHTML(n.content.$t);
|
||||
chapterName = dom('.conteudo_teste center h1').text().trim();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
releaseTime: releaseTime.toISOString(),
|
||||
// chapterNumber: chapterNumber,
|
||||
});
|
||||
});
|
||||
|
||||
length = result.feed.entry.length;
|
||||
startIndex += maxResults;
|
||||
} while (length >= maxResults);
|
||||
|
||||
novel.chapters = chapters.reverse().map((c, i) => ({
|
||||
...c,
|
||||
name: c.name + ` - Ch. ${i + 1}`,
|
||||
chapterNumber: i + 1,
|
||||
}));
|
||||
return novel;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const params = new URLSearchParams();
|
||||
const maxResults = 10;
|
||||
|
||||
params.append('alt', 'json');
|
||||
if (pageNo > 1) {
|
||||
params.append('start-index', `${(pageNo - 1) * maxResults + 1}`);
|
||||
}
|
||||
params.append('max-results', `${maxResults}`);
|
||||
params.append('q', `label:Series ${searchTerm}`.trim());
|
||||
|
||||
const jsonUrl = `${this.site}/feeds/posts/summary?` + params.toString();
|
||||
const json = await fetchApi(jsonUrl).then(result => result.text());
|
||||
|
||||
return this.parseNovels(json);
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const $readerarea = loadedCheerio('.conteudo_teste');
|
||||
|
||||
// Remove empty paragraphs
|
||||
$readerarea.find('p').each((i, el) => {
|
||||
const $this = loadedCheerio(el);
|
||||
const $imgs = $this.find('img');
|
||||
const cleanContent = $this
|
||||
.text()
|
||||
?.replace(/\s| /g, '')
|
||||
?.replace(this.site, '');
|
||||
|
||||
// Without images and empty content
|
||||
if ($imgs?.length === 0 && cleanContent?.length === 0) {
|
||||
$this.remove();
|
||||
}
|
||||
});
|
||||
|
||||
return $readerarea.html() || '';
|
||||
}
|
||||
|
||||
filters = {} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new BlogDoAnonNovelsPlugin();
|
||||
@@ -0,0 +1,228 @@
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { load as parseHTML } from 'cheerio';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
class NovelMania implements Plugin.PluginBase {
|
||||
id = 'novelmania.com.br';
|
||||
name = 'Novel Mania';
|
||||
icon = 'src/pt-br/novelmania/icon.png';
|
||||
site = 'https://novelmania.com.br';
|
||||
version = '1.0.1';
|
||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = `${this.site}/novels?titulo=`;
|
||||
url += `&categoria=${filters?.genres.value}`;
|
||||
url += `&status=${filters?.status.value}`;
|
||||
url += `&nacionalidade=${filters?.type.value}`;
|
||||
url += `&ordem=${filters?.ordem.value}`;
|
||||
url += `&page%5Bpage%5D=${pageNo}`;
|
||||
|
||||
const body = await fetchApi(url).then(res => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2');
|
||||
|
||||
const novels: Plugin.NovelItem[] = load
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find('a.novel-title > h5').text(),
|
||||
cover: loadedCheerio(element)
|
||||
.find('a > div.card.c-size-1.border > img.card-image')
|
||||
.attr('src'),
|
||||
path: loadedCheerio(element).find('a.novel-title').attr('href') || '',
|
||||
}))
|
||||
.get()
|
||||
.filter(novel => novel.name && novel.path);
|
||||
return novels;
|
||||
}
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name:
|
||||
loadedCheerio(
|
||||
'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1',
|
||||
)
|
||||
.text()
|
||||
.trim() || 'Sem título',
|
||||
};
|
||||
|
||||
loadedCheerio('b').remove();
|
||||
|
||||
novel.name =
|
||||
loadedCheerio(
|
||||
'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1',
|
||||
)
|
||||
.text()
|
||||
.trim() || 'Sem título';
|
||||
novel.summary =
|
||||
loadedCheerio('div.tab-pane.fade.show.active > div.text > p')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join('\n\n')
|
||||
.trim() || '';
|
||||
novel.cover =
|
||||
loadedCheerio('div.novel-img > img.img-responsive').attr('src') ||
|
||||
defaultCover;
|
||||
novel.author = loadedCheerio('div.novel-info > span.authors.mb-1')
|
||||
.text()
|
||||
.trim();
|
||||
novel.genres = loadedCheerio('div.tags > ul.list-tags.mb-0 > li > a')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
const status = loadedCheerio('div.novel-info > span.authors.mb-3')
|
||||
.text()
|
||||
.trim();
|
||||
switch (status) {
|
||||
case 'Ativo':
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case 'Pausado':
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
case 'Completo':
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(
|
||||
'div.accordion.capitulo > div.card > div.collapse > div.card-body.p-0 > ol > li',
|
||||
).each((i, el) => {
|
||||
const chapterName = `${loadedCheerio(el).find('a > span.sub-vol').text().trim()} - ${loadedCheerio(el).find('a > strong').text().trim()}`;
|
||||
const chapterPath = loadedCheerio(el).find('a').attr('href');
|
||||
if (chapterPath) chapters.push({ name: chapterName, path: chapterPath });
|
||||
});
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const response = await fetchApi(`${this.site}${chapterPath}`).then(res =>
|
||||
res.text(),
|
||||
);
|
||||
const loadedCheerio = parseHTML(response);
|
||||
return loadedCheerio('div#chapter-content').html() || '';
|
||||
}
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/novels?titulo=${searchTerm}&page%5Bpage%5D=${pageNo}`;
|
||||
const body = await fetchApi(url).then(res => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2');
|
||||
|
||||
const novels: Plugin.NovelItem[] = load
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find('a.novel-title > h5').text(),
|
||||
cover: loadedCheerio(element)
|
||||
.find('a > div.card.c-size-1.border > img.card-image')
|
||||
.attr('src'),
|
||||
path: loadedCheerio(element).find('a.novel-title').attr('href') || '',
|
||||
}))
|
||||
.get()
|
||||
.filter(novel => novel.name && novel.path);
|
||||
return novels;
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||
this.site + (isNovel ? '/book/' : '/chapter/') + path;
|
||||
|
||||
filters = {
|
||||
genres: {
|
||||
value: '',
|
||||
label: 'Gêneros',
|
||||
options: [
|
||||
{ label: 'Todos', value: '' },
|
||||
{ label: 'Ação', value: '01' },
|
||||
{ label: 'Adulto', value: '02' },
|
||||
{ label: 'Artes Marciais', value: '07' },
|
||||
{ label: 'Aventura', value: '03' },
|
||||
{ label: 'Comédia', value: '04' },
|
||||
{ label: 'Cotidiano', value: '16' },
|
||||
{ label: 'Drama', value: '23' },
|
||||
{ label: 'Ecchi', value: '27' },
|
||||
{ label: 'Erótico', value: '22' },
|
||||
{ label: 'Escolar', value: '13' },
|
||||
{ label: 'Fantasia', value: '05' },
|
||||
{ label: 'Harém', value: '21' },
|
||||
{ label: 'Isekai', value: '30' },
|
||||
{ label: 'Magia', value: '26' },
|
||||
{ label: 'Mecha', value: '08' },
|
||||
{ label: 'Medieval', value: '31' },
|
||||
{ label: 'Militar', value: '24' },
|
||||
{ label: 'Mistério', value: '09' },
|
||||
{ label: 'Mitologia', value: '10' },
|
||||
{ label: 'Psicológico', value: '11' },
|
||||
{ label: 'Realidade Virtual', value: '36' },
|
||||
{ label: 'Romance', value: '12' },
|
||||
{ label: 'Sci-fi', value: '14' },
|
||||
{ label: 'Sistema de Jogo', value: '15' },
|
||||
{ label: 'Sobrenatural', value: '17' },
|
||||
{ label: 'Suspense', value: '29' },
|
||||
{ label: 'Terror', value: '06' },
|
||||
{ label: 'Wuxia', value: '18' },
|
||||
{ label: 'Xianxia', value: '19' },
|
||||
{ label: 'Xuanhuan', value: '20' },
|
||||
{ label: 'Yaoi', value: '35' },
|
||||
{ label: 'Yuri', value: '37' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'Todos', value: '' },
|
||||
{ label: 'Ativo', value: 'ativo' },
|
||||
{ label: 'Completo', value: 'Completo' },
|
||||
{ label: 'Pausado', value: 'pausado' },
|
||||
{ label: 'Parado', value: 'Parado' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
label: 'Type',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'Todas', value: '' },
|
||||
{ label: 'Americana', value: 'americana' },
|
||||
{ label: 'Angolana', value: 'angolana' },
|
||||
{ label: 'Brasileira', value: 'brasileira' },
|
||||
{ label: 'Chinesa', value: 'chinesa' },
|
||||
{ label: 'Coreana', value: 'coreana' },
|
||||
{ label: 'Japonesa', value: 'japonesa' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
ordem: {
|
||||
label: 'Ordenar',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'Qualquer ordem', value: '' },
|
||||
{ label: 'Ordem alfabética', value: '1' },
|
||||
{ label: 'Nº de Capítulos', value: '2' },
|
||||
{ label: 'Popularidade', value: '3' },
|
||||
{ label: 'Novidades', value: '4' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelMania();
|
||||
@@ -0,0 +1,290 @@
|
||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import dayjs from 'dayjs';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
|
||||
class TsundokuPlugin implements Plugin.PluginBase {
|
||||
id = 'tsundoku';
|
||||
name = 'Tsundoku Traduções';
|
||||
version = '1.0.1';
|
||||
icon = 'src/pt-br/tsundoku/icon.png';
|
||||
site = 'https://tsundoku.com.br';
|
||||
|
||||
parseDate(date: string): string {
|
||||
const monthMapping: Record<string, number> = {
|
||||
janeiro: 1,
|
||||
fevereiro: 2,
|
||||
marco: 3,
|
||||
abril: 4,
|
||||
maio: 5,
|
||||
junho: 6,
|
||||
julho: 7,
|
||||
agosto: 8,
|
||||
setembro: 9,
|
||||
outubro: 10,
|
||||
novembro: 11,
|
||||
dezembro: 12,
|
||||
};
|
||||
const [month, day, year] = date.split(/,?\s+/);
|
||||
return dayjs(
|
||||
`${year}-${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]}-${day}`,
|
||||
).toISOString();
|
||||
}
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio('.listupd .bsx').each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find('.tt').text().trim();
|
||||
const novelUrl = loadedCheerio(ele).find('a').attr('href');
|
||||
const coverUrl = loadedCheerio(ele).find('img').attr('src');
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: coverUrl || defaultCover,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (page > 1) {
|
||||
params.append('page', `${page}`);
|
||||
}
|
||||
params.append('type', 'novel');
|
||||
|
||||
if (showLatestNovels) {
|
||||
params.append('order', 'latest');
|
||||
} else if (filters) {
|
||||
if (filters.genre.value.length) {
|
||||
filters.genre.value.forEach(value => {
|
||||
params.append('genre[]', value);
|
||||
});
|
||||
}
|
||||
params.append('order', filters.order.value);
|
||||
}
|
||||
|
||||
const url = `${this.site}/manga/?` + params.toString();
|
||||
|
||||
const body = await fetchApi(url).then(result => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then(r => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio('h1.entry-title').text() || 'Untitled',
|
||||
cover: loadedCheerio('.main-info .thumb img').attr('src'),
|
||||
summary: loadedCheerio('.entry-content.entry-content-single div:eq(0)')
|
||||
.text()
|
||||
.trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.author = loadedCheerio('.tsinfo .imptdt:contains("Autor")')
|
||||
.text()
|
||||
.replace('Autor ', '')
|
||||
.trim();
|
||||
|
||||
novel.artist = loadedCheerio('.tsinfo .imptdt:contains("Artista")')
|
||||
.text()
|
||||
.replace('Artista ', '')
|
||||
.trim();
|
||||
|
||||
novel.status = loadedCheerio('.tsinfo .imptdt:contains("Status")')
|
||||
.text()
|
||||
.replace('Status ', '')
|
||||
.trim();
|
||||
|
||||
novel.genres = loadedCheerio('.mgen a')
|
||||
.map((_, ex) => loadedCheerio(ex).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio('#chapterlist ul > li').each((idx, ele) => {
|
||||
const chapterName = loadedCheerio(ele).find('.chapternum').text().trim();
|
||||
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
|
||||
const releaseDate = loadedCheerio(ele).find('.chapterdate').text();
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
releaseTime: this.parseDate(releaseDate),
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse().map((c, i) => ({
|
||||
...c,
|
||||
name: c.name + ` - Ch. ${i + 1}`,
|
||||
chapterNumber: i + 1,
|
||||
}));
|
||||
return novel;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (pageNo > 1) {
|
||||
params.append('page', `${pageNo}`);
|
||||
}
|
||||
params.append('type', 'novel');
|
||||
params.append('title', searchTerm);
|
||||
|
||||
const url = `${this.site}/manga/?` + params.toString();
|
||||
|
||||
const body = await fetchApi(url).then(result => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterTitle = loadedCheerio('.headpost .entry-title').text();
|
||||
const novelTitle = loadedCheerio('.headpost a').text();
|
||||
const title = chapterTitle
|
||||
.replace(novelTitle, '')
|
||||
.replace(/^\W+/, '')
|
||||
.trim();
|
||||
|
||||
const spoilerContent = loadedCheerio(
|
||||
'#readerarea .collapseomatic_content',
|
||||
).html();
|
||||
if (spoilerContent) {
|
||||
return `<h1>${title}</h1>\n${spoilerContent}`;
|
||||
}
|
||||
|
||||
const $readerarea = loadedCheerio('#readerarea');
|
||||
$readerarea.find('img.wp-image-15656').remove(); // Remove logo messages
|
||||
|
||||
// Remove empty paragraphs
|
||||
$readerarea.find('p').each((i, el) => {
|
||||
const $this = loadedCheerio(el);
|
||||
const $imgs = $this.find('img');
|
||||
const cleanContent = $this
|
||||
.text()
|
||||
?.replace(/\s| /g, '')
|
||||
?.replace(this.site, '');
|
||||
|
||||
// Without images and empty content
|
||||
if ($imgs?.length === 0 && cleanContent?.length === 0) {
|
||||
$this.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const chapterText = $readerarea.html() || '';
|
||||
const parts = chapterText.split(/<hr ?\/?>/);
|
||||
const lastPart = parts[parts.length - 1];
|
||||
if (parts.length > 1 && lastPart.includes('https://discord')) {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
return `<h1>${title}</h1>\n${parts.join('<hr />')}`;
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
label: 'Ordenar por',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'Padrão', value: '' },
|
||||
{ label: 'A-Z', value: 'title' },
|
||||
{ label: 'Z-A', value: 'titlereverse' },
|
||||
{ label: 'Atualizar', value: 'update' },
|
||||
{ label: 'Adicionar', value: 'latest' },
|
||||
{ label: 'Popular', value: 'popular' },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
label: 'Gênero',
|
||||
value: [],
|
||||
options: [
|
||||
{ label: 'Ação', value: '328' },
|
||||
{ label: 'Adult', value: '343' },
|
||||
{ label: 'Anatomia', value: '408' },
|
||||
{ label: 'Artes Marciais', value: '340' },
|
||||
{ label: 'Aventura', value: '315' },
|
||||
{ label: 'Ciência', value: '398' },
|
||||
{ label: 'Comédia', value: '322' },
|
||||
{ label: 'Comédia Romântica', value: '378' },
|
||||
{ label: 'Cotidiano', value: '399' },
|
||||
{ label: 'Drama', value: '311' },
|
||||
{ label: 'Ecchi', value: '329' },
|
||||
{ label: 'Fantasia', value: '316' },
|
||||
{ label: 'Feminismo', value: '362' },
|
||||
{ label: 'Gender Bender', value: '417' },
|
||||
{ label: 'Guerra', value: '368' },
|
||||
{ label: 'Harém', value: '350' },
|
||||
{ label: 'Hentai', value: '344' },
|
||||
{ label: 'História', value: '400' },
|
||||
{ label: 'Histórico', value: '380' },
|
||||
{ label: 'Horror', value: '317' },
|
||||
{ label: 'Humor Negro', value: '363' },
|
||||
{ label: 'Isekai', value: '318' },
|
||||
{ label: 'Josei', value: '356' },
|
||||
{ label: 'Joshikousei', value: '364' },
|
||||
{ label: 'LitRPG', value: '387' },
|
||||
{ label: 'Maduro', value: '351' },
|
||||
{ label: 'Mágia', value: '372' },
|
||||
{ label: 'Mecha', value: '335' },
|
||||
{ label: 'Militar', value: '414' },
|
||||
{ label: 'Mistério', value: '319' },
|
||||
{ label: 'Otaku', value: '365' },
|
||||
{ label: 'Psicológico', value: '320' },
|
||||
{ label: 'Reencarnação', value: '358' },
|
||||
{ label: 'Romance', value: '312' },
|
||||
{ label: 'RPG', value: '366' },
|
||||
{ label: 'Sátira', value: '367' },
|
||||
{ label: 'Sci-fi', value: '371' },
|
||||
{ label: 'Seinen', value: '326' },
|
||||
{ label: 'Sexo Explícito', value: '345' },
|
||||
{ label: 'Shoujo', value: '323' },
|
||||
{ label: 'Shounen', value: '341' },
|
||||
{ label: 'Slice-of-Life', value: '324' },
|
||||
{ label: 'Sobrenatural', value: '359' },
|
||||
{ label: 'Supernatural', value: '401' },
|
||||
{ label: 'Suspense', value: '407' },
|
||||
{ label: 'Thriller', value: '410' },
|
||||
{ label: 'Tragédia', value: '352' },
|
||||
{ label: 'Vida Escolar', value: '331' },
|
||||
{ label: 'Webtoon', value: '381' },
|
||||
{ label: 'Xianxia', value: '357' },
|
||||
{ label: 'Xuanhuan', value: '395' },
|
||||
{ label: 'Yuri', value: '313' },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new TsundokuPlugin();
|
||||
Reference in New Issue
Block a user