@techcake/broadcake-sdk 0.1.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/README.md +201 -0
- package/dist/index.cjs +219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +350 -0
- package/dist/index.d.ts +350 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @techcake/broadcake-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the [Broadcake](https://broadcake.com) radio station management API. Zero dependencies, works in browsers and Node.js 18+.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @techcake/broadcake-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Broadcake } from '@techcake/broadcake-sdk'
|
|
15
|
+
|
|
16
|
+
const bc = new Broadcake('your-station-slug')
|
|
17
|
+
|
|
18
|
+
// What's on right now?
|
|
19
|
+
const { now, next } = await bc.nowPlaying()
|
|
20
|
+
console.log(`Now: ${now?.show_name} | Up next: ${next?.show_name}`)
|
|
21
|
+
|
|
22
|
+
// This week's schedule
|
|
23
|
+
const week = await bc.schedule({ from: '2026-03-24' })
|
|
24
|
+
for (const day of week.days) {
|
|
25
|
+
console.log(`${day.date}: ${day.slots.length} shows`)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Default: connects to https://app.broadcake.com
|
|
33
|
+
const bc = new Broadcake('my-station')
|
|
34
|
+
|
|
35
|
+
// Self-hosted or development
|
|
36
|
+
const bc = new Broadcake('my-station', {
|
|
37
|
+
baseUrl: 'https://my-broadcake-instance.com'
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### Station
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Get station details (timezone, broadcast day start, listen URL)
|
|
47
|
+
const station = await bc.station()
|
|
48
|
+
|
|
49
|
+
// List all stations on the platform
|
|
50
|
+
const stations = await bc.stations()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Schedule
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Single day — returns ScheduleSlot[]
|
|
57
|
+
const slots = await bc.schedule('2026-03-24')
|
|
58
|
+
|
|
59
|
+
// Multi-day — returns WeekSchedule with station timezone
|
|
60
|
+
// Defaults to 7 days when `to` is omitted
|
|
61
|
+
const week = await bc.schedule({ from: '2026-03-24' })
|
|
62
|
+
|
|
63
|
+
// Custom range (up to 14 days)
|
|
64
|
+
const range = await bc.schedule({ from: '2026-03-24', to: '2026-03-28' })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each `ScheduleSlot` includes:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
{
|
|
71
|
+
slot_start: '07:00:00', // HH:MM:SS
|
|
72
|
+
slot_end: '10:00:00',
|
|
73
|
+
show_name: 'Morning Show',
|
|
74
|
+
show_slug: 'morning-show',
|
|
75
|
+
show_description: '...',
|
|
76
|
+
show_tagline: '...',
|
|
77
|
+
source: 'rule', // 'rule' | 'automation' | 'override'
|
|
78
|
+
genres: [{ name: 'Jazz', slug: 'jazz' }],
|
|
79
|
+
presenters: [{ name: 'DJ Max', role: 'host', pronouns: 'he/him' }],
|
|
80
|
+
// ...plus is_repeat, override_type, event info, etc.
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Now Playing
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// One-off fetch
|
|
88
|
+
const { station, now, next } = await bc.nowPlaying()
|
|
89
|
+
|
|
90
|
+
// Poll with auto-refresh (default: every 10 seconds)
|
|
91
|
+
const unsubscribe = bc.nowPlaying.subscribe((data) => {
|
|
92
|
+
console.log('Now:', data.now?.show_name)
|
|
93
|
+
console.log('Next:', data.next?.show_name)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Custom interval
|
|
97
|
+
const unsubscribe = bc.nowPlaying.subscribe(callback, { interval: 5000 })
|
|
98
|
+
|
|
99
|
+
// Stop polling
|
|
100
|
+
unsubscribe()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Shows
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// List all active shows
|
|
107
|
+
const shows = await bc.shows()
|
|
108
|
+
|
|
109
|
+
// Get show detail (includes presenter bios, pronouns, roles)
|
|
110
|
+
const show = await bc.show('morning-show')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Presenters
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// List all active presenters
|
|
117
|
+
const presenters = await bc.presenters()
|
|
118
|
+
|
|
119
|
+
// Get presenter detail (includes show descriptions)
|
|
120
|
+
const presenter = await bc.presenter('dj-max')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Archives
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Station-wide archives (paginated)
|
|
127
|
+
const page = await bc.archives()
|
|
128
|
+
// page.data — archive entries with show/event info and links
|
|
129
|
+
// page.next_cursor — pass to next call for more results
|
|
130
|
+
// page.count — total number of archives
|
|
131
|
+
|
|
132
|
+
// Filter by show
|
|
133
|
+
const filtered = await bc.archives({ show: 'morning-show' })
|
|
134
|
+
|
|
135
|
+
// Cursor-based pagination
|
|
136
|
+
const page2 = await bc.archives({ cursor: page.next_cursor })
|
|
137
|
+
|
|
138
|
+
// Offset-based pagination
|
|
139
|
+
const page3 = await bc.archives({ offset: 20, limit: 10 })
|
|
140
|
+
|
|
141
|
+
// Per-show archives
|
|
142
|
+
const showArchives = await bc.showArchives('morning-show')
|
|
143
|
+
const morePage = await bc.showArchives('morning-show', { cursor: '...' })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Events
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// List upcoming events
|
|
150
|
+
const events = await bc.events()
|
|
151
|
+
|
|
152
|
+
// Get event detail (includes segments with presenters)
|
|
153
|
+
const event = await bc.event('summer-festival')
|
|
154
|
+
// event.shows — array of segments with show_date, start_time, end_time, presenters
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Error Handling
|
|
158
|
+
|
|
159
|
+
All errors throw `BroadcakeError`:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { Broadcake, BroadcakeError } from '@techcake/broadcake-sdk'
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const show = await bc.show('nonexistent')
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof BroadcakeError) {
|
|
168
|
+
console.log(err.status) // 404
|
|
169
|
+
console.log(err.message) // 'Not found'
|
|
170
|
+
console.log(err.endpoint) // full URL that was called
|
|
171
|
+
console.log(err.body) // raw JSON error response
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Network failures (no internet, DNS errors) also throw `BroadcakeError` with `status: 0`.
|
|
177
|
+
|
|
178
|
+
## TypeScript
|
|
179
|
+
|
|
180
|
+
All response types are exported:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import type {
|
|
184
|
+
Station, StationDetail,
|
|
185
|
+
ScheduleSlot, WeekSchedule,
|
|
186
|
+
NowPlaying,
|
|
187
|
+
Show, ShowPresenter, ShowGenre,
|
|
188
|
+
Presenter, PresenterShow,
|
|
189
|
+
Archive, StationArchive, ArchivePage, StationArchivePage, ArchiveLink,
|
|
190
|
+
Event, EventDetail, EventSegment,
|
|
191
|
+
} from '@techcake/broadcake-sdk'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Requirements
|
|
195
|
+
|
|
196
|
+
- Node.js 18+ (uses native `fetch`)
|
|
197
|
+
- Or any modern browser
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Broadcake: () => Broadcake,
|
|
24
|
+
BroadcakeError: () => BroadcakeError
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/errors.ts
|
|
29
|
+
var BroadcakeError = class extends Error {
|
|
30
|
+
constructor(status, message, endpoint, body) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "BroadcakeError";
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.endpoint = endpoint;
|
|
35
|
+
this.body = body;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/subscribe.ts
|
|
40
|
+
function createNowPlayingSubscribe(fetcher) {
|
|
41
|
+
return (callback, options) => {
|
|
42
|
+
const interval = options?.interval ?? 1e4;
|
|
43
|
+
let active = true;
|
|
44
|
+
let timer;
|
|
45
|
+
const poll = async () => {
|
|
46
|
+
if (!active) return;
|
|
47
|
+
try {
|
|
48
|
+
const data = await fetcher();
|
|
49
|
+
if (active) {
|
|
50
|
+
try {
|
|
51
|
+
callback(data);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
timer = setInterval(poll, interval);
|
|
59
|
+
poll();
|
|
60
|
+
return () => {
|
|
61
|
+
active = false;
|
|
62
|
+
if (timer !== void 0) clearInterval(timer);
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/client.ts
|
|
68
|
+
var DEFAULT_BASE_URL = "https://app.broadcake.com";
|
|
69
|
+
var SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
70
|
+
function validateSlug(value, name) {
|
|
71
|
+
if (!value?.trim()) {
|
|
72
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
73
|
+
}
|
|
74
|
+
if (!SLUG_PATTERN.test(value)) {
|
|
75
|
+
throw new TypeError(`${name} contains invalid characters`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
var Broadcake = class {
|
|
80
|
+
constructor(stationSlug, options) {
|
|
81
|
+
this.stationSlug = validateSlug(stationSlug, "stationSlug");
|
|
82
|
+
this.baseUrl = (options?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
83
|
+
const fetcher = () => this.request(this.stationPath("/now-playing"));
|
|
84
|
+
const fn = (() => fetcher());
|
|
85
|
+
fn.subscribe = createNowPlayingSubscribe(fetcher);
|
|
86
|
+
this.nowPlaying = fn;
|
|
87
|
+
}
|
|
88
|
+
// ── Internal ─────────────────────────────────────────────────────────────
|
|
89
|
+
async request(path, init) {
|
|
90
|
+
const endpoint = `${this.baseUrl}${path}`;
|
|
91
|
+
let res;
|
|
92
|
+
try {
|
|
93
|
+
res = await fetch(endpoint, init);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new BroadcakeError(
|
|
96
|
+
0,
|
|
97
|
+
err instanceof Error ? err.message : "Network error",
|
|
98
|
+
endpoint
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
let body;
|
|
103
|
+
try {
|
|
104
|
+
body = await res.json();
|
|
105
|
+
} catch {
|
|
106
|
+
body = null;
|
|
107
|
+
}
|
|
108
|
+
const message = body && typeof body === "object" && "error" in body ? String(body.error) : `HTTP ${res.status}`;
|
|
109
|
+
throw new BroadcakeError(res.status, message, endpoint, body);
|
|
110
|
+
}
|
|
111
|
+
return res.json();
|
|
112
|
+
}
|
|
113
|
+
post(path, data) {
|
|
114
|
+
return this.request(path, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify(data)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
stationPath(suffix = "") {
|
|
121
|
+
return `/api/v1/stations/${this.stationSlug}${suffix}`;
|
|
122
|
+
}
|
|
123
|
+
// ── Stations ─────────────────────────────────────────────────────────────
|
|
124
|
+
/** List all stations. */
|
|
125
|
+
async stations() {
|
|
126
|
+
return this.request("/api/v1/stations");
|
|
127
|
+
}
|
|
128
|
+
/** Get this station's details. */
|
|
129
|
+
async station() {
|
|
130
|
+
return this.request(this.stationPath());
|
|
131
|
+
}
|
|
132
|
+
async schedule(dateOrRange) {
|
|
133
|
+
if (typeof dateOrRange === "string") {
|
|
134
|
+
return this.request(this.stationPath(`/schedule/${dateOrRange}`));
|
|
135
|
+
}
|
|
136
|
+
const params = new URLSearchParams({ from: dateOrRange.from });
|
|
137
|
+
if (dateOrRange.to) params.set("to", dateOrRange.to);
|
|
138
|
+
return this.request(this.stationPath(`/schedule?${params}`));
|
|
139
|
+
}
|
|
140
|
+
/** Get today's schedule (auto-resolves date in station timezone). */
|
|
141
|
+
async scheduleToday() {
|
|
142
|
+
return this.request(this.stationPath("/schedule/today"));
|
|
143
|
+
}
|
|
144
|
+
// ── Shows ────────────────────────────────────────────────────────────────
|
|
145
|
+
/** List all shows. */
|
|
146
|
+
async shows() {
|
|
147
|
+
return this.request(this.stationPath("/shows"));
|
|
148
|
+
}
|
|
149
|
+
/** Get a show by slug. */
|
|
150
|
+
async show(showSlug) {
|
|
151
|
+
return this.request(this.stationPath(`/shows/${validateSlug(showSlug, "showSlug")}`));
|
|
152
|
+
}
|
|
153
|
+
/** Get a show's recurring schedule slots (when it airs). */
|
|
154
|
+
async showSchedule(showSlug) {
|
|
155
|
+
return this.request(this.stationPath(`/shows/${validateSlug(showSlug, "showSlug")}/schedule`));
|
|
156
|
+
}
|
|
157
|
+
// ── Presenters ───────────────────────────────────────────────────────────
|
|
158
|
+
/** List all presenters. */
|
|
159
|
+
async presenters() {
|
|
160
|
+
return this.request(this.stationPath("/presenters"));
|
|
161
|
+
}
|
|
162
|
+
/** Get a presenter by slug. */
|
|
163
|
+
async presenter(presenterSlug) {
|
|
164
|
+
return this.request(this.stationPath(`/presenters/${validateSlug(presenterSlug, "presenterSlug")}`));
|
|
165
|
+
}
|
|
166
|
+
// ── Archives ─────────────────────────────────────────────────────────────
|
|
167
|
+
/** List station-wide archives (paginated). */
|
|
168
|
+
async archives(options) {
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
if (options?.show) params.set("show", options.show);
|
|
171
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
172
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
173
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
174
|
+
const qs = params.toString();
|
|
175
|
+
return this.request(this.stationPath(`/archives${qs ? `?${qs}` : ""}`));
|
|
176
|
+
}
|
|
177
|
+
/** List archives for a specific show (cursor-paginated). */
|
|
178
|
+
async showArchives(showSlug, options) {
|
|
179
|
+
validateSlug(showSlug, "showSlug");
|
|
180
|
+
const params = new URLSearchParams();
|
|
181
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
182
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
183
|
+
const qs = params.toString();
|
|
184
|
+
return this.request(
|
|
185
|
+
this.stationPath(`/shows/${showSlug}/archives${qs ? `?${qs}` : ""}`)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// ── Events ───────────────────────────────────────────────────────────────
|
|
189
|
+
/** List upcoming events. */
|
|
190
|
+
async events() {
|
|
191
|
+
return this.request(this.stationPath("/events"));
|
|
192
|
+
}
|
|
193
|
+
/** Get an event by slug (includes segments and presenters). */
|
|
194
|
+
async event(eventSlug) {
|
|
195
|
+
return this.request(this.stationPath(`/events/${validateSlug(eventSlug, "eventSlug")}`));
|
|
196
|
+
}
|
|
197
|
+
// ── Forms ────────────────────────────────────────────────────────────────
|
|
198
|
+
/** List all published forms. */
|
|
199
|
+
async forms() {
|
|
200
|
+
return this.request(this.stationPath("/forms"));
|
|
201
|
+
}
|
|
202
|
+
/** Get a form by slug (includes connected data for genre/show/presenter fields). */
|
|
203
|
+
async form(formSlug) {
|
|
204
|
+
return this.request(this.stationPath(`/forms/${validateSlug(formSlug, "formSlug")}`));
|
|
205
|
+
}
|
|
206
|
+
/** Submit data to a form. */
|
|
207
|
+
async submitForm(formSlug, data) {
|
|
208
|
+
return this.post(
|
|
209
|
+
this.stationPath(`/forms/${validateSlug(formSlug, "formSlug")}/submit`),
|
|
210
|
+
data
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
215
|
+
0 && (module.exports = {
|
|
216
|
+
Broadcake,
|
|
217
|
+
BroadcakeError
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/subscribe.ts","../src/client.ts"],"sourcesContent":["export { Broadcake } from './client'\nexport { BroadcakeError } from './errors'\nexport type {\n\tStation,\n\tStationDetail,\n\tStationStream,\n\tStationSocialLink,\n\tScheduleSlot,\n\tWeekSchedule,\n\tWeekScheduleDay,\n\tNowPlaying,\n\tGenre,\n\tSlotPresenter,\n\tShow,\n\tShowPresenter,\n\tShowGenre,\n\tPresenter,\n\tPresenterShow,\n\tArchive,\n\tStationArchive,\n\tArchiveLink,\n\tArchiveShow,\n\tArchiveEvent,\n\tArchivePage,\n\tStationArchivePage,\n\tEvent,\n\tEventDetail,\n\tEventShow,\n\tEventSegment,\n\tEventSegmentPresenter,\n\tShowSchedule,\n\tShowScheduleSlot,\n\tTodaySchedule,\n\tForm,\n\tFormDetail,\n\tFormField,\n\tFormConnectedData,\n\tFormAvailability,\n\tFormSubmitResult,\n\tBroadcakeOptions,\n\tScheduleRangeOptions,\n\tArchiveListOptions,\n\tShowArchiveListOptions,\n\tSubscribeOptions,\n} from './types'\nexport type { NowPlayingCallback, Unsubscribe } from './subscribe'\n","export class BroadcakeError extends Error {\n\tpublic readonly status: number\n\tpublic readonly endpoint: string\n\tpublic readonly body: unknown\n\n\tconstructor(status: number, message: string, endpoint: string, body?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'BroadcakeError'\n\t\tthis.status = status\n\t\tthis.endpoint = endpoint\n\t\tthis.body = body\n\t}\n}\n","import type { NowPlaying, SubscribeOptions } from './types'\n\nexport type NowPlayingCallback = (data: NowPlaying) => void\nexport type Unsubscribe = () => void\n\nexport function createNowPlayingSubscribe(\n\tfetcher: () => Promise<NowPlaying>,\n): (callback: NowPlayingCallback, options?: SubscribeOptions) => Unsubscribe {\n\treturn (callback: NowPlayingCallback, options?: SubscribeOptions): Unsubscribe => {\n\t\tconst interval = options?.interval ?? 10_000\n\t\tlet active = true\n\t\tlet timer: ReturnType<typeof setInterval> | undefined\n\n\t\tconst poll = async () => {\n\t\t\tif (!active) return\n\t\t\ttry {\n\t\t\t\tconst data = await fetcher()\n\t\t\t\tif (active) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcallback(data)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Callback errors don't break the polling loop\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Silently skip failed fetches — next tick retries\n\t\t\t}\n\t\t}\n\n\t\t// Set up interval before initial poll to avoid edge cases\n\t\ttimer = setInterval(poll, interval)\n\t\tpoll()\n\n\t\treturn () => {\n\t\t\tactive = false\n\t\t\tif (timer !== undefined) clearInterval(timer)\n\t\t}\n\t}\n}\n","import { BroadcakeError } from './errors'\nimport { createNowPlayingSubscribe } from './subscribe'\nimport type {\n\tStation,\n\tStationDetail,\n\tScheduleSlot,\n\tWeekSchedule,\n\tNowPlaying,\n\tShow,\n\tPresenter,\n\tEvent,\n\tEventDetail,\n\tArchivePage,\n\tStationArchivePage,\n\tForm,\n\tFormDetail,\n\tFormSubmitResult,\n\tBroadcakeOptions,\n\tScheduleRangeOptions,\n\tArchiveListOptions,\n\tShowArchiveListOptions,\n\tSubscribeOptions,\n\tShowSchedule,\n\tTodaySchedule,\n} from './types'\nimport type { NowPlayingCallback, Unsubscribe } from './subscribe'\n\nconst DEFAULT_BASE_URL = 'https://app.broadcake.com'\nconst SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i\n\nfunction validateSlug(value: string, name: string): string {\n\tif (!value?.trim()) {\n\t\tthrow new TypeError(`${name} must be a non-empty string`)\n\t}\n\tif (!SLUG_PATTERN.test(value)) {\n\t\tthrow new TypeError(`${name} contains invalid characters`)\n\t}\n\treturn value\n}\n\ntype NowPlayingFn = {\n\t(): Promise<NowPlaying>\n\tsubscribe: (callback: NowPlayingCallback, options?: SubscribeOptions) => Unsubscribe\n}\n\nexport class Broadcake {\n\tprivate readonly stationSlug: string\n\tprivate readonly baseUrl: string\n\n\t/** Fetch current now-playing data, or subscribe for polling updates. */\n\tpublic readonly nowPlaying: NowPlayingFn\n\n\tconstructor(stationSlug: string, options?: BroadcakeOptions) {\n\t\tthis.stationSlug = validateSlug(stationSlug, 'stationSlug')\n\t\tthis.baseUrl = (options?.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '')\n\n\t\tconst fetcher = () => this.request<NowPlaying>(this.stationPath('/now-playing'))\n\t\tconst fn = (() => fetcher()) as NowPlayingFn\n\t\tfn.subscribe = createNowPlayingSubscribe(fetcher)\n\t\tthis.nowPlaying = fn\n\t}\n\n\t// ── Internal ─────────────────────────────────────────────────────────────\n\n\tprivate async request<T>(path: string, init?: RequestInit): Promise<T> {\n\t\tconst endpoint = `${this.baseUrl}${path}`\n\n\t\tlet res: Response\n\t\ttry {\n\t\t\tres = await fetch(endpoint, init)\n\t\t} catch (err) {\n\t\t\tthrow new BroadcakeError(\n\t\t\t\t0,\n\t\t\t\terr instanceof Error ? err.message : 'Network error',\n\t\t\t\tendpoint,\n\t\t\t)\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tlet body: unknown\n\t\t\ttry {\n\t\t\t\tbody = await res.json()\n\t\t\t} catch {\n\t\t\t\tbody = null\n\t\t\t}\n\t\t\tconst message =\n\t\t\t\tbody && typeof body === 'object' && 'error' in body\n\t\t\t\t\t? String((body as Record<string, unknown>).error)\n\t\t\t\t\t: `HTTP ${res.status}`\n\t\t\tthrow new BroadcakeError(res.status, message, endpoint, body)\n\t\t}\n\n\t\treturn res.json() as Promise<T>\n\t}\n\n\tprivate post<T>(path: string, data: unknown): Promise<T> {\n\t\treturn this.request<T>(path, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify(data),\n\t\t})\n\t}\n\n\tprivate stationPath(suffix: string = ''): string {\n\t\treturn `/api/v1/stations/${this.stationSlug}${suffix}`\n\t}\n\n\t// ── Stations ─────────────────────────────────────────────────────────────\n\n\t/** List all stations. */\n\tasync stations(): Promise<Station[]> {\n\t\treturn this.request<Station[]>('/api/v1/stations')\n\t}\n\n\t/** Get this station's details. */\n\tasync station(): Promise<StationDetail> {\n\t\treturn this.request<StationDetail>(this.stationPath())\n\t}\n\n\t// ── Schedule ─────────────────────────────────────────────────────────────\n\n\t/** Get the schedule for a single day. */\n\tasync schedule(date: string): Promise<ScheduleSlot[]>\n\t/** Get the schedule for a date range (defaults to 7 days if `to` is omitted). */\n\tasync schedule(range: ScheduleRangeOptions): Promise<WeekSchedule>\n\tasync schedule(dateOrRange: string | ScheduleRangeOptions): Promise<ScheduleSlot[] | WeekSchedule> {\n\t\tif (typeof dateOrRange === 'string') {\n\t\t\treturn this.request<ScheduleSlot[]>(this.stationPath(`/schedule/${dateOrRange}`))\n\t\t}\n\t\tconst params = new URLSearchParams({ from: dateOrRange.from })\n\t\tif (dateOrRange.to) params.set('to', dateOrRange.to)\n\t\treturn this.request<WeekSchedule>(this.stationPath(`/schedule?${params}`))\n\t}\n\n\t/** Get today's schedule (auto-resolves date in station timezone). */\n\tasync scheduleToday(): Promise<TodaySchedule> {\n\t\treturn this.request<TodaySchedule>(this.stationPath('/schedule/today'))\n\t}\n\n\t// ── Shows ────────────────────────────────────────────────────────────────\n\n\t/** List all shows. */\n\tasync shows(): Promise<Show[]> {\n\t\treturn this.request<Show[]>(this.stationPath('/shows'))\n\t}\n\n\t/** Get a show by slug. */\n\tasync show(showSlug: string): Promise<Show> {\n\t\treturn this.request<Show>(this.stationPath(`/shows/${validateSlug(showSlug, 'showSlug')}`))\n\t}\n\n\t/** Get a show's recurring schedule slots (when it airs). */\n\tasync showSchedule(showSlug: string): Promise<ShowSchedule> {\n\t\treturn this.request<ShowSchedule>(this.stationPath(`/shows/${validateSlug(showSlug, 'showSlug')}/schedule`))\n\t}\n\n\t// ── Presenters ───────────────────────────────────────────────────────────\n\n\t/** List all presenters. */\n\tasync presenters(): Promise<Presenter[]> {\n\t\treturn this.request<Presenter[]>(this.stationPath('/presenters'))\n\t}\n\n\t/** Get a presenter by slug. */\n\tasync presenter(presenterSlug: string): Promise<Presenter> {\n\t\treturn this.request<Presenter>(this.stationPath(`/presenters/${validateSlug(presenterSlug, 'presenterSlug')}`))\n\t}\n\n\t// ── Archives ─────────────────────────────────────────────────────────────\n\n\t/** List station-wide archives (paginated). */\n\tasync archives(options?: ArchiveListOptions): Promise<StationArchivePage> {\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.show) params.set('show', options.show)\n\t\tif (options?.cursor) params.set('cursor', options.cursor)\n\t\tif (options?.offset !== undefined) params.set('offset', String(options.offset))\n\t\tif (options?.limit !== undefined) params.set('limit', String(options.limit))\n\t\tconst qs = params.toString()\n\t\treturn this.request<StationArchivePage>(this.stationPath(`/archives${qs ? `?${qs}` : ''}`))\n\t}\n\n\t/** List archives for a specific show (cursor-paginated). */\n\tasync showArchives(showSlug: string, options?: ShowArchiveListOptions): Promise<ArchivePage> {\n\t\tvalidateSlug(showSlug, 'showSlug')\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.cursor) params.set('cursor', options.cursor)\n\t\tif (options?.limit !== undefined) params.set('limit', String(options.limit))\n\t\tconst qs = params.toString()\n\t\treturn this.request<ArchivePage>(\n\t\t\tthis.stationPath(`/shows/${showSlug}/archives${qs ? `?${qs}` : ''}`),\n\t\t)\n\t}\n\n\t// ── Events ───────────────────────────────────────────────────────────────\n\n\t/** List upcoming events. */\n\tasync events(): Promise<Event[]> {\n\t\treturn this.request<Event[]>(this.stationPath('/events'))\n\t}\n\n\t/** Get an event by slug (includes segments and presenters). */\n\tasync event(eventSlug: string): Promise<EventDetail> {\n\t\treturn this.request<EventDetail>(this.stationPath(`/events/${validateSlug(eventSlug, 'eventSlug')}`))\n\t}\n\n\t// ── Forms ────────────────────────────────────────────────────────────────\n\n\t/** List all published forms. */\n\tasync forms(): Promise<Form[]> {\n\t\treturn this.request<Form[]>(this.stationPath('/forms'))\n\t}\n\n\t/** Get a form by slug (includes connected data for genre/show/presenter fields). */\n\tasync form(formSlug: string): Promise<FormDetail> {\n\t\treturn this.request<FormDetail>(this.stationPath(`/forms/${validateSlug(formSlug, 'formSlug')}`))\n\t}\n\n\t/** Submit data to a form. */\n\tasync submitForm(formSlug: string, data: Record<string, unknown>): Promise<FormSubmitResult> {\n\t\treturn this.post<FormSubmitResult>(\n\t\t\tthis.stationPath(`/forms/${validateSlug(formSlug, 'formSlug')}/submit`),\n\t\t\tdata,\n\t\t)\n\t}\n}\n"],"mappings":"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","names":[]}
|