Skip to content

Commit da79bf0

Browse files
authored
Merge branch 'LNReader:master' into progress-tracking
2 parents 0cefbf1 + 5387e09 commit da79bf0

File tree

6 files changed

+304
-10
lines changed

6 files changed

+304
-10
lines changed
8.1 KB
Loading
-1.74 KB
Binary file not shown.
6.2 KB
Loading

scripts/multisrc/lightnovelwp/sources.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
},
1111
{
1212
"id": "kolnovel",
13-
"sourceSite": "https://kolbook.xyz/",
13+
"sourceSite": "https://kolcars.shop/",
1414
"sourceName": "Kol Novel",
1515
"options": {
1616
"lang": "Arabic",
1717
"reverseChapters": true,
1818
"customJs": "let ignore = $('article > style').text().trim().split(',');ignore.push(...(ignore.pop()?.match(/^\\.\\w+/) || []));ignore.map(tag => $(`p${tag}`).remove());",
19-
"versionIncrements": 2
19+
"versionIncrements": 3
2020
}
2121
},
2222
{
@@ -209,5 +209,13 @@
209209
"reverseChapters": true,
210210
"customJs": "\n $('div.entry-content > p').each(function () {\n $(this).text($(this).text().replace(/[\\u1000-\\u105F]/g, function (c) {\n return String.fromCharCode(c.charCodeAt(0) - 4063);\n }));\n });\n"
211211
}
212+
},
213+
{
214+
"id": "betternovels",
215+
"sourceSite": "https://betternovels.net/",
216+
"sourceName": "Better Novels",
217+
"options": {
218+
"lang": "Portuguese"
219+
}
212220
}
213221
]

scripts/multisrc/madara/sources.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,14 +228,6 @@
228228
"lang": "Portuguese"
229229
}
230230
},
231-
{
232-
"id": "e-novel",
233-
"sourceSite": "https://e-novel.online/",
234-
"sourceName": "E-Novel",
235-
"options": {
236-
"lang": "Portuguese"
237-
}
238-
},
239231
{
240232
"id": "panchotranslations",
241233
"sourceSite": "https://panchotranslations.com/",

src/plugins/portuguese/tsundoku.ts

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { CheerioAPI, load as parseHTML } from 'cheerio';
2+
import { fetchApi } from '@libs/fetch';
3+
import { Plugin } from '@typings/plugin';
4+
import { defaultCover } from '@libs/defaultCover';
5+
import dayjs from 'dayjs';
6+
import { Filters, FilterTypes } from '@libs/filterInputs';
7+
8+
class TsundokuPlugin implements Plugin.PluginBase {
9+
id = 'tsundoku';
10+
name = 'Tsundoku Traduções';
11+
version = '1.0.0';
12+
icon = 'src/pt-br/tsundoku/icon.png';
13+
site = 'https://tsundoku.com.br';
14+
15+
parseDate(date: string): string {
16+
const monthMapping: Record<string, number> = {
17+
janeiro: 1,
18+
fevereiro: 2,
19+
marco: 3,
20+
abril: 4,
21+
maio: 5,
22+
junho: 6,
23+
julho: 7,
24+
agosto: 8,
25+
setembro: 9,
26+
outubro: 10,
27+
novembro: 11,
28+
dezembro: 12,
29+
};
30+
const [month, day, year] = date.split(/,?\s+/);
31+
return dayjs(
32+
`${year}-${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]}-${day}`,
33+
).toISOString();
34+
}
35+
36+
parseNovels(loadedCheerio: CheerioAPI) {
37+
const novels: Plugin.NovelItem[] = [];
38+
39+
loadedCheerio('.listupd .bsx').each((idx, ele) => {
40+
const novelName = loadedCheerio(ele).find('.tt').text().trim();
41+
const novelUrl = loadedCheerio(ele).find('a').attr('href');
42+
const coverUrl = loadedCheerio(ele).find('img').attr('src');
43+
if (!novelUrl) return;
44+
45+
const novel = {
46+
name: novelName,
47+
cover: coverUrl || defaultCover,
48+
path: novelUrl.replace(this.site, ''),
49+
};
50+
51+
novels.push(novel);
52+
});
53+
54+
return novels;
55+
}
56+
57+
async popularNovels(
58+
page: number,
59+
{
60+
showLatestNovels,
61+
filters,
62+
}: Plugin.PopularNovelsOptions<typeof this.filters>,
63+
): Promise<Plugin.NovelItem[]> {
64+
const params = new URLSearchParams();
65+
66+
if (page > 1) {
67+
params.append('page', `${page}`);
68+
}
69+
params.append('type', 'novel');
70+
71+
if (showLatestNovels) {
72+
params.append('order', 'latest');
73+
} else if (filters) {
74+
if (filters.genre.value.length) {
75+
filters.genre.value.forEach(value => {
76+
params.append('genre[]', value);
77+
});
78+
}
79+
params.append('order', filters.order.value);
80+
}
81+
82+
const url = `${this.site}/manga/?` + params.toString();
83+
84+
const body = await fetchApi(url).then(result => result.text());
85+
86+
const loadedCheerio = parseHTML(body);
87+
return this.parseNovels(loadedCheerio);
88+
}
89+
90+
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
91+
const body = await fetchApi(this.site + novelPath).then(r => r.text());
92+
93+
const loadedCheerio = parseHTML(body);
94+
95+
const novel: Plugin.SourceNovel = {
96+
path: novelPath,
97+
name: loadedCheerio('h1.entry-title').text() || 'Untitled',
98+
cover: loadedCheerio('.main-info .thumb img').attr('src'),
99+
summary: loadedCheerio('.entry-content.entry-content-single div:eq(0)')
100+
.text()
101+
.trim(),
102+
chapters: [],
103+
};
104+
105+
novel.author = loadedCheerio('.tsinfo .imptdt:contains("Autor")')
106+
.text()
107+
.replace('Autor ', '')
108+
.trim();
109+
110+
novel.artist = loadedCheerio('.tsinfo .imptdt:contains("Artista")')
111+
.text()
112+
.replace('Artista ', '')
113+
.trim();
114+
115+
novel.status = loadedCheerio('.tsinfo .imptdt:contains("Status")')
116+
.text()
117+
.replace('Status ', '')
118+
.trim();
119+
120+
novel.genres = loadedCheerio('.mgen a')
121+
.map((_, ex) => loadedCheerio(ex).text())
122+
.toArray()
123+
.join(',');
124+
125+
const chapters: Plugin.ChapterItem[] = [];
126+
127+
loadedCheerio('#chapterlist ul > li').each((idx, ele) => {
128+
const chapterName = loadedCheerio(ele).find('.chapternum').text().trim();
129+
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
130+
const releaseDate = loadedCheerio(ele).find('.chapterdate').text();
131+
if (!chapterUrl) return;
132+
133+
chapters.push({
134+
name: chapterName,
135+
path: chapterUrl.replace(this.site, ''),
136+
releaseTime: this.parseDate(releaseDate),
137+
});
138+
});
139+
140+
novel.chapters = chapters.reverse().map((c, i) => ({
141+
...c,
142+
name: c.name + ` - Ch. ${i + 1}`,
143+
chapterNumber: i + 1,
144+
}));
145+
return novel;
146+
}
147+
148+
async searchNovels(
149+
searchTerm: string,
150+
pageNo: number,
151+
): Promise<Plugin.NovelItem[]> {
152+
const params = new URLSearchParams();
153+
154+
if (pageNo > 1) {
155+
params.append('page', `${pageNo}`);
156+
}
157+
params.append('type', 'novel');
158+
params.append('title', searchTerm);
159+
160+
const url = `${this.site}/manga/?` + params.toString();
161+
162+
const body = await fetchApi(url).then(result => result.text());
163+
164+
const loadedCheerio = parseHTML(body);
165+
return this.parseNovels(loadedCheerio);
166+
}
167+
168+
async parseChapter(chapterPath: string): Promise<string> {
169+
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
170+
const loadedCheerio = parseHTML(body);
171+
172+
const chapterTitle = loadedCheerio('.headpost .entry-title').text();
173+
const novelTitle = loadedCheerio('.headpost a').text();
174+
const title = chapterTitle
175+
.replace(novelTitle, '')
176+
.replace(/^\W+/, '')
177+
.trim();
178+
179+
const spoilerContent = loadedCheerio(
180+
'#readerarea .collapseomatic_content',
181+
).html();
182+
if (spoilerContent) {
183+
return `<h1>${title}</h1>\n${spoilerContent}`;
184+
}
185+
186+
const $readerarea = loadedCheerio('#readerarea');
187+
$readerarea.find('img.wp-image-15656').remove(); // Remove logo messages
188+
189+
// Remove empty paragraphs
190+
$readerarea.find('p').each((i, el) => {
191+
const $this = loadedCheerio(el);
192+
const $imgs = $this.find('img');
193+
const cleanContent = $this
194+
.text()
195+
?.replace(/\s|&nbsp;/g, '')
196+
?.replace(this.site, '');
197+
198+
// Without images and empty content
199+
if ($imgs?.length === 0 && cleanContent?.length === 0) {
200+
$this.remove();
201+
}
202+
});
203+
204+
const chapterText = $readerarea.html() || '';
205+
const parts = chapterText.split(/<hr ?\/?>/);
206+
if (
207+
parts.length > 1 &&
208+
parts[parts.length - 1].includes(
209+
'Agradecemos a todos que leram diretamente aqui',
210+
)
211+
) {
212+
parts.pop();
213+
}
214+
215+
return `<h1>${title}</h1>\n${parts.join('<hr />')}`;
216+
}
217+
218+
filters = {
219+
order: {
220+
label: 'Ordenar por',
221+
value: '',
222+
options: [
223+
{ label: 'Padrão', value: '' },
224+
{ label: 'A-Z', value: 'title' },
225+
{ label: 'Z-A', value: 'titlereverse' },
226+
{ label: 'Atualizar', value: 'update' },
227+
{ label: 'Adicionar', value: 'latest' },
228+
{ label: 'Popular', value: 'popular' },
229+
],
230+
type: FilterTypes.Picker,
231+
},
232+
genre: {
233+
label: 'Gênero',
234+
value: [],
235+
options: [
236+
{ label: 'Ação', value: '328' },
237+
{ label: 'Adult', value: '343' },
238+
{ label: 'Anatomia', value: '408' },
239+
{ label: 'Artes Marciais', value: '340' },
240+
{ label: 'Aventura', value: '315' },
241+
{ label: 'Ciência', value: '398' },
242+
{ label: 'Comédia', value: '322' },
243+
{ label: 'Comédia Romântica', value: '378' },
244+
{ label: 'Cotidiano', value: '399' },
245+
{ label: 'Drama', value: '311' },
246+
{ label: 'Ecchi', value: '329' },
247+
{ label: 'Fantasia', value: '316' },
248+
{ label: 'Feminismo', value: '362' },
249+
{ label: 'Gender Bender', value: '417' },
250+
{ label: 'Guerra', value: '368' },
251+
{ label: 'Harém', value: '350' },
252+
{ label: 'Hentai', value: '344' },
253+
{ label: 'História', value: '400' },
254+
{ label: 'Histórico', value: '380' },
255+
{ label: 'Horror', value: '317' },
256+
{ label: 'Humor Negro', value: '363' },
257+
{ label: 'Isekai', value: '318' },
258+
{ label: 'Josei', value: '356' },
259+
{ label: 'Joshikousei', value: '364' },
260+
{ label: 'LitRPG', value: '387' },
261+
{ label: 'Maduro', value: '351' },
262+
{ label: 'Mágia', value: '372' },
263+
{ label: 'Mecha', value: '335' },
264+
{ label: 'Militar', value: '414' },
265+
{ label: 'Mistério', value: '319' },
266+
{ label: 'Otaku', value: '365' },
267+
{ label: 'Psicológico', value: '320' },
268+
{ label: 'Reencarnação', value: '358' },
269+
{ label: 'Romance', value: '312' },
270+
{ label: 'RPG', value: '366' },
271+
{ label: 'Sátira', value: '367' },
272+
{ label: 'Sci-fi', value: '371' },
273+
{ label: 'Seinen', value: '326' },
274+
{ label: 'Sexo Explícito', value: '345' },
275+
{ label: 'Shoujo', value: '323' },
276+
{ label: 'Shounen', value: '341' },
277+
{ label: 'Slice-of-Life', value: '324' },
278+
{ label: 'Sobrenatural', value: '359' },
279+
{ label: 'Supernatural', value: '401' },
280+
{ label: 'Suspense', value: '407' },
281+
{ label: 'Thriller', value: '410' },
282+
{ label: 'Tragédia', value: '352' },
283+
{ label: 'Vida Escolar', value: '331' },
284+
{ label: 'Webtoon', value: '381' },
285+
{ label: 'Xianxia', value: '357' },
286+
{ label: 'Xuanhuan', value: '395' },
287+
{ label: 'Yuri', value: '313' },
288+
],
289+
type: FilterTypes.CheckboxGroup,
290+
},
291+
} satisfies Filters;
292+
}
293+
294+
export default new TsundokuPlugin();

0 commit comments

Comments
 (0)