diff --git a/public/static/src/pt-br/tsundoku/icon.png b/public/static/src/pt-br/tsundoku/icon.png new file mode 100644 index 0000000..ccec63c Binary files /dev/null and b/public/static/src/pt-br/tsundoku/icon.png differ diff --git a/src/plugins/portuguese/tsundoku.ts b/src/plugins/portuguese/tsundoku.ts new file mode 100644 index 0000000..dddd889 --- /dev/null +++ b/src/plugins/portuguese/tsundoku.ts @@ -0,0 +1,294 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@typings/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.0'; + icon = 'src/pt-br/tsundoku/icon.png'; + site = 'https://tsundoku.com.br'; + + parseDate(date: string): string { + const monthMapping: Record = { + 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, + ): Promise { + 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 { + 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 { + 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 { + 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 `

${title}

\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(/
/); + if ( + parts.length > 1 && + parts[parts.length - 1].includes( + 'Agradecemos a todos que leram diretamente aqui', + ) + ) { + parts.pop(); + } + + return `

${title}

\n${parts.join('
')}`; + } + + 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();