// Torrent Info // ©2024 bytim //参考: //https://en.wikipedia.org/wiki/Torrent_file //https://en.wikipedia.org/wiki/Magnet_uri //https://ted423.github.io/Document/Network/magnet/ //http://www.bittorrent.org/beps/bep_0000.html BitTorrent增强建议索引 //http://bittorrent.org/beps/bep_0009.html //https://github.com/bittorrent/bittorrent.org //http://www.python.org/dev/peps/pep-0001 //https://github.com/nutbread/t2m/tree/gh-pages JS种子转磁力链接,速度极快 //https://blog.rhilip.info/archives/1255/ Bittorrent v2对比及实践相关 //https://pypi.org/project/BitTorrent-bencode/#files //https://github.com/arvidn/libtorrent/ //https://resource.dopus.com/t/torrent-file-info/39836 //010 Editor 的脚本模板 Torrent.bt (支持torrent v2) //https://www.sweetscape.com/010editor/repository/templates/ //https://www.sweetscape.com/010editor/repository/files/Torrent.bt //v1.0 2023.11.06 //实现基本decode功能,及自定义列 //v1.1 2023.12.14 //添加了3个分组规则:总大小 文件数 制种日期 //v1.2.1 2023.12.19 //优化了Decode函数,更简洁 //v1.3.1 2023.12.21 //添加了Comment备注列 //添加了对GBK编码的处理 //添加了Publisher列 //v1.3.2 2024.2.2 //修正了对 "10:created by0:" 这种长度为0的字符串的处理 //修正了对 "9:publisher"与"19:publisher-url.utf-8" 可能混淆的解析 //v1.3.3 2024.4.15 //addColumn添加列时,"CreationDate" 改为 Global["Torrent.DateGroup"] //全局定义脚本的基本信息,用于初始化OnInit和关于OnAboutScript等函数============= var Global = { }; Global.SCRIPT_NAME = "Torrent Info"; Global.SCRIPT_DESC = DOpus.strings.Get("ScriptDesc"); Global.SCRIPT_VERSION = 'v1.3.3'; Global.SCRIPT_COPYRIGHT = '©2024 bytim'; Global.SCRIPT_MIN_VERSION = '12.23'; Global.SCRIPT_DATE = '2024.4.15'; Global.SCRIPT_GROUP = 'bytim'; Global.cfgGroupRange = DOpus.strings.Get("cfgGroupRange"); Global["Torrent.SizeGroup"] = 'TotalSize'; Global["Torrent.SizeByteGroup"] = 'TotalSizeByte'; Global["Torrent.CountGroup"] = 'FileCount'; Global["Torrent.DateGroup"] = 'CreationDate'; //=============================================================== function OnInit(data) { data.name = Global.SCRIPT_NAME; data.version = Global.SCRIPT_VERSION; data.copyright = Global.SCRIPT_COPYRIGHT; data.url = "https://resource.dopus.com/t/column-torrentinfo-bt-bdecode-parsing-torrent-file-to-generate-metadata-columns/49741"; data.desc = Global.SCRIPT_DESC; data.default_enable = true; data.min_version = Global.SCRIPT_MIN_VERSION; //data.group = Global.SCRIPT_GROUP /////////////////////////////////////////////////////////////////////////// function ConfigHelper(data){ //v1.3 add config_groups var t=this; t.d=data; t.c=data.config; t.cd=DOpus.Create.Map();t.cg=DOpus.Create.Map(); t.add=function(name, val, des){ t.l={n:name,ln:name. toLowerCase()}; return t.val(val).des(des);} t.des=function(des){ if (!des) return t; if (t.cd.empty) t.d.config_desc=t.cd; t.cd(t.l.n)=des; return t;} t.val=function(val){ var l=t.l; if (l.v!==l.x&&typeof l.v=="object") l.v.push_back(val);else l.v=t.c[l.n]=val;return t;} t.grp=function(grp){ if (!grp) return t; if (t.cg.empty) t.d.config_groups=t.cg; t.cg(t.l.n)=grp; return t;} t.trn=function(){return t.des(t("script.config."+t.l.ln));} } /////////////////////////////////////////////////////////////////////////// var cfg = new ConfigHelper(data); cfg.add('Torrent.DateGroup', DOpus.Create.Vector()) .val("0;86400;"+DOpus.strings.Get("DateGroup1")) .val("86400;259200;"+DOpus.strings.Get("DateGroup2")) .val("259200;604800;"+DOpus.strings.Get("DateGroup3")) .val("604800;1296000;"+DOpus.strings.Get("DateGroup4")) .val("1296000;2592000;"+DOpus.strings.Get("DateGroup5")) .val("2592000;7776000;"+DOpus.strings.Get("DateGroup6")) .val("7776000;31557600;"+DOpus.strings.Get("DateGroup7")) .val("31557600;63115200;"+DOpus.strings.Get("DateGroup8")) .val("63115200;94672800;"+DOpus.strings.Get("DateGroup9")) .val("94672800;0;"+DOpus.strings.Get("DateGroup10")) .des('制种时间分组范围与名称') .grp(Global.cfgGroupRange); cfg.add('Torrent.SizeGroup', DOpus.Create.Vector()) .val("0;1048576;0 - 1 MB;"+DOpus.strings.Get("SizeGroup1")) .val("1048576;52428800;1 - 50 MB;"+DOpus.strings.Get("SizeGroup2")) .val("52428800;524288000;50 - 500 MB;"+DOpus.strings.Get("SizeGroup3")) .val("524288000;2147483648;500 MB - 2 GB;"+DOpus.strings.Get("SizeGroup4")) .val("2147483648;53687091200;2 - 50 GB;"+DOpus.strings.Get("SizeGroup5")) .val("53687091200;0;> 50 GB;"+DOpus.strings.Get("SizeGroup6")) .des('总大小分组范围与名称') .grp(Global.cfgGroupRange); cfg.add('Torrent.CountGroup', DOpus.Create.Vector()) .val("0;2;"+DOpus.strings.Get("CountGroup1")) .val("2;11;"+DOpus.strings.Get("CountGroup2")) .val("11;51;"+DOpus.strings.Get("CountGroup3")) .val("51;151;"+DOpus.strings.Get("CountGroup4")) .val("151;1001;"+DOpus.strings.Get("CountGroup5")) .val("1001;10001;"+DOpus.strings.Get("CountGroup6")) .val("10001;0;"+DOpus.strings.Get("CountGroup7")) .des('文件数分组范围与名称') .grp(Global.cfgGroupRange); } function OnAddColumns(AddColData){ addColumn(AddColData, "MetaName", "Column_GetMultiInfo", "left", "text", true, false, true,"","","","m"); addColumn(AddColData, Global["Torrent.DateGroup"], "Column_GetMultiInfo", "center", "datetime", true, true, false, "Torrent.DateGroup", 0); addColumn(AddColData, "Tracker", "Column_GetMultiInfo", "left", "text", true, true, true); addColumn(AddColData, "CreatedBy", "Column_GetMultiInfo", "left", "text", true, true, true); addColumn(AddColData, "Encoding", "Column_GetMultiInfo", "center", "text", true, false, true); addColumn(AddColData, "Comment", "Column_GetMultiInfo", "left", "text", true, false, true); addColumn(AddColData, "Publisher", "Column_GetMultiInfo", "left", "text", true, false, true); addColumn(AddColData, Global["Torrent.SizeGroup"], "Column_SizeCount", "right", "size", true, false, false, "Torrent.SizeGroup", 1); addColumn(AddColData, Global["Torrent.SizeByteGroup"], "Column_SizeCount", "right", "", true, false, false, "Torrent.SizeGroup", 1); addColumn(AddColData, Global["Torrent.CountGroup"], "Column_SizeCount", "right", "number", true, false, false, "Torrent.CountGroup", 1, -1); } function addColumn(data, name, method, justify, type, multicol, autorefresh, autogroup, cfgGroupKey, grpOrder, defsort, ellipsis) { var colPrefix = 'Ti '; var col = data.AddColumn(); col.keyscroll = true; col.name = name; col.header = DOpus.strings.Get(name); col.label = colPrefix + col.header; col.method = method; col.justify = justify; col.type = type; col.multicol = multicol; col.autorefresh = autorefresh; if(!autogroup) { col.autogroup = autogroup; col.grouporder = OrderGroup(cfgGroupKey, grpOrder); } if(defsort==-1) col.defsort = -1;//列的排序默认是asc升序,若 defsort=-1 则是desc逆序,日期、大小类型默认是desc,无须设置此值 if(ellipsis=="m" && DOpus.version.AtLeast("13.1")) //超长时省略,默认是在尾部省略为"...",ellipsis="m" 则将中间省略为"..." col.ellipsis = "m"; //v13才增加的属性 (v12会报错不支持此属性) //col.graph_colors.push_back(graph_colors) } function OrderGroup(cfgGroupKey, grpOrder){ //order=0, asc; order=1, desc var groupNames = new Array(); cfgItem = Script.config[cfgGroupKey]; for(var v=0;v0){ //DOpus.Output("typeof fileList(0): " + DOpus.TypeOf(fileList(0))) for (var e = new Enumerator(fileList); !e.atEnd(); e.moveNext()) { if(e.item().exists("path") && e.item()("path")(0).search(/^_____padding_file/i) != -1 ) { //DOpus.Output(e.item()("path")(0)) continue; } else { fileCount++; totalSize.add(e.item()("length")); } } } */ data.columns(Global["Torrent.SizeGroup"]).value = total.totalSize; data.columns(Global["Torrent.SizeByteGroup"]).value = total.totalSize; data.columns(Global["Torrent.CountGroup"]).value = total.fileCount; data.columns(Global["Torrent.SizeGroup"]).group = InGroup("Torrent.SizeGroup", total.totalSize); data.columns(Global["Torrent.SizeByteGroup"]).group = InGroup("Torrent.SizeGroup", total.totalSize); data.columns(Global["Torrent.CountGroup"]).group = InGroup("Torrent.CountGroup", total.fileCount); } //分组归属处理 function InGroup(cfgkey, value){ var result = ""; for(var v=0;v=min){ result = parts[3]+" ("+parts[2]+")"; break; } } else if (value=min){ result = parts[3]+" ("+parts[2]+")"; break; } } return result; } //================================================================ function getChar(Blob, pos, size, enc) { if (size==0) return ""; else { enc = typeof(enc)=="undefined"?"utf-8":enc; var tmp = DOpus.Create.Blob; tmp.CopyFrom(Blob,0,pos,size); return StrTools.decode(tmp, enc); } } function Decode(Blob, pos, total, enc){ var Char = getChar(Blob, ++pos, 1);//get next char, and move to next position; "++pos" also converts its type to number. //DOpus.Output("start: " + pos +" char:" + Char); switch (Char) { case "d": var dict = DOpus.Create.Map; var key = ""; Char = getChar(Blob, pos+1, 1);//get next char while (Char != 'e'){ var result = Decode(Blob, pos, total); key = result.c; pos = result.pos; key = key.toLowerCase(); //DOpus.Output("switch key: "+ key + " pos:" + pos); /* switch (key) { case "path": case "length": result = Decode(Blob, pos); dict(key) = result.c; pos = result.pos; //DOpus.Output("length: "+result.c+" pos:" + pos); break; default: result = Decode(Blob, pos); pos = result.pos; //DOpus.Output("other end pos: "+ pos ); } */ result = Decode(Blob, pos, total); pos = result.pos; if(key=="path" || key=="length") dict(key) = result.c; //if(DOpus.TypeOf(result.c)=="object.Vector") DOpus.Output("key:"+key+" result.c:" + result.c(0)); Char = getChar(Blob, pos+1, 1); } //DOpus.Output("dict end:" + pos); return {c:dict, pos:++pos} break; case "l": var list = DOpus.Create.Vector; Char = getChar(Blob, pos+1, 1); while (Char != 'e'){ var result = Decode(Blob, pos, total); //DOpus.Output("pos:" + pos + " " + DOpus.TypeOf(result.c)) if(DOpus.TypeOf(result.c)=="object.Map" && result.c.exists("length")){//若结果是文件字典,则加入计算【此if段是第二次优化所加,避免将结果返回到外部后再次遍历以求和,对文件数3W+的种子能稍为提高效率】 if(!result.c.exists("path") || result.c("path")(0).search(/^_____padding_file/i) == -1 ) {//排除padding_file填充文件 total.fileCount++; total.totalSize.Add(result.c("length")); } } list.push_back(result.c); pos = result.pos; Char = getChar(Blob, pos+1, 1); } //DOpus.Output("List end pos:" + pos); return {c:list, pos:++pos} break; case "i": Char = getChar(Blob, ++pos, 1); var result = "" while (Char != "e") { result += Char; Char = getChar(Blob, ++pos, 1); } //DOpus.Output("length end pos:" +pos); return {c:fsu.NewFileSize(result), pos:pos}; break; case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":case "8":case "9": var size = "" while (Char != ":") { size += Char; Char = getChar(Blob, ++pos, 1); } var str = getChar(Blob, pos+1, size, enc) //if(size==0)DOpus.Output("string is - " + str + " end at: " + (pos+Number(size))) return {c:str, pos:pos+Number(size)} } } /* function Decode(Blob, pos){ var Char = getChar(Blob, ++pos, 1);//get next char, and move to next position; "++pos" also converts its type to number. //DOpus.Output("start: " + pos +" char:" + Char); switch (Char) { case "d": var dict = DOpus.Create.Map; var key = ""; Char = getChar(Blob, pos+1, 1);//get next char while (Char != 'e'){ var result = Decode(Blob, pos); key = result.c; pos = result.pos; //DOpus.Output("switch key: "+ key + " pos:" + pos); key = key.toLowerCase(); result = Decode(Blob, pos); pos = result.pos; if(key=="path" || key=="length") dict(key) = result.c; Char = getChar(Blob, pos+1, 1); } //DOpus.Output("dict end:" + pos); return {c:dict, pos:++pos} break; case "l": var list = DOpus.Create.Vector; Char = getChar(Blob, pos+1, 1); while (Char != 'e'){ var result = Decode(Blob, pos); list.push_back(result.c); pos = result.pos; Char = getChar(Blob, pos+1, 1); } //DOpus.Output("List end pos:" + pos); return {c:list,pos:++pos} break; case "i": Char = getChar(Blob, ++pos, 1); var result = "" while (Char != "e") { result += Char; Char = getChar(Blob, ++pos, 1); } //DOpus.Output("length end pos:" +pos); return {c:fsu.NewFileSize(result), pos:pos}; break; case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":case "8":case "9": var size = "" while (Char != ":") { size += Char; Char = getChar(Blob, ++pos, 1); } var str = getChar(Blob, pos+1, size) //DOpus.Output("string is - " + str + " end at: " + (pos+Number(size))) return {c:str, pos:pos+Number(size)} } } */ //将种子数据编码为字节流========================================= /* function Bencode(outBlob,Element) { //Element是未经编码的数据(字典、列表等),outBlob是编码后的字节流结果 //DOpus.Output(DOpus.TypeOf(Element)) var temp = DOpus.Create.Blob var temp2 = DOpus.Create.Blob switch (DOpus.TypeOf(Element).toLowerCase()) { case "string"://字符串 temp.CopyFrom(Element,"utf8") temp2.CopyFrom(temp.size+":","utf8") outBlob.CopyFrom(temp2,outBlob.size) outBlob.CopyFrom(temp,outBlob.size) break; case "object.filesize"://数字 temp.CopyFrom("i"+Element+"e","utf8") outBlob.CopyFrom(temp,outBlob.size) break; case "object.blob": temp.CopyFrom(Element.size+":","utf8") outBlob.CopyFrom(temp,outBlob.size) outBlob.CopyFrom(Element,outBlob.size) break; case "object.vector"://列表list temp.CopyFrom("l","utf8") outBlob.CopyFrom(temp,outBlob.size) for (var e = new Enumerator(Element); !e.atEnd(); e.moveNext()) { //DOpus.Output("child file: " + DOpus.TypeOf(e.item())) Bencode(outBlob,e.item()); } temp.CopyFrom("e","utf8") outBlob.CopyFrom(temp,outBlob.size) break; case "object.map"://字典dictionary temp.CopyFrom("d","utf8") outBlob.CopyFrom(temp,outBlob.size) for (var e = new Enumerator(Element); !e.atEnd(); e.moveNext()) { var key = e.item(); //DOpus.Output(key + " " + DOpus.TypeOf(Element(key))) Bencode(outBlob,key); Bencode(outBlob,Element(key)); } temp.CopyFrom("e","utf8") outBlob.CopyFrom(temp,outBlob.size) } temp.Free() temp2.Free() } */ //================================================================ ==SCRIPT RESOURCES