@@ -9,7 +9,7 @@
// download(ctx) —— 下载 .torrent 种子文件字节(能力: download)
// sync(ctx) —— 站点信息同步( mediabot2 Host 调用:首页统计 + 账号资料)
// parse(content, url) —— 解析用户统计页(校验沙箱 /validate 使用, http 走 mock)
// rss(ctx) —— RSS/Atom 刷流(供 RssDiscoveryJob, 非标准能力)
// rss(ctx) —— RSS 刷流(供 RssDiscoveryJob, 非标准能力)
// ==================== 运行环境兼容层 ====================
// mediabot2 宿主注入: http / html / utils / json;
@@ -423,7 +423,7 @@ function parse(content, url) {
// 签到( signInPath 默认 /attendance.php, 可在站点 definition 中覆盖,如 BTSCHOOL: /index.php?action=addbonus)
// 成功时若响应页含 #info_block (含魔力值) 则一并返回 bonus 供服务端更新 Site.Statistics.Bonus;
// 站点页面不含 / 解析不到时缺省不返回该字段, 服务端不处理 (report §四 签到奖励自动更新).
function signIn ( input ) {
function sign ( input ) {
var ctx = _ _asCtx ( input ) ;
var signPath = ctx . signInPath || '/attendance.php' ;
var res = http . post ( signPath ) ;
@@ -555,6 +555,21 @@ function search(input) {
var title = titleEl . text ( ) ;
if ( ! title ) continue ;
// torrentnameHtml: 只定位、不解析. 把标题 <a> 所在 td 的 innerHTML 片段原样传给 C#,
// 由 TorrentNameHtmlParser 用 AngleSharp 精拆 subtitle + tags.
var torrentnameHtml = null ;
var titleTd = titleEl . closest ? titleEl . closest ( 'td' ) : null ;
if ( ! titleTd ) {
var p = titleEl . parentElement ;
while ( p && p . tagName && p . tagName . toLowerCase ( ) !== 'td' ) p = p . parentElement ;
titleTd = p ;
}
if ( titleTd && typeof titleTd . html === 'function' ) {
torrentnameHtml = titleTd . html ( ) ;
} else if ( titleTd && typeof titleTd . innerHTML !== 'undefined' ) {
torrentnameHtml = titleTd . innerHTML ;
}
var href = titleEl . attr ( 'href' ) ;
var detailUrl = null ;
if ( href ) {
@@ -624,7 +639,7 @@ function search(input) {
var encoding = parseMediaTag ( title , /\b(x264|x265|H\.?265|H\.?264|AVC|HEVC|VC-?1|XviD|AV1)\b/i ) ;
results . push ( {
title : title ,
torrentnameHtml : torrentnameHtml ,
detailUrl : detailUrl ,
downloadUrl : downloadUrl ,
size : size ,
@@ -659,6 +674,38 @@ function search(input) {
return results ;
}
// 从详情页 DOM 中提取外部 ID (doubanId / imdbId / tmdbId / tvdbId).
// NexusPHP 详情页的外部链接通常放在 #kdescr 描述块、#info_block 标签区或页面底部,
// 通过遍历所有 <a> 的 href 正则命中即可覆盖多数站点布局。
function extractExternalIds ( doc ) {
var ids = { } ;
try {
var links = doc . queryList ( 'a' ) ;
for ( var i = 0 ; i < links . length ; i ++ ) {
var href = links [ i ] . attr ( 'href' ) || '' ;
if ( ! ids . doubanId ) {
var dbm = href . match ( /douban\.com\/subject\/(\d+)/i ) ;
if ( dbm ) ids . doubanId = dbm [ 1 ] ;
}
if ( ! ids . imdbId ) {
var imm = href . match ( /imdb\.com\/title\/(tt\d+)/i ) ;
if ( imm ) ids . imdbId = imm [ 1 ] ;
}
if ( ! ids . tmdbId ) {
var tmm = href . match ( /themoviedb\.org\/(?:movie|tv)\/(\d+)/i ) ;
if ( tmm ) ids . tmdbId = parseInt ( tmm [ 1 ] , 10 ) ;
}
if ( ! ids . tvdbId ) {
var tvm = href . match ( /thetvdb\.com\/series\/(\d+)/i ) || href . match ( /tvdbid=(\d+)/i ) ;
if ( tvm ) ids . tvdbId = parseInt ( tvm [ 1 ] , 10 ) ;
}
// 四个全齐就提前退出
if ( ids . doubanId && ids . imdbId && ids . tmdbId && ids . tvdbId ) break ;
}
} catch ( e ) { /* 任何异常都静默降级, 返回部分结果 */ }
return ids ;
}
// ================= 能力: detail( 种子详情) =================
// ctx: { baseUrl, id (或 detailId), detailUrl?(可选, 直接指定详情页地址) }
// 输出字段与能力模板 detail 一致( size/seeders 等保留页面原始文本)。
@@ -722,9 +769,94 @@ function detail(input) {
if ( mhref . indexOf ( 'magnet:' ) === 0 ) { out . magnet = mhref ; break ; }
}
// 提取外部 ID (doubanId / imdbId / tmdbId / tvdbId)
var extIds = extractExternalIds ( doc ) ;
for ( var k in extIds ) if ( extIds [ k ] !== undefined ) out [ k ] = extIds [ k ] ;
return out ;
}
// fetchDetail: C# DetailVerifyJob 调用入口, 别名到 detail 函数.
// C# 端 Invoke("fetchDetail") 是外部约定, 这里保持兼容.
function fetchDetail ( input ) { return detail ( input ) ; }
// ================= 能力: hr( H&R 列表)=================
// 访问 myhr.php, 解析当前账号 H&R 考察中的种子列表.
// C# SiteHrSyncJob 约定入口名: "hr".
// ctx: { baseUrl, hrPath?(可选, 默认 /myhr.php) }
// 返回 { items: [...] } — 见 SiteScriptTorrentFetcher.ParseHrEntries 契约.
//
// #hr-table 表格列:
// H&R ID | 种子名称 | 上传量 | 下载量 | 分享率 | 还需做种时间 | 下载完成时间 | 剩余考察时间 | 备注 | 操作
//
// hrState 值: reviewing (考察中, 默认列表) | satisfied (已达标) | failed (未达标) | pardoned (已免罪)
// myhr.php 默认页 = status=1 考察中; 其他状态通过 ?status=2/3/4 切换.
function parseMyhr ( htmlStr ) {
var doc = html . parse ( htmlStr ) ;
var items = [ ] ;
var table = doc . querySelector ( '#hr-table' ) ;
if ( ! table ) return { items : items } ;
var rows = table . queryList ( 'tr' ) ;
for ( var i = 0 ; i < rows . length ; i ++ ) {
var tds = rows [ i ] . queryList ( 'td' ) ;
if ( tds . length < 5 ) continue ;
if ( ( tds [ 0 ] . attr ( 'class' ) || '' ) . indexOf ( 'colhead' ) !== - 1 ) continue ;
var hrId = ( tds [ 0 ] . text ( ) || '' ) . trim ( ) ;
if ( ! /^\d+$/ . test ( hrId ) ) continue ;
// 种子详情链接 → detailId
var nameTd = tds [ 1 ] ;
var detailId = '' ;
if ( nameTd ) {
var a = nameTd . querySelector ( 'a[href*="details.php"]' ) ;
if ( a ) {
var idM = ( a . attr ( 'href' ) || '' ) . match ( /details\.php\?.*?id=(\d+)/i ) ;
if ( idM ) detailId = idM [ 1 ] ;
}
}
if ( ! detailId ) continue ; // C# ParseHrEntries 跳过 detailId 为空的条目
// 剩余考察时间 → remainingSeconds
// 格式: "9天09:10:50" 或 "10:19:39" (不足一天时可能只有 HH:mm:ss)
var remain = tds [ 7 ] ? ( tds [ 7 ] . text ( ) || '' ) . trim ( ) : '' ;
var remainingSeconds = 0 ;
var daysMatch = remain . match ( /(\d+)\s*天/ ) ;
var days = daysMatch ? parseInt ( daysMatch [ 1 ] , 10 ) : 0 ;
var hms = remain . match ( /(\d{1,2}):(\d{2}):(\d{2})/ ) ;
if ( hms ) {
remainingSeconds = days * 86400
+ parseInt ( hms [ 1 ] , 10 ) * 3600
+ parseInt ( hms [ 2 ] , 10 ) * 60
+ parseInt ( hms [ 3 ] , 10 ) ;
} else if ( days > 0 ) {
remainingSeconds = days * 86400 ;
}
items . push ( {
detailId : detailId ,
remainingSeconds : remainingSeconds ,
hrState : remainingSeconds > 0 ? 'reviewing' : 'satisfied' ,
hrId : hrId
} ) ;
}
return { items : items } ;
}
function hr ( input ) {
var ctx = _ _asCtx ( input ) ;
var base = _ _resolveBase ( ctx ) ;
if ( ! base || base === 'http://mock.local' ) return { items : [ ] } ;
var path = ctx && ctx . hrPath ? ctx . hrPath : '/myhr.php' ;
var url = base + path ;
var htmlStr = http . get ( url ) ;
if ( ! htmlStr ) return { items : [ ] } ;
return parseMyhr ( htmlStr ) ;
}
// ================= 能力: download( 下载种子) =================
// 优先直接使用 ctx.downloadUrl / ctx.detailUrl (搜索解析时抓取的行内下载链接, 已含 passkey/uid 等完整参数, 如 pttime);
// 无行内链接时由 ctx.id (或 detailId) 构造, 可带 ctx.passkey (仅需 Cookie 的站点如 carpt 不传即可)。
@@ -766,9 +898,11 @@ function rss(ctx) {
var base = String ( ctx . baseUrl || '' ) . replace ( /\/+$/ , '' ) ;
var url = ctx . feedUrl ? String ( ctx . feedUrl ) : '' ;
if ( ! url ) {
var rssPath = ctx . rssPath || '/ torrentrss.php' ;
// NexusPHP RSS 入口有两种命名: getrss.php (新) 和 torrentrss.php (旧).
// 用户没手动填 feedUrl 时, 先试 getrss.php, 失败再回退 torrentrss.php.
var paths = ctx . rssPaths || [ '/getrss.php' , '/torrentrss.php' ] ;
if ( ! base ) return { error : 'no_feed_url' } ;
url = base + rssPath ;
url = base + paths [ 0 ] ;
}
var xml ;
try {