Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions 74.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
NIP-74
======

Podcasts
--------

`draft` `optional`

This NIP defines addressable events for podcast shows and episodes, optimized for
metadata, discovery, and long-lived references.

This NIP is compatible with Podcasting 2.0 metadata. Clients and publishers may
mirror Podcasting 2.0 fields as tags while keeping Nostr-native semantics.

## Event kinds

* `30074`: Podcast Show
* `30075`: Podcast Episode

## Podcast Show (`kind:30074`)

Represents a podcast show.

### Required tags

* `d` - unique show identifier (recommended: `podcast:guid:<guid>` or a stable slug)
* `title` - show title

### Recommended tags

* `summary` - short description
* `language` - BCP-47 language code (e.g. `en`, `en-US`)
* `explicit` - content rating (`true`/`false` or `clean`/`explicit`)
* `image` - show artwork URL
* `r` - website or RSS feed URL (repeatable)
* `i` - external IDs via NIP-73 (e.g. `podcast:guid:<guid>`, `podcast:publisher:guid:<guid>`)
* `t` - categories/keywords (repeatable)
* `p` - Nostr pubkeys for hosts/producers (repeatable)
* `person` - people with roles (repeatable, format below)
* `funding` - funding URL (repeatable, optional label)
* `value` - value container (see Podcasting 2.0 alignment below)
* `value_recipient` - value recipients (repeatable, see format below)
* `content-warning` - if applicable (NIP-36)

### Content

`content` MAY include a longer description.

## Podcast Episode (`kind:30075`)

Represents a single podcast episode, linked to a show.

### Required tags

* `d` - unique episode identifier (recommended: `podcast:item:guid:<guid>` or a stable slug)
* `title` - episode title
* `published_at` - unix timestamp (string) of original publication
* `a` - reference to the show: `30074:<pubkey>:<d>`

### Recommended tags

* `i` - external IDs via NIP-73 (e.g. `podcast:item:guid:<guid>`, optional show `podcast:guid:<guid>`)
* `duration` - episode length in seconds (string)
* `season` - season number (string)
* `episode` - episode number (string)
* `episode_type` - `full`, `trailer`, or `bonus`
* `language` - BCP-47 language code (e.g. `en`, `en-US`)
* `explicit` - content rating (`true`/`false` or `clean`/`explicit`)
* `image` - episode artwork URL (if different from show)
* `t` - categories/keywords (repeatable)
* `p` - Nostr pubkeys for guests/hosts (repeatable)
* `person` - people with roles (repeatable, format below)
* `chapters` - chapters URL and MIME type (e.g. `application/json+chapters`)
* `transcript` - transcript URL and MIME type (repeatable, optional language/rel)
* `soundbite` - highlight time range in seconds (repeatable; duration recommended 15–120s)
* `funding` - funding URL (repeatable, optional label)
* `value` - value container (see Podcasting 2.0 alignment below)
* `value_recipient` - value recipients (repeatable, see format below)
* `content-warning` - if applicable (NIP-36)

### Audio media

Episodes SHOULD include one or more `imeta` tags (NIP-92) describing audio variants,
using fields from NIP-94. At minimum:

* `url` - audio file URL
* `m` - MIME type (e.g. `audio/mpeg`, `audio/mp4`)
* `x` - SHA-256 of the file (hex)

Optional fields include `size`, `duration`, `bitrate`, `image`, and `fallback`.

## Podcasting 2.0 alignment (optional)

The following tags mirror common Podcasting 2.0 fields. They are OPTIONAL and
intended to improve interoperability with RSS-based tooling and indexers.

### Nostr-native tag formats (recommended)

These tags have Nostr-first semantics. Use them even if you are not mirroring RSS.

* `explicit` - `true`/`false` or `clean`/`explicit`
* `language` - BCP-47 language code (e.g. `en`, `en-US`)
* `duration` - episode length in seconds (string)
* `episode` - episode number (string; decimal allowed)
* `season` - season number (string; integer recommended)
* `episode_type` - `full`, `trailer`, or `bonus`
* `image` - single artwork URL (lossy vs `<podcast:image>`)
* `p` - Nostr pubkey references for people associated with the show/episode

### Podcasting 2.0 compatibility tag formats (optional)

These tags mirror Podcast Namespace RSS tags and should preserve required/optional
fields where possible.

* `person` - `["person", "<name>", "<role?>", "<group?>", "<href?>", "<img?>"]`
* Example: `["person", "Ada Lovelace", "host", "cast", "https://example.com", "https://example.com/ada.jpg"]`
* Example: `["person", "Ada Lovelace", "host", "cast", "nostr:nprofile1...", "https://example.com/ada.jpg"]`
* Defaults: `role=host`, `group=cast`. Episode-level `person` tags replace all show-level `person` tags.
* If a person has a Nostr identity, include a `p` tag for their pubkey; `person` remains optional for RSS compatibility.
* `funding` - `["funding", "<url>", "<label?>"]`
* `chapters` - `["chapters", "<url>", "<type>"]` (e.g. `application/json+chapters`)
* `transcript` - `["transcript", "<url>", "<type>", "<language?>", "<rel?>"]`
* `soundbite` - `["soundbite", "<start_seconds>", "<duration_seconds>", "<title?>"]` (recommended 15–120s)
* `value` - `["value", "<type>", "<method>", "<suggested?>"]`
* `value_recipient` - `["value_recipient", "<type>", "<address>", "<split>", "<name?>", "<fee?>", "<customKey?>", "<customValue?>"]`
* Example: `["value_recipient", "node", "02d5c1bf8b940dc9...", "40", "Alice (Podcaster)", "false"]`
* Example: `["value_recipient", "node", "03ae9f91a0cb8ff4...", "5", "Hosting Provider", "true", "customKey:boost", "customValue:thanks"]`
* Grouping: `value_recipient` tags belong to the nearest preceding `value` tag. If multiple `value` tags are present, group recipients by order.

### Mapping guidance (Podcast Namespace → NIP-74 tags)

| Podcast Namespace tag | NIP-74 tag |
|-------------------------------|-------------------|
| `podcast:guid` | `i` |
| `podcast:person` | `person` |
| `podcast:funding` | `funding` |
| `podcast:chapters` | `chapters` |
| `podcast:transcript` | `transcript` |
| `podcast:soundbite` | `soundbite` |
| `podcast:episode` | `episode` |
| `podcast:season` | `season` |
| `podcast:value` | `value` |
| `podcast:valueRecipient` | `value_recipient` |
| `podcast:image` | `image` |

### Other RSS/iTunes fields (not Podcast Namespace)

These are common in RSS feeds but are not `podcast:` tags.

* RSS `<language>` → `language`
* RSS `<guid>` (episode) → `i` tag `podcast:item:guid:<guid>` (via NIP-73)
* iTunes `<itunes:explicit>` → `explicit`
* iTunes `<itunes:episodeType>` → `episode_type`

## Notes

* `published_at` SHOULD reflect the original public release date; `created_at`
reflects when the Nostr event was created.
* Episode events SHOULD include the show GUID (`podcast:guid:<guid>`) in an `i`
tag when known.

## Discovery

* List shows: filter `kinds=[30074]`
* List episodes for a show: filter `kinds=[30075]` with `#a=["30074:<pubkey>:<d>"]`
* Resolve by external GUID:
* shows: `kinds=[30074]` with `#i=["podcast:guid:<guid>"]`
* episodes: `kinds=[30075]` with `#i=["podcast:item:guid:<guid>"]`

## Client behavior

* Clients SHOULD render `title`, `summary`, and `image` where available.
* Clients SHOULD prefer `imeta` audio variants and may fall back to other URLs.
* Comments SHOULD use NIP-22 and reactions SHOULD use NIP-25 with NIP-73 IDs.

## Examples

### Podcast Show

```jsonc
{
"kind": 30074,
"created_at": 1737936000,
"content": "A weekly show about distributed systems.",
"tags": [
["d", "podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc"],
["title", "Nostr Builders"],
["summary", "Interviews with builders in the Nostr ecosystem."],
["image", "https://example.com/show-art.jpg"],
["r", "https://example.com"],
["r", "https://example.com/feed.xml"],
["i", "podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc"],
["t", "nostr"],
["t", "technology"]
]
}
```

### Podcast Episode

```jsonc
{
"kind": 30075,
"created_at": 1738022400,
"content": "We discuss relay scaling, spam mitigation, and client UX.",
"tags": [
["d", "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f"],
["title", "Episode 42: Relays at Scale"],
["published_at", "1738020000"],
["a", "30074:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc"],
["i", "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f"],
["i", "podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc"],
["duration", "3600"],
["season", "4"],
["episode", "42"],
["t", "nostr"],
["t", "scaling"],
["imeta",
"url https://cdn.example.com/episodes/42.mp3",
"m audio/mpeg",
"x 3093509d1e0bc604ff60cb9286f4cd7c781553bc8991937befaacfdc28ec5cdc",
"size 56000000",
"duration 3600",
"bitrate 128000",
"image https://cdn.example.com/episodes/42.jpg",
"fallback https://backup.example.com/episodes/42.mp3"
]
]
}
```
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
- [NIP-71: Video Events](71.md)
- [NIP-72: Moderated Communities](72.md)
- [NIP-73: External Content IDs](73.md)
- [NIP-74: Podcasts](74.md)
- [NIP-75: Zap Goals](75.md)
- [NIP-77: Negentropy Syncing](77.md)
- [NIP-78: Application-specific data](78.md)
Expand Down Expand Up @@ -253,6 +254,8 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
| `30040` | Curated Publication Index | [NKBIP-01] |
| `30041` | Curated Publication Content | [NKBIP-01] |
| `30063` | Release artifact sets | [51](51.md) |
| `30074` | Podcast Show | [74](74.md) |
| `30075` | Podcast Episode | [74](74.md) |
| `30078` | Application-specific Data | [78](78.md) |
| `30166` | Relay Discovery | [66](66.md) |
| `30267` | App curation sets | [51](51.md) |
Expand Down Expand Up @@ -445,3 +448,4 @@ All NIPs are public domain.
<a align="center" href="https://github.com/nostr-protocol/nips/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nostr-protocol/nips" />
</a>