// NexusPHP 框架共用脚本:所有 NexusPHP 站点用同一份,站点差异由各自的 definition 选择器提供。 // mediabot2 经 Jint 沙箱执行,注入:content, url, http, html, json, utils // 入口函数: // parse(content, url) —— 解析用户统计页(校验沙箱 /validate 使用,http 走 mock) // sync(ctx) —— 同步站点信息:首页 index.php 解析统计 + userdetails.php?id={uid} 解析账号资料 // sign(ctx) —— 签到 // search(ctx, params) —— 搜索 // download(ctx) —— 下载 .torrent 种子文件字节 function unitToBytes(unit) { if (!unit) return 1; switch (String(unit).toUpperCase()) { case 'PB': return 1024 * 1024 * 1024 * 1024 * 1024; case 'TB': return 1024 * 1024 * 1024 * 1024; case 'GB': return 1024 * 1024 * 1024; case 'MB': return 1024 * 1024; case 'KB': return 1024; default: return 1; } } // 标签后直接匹配数值(兼容 "上传量: 1.23 TB" / "上传量 1.23 TB" / 表格布局) function parseSize(text, labelPattern) { var re = new RegExp(labelPattern + '\\s*[::]?\\s*([\\d.,]+)\\s*(TB|GB|MB|KB|PB|B)?', 'i'); var m = text.match(re); if (!m) return null; var num = parseFloat(m[1].replace(/,/g, '')); if (isNaN(num)) return null; return Math.round(num * unitToBytes(m[2])); } function parseDecimal(text, labelPattern) { var re = new RegExp(labelPattern + '\\s*[::]?\\s*([\\d.,]+)', 'i'); var m = text.match(re); if (!m) return null; var v = parseFloat(m[1].replace(/,/g, '')); return isNaN(v) ? null : v; } function parseInteger(text, labelPattern) { var re = new RegExp(labelPattern + '\\s*[::]?\\s*([\\d,]+)', 'i'); var m = text.match(re); if (!m) return null; var v = parseInt(m[1].replace(/,/g, ''), 10); return isNaN(v) ? null : v; } // 从信息块解析 uid / 用户名(userdetails 链接) function parseIdentity(block) { var uid = null; var userName = null; var link = block ? block.querySelector('a[href*="userdetails"]') : null; if (link) { var href = link.attr('href') || ''; var um = href.match(/[?&]id=(\d+)/i); if (um) uid = um[1]; var t = link.text(); if (t) userName = t; } return { uid: uid, userName: userName }; } // 解析首页统计:上传/下载/魔力/分享率/做种/H&R function parseIndexStats(indexHtml) { var doc = html.parse(indexHtml); var block = doc.querySelector('#info_block'); var text = block ? block.text() : indexHtml; var up = parseSize(text, '(?:上传量?|上傳量?)'); var down = parseSize(text, '(?:下载量?|下載量?)'); if (up === null && down === null) return null; var bonus = parseDecimal(text, '魔力值(?:\\[\\s*使用\\s*\\])?'); if (bonus === null) { // PTTIME: 魔力值(77.45魔力/小时)[使用&说明]: 799327.2 var bm = text.match(/魔力值.*?[::]\s*([\d.,]+)/i); if (bm) bonus = parseFloat(bm[1].replace(/,/g, '')); } var ratio = parseDecimal(text, '(?:分享率|分享比率)'); var seeding = null; var act = text.match(/当前活动\s*[::]\s*(\d+)\s+(\d+)/i); if (!act) act = text.match(/活动\s*[::]\s*[^\d]*(\d+)[^\d]*(\d+)/i); // PTTIME: "活动: ⬆16 ⬇2" if (act) { seeding = parseInt(act[1], 10); } else { seeding = parseInteger(text, '(?:做种|做種)'); } var hr = null; var hrM = text.match(/H&R\s*[::]\s*\[\s*\d+\s*\/\s*(\d+)\s*\/\s*\d+\s*\]/i); if (hrM) hr = parseInt(hrM[1], 10); var identity = parseIdentity(block || doc); return { uploaded: up, downloaded: down, bonus: bonus, ratio: ratio, seeding: seeding, hr: hr, uid: identity.uid, userName: identity.userName }; } // 解析 userdetails.php 账号资料:邮箱/等级/注册日期 + uid/用户名 // 注意:userdetails 的字段在 #info_block 之外的资料表格中,故用整页 body 文本解析 function parseUserDetails(userHtml) { var doc = html.parse(userHtml); var body = doc.querySelector('body') || doc; var text = body.text() || userHtml; var email = null; // DOM 优先:从 提取最准确 var mailLink = doc.querySelector('a[href*="mailto:"]'); if (mailLink) { var href = mailLink.attr('href') || ''; var mm = href.match(/mailto:([\w.+-]+@[\w.-]+\.\w+)/i); if (mm) email = mm[1]; else email = mailLink.text(); } // 退路:正则从 body text 搜 if (!email) { var em = text.match(/(?:邮箱|Email|E-mail)\s*[::]?\s*([\w.+-]+@[\w.-]+\.\w+)/i); if (em) email = em[1]; } // 等级:DOM 解析,遍历每行 tr,找 label td 文本为 等级/级别/Class 的行,取其右边 td 的内容 var classLevel = null; var rows = doc.queryList('tr'); for (var i = 0; i < rows.length && !classLevel; i++) { var cells = rows[i].queryList('td'); if (cells.length >= 2) { var label = cells[0].text(); if (label === '等级' || label === '级别' || label === 'Class' || label === '用户等级') { var img = cells[1].querySelector('img'); var b = cells[1].querySelector('b'); if (img) { classLevel = img.attr('alt'); } else if (b) { classLevel = b.text(); // PTTIME: 等级在 内 } else { classLevel = cells[1].text(); } // 去掉前导括号前缀,如 "(幼儿园)User" → "User" if (classLevel) { classLevel = classLevel.replace(/^\s*\([^)]*\)\s*/, ''); } } } } var registerDate = null; var regM = text.match(/(?:注册日期|加入日期|Registered)\s*[::]?\s*(\d{4}-\d{2}-\d{2})/i); if (regM) registerDate = regM[1]; var identity = parseIdentity(doc.querySelector('#info_block') || doc); return { uid: identity.uid, userName: identity.userName, email: email, classLevel: classLevel, registerDate: registerDate }; } // 同步入口:mediabot2 调用 sync(ctx) 完成站点信息同步 // ctx: { siteCode, siteId, baseUrl, uid, userName, utcNow, frameworkVersion } // 返回 null 表示首页未匹配(上传/下载都解析不到),Host 视为失败。 // sync entry: called by mediabot2 Host to sync site info. // ctx: { siteCode, siteId, baseUrl, uid, userName, utcNow, frameworkVersion } // Success: returns stats object. Failure: returns { error, html?, userHtml? } carrying raw HTML // so Host can report it verbatim to Registry. NEVER return null; always include `error`. function sync(ctx) { var base = String(ctx && ctx.baseUrl ? ctx.baseUrl : '').replace(/\/+$/, ''); if (!base) return { error: 'no_base_url' }; // 1. fetch index page (index.php first, root fallback) var indexHtml = null; var paths = ['/index.php', '/']; for (var i = 0; i < paths.length; i++) { try { indexHtml = http.get(base + paths[i]); } catch (e) { indexHtml = null; } if (indexHtml && indexHtml.length > 0) break; } if (!indexHtml) return { error: 'index_fetch_failed' }; var indexStats = parseIndexStats(indexHtml); if (!indexStats) return { error: 'index_no_match', html: indexHtml }; // 2. if uid known, fetch userdetails.php?id={uid} to parse profile var uid = indexStats.uid || (ctx && ctx.uid) || null; var profile = null; var userHtml = null; if (uid) { try { userHtml = http.get(base + '/userdetails.php?id=' + encodeURIComponent(uid)); if (userHtml && userHtml.length > 0) { profile = parseUserDetails(userHtml); // content fetched but no profile fields parsed (CAPTCHA / login wall): treat as failure if (profile && profile.email === null && profile.classLevel === null && profile.registerDate === null) { profile = null; } } } catch (e) { profile = null; } } // userdetails fetch/parse failed: stats available, attach userHtml for Host reporting if (uid && !profile) { return { uploaded: indexStats.uploaded, downloaded: indexStats.downloaded, bonus: indexStats.bonus, ratio: indexStats.ratio, seeding: indexStats.seeding, hr: indexStats.hr, uid: uid, userName: indexStats.userName || (ctx && ctx.userName) || null, email: null, classLevel: null, registerDate: null, error: 'userdetails_no_match', html: indexHtml, userHtml: userHtml }; } return { uploaded: indexStats.uploaded, downloaded: indexStats.downloaded, bonus: indexStats.bonus, ratio: indexStats.ratio, seeding: indexStats.seeding, hr: indexStats.hr, uid: uid, userName: (profile && profile.userName) || indexStats.userName || (ctx && ctx.userName) || null, email: (profile && profile.email) || null, classLevel: (profile && profile.classLevel) || null, registerDate: (profile && profile.registerDate) || null }; } function parse(content, url) { var doc = html.parse(content); return { uploaded: doc.queryText('#seedup a'), downloaded: doc.queryText('#seeddown a'), bonus: doc.queryText('#seedbonus') }; } // 签到(signInPath 默认 /attendance.php,可在站点 definition 中覆盖,如 BTSCHOOL: /index.php?action=addbonus) // 成功时若响应页含 #info_block (含魔力值) 则一并返回 bonus 供服务端更新 Site.Statistics.Bonus; // 站点页面不含 / 解析不到时缺省不返回该字段, 服务端不处理 (report §四 签到奖励自动更新). function sign(ctx) { var signPath = ctx.signInPath || '/attendance.php'; var res = http.post(signPath); if (res && res.indexOf('成功') >= 0) { var out = { success: true }; try { var stats = parseIndexStats(res); if (stats && stats.bonus !== null && stats.bonus !== undefined && !isNaN(stats.bonus)) { out.bonus = stats.bonus; } } catch (e) { // bonus 解析失败不影响签到成功结果 } return out; } return { success: false }; } // 解析裸大小文本 "1.23 GB" / "986.5 MB" / "1.2 TB" → 字节数 function parseSizeText(text) { if (!text) return 0; var m = String(text).match(/([\d.,]+)\s*(TB|GB|MB|KB|PB|B)\b/i); if (!m) return 0; var num = parseFloat(m[1].replace(/,/g, '')); if (isNaN(num)) return 0; return Math.round(num * unitToBytes(m[2])); } // 提取非负整数(忽略逗号与装饰性字符) function toInt(text) { if (text === null || text === undefined) return 0; var m = String(text).match(/[\d,]+/); if (!m) return 0; var v = parseInt(m[0].replace(/,/g, ''), 10); return isNaN(v) ? 0 : v; } // 从标题里按正则提取首个媒体标签 (分辨率/来源/编码),供订阅偏好过滤与质量评分 function parseMediaTag(title, re) { if (!title) return null; var m = String(title).match(re); if (!m) return null; var v = m[1] || m[0]; return v ? v.toUpperCase() : null; } // 搜索:在站点内搜索关键词,返回结构化种子列表(供订阅自动匹配)。 // ctx: { siteCode, baseUrl, keyword, page(=1), searchPath(可选, 默认 /torrents.php) } // 走 http(已注入 Cookie 的 HttpClient),解析 table.torrents 种子行。 function search(ctx) { var keyword = ctx && ctx.keyword ? String(ctx.keyword) : ''; if (!keyword) return []; var base = String(ctx.baseUrl || '').replace(/\/+$/, ''); if (!base) return []; var page = ctx.page ? parseInt(ctx.page, 10) : 1; if (isNaN(page) || page < 1) page = 1; var searchPath = ctx.searchPath || '/torrents.php'; // 首页不拼 &page=N:NexusPHP 分页分 0 基/1 基两种,部分站点(如 carpt)在 // ?search=X&page=1 时实际请求的是"第 2 页",而结果往往只有一页 → 返回空表(仅搜索框)。 // 统一用裸 ?search=X 取首页,对两种索引方案都正确返回第 1 页。 var query = '?search=' + encodeURIComponent(keyword); var url = (page > 1) ? (base + searchPath + query + '&page=' + page) : (base + searchPath + query); var htmlText = http.get(url); if (!htmlText) return []; var doc = html.parse(htmlText); if (!doc) return []; // ---- 诊断: 校验返回结构, 便于排查搜索空结果 (源码原始 html 特征) ---- // 注意: 不能用 "Powered by NexusPHP" 或站名判断登录墙 —— 这些字串每个 NexusPHP 站页面都有. // 登录墙的可靠特征是: 存在登录表单 (带 password input / action 指向 login 页). var htmlTag = null; var titleTag = (htmlText.match(/([^<]*)<\/title>/i) || [])[1] || ''; var tableCount = (htmlText.match(/<table[^>]*class="torrents"[^>]*>/i) || []).length; var hasLoginForm = /<input[^>]*type="Password"[^>]*>/i.test(htmlText) || /<form[^>]*action="[^"]*login[^"]*"[^>]*>/i.test(htmlText) || /name="(?:username|loginusername|userpasswd)"[^>]*>/i.test(htmlText) || /登录到|login in|请登陆|请登录/i.test(titleTag); // 采集页面所有的 table class + 种子详情链接数, 用于判断选择器是否匹配. var tableClasses = []; var tcm = htmlText.match(/<table[^>]*class="([^"]*)"[^>]*>/gi); if (tcm) for (var tc = 0; tc < tcm.length; tc++) { var cls = (tcm[tc].match(/class="([^"]*)"/i) || [])[1] || ''; if (cls && tableClasses.indexOf(cls) < 0) tableClasses.push(cls); } var detailLinkCount = (htmlText.match(/href="[^"]*details\.php[^"]*"/gi) || []).length; if (hasLoginForm) { htmlTag = 'loginwall'; } else if (detailLinkCount > 0) { htmlTag = 'has_details_' + detailLinkCount; } else if (titleTag && /无.*种子|没有|未找到|No|结果/i.test(titleTag)) { htmlTag = 'noresult:' + titleTag; } else if (tableCount === 0) { htmlTag = 'no_torrents_table'; } else { htmlTag = 'ok'; } // 候选选择器命中数: 定位 carpt 种子列表的真实容器 (标准 nexusphp 是 table.torrents, 自定义皮站常不同) var selCands = ['table.torrents tr', 'table[class*="torrent"] tr', '#torrents tr', 'tr.rowfollow', '.torrent_name', '.torrenttitle', 'td.rowfollow', 'table tr']; var selHits = []; for (var si = 0; si < selCands.length; si++) { try { selHits.push(selCands[si] + '=' + doc.queryList(selCands[si]).length); } catch (e) { selHits.push(selCands[si] + '=ERR'); } } // 原始响应 HTML 关键片段: 后端会将其落盘为快照文件 (search-snapshots/) 便于调试, 此处保留完整内容. // ---- 诊断结束 ---- // 取所有表格行, 再由 td.rowfollow + details 链接 过滤出真实种子行. // 注意: 不能用 table.torrents —— 不少自定义皮站 (如 carpt) 种子列表父表 class 不是 torrents. // 侧边搜索/筛选区同样含 td.rowfollow, 故必须额外要求含 details* 链接才算种子行. var rows = doc.queryList('table tr'); var results = []; for (var i = 0; i < rows.length; i++) { var row = rows[i]; // 仅取直接子 td:carpt 等站点在标题单元格内嵌套了 torrentname 子表, // 用 'td' 会把后代 td(embedded) 一并计入,导致列索引错位、体积/做种数全变 0. var tds = row.queryList(':scope > td'); // 种子行判定:含 td.rowfollow(排除搜索/筛选/表头) 且拥有多个直接子 td(标准约 9 列)。 // 仅 1 个直接子 td 的 tr 多为置顶/推荐/嵌套区块的重复条目(数字无法解析),直接跳过. if (tds.length < 6) continue; if (row.queryList('td.rowfollow').length === 0) continue; var titleEl = row.querySelector('a[href*="details"]'); if (!titleEl) continue; var title = titleEl.text(); if (!title) continue; var href = titleEl.attr('href'); var detailUrl = null; if (href) { detailUrl = (href.indexOf('http') === 0) ? href : (base + (href[0] === '/' ? href : '/' + href)); } // 行内下载链接: 下载图标指向 download.php?… (passkey 站点已带 passkey/uid 等完整参数, 如 pttime; // 仅需 Cookie 的站点如 carpt 则不带额外参数). 下载时直接复用该链接, 无需再拼 passkey. var downloadUrl = null; var dlLink = row.querySelector('a[href*="download.php"]'); if (dlLink) { var dhref = dlLink.attr('href') || ''; if (dhref) { downloadUrl = (dhref.indexOf('http') === 0) ? dhref : (base + (dhref[0] === '/' ? dhref : '/' + dhref)); } } var size = 0, seeders = 0, leechers = 0, completed = 0; if (tds.length > 7) { size = parseSizeText(tds[4] ? tds[4].text() : ''); seeders = toInt(tds[5] ? tds[5].text() : ''); leechers = toInt(tds[6] ? tds[6].text() : ''); completed = toInt(tds[7] ? tds[7].text() : ''); } var yearM = title.match(/\b(19|20)\d{2}\b/); var year = yearM ? parseInt(yearM[0], 10) : null; // 促销/免费/H&R 标记:图标位于标题单元格内 (img.pro_free / img.pro_50pctdown / img.hitandrun / alt) var isFree = false, isHr = false, freeDeadline = null, promo = null; var imgs = row.queryList('img'); for (var pi = 0; pi < imgs.length; pi++) { var pcls = imgs[pi].attr('class') || ''; var palt = imgs[pi].attr('alt') || ''; if (/pro_free|pro_100pctdown/i.test(pcls) || /\bfree\b|100%/i.test(palt)) { isFree = true; promo = 'free'; } else if (/pro_50pctdown|pro_free2up/i.test(pcls) || /50%|2x/i.test(palt)) { promo = 'half'; } if (/hitandrun/i.test(pcls) || /\bH&?R\b/i.test(palt)) isHr = true; } // 免费/促销截止时间 + 发布时间:行内真实存在的 <span title="2027-03-02 10:33:29">("剩余时间:"后), // 取最大者(未来截止>过去添加时间)为免费截止,取最小者(最早)为种子发布时间. var spans = row.queryList('span[title]'); var maxDt = null, minDt = null; for (var si = 0; si < spans.length; si++) { var sttl = spans[si].attr('title') || ''; var sdm = sttl.match(/^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/); if (!sdm) continue; if (maxDt === null || sdm[1] > maxDt) maxDt = sdm[1]; if (minDt === null || sdm[1] < minDt) minDt = sdm[1]; } if (maxDt) freeDeadline = maxDt.replace(' ', 'T'); var uploadAt = minDt ? minDt.replace(' ', 'T') : null; // 分类:行首分类图标 (class 以 c_ 开头, alt/title=音乐/Music 等),取首个匹配 var category = null; var cimgs = row.queryList('td.rowfollow img'); for (var ci = 0; ci < cimgs.length; ci++) { var ccls = cimgs[ci].attr('class') || ''; if (/^c_/i.test(ccls)) { category = cimgs[ci].attr('title') || cimgs[ci].attr('alt') || null; break; } } // 分辨率/来源/编码:从标题文本解析,供订阅偏好过滤 (PreferredResolution/Source/Encoding) 与质量评分 var resolution = parseMediaTag(title, /\b(\d{3,4}[pi])\b/i); var source = parseMediaTag(title, /\b(WEB-DL|WEBRip|WEB|HDTV|Blu-?Ray|DVD|Remux|BDrip|BDRip|HDrip|DVDRip)\b/i); var encoding = parseMediaTag(title, /\b(x264|x265|H\.?265|H\.?264|AVC|HEVC|VC-?1|XviD|AV1)\b/i); results.push({ title: title, detailUrl: detailUrl, downloadUrl: downloadUrl, size: size, seeders: seeders, leechers: leechers, completed: completed, year: year, resolution: resolution, source: source, encoding: encoding, category: category, isFree: isFree, isHr: isHr, uploadAt: uploadAt, freeDeadline: freeDeadline, promo: promo }); } if (results.length === 0) { // 无结果时附加诊断信息: 供后端日志区分登录墙/无结果页/选择器不匹配, 前端会按空 title 过滤 results.push({ title: '', __diag: htmlTag || 'unknown', __url: url, __len: htmlText ? htmlText.length : 0, __tables: tableClasses.join(','), __details: detailLinkCount, __sel: selHits.join(' | '), __html: htmlText || '' }); } return results; } // 下载种子入口:mediabot2 调用 download(ctx) 下载 .torrent 种子文件字节。 // 优先直接使用 ctx.downloadUrl (搜索解析时抓取的行内下载链接, 已含 passkey/uid 等完整参数, 如 pttime); // 无行内链接时由 detailId 构造, 可带 ctx.passkey (仅需 Cookie 的站点如 carpt 不传即可)。 // 站点会话 (Cookie) 已在 http 客户端注入, Referer 由 Host 设置。 // 必须走脚本内 http, 不能由外部用裸链接直接 GET。 // ctx: { siteCode, siteId, baseUrl, apiUrl, detailId, passkey?, downloadUrl?, downloadPath?(默认 /download.php) } // 返回 { bytes: byte[], url: string } 或 { error: string, url?: string }. function download(ctx) { var base = String(ctx.baseUrl || '').replace(/\/+$/, ''); var url = null; if (ctx.downloadUrl) { url = String(ctx.downloadUrl); if (url.indexOf('http') !== 0) url = base + (url[0] === '/' ? url : '/' + url); } else { var detailId = ctx && ctx.detailId ? String(ctx.detailId) : ''; if (!detailId) return { error: 'no_detail_id' }; if (!base) return { error: 'no_base_url' }; var path = ctx.downloadPath || '/download.php'; url = base + path + '?id=' + encodeURIComponent(detailId); var passkey = ctx.passkey ? String(ctx.passkey) : ''; if (passkey) url += '&passkey=' + encodeURIComponent(passkey); } try { var bytes = http.getBytes(url); if (!bytes || bytes.length === 0) return { error: 'empty_response', url: url }; return { bytes: bytes, url: url }; } catch (e) { return { error: String((e && e.message) || e), url: url }; } } // 刷流入口:拉取站点 RSS/Atom feed 并解析为标准条目列表,供 RssDiscoveryJob 刷流。 // 必须走脚本内 http(带站点 Cookie),与 search/download 一致的会话上下文。 // ctx: { siteCode, baseUrl, feedUrl, rssPath?(可选, 默认 /torrentrss.php) } // 优先使用 ctx.feedUrl(RssSubscription 配置的完整 RSS 链接,已含 passkey 等参数); // 缺省时回退到 baseUrl + rssPath 拼装。 // 返回: [{ title, detailUrl, downloadUrl, description, pubDate, guid }] 或 { error } function rss(ctx) { var base = String(ctx.baseUrl || '').replace(/\/+$/, ''); var url = ctx.feedUrl ? String(ctx.feedUrl) : ''; if (!url) { var rssPath = ctx.rssPath || '/torrentrss.php'; if (!base) return { error: 'no_feed_url' }; url = base + rssPath; } var xml; try { xml = http.get(url); } catch (e) { return { error: String((e && e.message) || e), url: url }; } if (!xml) return { error: 'empty_feed', url: url }; // Atom (<entry>) 与 RSS (<item>) 都支持:分别抽取后合并。 var items = []; function tag(block, name) { var m = block.match(new RegExp('<' + name + '[^>]*>([\\s\\S]*?)<\\/' + name + '>', 'i')); return m ? m[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').trim() : ''; } function attr(block, name, an) { var m = block.match(new RegExp(name + '\\s+' + an + '\\s*=\\s*["\']([^"\']*)["\']', 'i')); return m ? m[1] : ''; } // RSS 2.0: <item>…</item> var blocks = xml.match(/<item[\s\S]*?<\/item>/gi); if (blocks) { for (var i = 0; i < blocks.length; i++) { var b = blocks[i]; var title = tag(b, 'title'); var link = tag(b, 'link'); var desc = tag(b, 'description'); var guid = tag(b, 'guid') || link; var pub = tag(b, 'pubDate'); var enc = (b.match(/<enclosure[^>]*>/i) || [])[0] || ''; var enclosureUrl = attr(enc, 'enclosure', 'url'); items.push({ title: title, detailUrl: link, downloadUrl: enclosureUrl || link, description: desc, pubDate: pub, guid: guid }); } } // Atom: <entry>…</entry> var atomBlocks = xml.match(/<entry[\s\S]*?<\/entry>/gi); if (atomBlocks) { for (var j = 0; j < atomBlocks.length; j++) { var a = atomBlocks[j]; var at = tag(a, 'title'); var aid = tag(a, 'id'); var updated = tag(a, 'updated') || tag(a, 'published'); var summary = tag(a, 'summary') || tag(a, 'content'); // Atom link:优先 rel="alternate" 的 href;无则取首个 href var links = a.match(/<link[^>]*>/gi) || []; var altHref = '', firstHref = ''; for (var k = 0; k < links.length; k++) { var lh = attr(links[k], 'link', 'href'); if (!firstHref) firstHref = lh; if (/rel=["']alternate["']/i.test(links[k])) altHref = lh; } var href = altHref || firstHref; items.push({ title: at, detailUrl: href, downloadUrl: href, description: summary, pubDate: updated, guid: aid }); } } if (items.length === 0) { return { error: 'no_items', url: url, __len: xml.length, __html: xml }; } return items; }