在开发BitTorrent相关应用时,准确解析.torrent
文件并计算InfoHash是基础需求。本文介绍如何使用Java正确处理种子文件,特别是解决InfoHash计算中常见的编码问题,以及如何管理Tracker列表。
依赖配置
首先引入Bencode库处理种子文件的编码/解码:
<dependency>
<groupId>com.dampcake</groupId>
<artifactId>bencode</artifactId>
<version>1.4.2</version>
</dependency>
InfoHash计算核心问题
种子文件InfoHash计算的关键在于字符集选择。虽然常规解析可使用UTF-8,但计算InfoHash必须使用ISO-8859-1(Latin-1)编码,否则会得到错误的哈希值。
完整代码实现
import com.dampcake.bencode.Bencode;
import com.dampcake.bencode.Type;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 种子文件解析工具类
* 支持解析种子信息、计算InfoHash、修改Tracker等功能
*/
public class TorrentInfoHashCalculator {
public static void main(String[] args) {
try {
// 测试用的种子文件路径
File torrentFile = new File("课件.torrent");
// 1. 解析并显示种子信息
System.out.println("==== 1. 解析种子信息 ====");
displayTorrentInfo(torrentFile);
// 2. 计算InfoHash
System.out.println("\n==== 2. 计算InfoHash ====");
String infoHash = calculateInfoHash(torrentFile);
System.out.println("InfoHash (大写): " + infoHash.toUpperCase());
// 3. 修改Tracker的passkey
System.out.println("\n==== 3. 修改Tracker的passkey ====");
String passkey = "new-passkey-123456";
File modifiedTorrentFile = modifyTorrentTracker(torrentFile, passkey);
// 4. 显示修改后的种子信息
System.out.println("\n==== 4. 修改后的种子信息 ====");
displayTorrentInfo(modifiedTorrentFile);
// 5. 验证修改后的InfoHash (应该与原始InfoHash相同)
System.out.println("\n==== 5. 验证修改后的InfoHash ====");
String newInfoHash = calculateInfoHash(modifiedTorrentFile);
System.out.println("修改后的InfoHash (大写): " + newInfoHash.toUpperCase());
System.out.println("InfoHash是否匹配: " + infoHash.equalsIgnoreCase(newInfoHash));
// 6. 按层级添加Tracker
System.out.println("\n==== 6. 按层级添加Tracker ====");
// 创建分层的Tracker列表
List<List<String>> trackerTiers = new ArrayList<>();
// 第0层 (最高优先级)
List<String> tier0 = new ArrayList<>();
tier0.add("http://111.111.111.111:6969/new-passkey-123456/announce");
trackerTiers.add(tier0);
// 空行,代表层级分隔
trackerTiers.add(new ArrayList<>());
// 第1层
List<String> tier1 = new ArrayList<>();
tier1.add("http://tracker1.example.com/your-passkey/announce");
tier1.add("http://tracker2.example.com/your-passkey/announce");
tier1.add("http://tracker3.example.com/your-passkey/announce");
trackerTiers.add(tier1);
// 添加分层Tracker
File tieredTrackersFile = addTrackersByTier(torrentFile, trackerTiers);
// 7. 显示添加层级Tracker后的种子信息
System.out.println("\n==== 7. 添加层级Tracker后的种子信息 ====");
displayTorrentInfo(tieredTrackersFile);
// 8. 生成磁力链接
System.out.println("\n==== 8. 生成磁力链接 ====");
String magnetLink = generateMagnetLink(torrentFile);
System.out.println("磁力链接: " + magnetLink);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解析并显示种子文件信息 - 支持中文
*/
public static void displayTorrentInfo(File torrentFile) throws Exception {
// 使用ISO-8859-1字符集读取种子文件,确保二进制数据完整性
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
Map<String, Object> dict = bencode.decode(torrentData, Type.DICTIONARY);
System.out.println("种子文件: " + torrentFile.getName());
// 显示基本信息
if (dict.containsKey("announce")) {
System.out.println("主Tracker: " + dict.get("announce"));
}
if (dict.containsKey("created by")) {
System.out.println("创建者: " + dict.get("created by"));
}
if (dict.containsKey("creation date")) {
long creationDate = (Long) dict.get("creation date");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(new Date(creationDate * 1000));
System.out.println("创建日期: " + dateStr + " (" + creationDate + ")");
}
if (dict.containsKey("comment")) {
String comment = (String) dict.get("comment");
System.out.println("注释: " + isoToUtf8(comment));
}
// 显示Tracker列表
if (dict.containsKey("announce-list")) {
System.out.println("\nTracker列表:");
List<List<Object>> announceList = (List<List<Object>>) dict.get("announce-list");
int tierIndex = 0; // 使用单独的变量来追踪层级索引
for (int i = 0; i < announceList.size(); i++) {
List<Object> tier = announceList.get(i);
if (tier.isEmpty()) {
System.out.println(" [空行 - 层级分隔符]");
} else {
System.out.println(" 层级 " + tierIndex + ":");
for (Object announce : tier) {
System.out.println(" - " + announce);
}
tierIndex++; // 只有实际的Tracker层级才增加索引
}
}
}
// 获取info字典信息
Map<String, Object> info = (Map<String, Object>) dict.get("info");
System.out.println("\n文件信息:");
// 处理名称,优先使用UTF-8编码的名称字段
String name;
if (info.containsKey("name.utf-8")) {
name = (String) info.get("name.utf-8");
} else {
String nameISO = (String) info.get("name");
name = isoToUtf8(nameISO);
}
System.out.println("名称: " + name);
if (info.containsKey("piece length")) {
long pieceLength = (Long) info.get("piece length");
System.out.println("分块大小: " + pieceLength + " 字节 (" + formatFileSize(pieceLength) + ")");
}
if (info.containsKey("pieces")) {
// pieces包含所有文件块的SHA-1哈希值
Object pieces = info.get("pieces");
if (pieces instanceof byte[]) {
byte[] piecesBytes = (byte[]) pieces;
int pieceCount = piecesBytes.length / 20; // 每个SHA-1哈希值为20字节
System.out.println("文件块数量: " + pieceCount);
} else if (pieces instanceof String) {
String piecesStr = (String) pieces;
int pieceCount = piecesStr.length() / 20;
System.out.println("文件块数量: " + pieceCount);
}
}
// 检查是否为私有种子
if (info.containsKey("private") && ((Number)info.get("private")).intValue() == 1) {
System.out.println("私有种子: 是 (禁用DHT、PEX和LSD)");
} else {
System.out.println("私有种子: 否");
}
// 区分单文件和多文件种子
if (info.containsKey("length")) {
// 单文件种子
long fileSize = (Long) info.get("length");
System.out.println("种子类型: 单文件");
System.out.println("文件大小: " + fileSize + " 字节 (" + formatFileSize(fileSize) + ")");
} else if (info.containsKey("files")) {
// 多文件种子
List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
long totalSize = 0;
System.out.println("种子类型: 多文件");
System.out.println("文件数量: " + files.size());
System.out.println("\n文件列表:");
int count = 0;
for (Map<String, Object> file : files) {
if (count++ >= 5 && files.size() > 6) {
System.out.println("... 还有 " + (files.size() - 5) + " 个文件未显示 ...");
break;
}
long fileSize = (Long) file.get("length");
totalSize += fileSize;
// 优先使用UTF-8编码的路径字段
List<Object> pathElements;
if (file.containsKey("path.utf-8")) {
pathElements = (List<Object>) file.get("path.utf-8");
} else {
pathElements = (List<Object>) file.get("path");
}
StringBuilder pathBuilder = new StringBuilder();
for (Object element : pathElements) {
if (pathBuilder.length() > 0) {
pathBuilder.append("/");
}
// 转换路径元素为UTF-8
String pathPart = isoToUtf8((String) element);
pathBuilder.append(pathPart);
}
System.out.println(" - " + pathBuilder.toString() + " (" + formatFileSize(fileSize) + ")");
}
System.out.println("\n总大小: " + totalSize + " 字节 (" + formatFileSize(totalSize) + ")");
}
}
/**
* 计算种子文件的InfoHash
*/
public static String calculateInfoHash(File torrentFile) throws Exception {
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
// 使用ISO-8859-1字符集解析种子文件
Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
Map<String, Object> dict = bencode.decode(torrentData, Type.DICTIONARY);
// 提取info字典
Map<String, Object> info = (Map<String, Object>) dict.get("info");
// 对info字典进行bencode编码
byte[] encodedInfo = bencode.encode(info);
// 计算SHA-1哈希
return sha1(encodedInfo);
}
/**
* 修改种子文件的Tracker (更新passkey)
*/
public static File modifyTorrentTracker(File torrentFile, String passkey) throws Exception {
// 读取种子文件
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
// 使用ISO-8859-1字符集解析种子文件
Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
Map<String, Object> dict = bencode.decode(torrentData, Type.DICTIONARY);
// 获取原始announce URL
String originalAnnounce = (String) dict.get("announce");
System.out.println("原始主Tracker: " + originalAnnounce);
// 修改announce URL
if (originalAnnounce != null) {
String newAnnounce = updatePasskey(originalAnnounce, passkey);
dict.put("announce", newAnnounce);
System.out.println("新的主Tracker: " + newAnnounce);
}
// 同时更新announce-list (如果存在)
if (dict.containsKey("announce-list")) {
List<List<Object>> announceList = (List<List<Object>>) dict.get("announce-list");
System.out.println("\n更新Tracker列表:");
for (int i = 0; i < announceList.size(); i++) {
List<Object> tier = announceList.get(i);
for (int j = 0; j < tier.size(); j++) {
String announceUrl = (String) tier.get(j);
// 只更新与原始announce URL类似的URL
if (isSimilarTracker(originalAnnounce, announceUrl)) {
String newUrl = updatePasskey(announceUrl, passkey);
tier.set(j, newUrl);
System.out.println(" 从: " + announceUrl);
System.out.println(" 到: " + newUrl);
}
}
}
}
// 保存修改后的种子文件
String fileName = torrentFile.getName();
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
File outputFile = new File(torrentFile.getParent(), baseName + "_modified" + extension);
// 重新编码并保存
byte[] modifiedTorrentData = bencode.encode(dict);
Files.write(outputFile.toPath(), modifiedTorrentData);
System.out.println("\n修改后的种子文件已保存为: " + outputFile.getName());
return outputFile;
}
/**
* 向种子文件添加分层级的Tracker URL
*
* @param torrentFile 要修改的种子文件
* @param trackerTiers 分层级的Tracker列表,每个子列表代表一个层级
* @return 修改后的种子文件
*/
public static File addTrackersByTier(File torrentFile, List<List<String>> trackerTiers) throws Exception {
// 读取种子文件
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
// 使用ISO-8859-1字符集解析种子文件
Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
Map<String, Object> dict = bencode.decode(torrentData, Type.DICTIONARY);
// 获取原始announce URL
String originalAnnounce = (String) dict.get("announce");
System.out.println("原始主Tracker: " + originalAnnounce);
// 准备announce-list - 不预先添加任何tier
List<List<Object>> announceList = new ArrayList<>();
// 遍历并添加所有tier
System.out.println("\n按层级添加Tracker:");
for (int i = 0; i < trackerTiers.size(); i++) {
List<String> tierTrackers = trackerTiers.get(i);
if (tierTrackers.isEmpty()) {
// 空行代表层级分隔符 - 但我们不需要添加任何东西到announceList
System.out.println("层级分隔符");
continue; // 跳过空行,不添加到announceList
}
// 为非空tier创建一个新的tier列表
List<Object> newTier = new ArrayList<>();
System.out.println("层级 " + i + ":");
for (String tracker : tierTrackers) {
newTier.add(tracker);
System.out.println(" + " + tracker);
}
// 添加非空tier到announceList
if (!newTier.isEmpty()) {
announceList.add(newTier);
}
}
// 如果第一个tier非空,设置主announce
if (!announceList.isEmpty() && !announceList.get(0).isEmpty()) {
String mainAnnounce = (String) announceList.get(0).get(0);
dict.put("announce", mainAnnounce);
System.out.println("新的主Tracker: " + mainAnnounce);
}
// 更新announce-list
dict.put("announce-list", announceList);
// 保存修改后的种子文件
String fileName = torrentFile.getName();
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
File outputFile = new File(torrentFile.getParent(), baseName + "_tiered_trackers" + extension);
// 重新编码并保存
byte[] modifiedTorrentData = bencode.encode(dict);
Files.write(outputFile.toPath(), modifiedTorrentData);
System.out.println("\n添加分层Tracker后的种子文件已保存为: " + outputFile.getName());
return outputFile;
}
/**
* 生成磁力链接
*/
public static String generateMagnetLink(File torrentFile) throws Exception {
// 计算InfoHash
String infoHash = calculateInfoHash(torrentFile);
// 读取种子文件获取名称
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
Map<String, Object> dict = bencode.decode(torrentData, Type.DICTIONARY);
Map<String, Object> info = (Map<String, Object>) dict.get("info");
// 处理名称,优先使用UTF-8编码的名称字段
String name;
if (info.containsKey("name.utf-8")) {
name = (String) info.get("name.utf-8");
} else {
String nameISO = (String) info.get("name");
name = isoToUtf8(nameISO);
}
// 构建基本磁力链接
StringBuilder magnetLink = new StringBuilder();
magnetLink.append("magnet:?xt=urn:btih:").append(infoHash.toUpperCase());
// 添加名称
if (name != null && !name.isEmpty()) {
magnetLink.append("&dn=").append(urlEncode(name));
}
// 添加Tracker
if (dict.containsKey("announce")) {
String announce = (String) dict.get("announce");
magnetLink.append("&tr=").append(urlEncode(announce));
}
// 添加announce-list中的Tracker (最多添加5个)
if (dict.containsKey("announce-list")) {
List<List<Object>> announceList = (List<List<Object>>) dict.get("announce-list");
int trackerCount = 0;
for (List<Object> tier : announceList) {
for (Object tracker : tier) {
if (trackerCount++ < 5) { // 限制最多添加5个Tracker
String announceUrl = (String) tracker;
magnetLink.append("&tr=").append(urlEncode(announceUrl));
}
}
}
}
return magnetLink.toString();
}
/**
* 将ISO-8859-1编码的字符串转换为UTF-8
*/
private static String isoToUtf8(String isoString) {
try {
byte[] bytes = isoString.getBytes("ISO-8859-1");
return new String(bytes, "UTF-8");
} catch (Exception e) {
return isoString; // 转换失败则返回原始字符串
}
}
/**
* 更新Tracker URL中的passkey
*/
private static String updatePasskey(String announceUrl, String passkey) {
// 匹配Tracker URL的模式
// 例如: http://tracker.example.com/oldpasskey/announce 或 http://tracker.example.com/announce
Pattern pattern = Pattern.compile("(https?://[^/]+)(/[^/]+)?(/announce)");
Matcher matcher = pattern.matcher(announceUrl);
if (matcher.find()) {
// 提取域名部分
String domain = matcher.group(1); // http://tracker.example.com
String queryString = "";
// 处理可能的查询参数
if (announceUrl.contains("?")) {
queryString = "?" + announceUrl.substring(announceUrl.indexOf("?") + 1);
}
// 构建新的URL,插入passkey
return domain + "/" + passkey + "/announce" + queryString;
}
// 如果URL格式不匹配,返回原始URL
return announceUrl;
}
/**
* 判断两个Tracker URL是否相似
*/
private static boolean isSimilarTracker(String url1, String url2) {
if (url1 == null || url2 == null) {
return false;
}
// 简单判断: 相同的主机名
try {
String host1 = new java.net.URL(url1).getHost();
String host2 = new java.net.URL(url2).getHost();
return host1.equals(host2);
} catch (Exception e) {
// 如果解析URL出错,返回false
return false;
}
}
/**
* 计算SHA-1哈希值
*/
private static String sha1(byte[] bytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(bytes);
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 格式化文件大小
*/
private static String formatFileSize(long size) {
final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" };
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return String.format("%.2f %s", size / Math.pow(1024, digitGroups), units[digitGroups]);
}
/**
* URL编码字符串
*/
private static String urlEncode(String s) {
try {
return java.net.URLEncoder.encode(s, "UTF-8")
.replace("+", "%20") // 空格编码为%20而不是+
.replace("*", "%2A"); // 星号编码为%2A
} catch (Exception e) {
return s;
}
}
}
解析种子内容
此代码支持全面解析种子文件的各种信息:
- 基本信息:Tracker地址、创建者、创建时间、注释等
- Tracker列表:支持分层级显示Tracker,包括空行分隔符
- 文件信息:区分单文件和多文件种子,显示文件大小、路径等
- 技术细节:显示分块大小、是否为私有种子等
特殊功能
修改Tracker Passkey
代码支持智能修改Tracker URL中的passkey部分,保持URL的其他部分不变:
String passkey = "your-new-passkey";
File modifiedFile = modifyTorrentTracker(torrentFile, passkey);
这个功能会自动识别Tracker URL的格式,仅替换passkey部分。
设置Tracker层级
支持qBittorrent风格的Tracker层级设置,包括空行分隔符:
List<List<String>> trackerTiers = new ArrayList<>();
// 第0层 (最高优先级)
List<String> tier0 = new ArrayList<>();
tier0.add("http://main-tracker.com/passkey/announce");
trackerTiers.add(tier0);
// 空行分隔符
trackerTiers.add(new ArrayList<>());
// 第1层 (次优先级)
List<String> tier1 = new ArrayList<>();
tier1.add("http://backup1.com/passkey/announce");
tier1.add("http://backup2.com/passkey/announce");
trackerTiers.add(tier1);
File tieredFile = addTrackersByTier(torrentFile, trackerTiers);
这种方式添加的Tracker会按照层级优先级使用,符合qBittorrent的层级规范。
生成磁力链接
基于种子文件生成磁力链接:
String magnetLink = generateMagnetLink(torrentFile);
// 结果: magnet:?xt=urn:btih:A1B2C3...&dn=文件名&tr=http://tracker...
关键技术要点
- 字符集处理:始终使用ISO-8859-1字符集解析和编码种子文件,确保二进制数据完整性
- InfoHash计算:使用ISO-8859-1编码info字典,然后计算SHA-1哈希
- Tracker层级:处理空行分隔符,确保层级结构符合客户端规范
- 兼容性:生成的种子文件可被qBittorrent等主流客户端正确识别和使用
注意事项
- 修改种子文件时,切勿直接修改info字典内容,否则会导致InfoHash变化
- 确保使用ISO-8859-1字符集处理二进制数据,特别是pieces字段,以避免数据损坏
- 使用专门的方法修改Tracker URL,不要简单替换字符串,以处理各种URL格式
- 添加Tracker时注意层级结构,空行在qBittorrent中代表层级分隔
- 生成的种子文件需要经过客户端验证,确保没有"incorrect number of piece hashes"等错误
评论区