@syra.fm/sdk 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/client.js +236 -7
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/index.js +36 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/live/components/CreateRoomSheet.js +5 -5
- package/lib/commonjs/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/commonjs/live/components/RecordingsPanel.js +9 -9
- package/lib/commonjs/live/components/RecordingsPanel.js.map +1 -1
- package/lib/commonjs/live/services/spaceSocketService.js +1 -1
- package/lib/commonjs/live/validation.js +44 -15
- package/lib/commonjs/live/validation.js.map +1 -1
- package/lib/commonjs/schema.js +80 -9
- package/lib/commonjs/schema.js.map +1 -1
- package/lib/module/client.js +237 -8
- package/lib/module/client.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/live/components/CreateRoomSheet.js +5 -5
- package/lib/module/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/module/live/components/RecordingsPanel.js +9 -9
- package/lib/module/live/components/RecordingsPanel.js.map +1 -1
- package/lib/module/live/services/spaceSocketService.js +1 -1
- package/lib/module/live/validation.js +44 -15
- package/lib/module/live/validation.js.map +1 -1
- package/lib/module/schema.js +79 -8
- package/lib/module/schema.js.map +1 -1
- package/lib/typescript/commonjs/client.d.ts +159 -2
- package/lib/typescript/commonjs/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/validation.d.ts +17 -20
- package/lib/typescript/commonjs/live/validation.d.ts.map +1 -1
- package/lib/typescript/commonjs/schema.d.ts +97 -7
- package/lib/typescript/commonjs/schema.d.ts.map +1 -1
- package/lib/typescript/module/client.d.ts +159 -2
- package/lib/typescript/module/client.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/module/live/validation.d.ts +17 -20
- package/lib/typescript/module/live/validation.d.ts.map +1 -1
- package/lib/typescript/module/schema.d.ts +97 -7
- package/lib/typescript/module/schema.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/client.ts +418 -10
- package/src/index.ts +17 -0
- package/src/live/components/CreateRoomSheet.tsx +5 -5
- package/src/live/components/RecordingsPanel.tsx +9 -9
- package/src/live/components/RoomCard.tsx +1 -1
- package/src/live/services/spaceSocketService.ts +1 -1
- package/src/live/validation.ts +41 -15
- package/src/schema.ts +90 -7
- package/src/client.test.ts +0 -632
package/src/index.ts
CHANGED
|
@@ -18,18 +18,35 @@ export type {
|
|
|
18
18
|
ArtworkSource,
|
|
19
19
|
PodcastArtworkSource,
|
|
20
20
|
EpisodeArtworkSource,
|
|
21
|
+
CreatePodcastInput,
|
|
22
|
+
UpdatePodcastInput,
|
|
23
|
+
CreateEpisodeDraftInput,
|
|
24
|
+
IngestEpisodeInput,
|
|
25
|
+
UploadPayload,
|
|
21
26
|
} from './client';
|
|
22
27
|
export {
|
|
23
28
|
trackSummarySchema,
|
|
24
29
|
podcastSummarySchema,
|
|
30
|
+
podcastVisibilitySchema,
|
|
25
31
|
episodeSummarySchema,
|
|
32
|
+
episodeAudioSourceSchema,
|
|
33
|
+
episodeStatusSchema,
|
|
34
|
+
episodeDraftSchema,
|
|
35
|
+
episodeStreamSchema,
|
|
36
|
+
uploadedImageSchema,
|
|
26
37
|
coverArtSizesSchema,
|
|
27
38
|
coverArtVariantSchema,
|
|
28
39
|
} from './schema';
|
|
29
40
|
export type {
|
|
30
41
|
TrackSummary,
|
|
31
42
|
PodcastSummary,
|
|
43
|
+
PodcastVisibility,
|
|
32
44
|
EpisodeSummary,
|
|
45
|
+
EpisodeAudioSource,
|
|
46
|
+
EpisodeStatus,
|
|
47
|
+
EpisodeDraft,
|
|
48
|
+
EpisodeStream,
|
|
49
|
+
UploadedImage,
|
|
33
50
|
CoverArtSizes,
|
|
34
51
|
CoverArtVariant,
|
|
35
52
|
ArtworkSize,
|
|
@@ -97,7 +97,7 @@ export const CreateRoomSheet = forwardRef<CreateRoomSheetRef, CreateRoomSheetPro
|
|
|
97
97
|
speakerPermission: isBroadcast ? 'invited' as const : speakerPermission,
|
|
98
98
|
type: roomType,
|
|
99
99
|
ownerType: selectedHouse ? 'house' as const : 'profile' as const,
|
|
100
|
-
houseId: selectedHouse?.
|
|
100
|
+
houseId: selectedHouse?.id,
|
|
101
101
|
recordingEnabled,
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -109,10 +109,10 @@ export const CreateRoomSheet = forwardRef<CreateRoomSheetRef, CreateRoomSheetPro
|
|
|
109
109
|
const room = await roomsService.createRoom(buildCreatePayload());
|
|
110
110
|
|
|
111
111
|
if (room) {
|
|
112
|
-
const started = await roomsService.startRoom(room.
|
|
112
|
+
const started = await roomsService.startRoom(room.id);
|
|
113
113
|
onClose();
|
|
114
114
|
if (started) {
|
|
115
|
-
joinLiveRoom(room.
|
|
115
|
+
joinLiveRoom(room.id);
|
|
116
116
|
} else {
|
|
117
117
|
toast.error('Room created but failed to start');
|
|
118
118
|
}
|
|
@@ -317,10 +317,10 @@ export const CreateRoomSheet = forwardRef<CreateRoomSheetRef, CreateRoomSheetPro
|
|
|
317
317
|
horizontal
|
|
318
318
|
showsHorizontalScrollIndicator={false}
|
|
319
319
|
data={[null, ...houses]}
|
|
320
|
-
keyExtractor={(item) => item?.
|
|
320
|
+
keyExtractor={(item) => item?.id ?? 'personal'}
|
|
321
321
|
contentContainerStyle={styles.chipList}
|
|
322
322
|
renderItem={({ item }) => {
|
|
323
|
-
const selected = item === null ? !selectedHouse : selectedHouse?.
|
|
323
|
+
const selected = item === null ? !selectedHouse : selectedHouse?.id === item.id;
|
|
324
324
|
return (
|
|
325
325
|
<TouchableOpacity
|
|
326
326
|
style={[
|
|
@@ -49,9 +49,9 @@ export function RecordingsPanel({ roomId, isHost, theme, onClose, onPlay }: Reco
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const handlePlay = async (recording: Recording) => {
|
|
52
|
-
setPlayingId(recording.
|
|
52
|
+
setPlayingId(recording.id);
|
|
53
53
|
try {
|
|
54
|
-
const result = await roomsService.getRecording(recording.
|
|
54
|
+
const result = await roomsService.getRecording(recording.id);
|
|
55
55
|
if (result?.playbackUrl) {
|
|
56
56
|
onPlay?.(result.playbackUrl, recording);
|
|
57
57
|
} else {
|
|
@@ -66,10 +66,10 @@ export function RecordingsPanel({ roomId, isHost, theme, onClose, onPlay }: Reco
|
|
|
66
66
|
|
|
67
67
|
const handleToggleAccess = async (recording: Recording) => {
|
|
68
68
|
const newAccess = recording.access === 'public' ? 'participants' as const : 'public' as const;
|
|
69
|
-
const success = await roomsService.updateRecordingAccess(recording.
|
|
69
|
+
const success = await roomsService.updateRecordingAccess(recording.id, newAccess);
|
|
70
70
|
if (success) {
|
|
71
71
|
setRecordings((prev) =>
|
|
72
|
-
prev.map((r) => r.
|
|
72
|
+
prev.map((r) => r.id === recording.id ? { ...r, access: newAccess } : r)
|
|
73
73
|
);
|
|
74
74
|
toast.success(`Recording is now ${newAccess}`);
|
|
75
75
|
} else {
|
|
@@ -78,9 +78,9 @@ export function RecordingsPanel({ roomId, isHost, theme, onClose, onPlay }: Reco
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
const handleDelete = async (recording: Recording) => {
|
|
81
|
-
const success = await roomsService.deleteRecording(recording.
|
|
81
|
+
const success = await roomsService.deleteRecording(recording.id);
|
|
82
82
|
if (success) {
|
|
83
|
-
setRecordings((prev) => prev.filter((r) => r.
|
|
83
|
+
setRecordings((prev) => prev.filter((r) => r.id !== recording.id));
|
|
84
84
|
toast.success('Recording deleted');
|
|
85
85
|
} else {
|
|
86
86
|
toast.error('Failed to delete recording');
|
|
@@ -110,7 +110,7 @@ export function RecordingsPanel({ roomId, isHost, theme, onClose, onPlay }: Reco
|
|
|
110
110
|
) : (
|
|
111
111
|
recordings.map((recording) => (
|
|
112
112
|
<View
|
|
113
|
-
key={recording.
|
|
113
|
+
key={recording.id}
|
|
114
114
|
style={[styles.recordingCard, { backgroundColor: `${theme.colors.card}80`, borderColor: theme.colors.border }]}
|
|
115
115
|
>
|
|
116
116
|
<View style={styles.recordingHeader}>
|
|
@@ -144,10 +144,10 @@ export function RecordingsPanel({ roomId, isHost, theme, onClose, onPlay }: Reco
|
|
|
144
144
|
|
|
145
145
|
<TouchableOpacity
|
|
146
146
|
onPress={() => handlePlay(recording)}
|
|
147
|
-
disabled={playingId === recording.
|
|
147
|
+
disabled={playingId === recording.id}
|
|
148
148
|
style={[styles.playButton, { backgroundColor: theme.colors.primary }]}
|
|
149
149
|
>
|
|
150
|
-
{playingId === recording.
|
|
150
|
+
{playingId === recording.id ? (
|
|
151
151
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
152
152
|
) : (
|
|
153
153
|
<MaterialCommunityIcons name="play" size={22} color="#FFFFFF" />
|
|
@@ -26,7 +26,7 @@ export class RoomSocketService {
|
|
|
26
26
|
this.socket = null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const baseUrl = this.socketUrl || 'ws://localhost:
|
|
29
|
+
const baseUrl = this.socketUrl || 'ws://localhost:4120';
|
|
30
30
|
|
|
31
31
|
this.socket = io(`${baseUrl}/rooms`, {
|
|
32
32
|
transports: ['websocket', 'polling'],
|
package/src/live/validation.ts
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Accept a document whose identity arrives as `_id` and present it as `id`.
|
|
5
|
+
*
|
|
6
|
+
* The PostgreSQL migration made `id` REQUIRED on the four schemas below, which is
|
|
7
|
+
* correct against the ported backend — `_id` left the wire contract there. But the
|
|
8
|
+
* frontend deploys on its own path (`deploy-frontends.yml`, on `packages/frontend`
|
|
9
|
+
* and `packages/shared-types`) while the backend deploy is gated, so a merge to
|
|
10
|
+
* `main` ships the client half ALONE. It did: `GET /rooms` still answers `_id`,
|
|
11
|
+
* every room failed `ZRoom`, and `validateRooms` DROPS what it cannot parse — so
|
|
12
|
+
* the Live screen went empty in production with only a `console.warn` to say so.
|
|
13
|
+
*
|
|
14
|
+
* Normalising here rather than teaching call sites to read `room.id ?? room._id`
|
|
15
|
+
* is the architecture this repo already states: parse once at the API boundary and
|
|
16
|
+
* return typed data. One place, no UI knows, and it is correct against BOTH
|
|
17
|
+
* backends — so the cutover needs no coordinated client flip, which is the failure
|
|
18
|
+
* this comment exists because of.
|
|
19
|
+
*
|
|
20
|
+
* It can be deleted once no deployed Syra backend serves `_id`. Nothing breaks if
|
|
21
|
+
* it outlives that: a payload carrying only `id` passes through untouched.
|
|
22
|
+
*/
|
|
23
|
+
function withMongoIdFallback<T extends z.ZodTypeAny>(schema: T) {
|
|
24
|
+
return z.preprocess((value) => {
|
|
25
|
+
if (value === null || typeof value !== 'object') return value;
|
|
26
|
+
const doc = value as Record<string, unknown>;
|
|
27
|
+
if (typeof doc['id'] === 'string' || typeof doc['_id'] !== 'string') return value;
|
|
28
|
+
return { ...doc, id: doc['_id'] };
|
|
29
|
+
}, schema);
|
|
30
|
+
}
|
|
31
|
+
|
|
3
32
|
// --- Podcast queue ---
|
|
4
33
|
|
|
5
34
|
/**
|
|
@@ -17,9 +46,8 @@ export type PodcastQueueItem = z.infer<typeof ZPodcastQueueItem>;
|
|
|
17
46
|
|
|
18
47
|
// --- Room (replaces Space) ---
|
|
19
48
|
|
|
20
|
-
export const ZRoom = z.object({
|
|
21
|
-
|
|
22
|
-
id: z.string().optional(),
|
|
49
|
+
export const ZRoom = withMongoIdFallback(z.object({
|
|
50
|
+
id: z.string(),
|
|
23
51
|
title: z.string(),
|
|
24
52
|
description: z.string().nullish(),
|
|
25
53
|
|
|
@@ -78,7 +106,7 @@ export const ZRoom = z.object({
|
|
|
78
106
|
recordingEgressId: z.string().nullish(),
|
|
79
107
|
|
|
80
108
|
createdAt: z.string(),
|
|
81
|
-
}).passthrough();
|
|
109
|
+
}).passthrough());
|
|
82
110
|
|
|
83
111
|
export type Room = z.infer<typeof ZRoom>;
|
|
84
112
|
|
|
@@ -117,9 +145,8 @@ export const ZHouseVisibility = z.object({
|
|
|
117
145
|
|
|
118
146
|
export type HouseVisibility = z.infer<typeof ZHouseVisibility>;
|
|
119
147
|
|
|
120
|
-
export const ZHouse = z.object({
|
|
121
|
-
|
|
122
|
-
id: z.string().optional(),
|
|
148
|
+
export const ZHouse = withMongoIdFallback(z.object({
|
|
149
|
+
id: z.string(),
|
|
123
150
|
name: z.string(),
|
|
124
151
|
description: z.string().optional(),
|
|
125
152
|
avatar: z.string().optional().nullable(),
|
|
@@ -132,7 +159,7 @@ export const ZHouse = z.object({
|
|
|
132
159
|
visibility: ZHouseVisibility.default({ discovery: 'listed', rooms: 'anyone', join: 'invite' }),
|
|
133
160
|
tags: z.array(z.string()).optional(),
|
|
134
161
|
createdAt: z.string(),
|
|
135
|
-
}).passthrough();
|
|
162
|
+
}).passthrough());
|
|
136
163
|
|
|
137
164
|
export type House = z.infer<typeof ZHouse>;
|
|
138
165
|
|
|
@@ -167,9 +194,8 @@ export const ZSeriesEpisode = z.object({
|
|
|
167
194
|
|
|
168
195
|
export type SeriesEpisode = z.infer<typeof ZSeriesEpisode>;
|
|
169
196
|
|
|
170
|
-
export const ZSeries = z.object({
|
|
171
|
-
|
|
172
|
-
id: z.string().optional(),
|
|
197
|
+
export const ZSeries = withMongoIdFallback(z.object({
|
|
198
|
+
id: z.string(),
|
|
173
199
|
title: z.string(),
|
|
174
200
|
description: z.string().optional(),
|
|
175
201
|
coverImage: z.string().optional().nullable(),
|
|
@@ -181,14 +207,14 @@ export const ZSeries = z.object({
|
|
|
181
207
|
nextEpisodeNumber: z.number().default(1),
|
|
182
208
|
isActive: z.boolean().default(true),
|
|
183
209
|
createdAt: z.string(),
|
|
184
|
-
}).passthrough();
|
|
210
|
+
}).passthrough());
|
|
185
211
|
|
|
186
212
|
export type Series = z.infer<typeof ZSeries>;
|
|
187
213
|
|
|
188
214
|
// --- Recording ---
|
|
189
215
|
|
|
190
|
-
export const ZRecording = z.object({
|
|
191
|
-
|
|
216
|
+
export const ZRecording = withMongoIdFallback(z.object({
|
|
217
|
+
id: z.string(),
|
|
192
218
|
roomId: z.string(),
|
|
193
219
|
roomTitle: z.string(),
|
|
194
220
|
host: z.string(),
|
|
@@ -203,7 +229,7 @@ export const ZRecording = z.object({
|
|
|
203
229
|
participantIds: z.array(z.string()).default([]),
|
|
204
230
|
expiresAt: z.string(),
|
|
205
231
|
createdAt: z.string(),
|
|
206
|
-
}).passthrough();
|
|
232
|
+
}).passthrough());
|
|
207
233
|
|
|
208
234
|
export type Recording = z.infer<typeof ZRecording>;
|
|
209
235
|
|
package/src/schema.ts
CHANGED
|
@@ -59,6 +59,9 @@ export type TrackSummary = z.infer<typeof trackSummarySchema>;
|
|
|
59
59
|
* variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
|
|
60
60
|
* external artwork URL as an absolute fallback when re-hosting has not run yet.
|
|
61
61
|
*/
|
|
62
|
+
export const podcastVisibilitySchema = z.enum(['private', 'unlisted', 'public']);
|
|
63
|
+
export type PodcastVisibility = z.infer<typeof podcastVisibilitySchema>;
|
|
64
|
+
|
|
62
65
|
export const podcastSummarySchema = z.object({
|
|
63
66
|
id: z.string(),
|
|
64
67
|
title: z.string(),
|
|
@@ -67,6 +70,15 @@ export const podcastSummarySchema = z.object({
|
|
|
67
70
|
image: z.string().optional(),
|
|
68
71
|
imageSizes: coverArtSizesSchema.optional(),
|
|
69
72
|
imageSourceUrl: z.string().optional(),
|
|
73
|
+
/**
|
|
74
|
+
* Who may see this show. OPTIONAL here although the API always sends it: a
|
|
75
|
+
* published SDK is installed against whatever version of the API a consumer
|
|
76
|
+
* happens to be pointing at, and requiring a field the previous release did
|
|
77
|
+
* not send would make this SDK reject every response from it.
|
|
78
|
+
*/
|
|
79
|
+
visibility: podcastVisibilitySchema.optional(),
|
|
80
|
+
/** Disclosure: the show's content was machine-generated. Optional, same reason. */
|
|
81
|
+
aiGenerated: z.boolean().optional(),
|
|
70
82
|
});
|
|
71
83
|
export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
72
84
|
|
|
@@ -75,12 +87,28 @@ export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
|
75
87
|
* (`GET /api/podcasts/:id/episodes`, `GET /api/episodes/:id`) — just enough to
|
|
76
88
|
* list an episode and stream its audio.
|
|
77
89
|
*
|
|
78
|
-
* `enclosureUrl` is
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
90
|
+
* ## `enclosureUrl` is OPTIONAL, and that is a bug fix rather than a loosening
|
|
91
|
+
*
|
|
92
|
+
* It used to be required, on the reasoning that "an episode with no enclosure is
|
|
93
|
+
* unplayable, so a row missing it is malformed". That reasoning holds only for
|
|
94
|
+
* RSS-mirrored episodes. A SYRA-HOSTED episode — everything created through
|
|
95
|
+
* `createPodcast`/`uploadEpisode`/the ingest ticket — has no enclosure at all:
|
|
96
|
+
* its audio lives at `audioSource.url`, a path on the Syra API. So the required
|
|
97
|
+
* field made this SDK silently DROP every Syra-hosted episode from
|
|
98
|
+
* `getPodcastEpisodes` and throw on one from `getEpisode` — the entire
|
|
99
|
+
* first-party catalogue, invisible, with no error to notice.
|
|
100
|
+
*
|
|
101
|
+
* The two are alternatives, not a required field and an optional one:
|
|
102
|
+
*
|
|
103
|
+
* enclosureUrl an ABSOLUTE external URL (RSS mirror)
|
|
104
|
+
* audioSource.url a PATH on the Syra API (Syra-hosted)
|
|
105
|
+
*
|
|
106
|
+
* `SyraClient.episodeAudioUrl` is the one place that resolves either into
|
|
107
|
+
* something playable, so no consumer has to know which kind it holds.
|
|
108
|
+
*
|
|
109
|
+
* `enclosureType` / `enclosureLength` describe the external file (MIME type and
|
|
110
|
+
* byte length); `duration` is the runtime in seconds and `pubDate` the ISO
|
|
111
|
+
* publish timestamp.
|
|
84
112
|
*
|
|
85
113
|
* Artwork mirrors the podcast SHOW: `image` is the re-hosted Syra image id
|
|
86
114
|
* (resolved via `/api/images/:id`); `imageSizes` is the multi-resolution variant
|
|
@@ -88,18 +116,73 @@ export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
|
|
88
116
|
* original external artwork URL as an absolute fallback when re-hosting has not
|
|
89
117
|
* run yet.
|
|
90
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Where a Syra-hosted episode's audio actually is.
|
|
121
|
+
*
|
|
122
|
+
* `url` is a PATH on the Syra API (`/api/podcasts/episodes/:id/audio`), not an
|
|
123
|
+
* absolute URL — resolve it with `SyraClient.episodeAudioUrl` rather than
|
|
124
|
+
* handing it to a player directly.
|
|
125
|
+
*/
|
|
126
|
+
export const episodeAudioSourceSchema = z.object({
|
|
127
|
+
url: z.string(),
|
|
128
|
+
format: z.string().optional(),
|
|
129
|
+
bitrate: z.number().optional(),
|
|
130
|
+
duration: z.number().optional(),
|
|
131
|
+
});
|
|
132
|
+
export type EpisodeAudioSource = z.infer<typeof episodeAudioSourceSchema>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An episode's processing state. `ready` is playable; `processing` is an episode
|
|
136
|
+
* whose audio is still being packaged (including one drafted but not yet
|
|
137
|
+
* ingested), and only its OWNER is shown those.
|
|
138
|
+
*/
|
|
139
|
+
export const episodeStatusSchema = z.enum(['ready', 'processing', 'failed', 'unavailable']);
|
|
140
|
+
export type EpisodeStatus = z.infer<typeof episodeStatusSchema>;
|
|
141
|
+
|
|
91
142
|
export const episodeSummarySchema = z.object({
|
|
92
143
|
id: z.string(),
|
|
93
144
|
podcastId: z.string(),
|
|
94
145
|
title: z.string(),
|
|
95
146
|
description: z.string().optional(),
|
|
96
|
-
|
|
147
|
+
/** ABSOLUTE, and present only for RSS-mirrored episodes — see the note above. */
|
|
148
|
+
enclosureUrl: z.string().optional(),
|
|
97
149
|
enclosureType: z.string().optional(),
|
|
98
150
|
enclosureLength: z.number().optional(),
|
|
151
|
+
/** A PATH on the Syra API, present for Syra-hosted episodes. */
|
|
152
|
+
audioSource: episodeAudioSourceSchema.optional(),
|
|
99
153
|
duration: z.number().optional(),
|
|
100
154
|
pubDate: z.string().optional(),
|
|
155
|
+
season: z.number().optional(),
|
|
156
|
+
episodeNumber: z.number().optional(),
|
|
157
|
+
status: episodeStatusSchema.optional(),
|
|
158
|
+
/** Disclosure: this episode's content was machine-generated. */
|
|
159
|
+
aiGenerated: z.boolean().optional(),
|
|
101
160
|
image: z.string().optional(),
|
|
102
161
|
imageSizes: coverArtSizesSchema.optional(),
|
|
103
162
|
imageSourceUrl: z.string().optional(),
|
|
104
163
|
});
|
|
105
164
|
export type EpisodeSummary = z.infer<typeof episodeSummarySchema>;
|
|
165
|
+
|
|
166
|
+
/** What `createEpisodeDraft` hands back: the episode to fill, and the capability to fill it. */
|
|
167
|
+
export const episodeDraftSchema = z.object({
|
|
168
|
+
episodeId: z.string(),
|
|
169
|
+
ingestTicket: z.string(),
|
|
170
|
+
expiresAt: z.string(),
|
|
171
|
+
});
|
|
172
|
+
export type EpisodeDraft = z.infer<typeof episodeDraftSchema>;
|
|
173
|
+
|
|
174
|
+
/** What `getEpisodeStream` hands back: a tokenized HLS URL with its own deadline. */
|
|
175
|
+
export const episodeStreamSchema = z.object({
|
|
176
|
+
url: z.string(),
|
|
177
|
+
type: z.string(),
|
|
178
|
+
expiresAt: z.string().optional(),
|
|
179
|
+
});
|
|
180
|
+
export type EpisodeStream = z.infer<typeof episodeStreamSchema>;
|
|
181
|
+
|
|
182
|
+
/** What the image upload endpoint hands back. */
|
|
183
|
+
export const uploadedImageSchema = z.object({
|
|
184
|
+
id: z.string(),
|
|
185
|
+
primaryColor: z.string().optional(),
|
|
186
|
+
secondaryColor: z.string().optional(),
|
|
187
|
+
});
|
|
188
|
+
export type UploadedImage = z.infer<typeof uploadedImageSchema>;
|