live.voxport.podcast.episode
Schema Diff
+2 -2
Compatibility Analysis
Breaking Changes Detected
3 breaking changes, 2 non-breaking changes.
Breaking Changes (3)
- RemovedVertex RemovedVertex { vertex_id: "live.voxport.podcast.series" }
- RemovedEdge RemovedEdge { src: "live.voxport.podcast.episode:body.series", tgt: "live.voxport.podcast.series", kind: "ref", name: None }
- ConstraintTightened ConstraintTightened { vertex_id: "live.voxport.podcast.episode:body.series", sort: "ref", old_value: "live.voxport.podcast.series", new_value: "live.voxport.podcast.series#seriesRef" }
Non-Breaking Changes (2)
- AddedVertex AddedVertex { vertex_id: "live.voxport.podcast.series#seriesRef" }
- AddedEdge AddedEdge { src: "live.voxport.podcast.episode:body.series", tgt: "live.voxport.podcast.series#seriesRef", kind: "ref", name: None }
Migration Guidance
Removed Elements
RemovedVertex { vertex_id: "live.voxport.podcast.series" }
Added Elements
AddedVertex { vertex_id: "live.voxport.podcast.series#seriesRef" }
Constraint Changes
- ConstraintTightened ConstraintTightened { vertex_id: "live.voxport.podcast.episode:body.series", sort: "ref", old_value: "live.voxport.podcast.series", new_value: "live.voxport.podcast.series#seriesRef" }
Additional Notes
- Breaking: RemovedEdge { src: "live.voxport.podcast.episode:body.series", tgt: "live.voxport.podcast.series", kind: "ref", name: None }
- Non-breaking: AddedEdge { src: "live.voxport.podcast.episode:body.series", tgt: "live.voxport.podcast.series#seriesRef", kind: "ref", name: None }
1
1
{
2
2
"id": "live.voxport.podcast.episode",
3
3
"defs": {
4
4
"main": {
5
5
"key": "tid",
6
6
"type": "record",
7
7
"record": {
8
8
"type": "object",
9
9
"required": [
10
10
"series",
11
11
"title",
12
12
"createdAt"
13
13
],
14
14
"properties": {
15
15
"tags": {
16
16
"type": "array",
17
17
"items": {
18
18
"type": "string",
19
19
"maxLength": 64
20
20
},
21
21
"maxLength": 20,
22
22
"description": "Episode tags/keywords"
23
23
},
24
24
"audio": {
25
25
"type": "blob",
26
26
"accept": [
27
27
"audio/mpeg",
28
28
"audio/mp4",
29
29
"audio/wav",
30
30
"audio/ogg",
31
31
"audio/aac"
32
32
],
33
33
"maxSize": 524288000,
34
34
"description": "Audio file reference"
35
35
},
36
36
"image": {
37
37
"type": "blob",
38
38
"accept": [
39
39
"image/png",
40
40
"image/jpeg"
41
41
],
42
42
"maxSize": 2097152,
43
43
"description": "Episode-specific cover art"
44
44
},
45
45
"title": {
46
46
"type": "string",
47
47
"maxLength": 256,
48
48
"description": "Episode title"
49
49
},
50
50
"series": {
51
-
"ref": "live.voxport.podcast.series",
51
+
"ref": "live.voxport.podcast.series#seriesRef",
52
52
"type": "ref",
53
-
"description": "Reference to parent series"
53
+
"description": "Reference to parent series (uri + optional cid)"
54
54
},
55
55
"persons": {
56
56
"type": "array",
57
57
"items": {
58
58
"ref": "#person",
59
59
"type": "ref"
60
60
},
61
61
"maxLength": 50,
62
62
"description": "People associated with this episode — hosts, guests, co-hosts (Podcasting 2.0 podcast:person)"
63
63
},
64
64
"summary": {
65
65
"type": "string",
66
66
"maxLength": 10000,
67
67
"description": "Episode description/show notes (supports markdown)"
68
68
},
69
69
"audioUrl": {
70
70
"type": "string",
71
71
"format": "uri",
72
72
"description": "External audio URL (if not using blob)"
73
73
},
74
74
"chapters": {
75
75
"type": "array",
76
76
"items": {
77
77
"ref": "#chapter",
78
78
"type": "ref"
79
79
},
80
80
"maxLength": 100,
81
81
"description": "Episode chapters/timestamps"
82
82
},
83
83
"duration": {
84
84
"type": "integer",
85
85
"minimum": 0,
86
86
"description": "Duration in seconds"
87
87
},
88
88
"explicit": {
89
89
"type": "boolean",
90
90
"description": "Whether episode is explicit"
91
91
},
92
92
"audioType": {
93
93
"type": "string",
94
94
"maxLength": 64,
95
95
"description": "MIME type of the audio file when using audioUrl. Pairs with audioLength for RSS enclosure reconstruction.",
96
96
"knownValues": [
97
97
"audio/mpeg",
98
98
"audio/mp4",
99
99
"audio/wav",
100
100
"audio/ogg",
101
101
"audio/aac"
102
102
]
103
103
},
104
104
"createdAt": {
105
105
"type": "string",
106
106
"format": "datetime"
107
107
},
108
108
"updatedAt": {
109
109
"type": "string",
110
110
"format": "datetime"
111
111
},
112
112
"soundbites": {
113
113
"type": "array",
114
114
"items": {
115
115
"ref": "#soundbite",
116
116
"type": "ref"
117
117
},
118
118
"maxLength": 20,
119
119
"description": "Short clip markers for apps to surface (Podcasting 2.0 podcast:soundbite)"
120
120
},
121
121
"transcript": {
122
122
"ref": "#transcript",
123
123
"type": "ref",
124
124
"description": "Episode transcript (singular — deprecated, prefer transcripts array)"
125
125
},
126
126
"audioLength": {
127
127
"type": "integer",
128
128
"minimum": 0,
129
129
"description": "Byte size of the audio file when using audioUrl. Required for a valid RSS enclosure — without this, a firehose consumer cannot reconstruct the feed from records alone."
130
130
},
131
131
"episodeType": {
132
132
"type": "string",
133
133
"default": "full",
134
134
"maxLength": 16,
135
135
"description": "Episode type per RSS spec",
136
136
"knownValues": [
137
137
"full",
138
138
"trailer",
139
139
"bonus"
140
140
]
141
141
},
142
142
"publishedAt": {
143
143
"type": "string",
144
144
"format": "datetime",
145
145
"description": "Publication date (null = draft)"
146
146
},
147
147
"transcripts": {
148
148
"type": "array",
149
149
"items": {
150
150
"ref": "#transcript",
151
151
"type": "ref"
152
152
},
153
153
"maxLength": 20,
154
154
"description": "Episode transcripts — multiple entries for different languages/formats (Podcasting 2.0 podcast:transcript). Preferred over the singular transcript field."
155
155
},
156
156
"importedGuid": {
157
157
"type": "string",
158
158
"maxLength": 2048,
159
159
"description": "Original GUID from RSS feed (for imported episodes)"
160
160
},
161
161
"seasonNumber": {
162
162
"type": "integer",
163
163
"minimum": 1,
164
164
"description": "Season number"
165
165
},
166
166
"episodeNumber": {
167
167
"type": "integer",
168
168
"minimum": 1,
169
169
"description": "Episode number in series"
170
170
},
171
171
"blueskyPostRef": {
172
172
"ref": "com.atproto.repo.strongRef",
173
173
"type": "ref",
174
174
"description": "Reference to the Atmosphere cross-post (app.bsky.feed.post) for this episode. Set when the episode is auto-cross-posted, enabling custom feed operators to discover the cross-post from the episode record."
175
175
}
176
176
}
177
177
},
178
178
"description": "A podcast episode belonging to a series"
179
179
},
180
180
"person": {
181
181
"type": "object",
182
182
"required": [
183
183
"name"
184
184
],
185
185
"properties": {
186
186
"did": {
187
187
"type": "string",
188
188
"format": "did",
189
189
"description": "ATProto DID if the person has a verified identity"
190
190
},
191
191
"url": {
192
192
"type": "string",
193
193
"format": "uri",
194
194
"description": "External profile URL (maps to podcast:person href attribute)"
195
195
},
196
196
"name": {
197
197
"type": "string",
198
198
"maxLength": 128,
199
199
"description": "Full name or alias of the person (spec: max 128 chars)"
200
200
},
201
201
"role": {
202
202
"type": "string",
203
203
"maxLength": 64,
204
204
"description": "Role from the Podcast Taxonomy Project (e.g. host, guest, co-host). Defaults to 'host' if absent."
205
205
},
206
206
"group": {
207
207
"type": "string",
208
208
"maxLength": 64,
209
209
"description": "Group from the Podcast Taxonomy Project (e.g. cast, writing, visuals). Defaults to 'cast' if absent."
210
210
},
211
211
"image": {
212
212
"type": "string",
213
213
"format": "uri",
214
214
"description": "Profile image URL (maps to podcast:person img attribute)"
215
215
},
216
216
"handle": {
217
217
"type": "string",
218
218
"format": "handle",
219
219
"description": "ATProto handle (e.g. alice.bsky.social) if the person has a Bluesky account"
220
220
}
221
221
}
222
222
},
223
223
"chapter": {
224
224
"type": "object",
225
225
"required": [
226
226
"title",
227
227
"startTime"
228
228
],
229
229
"properties": {
230
230
"url": {
231
231
"type": "string",
232
232
"format": "uri",
233
233
"description": "Chapter link URL (Podcasting 2.0 chapter url)"
234
234
},
235
235
"image": {
236
236
"type": "string",
237
237
"format": "uri"
238
238
},
239
239
"title": {
240
240
"type": "string",
241
241
"maxLength": 256
242
242
},
243
243
"hidden": {
244
244
"type": "boolean",
245
245
"description": "Whether the chapter is hidden from the table of contents (Podcasting 2.0 toc:false)"
246
246
},
247
247
"endTime": {
248
248
"type": "integer",
249
249
"description": "End time in seconds (optional)"
250
250
},
251
251
"startTime": {
252
252
"type": "integer",
253
253
"description": "Start time in seconds"
254
254
}
255
255
}
256
256
},
257
257
"soundbite": {
258
258
"type": "object",
259
259
"required": [
260
260
"startTime",
261
261
"duration"
262
262
],
263
263
"properties": {
264
264
"title": {
265
265
"type": "string",
266
266
"maxLength": 128,
267
267
"description": "Title/label for the soundbite (spec: max 128 chars)"
268
268
},
269
269
"duration": {
270
270
"type": "string",
271
271
"maxLength": 16,
272
272
"description": "Duration in fractional seconds (e.g. '60.0', '42.25'). Matches Podcasting 2.0 spec exactly."
273
273
},
274
274
"startTime": {
275
275
"type": "string",
276
276
"maxLength": 16,
277
277
"description": "Start time in fractional seconds (e.g. '73.0', '1234.5'). Matches Podcasting 2.0 spec exactly."
278
278
}
279
279
}
280
280
},
281
281
"transcript": {
282
282
"type": "object",
283
283
"required": [
284
284
"url",
285
285
"type"
286
286
],
287
287
"properties": {
288
288
"rel": {
289
289
"type": "string",
290
290
"maxLength": 16,
291
291
"description": "If 'captions', the file is a closed captions file with time codes",
292
292
"knownValues": [
293
293
"captions"
294
294
]
295
295
},
296
296
"url": {
297
297
"type": "string",
298
298
"format": "uri",
299
299
"description": "URL of the transcript file"
300
300
},
301
301
"type": {
302
302
"type": "string",
303
303
"maxLength": 64,
304
304
"description": "MIME type of the transcript file",
305
305
"knownValues": [
306
306
"text/plain",
307
307
"text/html",
308
308
"text/vtt",
309
309
"application/x-subrip",
310
310
"application/json"
311
311
]
312
312
},
313
313
"language": {
314
314
"type": "string",
315
315
"format": "language",
316
316
"description": "Language of the transcript (defaults to feed language if absent)"
317
317
}
318
318
}
319
319
}
320
320
},
321
321
"$type": "com.atproto.lexicon.schema",
322
322
"lexicon": 1,
323
323
"description": "A podcast episode belonging to a series"
324
324
}