From 6610abeafc6f215b875e4c7c622c6dcf29b58cd6 Mon Sep 17 00:00:00 2001 From: r1di <33724815+r1di@users.noreply.github.com> Date: Fri, 11 Apr 2025 08:41:47 +0200 Subject: [PATCH] new plugin novel543(chinese) (#1624) * new plugin novel543(chinese) -search not working well --search over the novelid works --normal search might not work because of cloudflare... * changed icon format * from fetchImage to imageRequestInit --------- Co-authored-by: Ridi --- public/static/src/cn/novel543/icon.png | Bin 0 -> 4286 bytes src/plugins/chinese/novel543.ts | 328 +++++++++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 public/static/src/cn/novel543/icon.png create mode 100644 src/plugins/chinese/novel543.ts diff --git a/public/static/src/cn/novel543/icon.png b/public/static/src/cn/novel543/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b370a57c7bb20c1145f3887f620b4d2da382ef87 GIT binary patch literal 4286 zcmeH}@oOAK6vt=RaD))r5K0Lp$Pr2?slD#bUbPJ+lqx}j_(Ks1MM@|o^oI)hp%f`v z4JC+#5<-zuM1qKDks?w`kw$`u_6NcK2`ZsGvlrtPf6!LioS(OMw|AFp&Mn2?vd8yk z_RX93oq6-#+fmBlm(MHtS6}ofwNojz2Ov^MfXL&Av8`vF2liW~JnK4ssUs^yU+oK( zU?24Pz`+U#J@l|2h4uw0j)g-iB~EUNq`zJJZB} zUF}_tUt9h>qf}se;XQ)=4}kb+hJEH$fd94q$nlm3m0xn{9|hN9`1UpC^r^kfPkCIg zG?K>8T-o2Nv{PUn+`!jzHq+p+!&#{V_Y;fAs{D0{4mnr58=Qr|eh#zfod9FVin7)= z{HM|T#L>~~*m%zIi7h{U4??+tzrGG>53MN0*|Pp&^d`VL@ROrU-EH{;JKk(2txwFy zvD=nEWg_}F-QX$sb?6f7;+F|ApYZ!(7yCTOSPE-H^u+wZa);v)f4=p=8_d57h(9AG zw!wdJh4?cwj1ecMpo*rW!P@9$;Z{E#*+A>@3O7|^v=-# z7%b4Aqb2?f@oXeVp3eU;yS~kle??gY_Nx5*rFPcppbQlLQ20%9p%a}hH?W;R^q-vn z#XqIaa_;Qiw=cspPdnVoA9W(TOx_e|Q+;34&%|Hz|1|AIxaC|g0nWo7qxN@+EqQoP z{EzRcQ+5)5(Lc|;Q?z4F5DmMm%MGH;{zo^*TxwBB>=D}q_TT*YrTCkJPL=-xz83CF3o2in1k@NFSH~TzJds5^HKXUq~*mEw?PwAkO$^ZDPhxHca zyN1m%a0bX3%)!m~CXCNG;nx*ei_~p&4Btf~`P!rMYb~o_JQKgtVW0A&V*+fw1a>=t zA(m_3Y{@mAgkN5QRTak`If#!Y7@J`YlDn6YpL2rvT(a?ty-gnMlH=yM3@7|Lk~}MH zG9?kClZoH;sTBb}_Q@-4x4}1n4~V09KbMgAx9}fWk^G-ypHt*Vo_>xRnDEv+S^1ey z{j=m=L{7_h*t|_UPCQ_{TsPO)zer@wzA2(7-#Mw%S9q74JHm4_?Ep5Qg2(9koJP^ZFs~-L!4FbdL87a+w$Wp&VajBd?%X42K9hCqwROB zhkD8VZ(sFpu->-3qZh|t+E{IT0-f8ScJ7kzvA@1VCU?$j_;r$)BtJ`4GsWLI`%(@$ zxoSTJOWnj-loL72oChX(b1U>BhHr!(0(O z&ziK6we&N;055m1=rG@a0yc&~pZmgppHr&sE#a02{s#|Knx6M^Q_n2@v+#K7NAq}b z=3nrI~I{*Lx literal 0 HcmV?d00001 diff --git a/src/plugins/chinese/novel543.ts b/src/plugins/chinese/novel543.ts new file mode 100644 index 0000000..8bd5c86 --- /dev/null +++ b/src/plugins/chinese/novel543.ts @@ -0,0 +1,328 @@ +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 Novel543Plugin implements Plugin.PluginBase { + id = 'novel543'; + name = 'Novel543'; + site = 'https://www.novel543.com/'; + version = '1.0.0'; + icon = 'src/cn/novel543/icon.png'; + + imageRequestInit = { + headers: { + Referer: this.site, + }, + }; + + async popularNovels(pageNo: number): Promise { + if (pageNo > 1) return []; + + const result = await fetchApi(this.site); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const novels: Plugin.NovelItem[] = []; + const processedPaths = new Set(); + + $('ul.list > li.media, ul.list li > a[href^="/"][href$="/"]').each( + (_i, el) => { + const $el = $(el); + let novelPath: string | undefined; + let novelName: string | undefined; + let novelCover: string | undefined; + + if ($el.is('li.media')) { + const $link = $el.find('.media-content h3 a'); + novelPath = $link.attr('href')?.trim(); + novelName = $link.text().trim(); + novelCover = $el.find('.media-left img').attr('src')?.trim(); + } else if ($el.is('a')) { + novelPath = $el.attr('href')?.trim(); + novelName = + $el.find('h3, b, span').first().text().trim() || + $el.parent().find('h3').text().trim() || + $el.text().trim(); + novelCover = + $el.find('img').attr('src')?.trim() || + $el.parent().find('img').attr('src')?.trim(); + } + + if ( + novelPath && + novelName && + novelPath.match(/^\/\d+\/$/) && + !processedPaths.has(novelPath) + ) { + 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 $infoSection = $('section#detail div.media-content.info'); + const $modSection = $('section#detail div.mod'); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $infoSection.find('h1.title').text().trim() || 'Untitled', + cover: + makeAbsolute( + $('section#detail div.cover img').attr('src'), + this.site, + ) || defaultCover, + summary: $modSection.find('div.intro').text().trim() || undefined, + author: + $infoSection.find('p.meta span.author').text().trim() || undefined, + genres: + $infoSection + .find('p.meta a[href*="/bookstack/"]') + .map((_i, el) => $(el).text().trim()) + .get() + .join(', ') || undefined, + status: NovelStatus.Unknown, + chapters: [], + }; + + const chapterListPath = + $modSection + .find('p.action.buttons a.button.is-info[href$="/dir"]') + .attr('href') || + $infoSection.find('a.button.is-info[href$="/dir"]').attr('href'); + + if (chapterListPath) { + novel.chapters = await this.parseChapterList(chapterListPath); + } + + return novel; + } + + async parseChapterList( + chapterListPath: string, + ): Promise { + const chapterListUrl = makeAbsolute(chapterListPath, this.site); + if (!chapterListUrl) return []; + + const result = await fetchApi(chapterListUrl); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const chapters: Plugin.ChapterItem[] = []; + + $('div.chaplist ul.all li a').each((index, el) => { + const $el = $(el); + const chapterName = $el.text().trim(); + const chapterUrl = $el.attr('href')?.trim(); + + if (chapterName && chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl, + chapterNumber: index + 1, + }); + } + }); + + const sortButtonText = $('div.chaplist .header button.reverse span') + .last() + .text() + .trim(); + if (sortButtonText === '倒序') { + chapters.reverse(); + chapters.forEach((chap, index) => (chap.chapterNumber = index + 1)); + } + + return chapters; + } + + async parseChapter(chapterPath: string): Promise { + const chapterUrl = makeAbsolute(chapterPath, this.site); + if (!chapterUrl) throw new Error('Invalid chapter URL'); + + const result = await fetchApi(chapterUrl); + if (!result.ok) throw new Error('Failed to fetch chapter'); + + const $ = parseHTML(await result.text()); + const $content = $('div.content.py-5'); + if (!$content.length) return 'Error: Could not find chapter content'; + + $content + .find( + 'script, style, ins, iframe, [class*="ads"], [id*="ads"], [class*="google"], [id*="google"], [class*="recommend"], div[align="center"], p:contains("推薦本書"), a[href*="javascript:"]', + ) + .remove(); + + $content.find('p').each((_i, el) => { + const $p = $(el); + const pText = $p.text().trim(); + if ( + pText.includes('請記住本站域名') || + pText.includes('手機版閱讀網址') || + pText.includes('novel543') || + pText.includes('稷下書院') || + pText.includes('最快更新') || + pText.includes('最新章節') || + pText.includes('章節報錯') || + pText.match(/app|APP|下載|客户端|关注微信|公众号/i) || + pText.length === 0 || + ($p + .html() + ?.replace(/ /g, '') + .trim() === '' && + $p.find('img').length === 0) || + pText.includes('溫馨提示') + ) { + $p.remove(); + } + }); + + $content + .contents() + .filter(function () { + return this.type === 'comment'; + }) + .remove(); + + 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 { + if (pageNo > 1) return []; + + if (/^\d+$/.test(searchTerm)) { + try { + const novelPath = `/${searchTerm}/`; + const novel = await this.parseNovel(novelPath); + return [ + { + name: novel.name, + path: novelPath, + cover: novel.cover, + }, + ]; + } catch { + return []; + } + } + + const searchUrl = `${this.site}search/${encodeURIComponent(searchTerm)}`; + let body = ''; + + try { + const result = await fetchApi(searchUrl); + if (!result.ok) { + if (result.status === 403 || result.status === 503) { + throw new Error( + 'Cloudflare protection detected (HTTP error). Please try opening the plugin in WebView first to solve the challenge.', + ); + } + return []; + } + + body = await result.text(); + const $ = parseHTML(body); + const pageTitle = $('title').text().toLowerCase(); + + // Check for various Cloudflare challenge indicators + if ( + pageTitle.includes('attention required') || + pageTitle.includes('just a moment') || + pageTitle.includes('please wait') || + pageTitle.includes('verifying') || + body.includes('Verifying you are human') || + body.includes('cf-browser-verification') || + body.includes('cf_captcha_container') + ) { + throw new Error( + 'Cloudflare protection detected. Please try opening the plugin in WebView first to solve the challenge.', + ); + } + } catch (error) { + if (error instanceof Error) { + // If it's already our custom error, re-throw it + if (error.message.includes('Cloudflare protection detected')) { + throw error; + } + // For other errors, throw a generic error + throw new Error(`Failed to fetch search results: ${error.message}`); + } + throw error; + } + + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('div.search-list ul.list > li.media').each((_i, el) => { + const $el = $(el); + const $link = $el.find('.media-content h3 a'); + const novelPath = $link.attr('href')?.trim(); + const novelName = $link.text().trim(); + const novelCover = $el.find('.media-left img').attr('src')?.trim(); + + if (novelPath && novelName && novelPath.match(/^\/\d+\/$/)) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + } + }); + + return novels; + } +} + +export default new Novel543Plugin();