NIP-35: Torrents
在 Nostr 上分享和發現 BitTorrent 種子
概述
NIP-35 定義了在 Nostr 上分享 BitTorrent 種子(Torrents)的標準格式。 使用者可以發布種子資訊,其他人可以搜尋、訂閱和下載, 實現去中心化的種子分享和發現。
事件類型
| Kind | 說明 | 類型 |
|---|---|---|
2003 | 種子 (Torrent) | 一般 |
事件結構
Content
事件的 content 欄位包含種子的描述文字,可以使用 Markdown 格式。
必要標籤
| 標籤 | 說明 | 範例 |
|---|---|---|
title | 種子標題 | ["title", "Ubuntu 24.04 LTS"] |
x | Info Hash (v1) | ["x", "abc123..."] |
可選標籤
| 標籤 | 說明 | 範例 |
|---|---|---|
file | 檔案資訊(名稱、大小) | ["file", "ubuntu.iso", "4700000000"] |
tracker | Tracker URL | ["tracker", "udp://tracker.example.com:6969"] |
i | Info Hash (v2) | ["i", "btih:abc123..."] |
t | 主題標籤 | ["t", "linux"] |
image | 預覽圖片 | ["image", "https://..."] |
Magnet 連結
可以從事件的標籤中組合成 Magnet URI:
magnet:?xt=urn:btih:<info-hash>&dn=<title>&tr=<tracker> 範例
開源軟體種子
{
"kind": 2003,
"content": "Ubuntu 24.04 LTS (Noble Numbat) 官方 ISO 映像檔。\n\n## 特點\n- 長期支援版本(5年支援)\n- GNOME 46 桌面環境\n- Linux 內核 6.8\n\n請從官方來源驗證 SHA256 校驗碼。",
"tags": [
["title", "Ubuntu 24.04 LTS Desktop (64-bit)"],
["x", "abc123def456789..."],
["file", "ubuntu-24.04-desktop-amd64.iso", "5748072448"],
["tracker", "udp://tracker.ubuntu.com:6969"],
["tracker", "udp://tracker.opentrackr.org:1337"],
["t", "linux"],
["t", "ubuntu"],
["t", "開源"],
["t", "作業系統"],
["image", "https://ubuntu.com/wp-content/uploads/ubuntu-logo.png"]
]
} 多檔案種子
{
"kind": 2003,
"content": "Bitcoin 核心客戶端 v27.0 所有平台版本。\n\n包含 Windows、macOS、Linux 版本及簽名檔。",
"tags": [
["title", "Bitcoin Core v27.0"],
["x", "xyz789..."],
["file", "bitcoin-27.0-win64.zip", "45000000"],
["file", "bitcoin-27.0-x86_64-linux-gnu.tar.gz", "42000000"],
["file", "bitcoin-27.0-osx64.tar.gz", "38000000"],
["file", "SHA256SUMS.asc", "5000"],
["tracker", "udp://tracker.opentrackr.org:1337"],
["t", "bitcoin"],
["t", "軟體"],
["t", "區塊鏈"]
]
} TypeScript 實作
定義類型
interface TorrentFile {
name: string;
size: number;
}
interface Torrent {
title: string;
description: string;
infoHash: string;
infoHashV2?: string;
files: TorrentFile[];
trackers: string[];
tags?: string[];
image?: string;
} 建立種子事件
import { finalizeEvent } from 'nostr-tools';
function createTorrentEvent(
torrent: Torrent,
secretKey: Uint8Array
) {
const tags: string[][] = [
['title', torrent.title],
['x', torrent.infoHash],
];
if (torrent.infoHashV2) {
tags.push(['i', `btih:${torrent.infoHashV2}`]);
}
torrent.files.forEach((file) => {
tags.push(['file', file.name, file.size.toString()]);
});
torrent.trackers.forEach((tracker) => {
tags.push(['tracker', tracker]);
});
torrent.tags?.forEach((tag) => {
tags.push(['t', tag]);
});
if (torrent.image) {
tags.push(['image', torrent.image]);
}
const event = {
kind: 2003,
content: torrent.description,
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例
const torrentEvent = createTorrentEvent(
{
title: 'Debian 12 Bookworm',
description: 'Debian 12 官方 ISO,穩定版本。',
infoHash: 'abc123def456...',
files: [
{ name: 'debian-12-amd64-netinst.iso', size: 600000000 },
],
trackers: [
'udp://tracker.debian.org:6969',
'udp://tracker.opentrackr.org:1337',
],
tags: ['linux', 'debian', '開源'],
},
secretKey
); 解析種子事件
interface ParsedTorrent {
title: string;
description: string;
infoHash: string;
infoHashV2?: string;
files: TorrentFile[];
trackers: string[];
tags: string[];
image?: string;
magnetUri: string;
totalSize: number;
}
function parseTorrentEvent(event: any): ParsedTorrent | null {
if (event.kind !== 2003) return null;
const getTag = (name: string) => {
const tag = event.tags.find((t: string[]) => t[0] === name);
return tag?.[1];
};
const getAllTags = (name: string) => {
return event.tags
.filter((t: string[]) => t[0] === name)
.map((t: string[]) => t.slice(1));
};
const title = getTag('title');
const infoHash = getTag('x');
if (!title || !infoHash) return null;
const files = getAllTags('file').map((f: string[]) => ({
name: f[0],
size: parseInt(f[1] || '0'),
}));
const trackers = getAllTags('tracker').map((t: string[]) => t[0]);
const tags = getAllTags('t').map((t: string[]) => t[0]);
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
// 建立 Magnet URI
let magnetUri = `magnet:?xt=urn:btih:${infoHash}`;
magnetUri += `&dn=${encodeURIComponent(title)}`;
trackers.forEach((tracker) => {
magnetUri += `&tr=${encodeURIComponent(tracker)}`;
});
const iTag = event.tags.find((t: string[]) => t[0] === 'i');
const infoHashV2 = iTag?.[1]?.replace('btih:', '');
return {
title,
description: event.content,
infoHash,
infoHashV2,
files,
trackers,
tags,
image: getTag('image'),
magnetUri,
totalSize,
};
}
// 使用範例
const torrent = parseTorrentEvent(event);
if (torrent) {
console.log(`標題: ${torrent.title}`);
console.log(`大小: ${formatBytes(torrent.totalSize)}`);
console.log(`Magnet: ${torrent.magnetUri}`);
} 搜尋種子
import { SimplePool } from 'nostr-tools';
interface TorrentSearchOptions {
tags?: string[];
author?: string;
limit?: number;
}
async function searchTorrents(
pool: SimplePool,
relays: string[],
options: TorrentSearchOptions = {}
): Promise<ParsedTorrent[]> {
const filter: any = {
kinds: [2003],
limit: options.limit || 50,
};
if (options.author) {
filter.authors = [options.author];
}
if (options.tags && options.tags.length > 0) {
filter['#t'] = options.tags;
}
const events = await pool.querySync(relays, filter);
return events
.map(parseTorrentEvent)
.filter((t): t is ParsedTorrent => t !== null)
.sort((a, b) => b.totalSize - a.totalSize);
}
// 使用範例
const torrents = await searchTorrents(pool, relays, {
tags: ['linux', '開源'],
limit: 20,
}); 格式化工具
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`;
}
function copyMagnetLink(magnetUri: string): void {
navigator.clipboard.writeText(magnetUri);
} React 元件
interface TorrentCardProps {
torrent: ParsedTorrent;
}
function TorrentCard({ torrent }: TorrentCardProps) {
const handleCopyMagnet = () => {
navigator.clipboard.writeText(torrent.magnetUri);
// 顯示提示
};
return (
<div className="torrent-card">
{torrent.image && (
<img src={torrent.image} alt={torrent.title} className="preview" />
)}
<div className="info">
<h3>{torrent.title}</h3>
<p className="description">{torrent.description}</p>
<div className="meta">
<span className="size">{formatBytes(torrent.totalSize)}</span>
<span className="files">{torrent.files.length} 個檔案</span>
</div>
<div className="tags">
{torrent.tags.map((tag) => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
<div className="actions">
<a href={torrent.magnetUri} className="magnet-btn">
🧲 開啟 Magnet
</a>
<button onClick={handleCopyMagnet}>複製連結</button>
</div>
<div className="files-list">
<h4>檔案列表</h4>
{torrent.files.map((file, i) => (
<div key={i} className="file">
<span className="name">{file.name}</span>
<span className="size">{formatBytes(file.size)}</span>
</div>
))}
</div>
</div>
</div>
);
} 使用場景
開源軟體分發
- Linux 發行版 ISO
- 開源應用程式
- 區塊鏈客戶端
內容創作
- 創用 CC 授權內容
- 公共領域作品
- 教育資源
數據集分享
- 公開數據集
- 研究資料
- 歷史存檔
最佳實踐
- 提供多個 Tracker:增加下載可用性
- 詳細描述:說明內容和來源
- 標籤分類:幫助搜尋和發現
- 提供校驗碼:在描述中包含 SHA256
- 合法內容:只分享合法授權的內容
相關 NIPs
參考資源
已複製連結