[Syosetu] Extract all web novel chapters listed in multiple pages (#1346)
* [Syosetu] Extract all chapters * [Syosetu] delete duplicated import * [Syosetu]: bump plugin version to 1.1.1
This commit is contained in:
@@ -13,7 +13,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
icon = 'src/jp/syosetu/icon.png';
|
icon = 'src/jp/syosetu/icon.png';
|
||||||
site = 'https://yomou.syosetu.com/';
|
site = 'https://yomou.syosetu.com/';
|
||||||
novelPrefix = 'https://ncode.syosetu.com';
|
novelPrefix = 'https://ncode.syosetu.com';
|
||||||
version = '1.1.0';
|
version = '1.1.1';
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
@@ -69,15 +69,42 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
const novels = await getNovelsFromPage(pageNo);
|
const novels = await getNovelsFromPage(pageNo);
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
private async parseChaptersFromPage(
|
||||||
|
loadedCheerio: cheerio.CheerioAPI,
|
||||||
|
): Promise<Plugin.ChapterItem[]> {
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
|
||||||
|
loadedCheerio('.p-eplist__sublist').each((_, element) => {
|
||||||
|
const chapterLink = loadedCheerio(element).find('a');
|
||||||
|
const chapterUrl = chapterLink.attr('href');
|
||||||
|
const chapterName = chapterLink.text().trim();
|
||||||
|
const releaseDate = loadedCheerio(element)
|
||||||
|
.find('.p-eplist__update')
|
||||||
|
.text()
|
||||||
|
.trim()
|
||||||
|
.split(' ')[0]
|
||||||
|
.replace(/\//g, '-');
|
||||||
|
|
||||||
|
if (chapterUrl) {
|
||||||
|
chapters.push({
|
||||||
|
name: chapterName,
|
||||||
|
releaseTime: releaseDate,
|
||||||
|
path: chapterUrl.replace(this.novelPrefix, ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
|
// First fetch main page
|
||||||
const result = await fetchApi(this.novelPrefix + novelPath, {
|
const result = await fetchApi(this.novelPrefix + novelPath, {
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
});
|
});
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
const loadedCheerio = loadCheerio(body, { decodeEntities: false });
|
const loadedCheerio = loadCheerio(body, { decodeEntities: false });
|
||||||
|
|
||||||
// Parse novel status
|
// Parse status
|
||||||
let status = 'Unknown';
|
let status = 'Unknown';
|
||||||
if (loadedCheerio('.c-announce').text().includes('連載中')) {
|
if (loadedCheerio('.c-announce').text().includes('連載中')) {
|
||||||
status = 'Ongoing';
|
status = 'Ongoing';
|
||||||
@@ -87,7 +114,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
status = 'On Hiatus';
|
status = 'On Hiatus';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create novel object with basic metadata
|
// Create novel object with metadata
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
name: loadedCheerio('.p-novel__title').text(),
|
name: loadedCheerio('.p-novel__title').text(),
|
||||||
@@ -96,32 +123,77 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
.replace('作者:', '')
|
.replace('作者:', '')
|
||||||
.trim(),
|
.trim(),
|
||||||
status: status,
|
status: status,
|
||||||
artist: '', // Not available on syosetu
|
artist: '',
|
||||||
cover: defaultCover,
|
cover: defaultCover,
|
||||||
|
chapters: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get summary if available
|
// Get summary if available
|
||||||
novel.summary = loadedCheerio('#novel_ex').text().trim();
|
novel.summary = loadedCheerio('#novel_ex').text().trim();
|
||||||
|
|
||||||
// Parse chapters using the correct selectors
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
loadedCheerio('.p-eplist__sublist').each((_, element) => {
|
|
||||||
const chapterLink = loadedCheerio(element).find('a');
|
// Get last page URL first
|
||||||
const chapterUrl = chapterLink.attr('href');
|
const lastPageLink = loadedCheerio('.c-pager__item--last').attr('href');
|
||||||
const chapterName = chapterLink.text().trim();
|
|
||||||
const releaseDate = loadedCheerio(element)
|
if (!lastPageLink) {
|
||||||
.find('.p-eplist__update')
|
// If no pagination, just parse chapters from the current page
|
||||||
.text()
|
loadedCheerio('.p-eplist__sublist').each((_, element) => {
|
||||||
.trim()
|
const chapterLink = loadedCheerio(element).find('a');
|
||||||
.split(' ')[0] // Get just the date part
|
const chapterUrl = chapterLink.attr('href');
|
||||||
.replace(/\//g, '-'); // Format date as YYYY-MM-DD
|
const chapterName = chapterLink.text().trim();
|
||||||
if (chapterUrl) {
|
const releaseDate = loadedCheerio(element)
|
||||||
chapters.push({
|
.find('.p-eplist__update')
|
||||||
name: chapterName,
|
.text()
|
||||||
releaseTime: releaseDate,
|
.trim()
|
||||||
path: chapterUrl.replace(this.novelPrefix, ''),
|
.split(' ')[0]
|
||||||
|
.replace(/\//g, '-');
|
||||||
|
|
||||||
|
if (chapterUrl) {
|
||||||
|
chapters.push({
|
||||||
|
name: chapterName,
|
||||||
|
releaseTime: releaseDate,
|
||||||
|
path: chapterUrl.replace(this.novelPrefix, ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const lastPageMatch = lastPageLink.match(/\?p=(\d+)/);
|
||||||
|
const totalPages = lastPageMatch ? parseInt(lastPageMatch[1]) : 1;
|
||||||
|
|
||||||
|
// Fetch all pages in parallel for better performance
|
||||||
|
const pagePromises = Array.from({ length: totalPages }, (_, i) =>
|
||||||
|
fetchApi(`${this.novelPrefix}${novelPath}?p=${i + 1}`).then(r =>
|
||||||
|
r.text(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageResults = await Promise.all(pagePromises);
|
||||||
|
|
||||||
|
// Process each page's chapters
|
||||||
|
pageResults.forEach(pageBody => {
|
||||||
|
const pageCheerio = loadCheerio(pageBody, { decodeEntities: false });
|
||||||
|
pageCheerio('.p-eplist__sublist').each((_, element) => {
|
||||||
|
const chapterLink = pageCheerio(element).find('a');
|
||||||
|
const chapterUrl = chapterLink.attr('href');
|
||||||
|
const chapterName = chapterLink.text().trim();
|
||||||
|
const releaseDate = pageCheerio(element)
|
||||||
|
.find('.p-eplist__update')
|
||||||
|
.text()
|
||||||
|
.trim()
|
||||||
|
.split(' ')[0]
|
||||||
|
.replace(/\//g, '-');
|
||||||
|
|
||||||
|
if (chapterUrl) {
|
||||||
|
chapters.push({
|
||||||
|
name: chapterName,
|
||||||
|
releaseTime: releaseDate,
|
||||||
|
path: chapterUrl.replace(this.novelPrefix, ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
novel.chapters = chapters;
|
novel.chapters = chapters;
|
||||||
return novel;
|
return novel;
|
||||||
|
|||||||
Reference in New Issue
Block a user