From f6ae42a5d15222d6ff5e6d6da5ac8939a2775489 Mon Sep 17 00:00:00 2001 From: Edgard Lorraine Messias Date: Thu, 18 Jul 2024 10:43:19 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20Tsundoku=20Tradu=C3=A7=C3=B5es=20?= =?UTF-8?q?(#1067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/static/src/pt-br/tsundoku/icon.png | Bin 0 -> 6347 bytes src/plugins/portuguese/tsundoku.ts | 294 ++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 public/static/src/pt-br/tsundoku/icon.png create mode 100644 src/plugins/portuguese/tsundoku.ts 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 0000000000000000000000000000000000000000..ccec63ce649b14907fd72f6e1903156b9025c717 GIT binary patch literal 6347 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 N002ovPDHLkV1ju~{yP8w literal 0 HcmV?d00001 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();