diff --git a/index.json b/index.json index e18dcc3..2e1e542 100644 --- a/index.json +++ b/index.json @@ -151,5 +151,11 @@ "fileName": "mxs.js", "key": "mxs", "version": "1.0.0" + }, + { + "name": "漫画人", + "fileName": "manhuaren.js", + "key": "manhuaren", + "version": "1.0.0" } ] diff --git a/manhuaren.js b/manhuaren.js new file mode 100644 index 0000000..a1c5270 --- /dev/null +++ b/manhuaren.js @@ -0,0 +1,979 @@ +/** @type {import('../_venera_.js')} */ +class ManHuaRen extends ComicSource { + name = "漫画人" + + key = "manhuaren" + + version = "1.0.0" + + minAppVersion = "1.6.0" + + url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manhuaren.js" + + + init() { + + } + + get baseUrl() { + return "https://www.manhuaren.com"; + } + + // helper to build common request headers + _buildHeaders() { + return { + 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36', + 'accept': '*/*', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + 'sec-ch-ua-mobile': '?1', + 'sec-ch-ua-platform': '"Android"', + 'host': 'www.manhuaren.com' + } + } + + + _buildImageHeaders(imageUrl, referer) { + let host = ''; + try { + let u = new URL(imageUrl); + host = u.host; + } catch (e) { + // fallback: try to extract host from string + let m = imageUrl.match(/^https?:\/\/([^\/]+)/i); + host = m ? m[1] : ''; + } + + return { + 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + //'Host': host || '', + 'Pragma': 'no-cache', + 'Referer': referer || (this.baseUrl + '/'), + 'Sec-Fetch-Dest': 'image', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-Fetch-Storage-Access': 'active', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36', + 'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + 'sec-ch-ua-mobile': '?1', + 'sec-ch-ua-platform': '"Android"' + } + } + + // explore page list + explore = [ + { + title: "漫画人", + type: "multiPartPage", + load: async (page) => { + let url = this.baseUrl + '/'; + let res = await Network.get( + url, + this._buildHeaders() + ); + + if (res.status !== 200) { + throw `Invalid status code: ${res.status}` + } + + let html = res.body || ''; + let doc = new HtmlDocument(html); + let parts = []; + + // Banner + let banner = doc.querySelector('.index-banner'); + if (banner) { + let comics = []; + let items = banner.querySelectorAll('li'); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let a = item.querySelector('a'); + if (!a) continue; + let img = item.querySelector('img'); + + let href = a.attributes['href']; + let title = a.attributes['title']; + let cover = img ? (img.attributes['src'] || img.attributes['data-src']) : ''; + + if (href) { + if (!href.startsWith('http')) href = this.baseUrl + href; + if (cover && !cover.startsWith('http')) { + if (cover.startsWith('//')) cover = 'https:' + cover; + else cover = this.baseUrl + cover; + } + comics.push(new Comic({ + id: href, + title: title || '', + cover: cover || '', + description: '' + })); + } + } + if (comics.length > 0) { + parts.push({ title: '热门推荐', comics: comics }); + } + } + + // Lists + let lists = doc.querySelectorAll('.manga-list'); + for (let i = 0; i < lists.length; i++) { + let list = lists[i]; + let titleNode = list.querySelector('.manga-list-title'); + let title = titleNode ? titleNode.text.trim() : ''; + + let viewMore = null; + if (titleNode) { + let moreNode = titleNode.querySelector('a'); + if (moreNode) { + let href = moreNode.attributes['href']; + if (href) { + if (!href.startsWith('http')) href = this.baseUrl + href; + viewMore = href; + } + } + } + + let comics = []; + let items = list.querySelectorAll('li'); + for (let j = 0; j < items.length; j++) { + let item = items[j]; + let a = item.querySelector('a'); + if (!a) continue; + + let href = a.attributes['href']; + let comicTitle = a.attributes['title']; + + if (!comicTitle) { + let t = item.querySelector('.manga-list-2-title'); + if (t) comicTitle = t.text.trim(); + } + + let img = item.querySelector('img'); + let cover = img ? (img.attributes['data-src'] || img.attributes['src']) : ''; + + let tip = item.querySelector('.manga-list-1-tip') || item.querySelector('.manga-list-2-tip'); + let desc = tip ? tip.text.trim() : ''; + + let badgeNode = item.querySelector('.manga-list-1-cover-logo-font'); + let badge = badgeNode ? badgeNode.text.trim() : ''; + + if (href) { + if (!href.startsWith('http')) href = this.baseUrl + href; + if (cover && !cover.startsWith('http')) { + if (cover.startsWith('//')) cover = 'https:' + cover; + else cover = this.baseUrl + cover; + } + + comics.push(new Comic({ + id: href, + title: comicTitle || '', + cover: cover || '', + description: desc, + tags: badge ? [badge] : [] + })); + } + } + + if (comics.length > 0) { + if (!title) { + if (comics[0].tags && comics[0].tags.length > 0) { + title = comics[0].tags[0]; + } else { + title = '漫画列表'; + } + } + let part = { title: title, comics: comics }; + if (viewMore) part.viewMore = viewMore; + parts.push(part); + } + } + + return parts; + }, + loadNext(next) { } + } + ] + + // categories + category = { + /// title of the category page, used to identify the page, it should be unique + title: "漫画人", + parts: [ + { + // title of the part + name: "类型", + + // fixed list of categories + type: "fixed", + itemType: "category", + + // human readable categories and params mapped in categoryParams + categories: [ + "全部", + "热血", + "恋爱", + "校园", + "伪娘", + "冒险", + "职场", + "后宫", + "治愈", + "科幻", + "轻小说", + "励志", + "生活", + "战争", + "悬疑", + "推理", + "搞笑", + "奇幻", + "魔法", + "神鬼", + "萌系", + "历史", + "美食", + "同人", + "运动", + "绅士", + "机甲", + "百合", + ], + // corresponding params (tag ids). Keep order aligned with `categories` above. + categoryParams: [ + "", // 全部 + "31", // 热血 + "26", // 恋爱 + "1", // 校园 + "5", // 伪娘 + "2", // 冒险 + "6", // 职场 + "8", // 后宫 + "9", // 治愈 + "25", // 科幻 + "156", // 轻小说 + "10", // 励志 + "11", // 生活 + "12", // 战争 + "17", // 悬疑 + "33", // 推理 + "37", // 搞笑 + "14", // 奇幻 + "15", // 魔法 + "20", // 神鬼 + "21", // 萌系 + "4", // 历史 + "7", // 美食 + "30", // 同人 + "34", // 运动 + "36", // 绅士 + "40", // 机甲 + "3", // 百合 + ], + } + ], + // enable ranking page + enableRankingPage: false, + } + + categoryComics = { + load: async (category, param, options, page) => { + // param is expected to be the tag id (e.g. "31"). + let tag = param || ''; + + // options: [statusOption, sortOption] + // option values use left side before '-' (e.g. 'st1-连载' -> 'st1') + let statusOpt = (options && options[0]) ? options[0].split('-')[0] : ''; + let sortOpt = (options && options[1]) ? options[1].split('-')[0] : ''; + + // Build path like: manhua-list(-tag{tag})?(-{status})?(-{sort})?/dm5.ashx + let path = 'manhua-list'; + if (tag) path += `-tag${tag}`; + if (statusOpt) path += `-${statusOpt}`; + if (sortOpt) path += `-${sortOpt}`; + + let url = `${this.baseUrl}/${path}/dm5.ashx`; + // POST body: use site form-data fields + // action=getclasscomics&pageindex=3&pagesize=21&categoryid=0&tagid=0&status=1&usergroup=0&pay=-1&areaid=0&sort=2&iscopyright=0 + let pageIndex = Math.max(0, (parseInt(page) || 1)); + let pageSize = 21; + // map status option like 'st1' -> 1, 'st2' -> 2 + let statusNum = 0; + if (statusOpt && statusOpt.startsWith('st')) { + let m = statusOpt.match(/st(\d+)/); + if (m) statusNum = parseInt(m[1]); + } + // map sort option like 's2' -> 2, 's18' -> 18 + let sortNum = 0; + if (sortOpt && sortOpt.startsWith('s')) { + let m = sortOpt.match(/s(\d+)/); + if (m) sortNum = parseInt(m[1]); + } + // tag id (tag param) - if empty use 0 + let tagId = tag && tag.length > 0 ? tag : '0'; + + let body = `action=getclasscomics&pageindex=${pageIndex}&pagesize=${pageSize}&categoryid=0&tagid=${encodeURIComponent(tagId)}&status=${statusNum}&usergroup=0&pay=-1&areaid=0&sort=${sortNum}&iscopyright=0`; + + // 使用站点期望的 AJAX 请求头(不包含 cookie) + let categoryHeaders = { + 'accept': 'application/json, text/javascript, */*; q=0.01', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'cache-control': 'no-cache', + 'connection': 'keep-alive', + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'host': 'www.manhuaren.com', + 'origin': this.baseUrl, + 'pragma': 'no-cache', + 'referer': `${this.baseUrl}/${path}/`, + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1', + 'x-requested-with': 'XMLHttpRequest' + }; + + let res = await Network.post(url, categoryHeaders, body); + if (res.status !== 200) { + throw `加载分类漫画失败: ${res.status}`; + } + + let data = {}; + try { + data = JSON.parse(res.body || '{}'); + } catch (e) { + throw '解析分类返回数据失败'; + } + + let items = data.UpdateComicItems || []; + let comics = items.map(it => { + // UrlKey already contains path like "manhua-xxxx" + let id = it.UrlKey ? `/${it.UrlKey}/` : (it.ID ? `/m${it.ID}/` : ''); + let cover = it.ShowPicUrlB || it.ShowConver || ''; + if (cover && cover.startsWith('//')) cover = 'https:' + cover; + if (cover && !cover.startsWith('http')) cover = this.baseUrl + cover; + + let tags = []; + if (it.Author && Array.isArray(it.Author)) tags = it.Author.slice(0,3); + + return new Comic({ + id: id, + title: it.Title, + cover: cover, + description: it.Content || '', + tags: tags + }); + }); + + let perPage = items.length || 20; + let total = data.Count || 0; + let maxPage = perPage > 0 ? Math.max(1, Math.ceil(total / perPage)) : (comics.length > 0 ? page + 1 : page); + + return { + comics: comics, + maxPage: maxPage+1 + }; + }, + + // provide options for category comic loading: status and sort + optionList: [ + { + type: 'select', + label: '状态', + options: [ + 'st0-全部', + 'st1-连载', + 'st2-已完结' + ], + default: 'st0' + }, + { + type: 'select', + label: '排序', + options: [ + 's2-最近更新', + 's10-人气最旺', + 's18-最近上架' + ], + default: 's2' + } + ], + } + + /// search related + search = { + /** + * load search result + * @param keyword {string} + * @param options {string[]} - options from optionList + * @param page {number} + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (keyword, options, page) => { + let url = `${this.baseUrl}/search?title=${encodeURIComponent(keyword)}&language=1&page=${page}`; + + let res = await Network.get(url, this._buildHeaders()); + if (res.status !== 200) { + throw `Search failed: ${res.status}`; + } + + let doc = new HtmlDocument(res.body); + let comics = []; + let list = doc.querySelectorAll('.book-list > li'); + + for (let item of list) { + let link = item.querySelector('.book-list-info > a'); + let href = link?.attributes['href']; + if (!href) continue; + + if (!href.startsWith('http')) { + href = this.baseUrl + href; + } + + let title = item.querySelector('.book-list-info-title')?.text?.trim(); + let coverEl = item.querySelector('.book-list-cover-img'); + let cover = coverEl?.attributes['src']; + if (cover) { + if (cover.startsWith('//')) cover = 'https:' + cover; + else if (!cover.startsWith('http')) cover = this.baseUrl + cover; + } + + let desc = item.querySelector('.book-list-info-desc')?.text?.trim(); + + let tags = []; + let tagEls = item.querySelectorAll('.book-list-info-bottom-item'); + for (let t of tagEls) { + tags.push(t.text.trim()); + } + + let status = item.querySelector('.book-list-info-bottom-right-font')?.text?.trim(); + if (status) tags.push(status); + + comics.push(new Comic({ + id: href, + title: title, + cover: cover, + description: desc, + tags: tags + })); + } + + let maxPage = comics.length > 0 ? page + 1 : page; + + return { + comics: comics, + maxPage: maxPage + }; + }, + + optionList: [], + + enableTagsSuggestions: false, + } + + /// single comic related + comic = { + /** + * load comic info + * @param id {string} + * @returns {Promise} + */ + loadInfo: async (id) => { + if (!id || typeof id !== 'string') { + throw "ID不能为空"; + } + + let targetUrl = id; + if (!targetUrl.startsWith('http')) { + if (targetUrl.startsWith('/')) targetUrl = this.baseUrl + targetUrl; + else targetUrl = this.baseUrl + '/' + targetUrl; + } + + let res = await Network.get( + targetUrl, + this._buildHeaders() + ); + if (res.status !== 200) { + throw `请求失败,状态码: ${res.status},URL: ${targetUrl}`; + } + + let html = res.body || ''; + this.comic.id = id; + + let toAbsUrl = (value) => { + if (!value) return ''; + let trimmed = value.trim(); + if (trimmed.startsWith('http')) return trimmed; + if (trimmed.startsWith('//')) return 'https:' + trimmed; + if (trimmed.startsWith('/')) return this.baseUrl + trimmed; + return this.baseUrl + '/' + trimmed; + }; + + let doc = new HtmlDocument(html); + + let title = doc.querySelector('p.detail-main-info-title')?.text?.trim() + || doc.querySelector('span.normal-top-title')?.text?.trim() + || doc.querySelector('title')?.text?.trim()?.replace(/漫画.*$/i, '') + || '未知标题'; + + let coverEl = doc.querySelector('.detail-main-cover img') + || doc.querySelector('.detail-main-cover .cover-img img'); + let cover = toAbsUrl(coverEl?.attributes?.src || coverEl?.attributes?.['data-src'] || ''); + + let authorContainer = doc.querySelector('.detail-main-info-author'); + let author = '未知作者'; + if (authorContainer) { + let authors = []; + let links = authorContainer.querySelectorAll('a') || []; + for (let i = 0; i < links.length; i++) { + let text = links[i].text?.trim(); + if (text) authors.push(text); + } + if (authors.length > 0) { + author = authors.join(','); + } else { + let raw = authorContainer.text?.replace(/作者[::]/, '').trim(); + if (raw) author = raw; + } + } else { + let metaAuthor = doc.querySelector('meta[name="Author"]')?.attributes?.content; + if (metaAuthor) { + author = metaAuthor.includes(':') ? metaAuthor.split(':').pop().trim() : metaAuthor.trim(); + } + } + + let status = doc.querySelector('.detail-list-title-1')?.text?.trim() || '未知状态'; + + let descriptionEl = doc.querySelector('.detail-desc'); + let description = descriptionEl?.text?.trim() || ''; + if (!description) { + description = doc.querySelector('meta[name="Description"]')?.attributes?.content || ''; + } + + let tags = []; + let tagElements = doc.querySelectorAll('.detail-main-info-class a') || []; + for (let i = 0; i < tagElements.length; i++) { + let tagText = tagElements[i].text?.trim(); + if (tagText) tags.push(tagText); + } + + let updateTime = doc.querySelector('.detail-list-title-3')?.text?.trim() || ''; + + let starValue = null; + let starElement = doc.querySelector('.detail-main-info-star'); + if (starElement && starElement.attributes && starElement.attributes['class']) { + let starClass = starElement.attributes['class']; + let match = starClass.match(/star-(\d+)/i); + if (match && match[1]) { + let num = parseInt(match[1], 10); + if (!isNaN(num)) { + starValue = num; + } + } + } + + let chapters = new Map(); + let selectorItems = doc.querySelectorAll('.detail-selector .detail-selector-item'); + + if (selectorItems.length > 0) { + for (let item of selectorItems) { + let groupName = item.text?.trim(); + if (!groupName || groupName.includes('评论')) continue; + + let onclick = item.attributes['onclick']; + let listId = null; + if (onclick) { + let match = onclick.match(/titleSelect\(.*?,.*?, *['"](.*?)['"]\)/); + if (match) { + listId = match[1]; + } + } + + if (listId) { + let listEl = doc.getElementById(listId); + if (listEl) { + let groupChapters = new Map(); + let links = listEl.querySelectorAll('a.chapteritem'); + for (let link of links) { + let href = link.attributes['href']; + let title = link.text?.trim() || link.attributes['title']?.trim(); + if (href && title) { + if (!href.startsWith('http')) href = toAbsUrl(href); + groupChapters.set(href, title); + } + } + if (groupChapters.size > 0) { + chapters.set(groupName, groupChapters); + } + } + } + } + } + + if (chapters.size === 0) { + let groupChapters = new Map(); + let links = doc.querySelectorAll('a.chapteritem'); + for (let link of links) { + let href = link.attributes['href']; + let title = link.text?.trim() || link.attributes['title']?.trim(); + if (href && title) { + if (!href.startsWith('http')) href = toAbsUrl(href); + groupChapters.set(href, title); + } + } + if (groupChapters.size > 0) { + chapters.set('连载', groupChapters); + } + } + + let parseRecommends = (htmlContent) => { + let recs = []; + let recPattern = /]*class=["'][^"']*(?:list-comic|rec|recommend)[^"']*["'][^>]*>[\s\S]*?]*href=["']([^"']+)["'][^>]*>[\s\S]*?]*src=["']([^"']+)["'][^>]*>[^<]*<\/a>[\s\S]*?]*>\s*([^<]+)\s*<\/a>/gi; + let m; + let count = 0; + while ((m = recPattern.exec(htmlContent)) !== null && count < 12) { + let url = m[1]; + let cover = m[2]; + let titleText = (m[3] || '').trim(); + if (!url || !titleText) continue; + if (!url.startsWith('http')) url = toAbsUrl(url); + if (cover && !cover.startsWith('http')) cover = toAbsUrl(cover); + recs.push(new Comic({ id: url, title: titleText, cover: cover })); + count++; + } + return recs; + }; + + let recommends = parseRecommends(html); + + // 提取 mid + let midMatch = html.match(/mid["\s:]*(\d+)/i) || html.match(/var mid = (\d+)/i) || html.match(/mid=(\d+)/i) || html.match(/var DM5_MID = (\d+)/i) || html.match(/var COMIC_MID=(\d+)/i); + if (midMatch) { + this.comic.mid = parseInt(midMatch[1]); + } + + return new ComicDetails({ + title, + cover, + description: description || '暂无描述', + tags: { + '作者': [author || '未知作者'], + '状态': [status || '未知状态'], + '标签': tags + }, + chapters: chapters, + recommend: recommends, + updateTime: updateTime, + stars: starValue, + subId: this.comic.mid ? this.comic.mid.toString() : '73225' + }); + }, + + /** + * load images of a chapter + * @param comicId {string} + * @param epId {string?} + * @returns {Promise<{images: string[]}>} + */ + loadEp: async (comicId, epId) => { + let url = `${epId}/`; + let res = await Network.get(url, this._buildHeaders()); + if (res.status !== 200) throw new Error('获取章节内容失败: ' + res.status); + let html = res.body; + let document = new HtmlDocument(html); + let scripts = document.querySelectorAll("script"); + let script = null; + for (let s of scripts) { + if (s.innerHTML.includes('eval(function(p,a,c,k,e,d)')) { + script = s.innerHTML; + break; + } + } + if (!script) throw ('无法显示付费内容/章节不存在'); + + let pStart = script.indexOf("}('") + 3; + let boundaryMatch = script.substring(pStart).match(/',(\d+),(\d+),'/); + if (!boundaryMatch) throw new Error('无法解析脚本参数边界'); + + let boundaryIndex = boundaryMatch.index + pStart; + let rawP = script.substring(pStart, boundaryIndex); + let a = parseInt(boundaryMatch[1]); + let c = parseInt(boundaryMatch[2]); + + let kContentStart = boundaryIndex + boundaryMatch[0].length; + let kEnd = script.indexOf("'.split", kContentStart); + let rawK = script.substring(kContentStart, kEnd); + let dict = rawK.split('|'); + + let decrypt = (p, a, c, k) => { + let e = (c) => (c < a ? '' : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36)); + let d = {}; + while (c--) d[e(c)] = k[c] || e(c); + return p.replace(/\b\w+\b/g, w => d[w] || w); + }; + + let decrypted = decrypt(rawP, a, c, dict); + + let arrayMatch = decrypted.match(/\[(.*?)\]/); + if (!arrayMatch) throw new Error('无法从解密后的脚本中提取图片数组'); + + let arrayContent = arrayMatch[1]; + let images = arrayContent.split(',').map(item => { + // 去除引号和反斜杠 + return item.trim().replace(/^\\?['"]|\\?['"]$/g, ''); + }).filter(url => url && url.startsWith('http')); + + return { images }; + }, + /** + * [Optional] provide configs for an image loading + * @param url + * @param comicId + * @param epId + * @returns {ImageLoadingConfig | Promise} + */ + onImageLoad: (url, comicId, epId) => { + let referer = ''; + if (epId && typeof epId === 'string') { + if (!epId.startsWith('http')) { + referer = this.baseUrl + epId; + } else { + referer = epId; + } + } else { + referer = this.baseUrl + '/'; + } + + return { + headers: this._buildImageHeaders(url, referer) + }; + }, + /** + * [Optional] provide configs for a thumbnail loading + * @param url {string} + * @returns {ImageLoadingConfig | Promise} + * + * `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored. + * They are not supported for thumbnails. + */ + onThumbnailLoad: (url) => { + return { + headers: this._buildImageHeaders(url, this.baseUrl + '/') + } + }, + /** + * [Optional] like or unlike a comic + * @param id {string} + * @param isLike {boolean} - true for like, false for unlike + * @returns {Promise} + */ + likeComic: async (id, isLike) => { + + }, + /** + * [Optional] load comments + * + * Since app version 1.0.6, rich text is supported in comments. + * Following html tags are supported: ['a', 'b', 'i', 'u', 's', 'br', 'span', 'img']. + * span tag supports style attribute, but only support font-weight, font-style, text-decoration. + * All images will be placed at the end of the comment. + * Auto link detection is enabled, but only http/https links are supported. + * @param comicId {string} + * @param subId {string?} - ComicDetails.subId + * @param page {number} + * @param replyTo {string?} - commentId to reply, not null when reply to a comment + * @returns {Promise<{comments: Comment[], maxPage: number?}>} + */ + loadComments: async (comicId, subId, page, replyTo) => { + if (!subId) { + throw new Error('漫画ID未找到,无法加载评论'); + } + + let requestPage = page; + let targetCommentId = null; + if (replyTo) { + let parts = replyTo.split('//'); + targetCommentId = parts[0]; + requestPage = parseInt(parts[1]); + } + + let url = `${this.baseUrl}/manhua-${comicId}/pagerdata.ashx`; + let params = { + d: Date.now(), + pageindex: (requestPage - 1), + pagesize: 767, + mid: subId, + t: 4 + }; + let query = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&'); + url += '?' + query; + + let headers = { + 'accept': '*/*', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'cache-control': 'no-cache', + 'connection': 'keep-alive', + 'host': 'www.manhuaren.com', + 'pragma': 'no-cache', + 'referer': `${this.baseUrl}/manhua-${comicId}/`, + 'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + 'sec-ch-ua-mobile': '?1', + 'sec-ch-ua-platform': '"Android"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36', + 'x-requested-with': 'XMLHttpRequest' + }; + + let res = await Network.get(url, headers); + if (res.status !== 200) { + throw new Error(`加载评论失败,状态码: ${res.status}`); + } + + let data = JSON.parse(res.body); + let comments = []; + + let maxPage = 0 + if (replyTo) { + let target = data.find(item => item.Id.toString() === targetCommentId); + if (target && target.ToPostShowDataItems) { + comments = target.ToPostShowDataItems.map(item => new Comment({ + id: item.Id.toString(), + userName: item.Poster, + content: item.PostContent, + time: item.PostTime, + avatar: item.HeadUrl, + likeCount: item.PraiseCount, + isLiked: item.IsPraise, + replyCount: 0 + })); + } + } else { + comments = data.map(item => new Comment({ + id: `${item.Id}//${page}`, + userName: item.Poster, + content: item.PostContent, + time: item.PostTime, + avatar: item.HeadUrl, + likeCount: item.PraiseCount, + isLiked: item.IsPraise, + replyCount: item.ToPostShowDataItems ? item.ToPostShowDataItems.length : 0 + })); + if (comments == []){ + maxPage = page; + }else{ + maxPage = null; + } + } + + return { + comments: comments, + maxPage: replyTo? 1 : maxPage + }; + }, + + /** + * load chapter comments + * @param comicId {string} + * @param epId {string} + * @param page {number} + * @param replyTo {string?} + * @returns {Promise<{comments: Comment[], maxPage: number}>} + */ + loadChapterComments: async (comicId, epId, page, replyTo) => { + let cidMatch = epId.match(/m(\d+)/); + let cid = cidMatch ? cidMatch[1] : null; + if (!cid) { + let match = epId.match(/(\d+)\/?$/); + if (match) cid = match[1]; + } + + if (!cid) return { comments: [], maxPage: page }; + + let requestPage = page; + let targetCommentId = null; + if (replyTo) { + let parts = replyTo.split('//'); + targetCommentId = parts[0]; + requestPage = parseInt(parts[1]); + } + + let pageSize = 20; + let url = `https://www.manhuaren.com/showcomment/pagerdata.ashx?d=${Date.now()}&pageindex=${requestPage}&pagesize=${pageSize}&cid=${cid}&t=9`; + + let headers = { + 'accept': '*/*', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'cache-control': 'no-cache', + 'connection': 'keep-alive', + 'host': 'www.manhuaren.com', + 'pragma': 'no-cache', + 'referer': `https://www.manhuaren.com/showcomment/?cid=${cid}`, + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1', + 'x-requested-with': 'XMLHttpRequest' + }; + + let res = await Network.get(url, headers); + if (res.status !== 200) return { comments: [], maxPage: page }; + + let data = []; + try { + data = JSON.parse(res.body); + } catch (e) {} + + if (!Array.isArray(data)) return { comments: [], maxPage: page }; + + let comments = []; + let maxPage = 0 + if (replyTo) { + let target = data.find(item => item.Id.toString() === targetCommentId); + if (target && target.ToPostShowDataItems) { + comments = target.ToPostShowDataItems.map(item => new Comment({ + id: item.Id.toString(), + userName: item.Poster, + content: item.PostContent, + time: item.PostTime, + avatar: item.HeadUrl, + likeCount: item.PraiseCount, + isLiked: item.IsPraise, + replyCount: 0 + })); + } + } else { + comments = data.map(item => new Comment({ + id: `${item.Id}//${page}`, + userName: item.Poster, + content: item.PostContent, + time: item.PostTime, + avatar: item.HeadUrl, + likeCount: item.PraiseCount, + isLiked: item.IsPraise, + replyCount: item.ToPostShowDataItems ? item.ToPostShowDataItems.length : 0 + })); + if (comments == []){ + maxPage = page; + }else{ + maxPage = null; + } + } + + return { + comments: comments, + maxPage: replyTo? 1 : maxPage + }; + }, + } + +} \ No newline at end of file