概述
NIP-73 定義了 i 標籤,用於將 Nostr 事件關聯到外部系統中的內容。 這讓使用者可以對電影、音樂、書籍、Podcast
等外部內容發表評論、評分或討論, 同時保持與原始內容的可追蹤連結。
["i", "<type>:<id>", "<proof-url>"]
| 欄位 | 說明 | 必要性 |
type | 外部系統/平台類型 | 必要 |
id | 該系統中的唯一識別符 | 必要 |
proof-url | 證明連結(如適用) | 可選 |
支援的類型
媒體與娛樂
| 類型 | 說明 | ID 格式 |
imdb | IMDb 電影/電視 | tt1234567 |
tmdb | TheMovieDB | movie:12345 或 tv:12345 |
spotify | Spotify 音樂 | track:xxx、album:xxx |
tidal | Tidal 音樂 | track:12345 |
deezer | Deezer 音樂 | track:12345 |
podcast:guid | Podcast 劇集 GUID | UUID 格式 |
podcast:item:guid | Podcast 項目 GUID | 項目唯一識別符 |
書籍與出版
| 類型 | 說明 | ID 格式 |
isbn | 國際標準書號 | 10 或 13 位 ISBN |
openlibrary | Open Library | OL12345W |
goodreads | Goodreads 書籍 | 書籍 ID |
doi | 數位物件識別碼 | 10.1000/xyz123 |
遊戲與軟體
| 類型 | 說明 | ID 格式 |
steam | Steam 遊戲 | App ID |
igdb | IGDB 遊戲資料庫 | 遊戲 ID |
地理位置
| 類型 | 說明 | ID 格式 |
geo | 地理座標 | lat,lon |
geohash | Geohash 編碼 | Geohash 字串 |
osm | OpenStreetMap | node:12345、way:12345 |
範例
電影評論
{
"kind": 1,
"content": "剛看完《乘風破浪》,非常精彩的電影!劇情緊湊,演員演技出色。推薦!⭐⭐⭐⭐⭐",
"tags": [
["i", "imdb:tt1234567"],
["i", "tmdb:movie:54321"]
]
}
音樂分享
{
"kind": 1,
"content": "這首歌最近單曲循環中 🎵",
"tags": [
["i", "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"],
["i", "tidal:track:12345678"]
]
}
書籍討論
{
"kind": 1,
"content": "終於讀完了《精通比特幣》,強烈推薦給所有想深入了解區塊鏈的人!",
"tags": [
["i", "isbn:9781491954386"],
["i", "openlibrary:OL26201858W"]
]
}
Podcast 討論
{
"kind": 1,
"content": "今天這集 Podcast 聊到了很多有趣的觀點!",
"tags": [
["i", "podcast:guid:917393e3-1b1e-5cef-ace4-edaa54e1f82d"],
["i", "podcast:item:guid:ep-2024-01-15"]
]
}
地點打卡
{
"kind": 1,
"content": "今天來到這家咖啡廳,環境很棒!☕",
"tags": [
["i", "geo:25.0330,121.5654"],
["i", "geohash:wsqqj"],
["i", "osm:node:123456789"]
]
}
遊戲評論
{
"kind": 1,
"content": "剛通關這款遊戲,故事線非常感人!",
"tags": [
["i", "steam:1234567"],
["i", "igdb:12345"]
]
}
TypeScript 實作
建立外部內容引用
import { finalizeEvent } from 'nostr-tools';
type ExternalContentType =
| 'imdb'
| 'tmdb'
| 'spotify'
| 'tidal'
| 'deezer'
| 'isbn'
| 'openlibrary'
| 'goodreads'
| 'doi'
| 'steam'
| 'igdb'
| 'geo'
| 'geohash'
| 'osm'
| 'podcast:guid'
| 'podcast:item:guid';
interface ExternalReference {
type: ExternalContentType;
id: string;
proofUrl?: string;
}
function createExternalReferenceEvent(
content: string,
references: ExternalReference[],
secretKey: Uint8Array,
additionalTags: string[][] = []
) {
const iTags = references.map((ref) => {
const tag = ['i', `${ref.type}:${ref.id}`];
if (ref.proofUrl) {
tag.push(ref.proofUrl);
}
return tag;
});
const event = {
kind: 1,
content,
tags: [...iTags, ...additionalTags],
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例:電影評論
const movieReview = createExternalReferenceEvent(
'非常棒的電影!劇情緊湊,特效出色。',
[
{ type: 'imdb', id: 'tt1234567' },
{ type: 'tmdb', id: 'movie:54321' },
],
secretKey
);
// 使用範例:音樂分享
const musicShare = createExternalReferenceEvent(
'這首歌太好聽了!🎵',
[
{ type: 'spotify', id: 'track:4iV5W9uYEdYUVa79Axb7Rh' },
],
secretKey
);
解析外部引用
interface ParsedExternalRef {
type: string;
id: string;
proofUrl?: string;
fullId: string;
}
function parseExternalReferences(event: any): ParsedExternalRef[] {
const iTags = event.tags.filter(
(tag: string[]) => tag[0] === 'i' && tag.length >= 2
);
return iTags.map((tag: string[]) => {
const fullId = tag[1];
const colonIndex = fullId.indexOf(':');
let type: string;
let id: string;
if (colonIndex > 0) {
type = fullId.slice(0, colonIndex);
id = fullId.slice(colonIndex + 1);
} else {
type = 'unknown';
id = fullId;
}
return {
type,
id,
proofUrl: tag[2],
fullId,
};
});
}
// 使用範例
const refs = parseExternalReferences(event);
refs.forEach((ref) => {
console.log(`類型: ${ref.type}, ID: ${ref.id}`);
});
產生外部連結
function getExternalUrl(ref: ParsedExternalRef): string | null {
switch (ref.type) {
case 'imdb':
return `https://www.imdb.com/title/${ref.id}`;
case 'tmdb':
const [tmdbType, tmdbId] = ref.id.split(':');
return `https://www.themoviedb.org/${tmdbType}/${tmdbId}`;
case 'spotify':
const [spotifyType, spotifyId] = ref.id.split(':');
return `https://open.spotify.com/${spotifyType}/${spotifyId}`;
case 'tidal':
const [tidalType, tidalId] = ref.id.split(':');
return `https://tidal.com/${tidalType}/${tidalId}`;
case 'isbn':
return `https://openlibrary.org/isbn/${ref.id}`;
case 'openlibrary':
return `https://openlibrary.org/works/${ref.id}`;
case 'goodreads':
return `https://www.goodreads.com/book/show/${ref.id}`;
case 'doi':
return `https://doi.org/${ref.id}`;
case 'steam':
return `https://store.steampowered.com/app/${ref.id}`;
case 'igdb':
return `https://www.igdb.com/games/${ref.id}`;
case 'geo':
const [lat, lon] = ref.id.split(',');
return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}`;
case 'geohash':
return `https://geohash.org/${ref.id}`;
case 'osm':
const [osmType, osmId] = ref.id.split(':');
return `https://www.openstreetmap.org/${osmType}/${osmId}`;
default:
return null;
}
}
// 使用範例
const refs = parseExternalReferences(event);
refs.forEach((ref) => {
const url = getExternalUrl(ref);
if (url) {
console.log(`${ref.type}: ${url}`);
}
});
搜尋相關事件
import { SimplePool } from 'nostr-tools';
async function findEventsForContent(
pool: SimplePool,
relays: string[],
type: ExternalContentType,
id: string
): Promise<any[]> {
const filter = {
kinds: [1],
'#i': [`${type}:${id}`],
limit: 100,
};
const events = await pool.querySync(relays, filter);
return events.sort((a, b) => b.created_at - a.created_at);
}
// 使用範例:查詢關於特定電影的所有討論
const movieDiscussions = await findEventsForContent(
pool,
['wss://relay.example.com'],
'imdb',
'tt1234567'
);
console.log(`找到 ${movieDiscussions.length} 則相關討論`);
React 元件
interface ExternalContentCardProps {
reference: ParsedExternalRef;
}
function ExternalContentCard({ reference }: ExternalContentCardProps) {
const url = getExternalUrl(reference);
const typeLabels: Record<string, string> = {
imdb: 'IMDb',
tmdb: 'TMDb',
spotify: 'Spotify',
tidal: 'Tidal',
isbn: 'ISBN',
openlibrary: 'Open Library',
goodreads: 'Goodreads',
doi: 'DOI',
steam: 'Steam',
igdb: 'IGDB',
geo: '位置',
geohash: 'Geohash',
osm: 'OpenStreetMap',
};
const typeIcons: Record<string, string> = {
imdb: '🎬',
tmdb: '🎥',
spotify: '🎵',
tidal: '🎧',
isbn: '📚',
openlibrary: '📖',
goodreads: '📕',
doi: '📄',
steam: '🎮',
igdb: '🕹️',
geo: '📍',
geohash: '🗺️',
osm: '🌍',
};
return (
<div className="external-content-card">
<span className="icon">
{typeIcons[reference.type] || '🔗'}
</span>
<span className="label">
{typeLabels[reference.type] || reference.type}
</span>
<span className="id">{reference.id}</span>
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="link"
>
開啟
</a>
)}
</div>
);
}
function EventExternalRefs({ event }: { event: any }) {
const refs = parseExternalReferences(event);
if (refs.length === 0) {
return null;
}
return (
<div className="external-refs">
{refs.map((ref, i) => (
<ExternalContentCard key={i} reference={ref} />
))}
</div>
);
}
使用場景
媒體評論與討論
- 電影、電視劇評論
- 音樂專輯、歌曲討論
- Podcast 節目評論
- 遊戲評測
書籍社群
- 讀書筆記與心得
- 書籍推薦列表
- 學術論文討論(DOI)
位置相關內容
內容聚合
- 聚合所有關於特定電影的討論
- 建立圍繞特定書籍的社群
- 追蹤特定音樂的熱度
最佳實踐
- 使用標準識別符:優先使用廣泛認可的 ID 系統(如 IMDb、ISBN)
- 多重引用:同時使用多個來源 ID 提高可發現性
- 提供證明連結:在可能的情況下包含 proof URL
- 客戶端豐富化:從外部 API 獲取詳細資訊並展示
- 快取外部資料:減少對外部服務的請求
參考資源