Files
par/adapters/demo-pt/adapter.js
2026-08-29 15:13:08 +00:00

613 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 优先:从 <a href="mailto:"> 提取最准确
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: 等级在 <b class="fcr"> 内
} 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>([^<]*)<\/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;
}