From cd583a03f5afc85419474c05acb5d7894efd94e4 Mon Sep 17 00:00:00 2001 From: yammyy Date: Wed, 17 Sep 2025 04:07:25 +0300 Subject: [PATCH] =?UTF-8?q?New=20source=20ixdzs8=20(=E7=88=B1=E4=B8=8B?= =?UTF-8?q?=E7=94=B5=E5=AD=90=E4=B9=A6)=20(Chinese)=20(#1801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This site redirects request for a chapter to challenge, so it has two request - first getting a token, second - getting the chapter; - Duplicates novels in search; - Has a problem with layout. --- public/static/src/cn/ixdzs8/favicon.png | Bin 0 -> 3413 bytes src/plugins/chinese/ixdzs8.ts | 300 ++++++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 public/static/src/cn/ixdzs8/favicon.png create mode 100644 src/plugins/chinese/ixdzs8.ts diff --git a/public/static/src/cn/ixdzs8/favicon.png b/public/static/src/cn/ixdzs8/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..073a60a781499ac3d5ab6be022dba12447c009a0 GIT binary patch literal 3413 zcmV-b4XW~qP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/src/plugins/chinese/ixdzs8.ts b/src/plugins/chinese/ixdzs8.ts new file mode 100644 index 0000000..672a2f3 --- /dev/null +++ b/src/plugins/chinese/ixdzs8.ts @@ -0,0 +1,300 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +const makeAbsolute = ( + relativeUrl: string | undefined, + baseUrl: string, +): string | undefined => { + if (!relativeUrl) return undefined; + try { + if (relativeUrl.startsWith('//')) { + return new URL(baseUrl).protocol + relativeUrl; + } + if ( + relativeUrl.startsWith('http://') || + relativeUrl.startsWith('https://') + ) { + return relativeUrl; + } + return new URL(relativeUrl, baseUrl).href; + } catch { + return undefined; + } +}; + +class ixdzs8Plugin implements Plugin.PluginBase { + id = 'ixdzs8'; + name = '爱下电子书'; + site = 'https://ixdzs8.com/'; + version = '2.2.8'; + icon = 'src/cn/ixdzs8/favicon.png'; + + imageRequestInit = { + headers: { + Referer: this.site, + }, + }; + + async popularNovels(pageNo: number): Promise { + let url = `${this.site}hot/?page=${pageNo}`; + const result = await fetchApi(url); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const novels: Plugin.NovelItem[] = []; + const processedPaths = new Set(); + + $('ul.u-list > li.burl').each((_i, el) => { + const $el = $(el); + let novelPath: string | undefined; + let novelName: string | undefined; + let novelCover: string | undefined; + + const $link = $el.find('.l-info h3 a'); + novelPath = $link.attr('href')?.trim(); + novelName = ($link.attr('title') || $link.text() || '').trim(); + novelCover = $el.find('.l-img img').attr('src')?.trim(); + + if (novelPath && novelName) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + processedPaths.add(novelPath); + } + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const novelUrl = makeAbsolute(novelPath, this.site); + if (!novelUrl) throw new Error('Invalid novel URL'); + + const result = await fetchApi(novelUrl); + if (!result.ok) throw new Error('Failed to fetch novel'); + + const $ = parseHTML(await result.text()); + const $novel = $('div.novel'); + const $intro = $('p#intro.pintro'); + // Remove any child elements you don't want, e.g., the "read more" icon + $intro.find('span.icon').remove(); + // Get text and normalize whitespace + const summary = $intro + .html() // get inner HTML + ?.replace(//gi, '\n') // convert
to line breaks + .replace(/<[^>]+>/g, '') // remove remaining tags like + .replace(/\u3000/g, ' ') // replace full-width spaces + .replace(/ /g, ' ') // replace spaces + .split('\n') // split into lines + .map(line => line.trim()) // trim each line + .filter(line => line.length > 0) // remove empty lines + .join(' '); // join into one paragraph + + const $tagsDiv = $('div.panel div.tags'); + // find all inside and get their text + // Select

that contains + + const statOngoing = $novel.find('.n-text p span.lz').text().trim(); + const statEnd = $novel.find('.n-text p span.end').text().trim(); + let detail: 'Ongoing' | 'Completed' | 'Unknown' = 'Unknown'; + if (statEnd.length > 0) { + detail = 'Completed'; + } else if (statOngoing.length > 0) { + detail = 'Ongoing'; + } else { + detail = 'Unknown'; + } + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $novel.find('.n-text h1').text().trim() || 'Untitled', + cover: + makeAbsolute($novel.find('.n-img img').attr('src'), this.site) || + defaultCover, + summary: summary, + author: $novel.find('.n-text p a.bauthor').text().trim() || undefined, + genres: $tagsDiv + .find('em a') + .map((_i, el) => $(el).text().trim()) + .get() + .filter(tag => tag.length > 0) // remove empty strings + .join(', '), + status: + detail === 'Ongoing' + ? NovelStatus.Ongoing + : detail === 'Completed' + ? NovelStatus.Completed + : NovelStatus.Unknown, + chapters: [], + }; + + const chapterListPath = $('#bid').attr('value'); // get bid from page + if (chapterListPath) { + novel.chapters = await this.parseChapterList(chapterListPath); + } + + return novel; + } + + async parseChapterList(bid: string | number): Promise { + // Convert bid to string just in case + const bookId = String(bid); + + // POST request to fetch chapter list JSON + let url = `${this.site}novel/clist/`; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `bid=${bookId}`, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch chapters for bid=${bookId}`); + } + + const json = await res.json(); + + if (json.rs !== 200 || !Array.isArray(json.data)) { + throw new Error('Invalid response format from chapter list'); + } + + // Build chapters array + const chapters: Plugin.ChapterItem[] = json.data.map((ch: any) => { + return { + name: ch.title, + path: ch.ctype === '0' ? `read/${bookId}/p${ch.ordernum}.html` : '', // only normal chapters get link + releaseTime: undefined, // optional, not provided here + }; + }); + + return chapters; + } + + async parseChapter(chapterPath: string): Promise { + const chapterUrl = makeAbsolute(chapterPath, this.site); + if (!chapterUrl) throw new Error('Invalid chapter URL'); + + // --- 1st request --- + let result = await fetchApi(chapterUrl); + if (!result.ok) throw new Error(`Failed to fetch chapter at ${chapterUrl}`); + + let html = await result.text(); + + // --- Check if we got challenge page --- + if (html.includes('正在進行安全驗證') || html.includes('challenge')) { + const tokenMatch = html.match(/let token\s*=\s*"([^"]+)"/); + if (tokenMatch) { + const challengeUrl = + chapterUrl + '?challenge=' + encodeURIComponent(tokenMatch[1]); + + result = await fetchApi(challengeUrl); + if (!result.ok) + throw new Error(`Failed after challenge redirect: ${challengeUrl}`); + html = await result.text(); + } + } + + // --- Parse content --- + const $ = parseHTML(html); + const $content = $('article section'); + + if (!$content.length) { + return `Error: Could not find chapter content at ${chapterUrl}`; + } + + // Remove ads & junk + $content + .find( + 'script, style, ins, iframe, [class*="abg"], [class*="ads"], [id*="ads"], [class*="google"], [id*="google"], [class*="recommend"], div[align="center"], p:contains("推薦本書"), a[href*="javascript:"]', + ) + .remove(); + + // Drop empty

+ $content.find('p').each((_i, el) => { + const $p = $(el); + if (!$p.text().trim()) $p.remove(); + }); + + // Remove HTML comments + $content + .contents() + .filter(function () { + return this.type === 'comment'; + }) + .remove(); + + // Unwrap + $content.find('font').each((_i, el) => { + const $el = $(el); + $el.replaceWith($el.html() || ''); + }); + + // Extract cleaned text + let chapterText = $content.html(); + if (!chapterText) return 'Error: Chapter content was empty'; + + chapterText = chapterText + .replace(/<\s*p[^>]*>/gi, '\n\n') + .replace(/<\s*br[^>]*>/gi, '\n'); + + chapterText = parseHTML(`

${chapterText}
`).text(); + + return chapterText + .replace(/[\t ]+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim(); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const searchUrl = `${this.site}bsearch?q=${encodeURIComponent(searchTerm)}`; + let body = ''; + + try { + const result = await fetchApi(searchUrl); + if (!result.ok) { + throw new Error( + `Failed to fetch search results: HTTP ${result.status}`, + ); + } + body = await result.text(); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch search results: ${error.message}`); + } + throw error; + } + + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('ul.u-list li.burl').each((_i, el) => { + const $el = $(el); + + const novelPath = $el.attr('data-url')?.trim(); + const novelName = $el.find('h3.bname a').text().trim(); + const novelCover = $el.find('.l-img img').attr('src')?.trim(); + + if (novelPath && novelName) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + } + }); + + return novels; + } +} + +export default new ixdzs8Plugin();