@syra.fm/sdk 0.1.0 → 0.3.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 +16 -4
- package/dist/cjs/client.js +77 -4
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/schema.js +20 -1
- package/dist/esm/client.js +77 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/schema.js +19 -0
- package/dist/types/client.d.ts +69 -6
- package/dist/types/index.d.ts +4 -4
- package/dist/types/schema.d.ts +281 -0
- package/package.json +1 -1
- package/src/client.test.ts +260 -9
- package/src/client.ts +178 -8
- package/src/index.ts +6 -0
- package/src/schema.ts +21 -0
package/README.md
CHANGED
|
@@ -18,11 +18,15 @@ import { createSyraClient } from '@syra.fm/sdk';
|
|
|
18
18
|
|
|
19
19
|
const syra = createSyraClient(); // defaults to https://api.syra.fm
|
|
20
20
|
|
|
21
|
-
// Search the catalog
|
|
22
|
-
|
|
21
|
+
// Search the catalog — returns one page; only tracks with a public preview are
|
|
22
|
+
// in `items`. Paginate for infinite scroll by advancing `offset` by `limit`.
|
|
23
|
+
const page = await syra.searchTracks('lofi beats', { limit: 10, offset: 0 });
|
|
24
|
+
if (page.hasMore) {
|
|
25
|
+
const next = await syra.searchTracks('lofi beats', { limit: 10, offset: 10 });
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
// Fetch a single track
|
|
25
|
-
const track = await syra.getTrack(
|
|
29
|
+
const track = await syra.getTrack(page.items[0].id);
|
|
26
30
|
|
|
27
31
|
// Build a public 30s preview URL (directly playable MP3)
|
|
28
32
|
const url = syra.previewUrl(track.id); // .../api/preview/<id>.mp3?start=0
|
|
@@ -48,10 +52,18 @@ authenticated transport can be layered in a future version.
|
|
|
48
52
|
|
|
49
53
|
| Method | Description |
|
|
50
54
|
| --- | --- |
|
|
51
|
-
| `searchTracks(query, { limit })` |
|
|
55
|
+
| `searchTracks(query, { limit, offset })` | A `SearchPage<TrackSummary>` of preview-available tracks (`{ items, hasMore, limit, offset }`). |
|
|
52
56
|
| `getTrack(id)` | A single `TrackSummary`, schema-validated. |
|
|
53
57
|
| `previewUrl(id, startSec = 0)` | Public 30s preview URL. |
|
|
54
58
|
| `artworkUrl(trackOrCoverArt, size?)` | Absolute artwork URL, or `undefined`. |
|
|
59
|
+
| `searchPodcasts(query, { limit, offset })` | A `SearchPage<PodcastSummary>` of podcast shows. |
|
|
60
|
+
| `getPodcast(id)` | A single `PodcastSummary`, schema-validated. |
|
|
61
|
+
| `podcastUrl(id)` | Syra web deep link (`/podcasts/:id`). |
|
|
62
|
+
| `podcastArtworkUrl(show, size?)` | Absolute show-artwork URL, or `undefined`. |
|
|
63
|
+
|
|
64
|
+
`hasMore` reflects the backend's pagination over the full result set, so it is
|
|
65
|
+
not affected by the client-side preview filter on `searchTracks` — paginate by
|
|
66
|
+
advancing `offset` by `limit`, never by `items.length`.
|
|
55
67
|
|
|
56
68
|
Responses are validated at runtime with the package's own self-contained Zod
|
|
57
69
|
schemas (`trackSummarySchema`), so there are no shared internal dependencies.
|
package/dist/cjs/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_SYRA_BASE_URL = void 0;
|
|
3
|
+
exports.DEFAULT_SYRA_WEB_BASE_URL = exports.DEFAULT_SYRA_BASE_URL = void 0;
|
|
4
4
|
exports.createSyraClient = createSyraClient;
|
|
5
5
|
const schema_1 = require("./schema");
|
|
6
6
|
const errors_1 = require("./errors");
|
|
7
7
|
/** Default base URL of the public Syra API. */
|
|
8
8
|
exports.DEFAULT_SYRA_BASE_URL = 'https://api.syra.fm';
|
|
9
|
+
/** Default base URL of the Syra web app, used for deep links. */
|
|
10
|
+
exports.DEFAULT_SYRA_WEB_BASE_URL = 'https://syra.fm';
|
|
9
11
|
/** Order used to pick the best available artwork variant when none is named. */
|
|
10
12
|
const ARTWORK_FALLBACK_ORDER = [
|
|
11
13
|
'original',
|
|
@@ -16,12 +18,17 @@ const ARTWORK_FALLBACK_ORDER = [
|
|
|
16
18
|
'small',
|
|
17
19
|
];
|
|
18
20
|
const OBJECT_ID_PATTERN = /^[a-f\d]{24}$/i;
|
|
21
|
+
/** Read a finite number from an unknown response field, else a fallback. */
|
|
22
|
+
function numberOr(value, fallback) {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
24
|
+
}
|
|
19
25
|
/**
|
|
20
26
|
* Create a headless client for the public Syra API. Public reads only — there
|
|
21
27
|
* is no authentication in this version.
|
|
22
28
|
*/
|
|
23
29
|
function createSyraClient(options = {}) {
|
|
24
30
|
const baseURL = (options.baseURL ?? exports.DEFAULT_SYRA_BASE_URL).replace(/\/+$/, '');
|
|
31
|
+
const webBaseURL = (options.webBaseURL ?? exports.DEFAULT_SYRA_WEB_BASE_URL).replace(/\/+$/, '');
|
|
25
32
|
function resolveFetch() {
|
|
26
33
|
if (options.fetch) {
|
|
27
34
|
return options.fetch;
|
|
@@ -64,17 +71,29 @@ function createSyraClient(options = {}) {
|
|
|
64
71
|
if (typeof searchOptions.limit === 'number') {
|
|
65
72
|
params.set('limit', String(searchOptions.limit));
|
|
66
73
|
}
|
|
74
|
+
if (typeof searchOptions.offset === 'number') {
|
|
75
|
+
params.set('offset', String(searchOptions.offset));
|
|
76
|
+
}
|
|
67
77
|
const json = (await getJson(`/api/search?${params.toString()}`));
|
|
68
78
|
const rawTracks = Array.isArray(json?.results?.tracks) ? json.results.tracks : [];
|
|
69
|
-
const
|
|
79
|
+
const items = [];
|
|
70
80
|
for (const raw of rawTracks) {
|
|
71
81
|
// A single malformed catalog row must not fail the whole search.
|
|
72
82
|
const parsed = schema_1.trackSummarySchema.safeParse(raw);
|
|
73
83
|
if (parsed.success && parsed.data.previewAvailable === true) {
|
|
74
|
-
|
|
84
|
+
items.push(parsed.data);
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
items,
|
|
89
|
+
// `hasMore` is sourced from the backend's pagination over the FULL result
|
|
90
|
+
// set; the client-side preview filter above may shrink `items` below
|
|
91
|
+
// `limit`, but must NOT corrupt `hasMore` (else a page whose tail was
|
|
92
|
+
// filtered out would falsely report the end of the catalog).
|
|
93
|
+
hasMore: json?.hasMore === true,
|
|
94
|
+
limit: numberOr(json?.limit, searchOptions.limit ?? rawTracks.length),
|
|
95
|
+
offset: numberOr(json?.offset, searchOptions.offset ?? 0),
|
|
96
|
+
};
|
|
78
97
|
},
|
|
79
98
|
async getTrack(id) {
|
|
80
99
|
const json = await getJson(`/api/tracks/${encodeURIComponent(id)}`);
|
|
@@ -108,5 +127,59 @@ function createSyraClient(options = {}) {
|
|
|
108
127
|
}
|
|
109
128
|
return undefined;
|
|
110
129
|
},
|
|
130
|
+
async searchPodcasts(query, searchOptions = {}) {
|
|
131
|
+
const params = new URLSearchParams({ q: query });
|
|
132
|
+
if (typeof searchOptions.limit === 'number') {
|
|
133
|
+
params.set('limit', String(searchOptions.limit));
|
|
134
|
+
}
|
|
135
|
+
if (typeof searchOptions.offset === 'number') {
|
|
136
|
+
params.set('offset', String(searchOptions.offset));
|
|
137
|
+
}
|
|
138
|
+
const json = (await getJson(`/api/podcasts/search?${params.toString()}`));
|
|
139
|
+
const rawPodcasts = Array.isArray(json?.data) ? json.data : [];
|
|
140
|
+
const items = [];
|
|
141
|
+
for (const raw of rawPodcasts) {
|
|
142
|
+
// A single malformed catalog row must not fail the whole search.
|
|
143
|
+
const parsed = schema_1.podcastSummarySchema.safeParse(raw);
|
|
144
|
+
if (parsed.success) {
|
|
145
|
+
items.push(parsed.data);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
items,
|
|
150
|
+
// `hasMore` reflects the backend's pagination over the full result set.
|
|
151
|
+
hasMore: json?.hasMore === true,
|
|
152
|
+
limit: numberOr(json?.limit, searchOptions.limit ?? rawPodcasts.length),
|
|
153
|
+
offset: numberOr(json?.offset, searchOptions.offset ?? 0),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
async getPodcast(id) {
|
|
157
|
+
const json = (await getJson(`/api/podcasts/${encodeURIComponent(id)}`));
|
|
158
|
+
return schema_1.podcastSummarySchema.parse(json?.data?.podcast);
|
|
159
|
+
},
|
|
160
|
+
podcastUrl(id) {
|
|
161
|
+
return `${webBaseURL}/podcasts/${encodeURIComponent(id)}`;
|
|
162
|
+
},
|
|
163
|
+
podcastArtworkUrl(source, size) {
|
|
164
|
+
if (size && source.imageSizes) {
|
|
165
|
+
const resolved = resolveImageRef(source.imageSizes[size]?.url);
|
|
166
|
+
if (resolved) {
|
|
167
|
+
return resolved;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const fromImage = resolveImageRef(source.image);
|
|
171
|
+
if (fromImage) {
|
|
172
|
+
return fromImage;
|
|
173
|
+
}
|
|
174
|
+
if (source.imageSizes) {
|
|
175
|
+
for (const key of ARTWORK_FALLBACK_ORDER) {
|
|
176
|
+
const resolved = resolveImageRef(source.imageSizes[key]?.url);
|
|
177
|
+
if (resolved) {
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return resolveImageRef(source.imageSourceUrl);
|
|
183
|
+
},
|
|
111
184
|
};
|
|
112
185
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SyraApiError = exports.coverArtVariantSchema = exports.coverArtSizesSchema = exports.trackSummarySchema = exports.DEFAULT_SYRA_BASE_URL = exports.createSyraClient = void 0;
|
|
3
|
+
exports.SyraApiError = exports.coverArtVariantSchema = exports.coverArtSizesSchema = exports.podcastSummarySchema = exports.trackSummarySchema = exports.DEFAULT_SYRA_WEB_BASE_URL = exports.DEFAULT_SYRA_BASE_URL = exports.createSyraClient = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "createSyraClient", { enumerable: true, get: function () { return client_1.createSyraClient; } });
|
|
6
6
|
Object.defineProperty(exports, "DEFAULT_SYRA_BASE_URL", { enumerable: true, get: function () { return client_1.DEFAULT_SYRA_BASE_URL; } });
|
|
7
|
+
Object.defineProperty(exports, "DEFAULT_SYRA_WEB_BASE_URL", { enumerable: true, get: function () { return client_1.DEFAULT_SYRA_WEB_BASE_URL; } });
|
|
7
8
|
var schema_1 = require("./schema");
|
|
8
9
|
Object.defineProperty(exports, "trackSummarySchema", { enumerable: true, get: function () { return schema_1.trackSummarySchema; } });
|
|
10
|
+
Object.defineProperty(exports, "podcastSummarySchema", { enumerable: true, get: function () { return schema_1.podcastSummarySchema; } });
|
|
9
11
|
Object.defineProperty(exports, "coverArtSizesSchema", { enumerable: true, get: function () { return schema_1.coverArtSizesSchema; } });
|
|
10
12
|
Object.defineProperty(exports, "coverArtVariantSchema", { enumerable: true, get: function () { return schema_1.coverArtVariantSchema; } });
|
|
11
13
|
var errors_1 = require("./errors");
|
package/dist/cjs/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.trackSummarySchema = exports.coverArtSizesSchema = exports.coverArtVariantSchema = void 0;
|
|
3
|
+
exports.podcastSummarySchema = exports.trackSummarySchema = exports.coverArtSizesSchema = exports.coverArtVariantSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
/**
|
|
6
6
|
* Minimal, self-contained schemas for the public Syra API response shapes this
|
|
@@ -41,3 +41,22 @@ exports.trackSummarySchema = zod_1.z.object({
|
|
|
41
41
|
coverArtSizes: exports.coverArtSizesSchema.optional(),
|
|
42
42
|
previewAvailable: zod_1.z.boolean().optional(),
|
|
43
43
|
});
|
|
44
|
+
/**
|
|
45
|
+
* The summary view of a podcast SHOW returned by the public podcast endpoints
|
|
46
|
+
* (`GET /api/podcasts/search`, `GET /api/podcasts/:id`) — just enough to render
|
|
47
|
+
* a show card and deep-link into the Syra app.
|
|
48
|
+
*
|
|
49
|
+
* Artwork mirrors tracks: `image` is the re-hosted Syra image id (resolved via
|
|
50
|
+
* `/api/images/:id`); `imageSizes` is the multi-resolution variant set (each
|
|
51
|
+
* variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
|
|
52
|
+
* external artwork URL as an absolute fallback when re-hosting has not run yet.
|
|
53
|
+
*/
|
|
54
|
+
exports.podcastSummarySchema = zod_1.z.object({
|
|
55
|
+
id: zod_1.z.string(),
|
|
56
|
+
title: zod_1.z.string(),
|
|
57
|
+
author: zod_1.z.string().optional(),
|
|
58
|
+
description: zod_1.z.string().optional(),
|
|
59
|
+
image: zod_1.z.string().optional(),
|
|
60
|
+
imageSizes: exports.coverArtSizesSchema.optional(),
|
|
61
|
+
imageSourceUrl: zod_1.z.string().optional(),
|
|
62
|
+
});
|
package/dist/esm/client.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { trackSummarySchema, } from './schema.js';
|
|
1
|
+
import { trackSummarySchema, podcastSummarySchema, } from './schema.js';
|
|
2
2
|
import { SyraApiError } from './errors.js';
|
|
3
3
|
/** Default base URL of the public Syra API. */
|
|
4
4
|
export const DEFAULT_SYRA_BASE_URL = 'https://api.syra.fm';
|
|
5
|
+
/** Default base URL of the Syra web app, used for deep links. */
|
|
6
|
+
export const DEFAULT_SYRA_WEB_BASE_URL = 'https://syra.fm';
|
|
5
7
|
/** Order used to pick the best available artwork variant when none is named. */
|
|
6
8
|
const ARTWORK_FALLBACK_ORDER = [
|
|
7
9
|
'original',
|
|
@@ -12,12 +14,17 @@ const ARTWORK_FALLBACK_ORDER = [
|
|
|
12
14
|
'small',
|
|
13
15
|
];
|
|
14
16
|
const OBJECT_ID_PATTERN = /^[a-f\d]{24}$/i;
|
|
17
|
+
/** Read a finite number from an unknown response field, else a fallback. */
|
|
18
|
+
function numberOr(value, fallback) {
|
|
19
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Create a headless client for the public Syra API. Public reads only — there
|
|
17
23
|
* is no authentication in this version.
|
|
18
24
|
*/
|
|
19
25
|
export function createSyraClient(options = {}) {
|
|
20
26
|
const baseURL = (options.baseURL ?? DEFAULT_SYRA_BASE_URL).replace(/\/+$/, '');
|
|
27
|
+
const webBaseURL = (options.webBaseURL ?? DEFAULT_SYRA_WEB_BASE_URL).replace(/\/+$/, '');
|
|
21
28
|
function resolveFetch() {
|
|
22
29
|
if (options.fetch) {
|
|
23
30
|
return options.fetch;
|
|
@@ -60,17 +67,29 @@ export function createSyraClient(options = {}) {
|
|
|
60
67
|
if (typeof searchOptions.limit === 'number') {
|
|
61
68
|
params.set('limit', String(searchOptions.limit));
|
|
62
69
|
}
|
|
70
|
+
if (typeof searchOptions.offset === 'number') {
|
|
71
|
+
params.set('offset', String(searchOptions.offset));
|
|
72
|
+
}
|
|
63
73
|
const json = (await getJson(`/api/search?${params.toString()}`));
|
|
64
74
|
const rawTracks = Array.isArray(json?.results?.tracks) ? json.results.tracks : [];
|
|
65
|
-
const
|
|
75
|
+
const items = [];
|
|
66
76
|
for (const raw of rawTracks) {
|
|
67
77
|
// A single malformed catalog row must not fail the whole search.
|
|
68
78
|
const parsed = trackSummarySchema.safeParse(raw);
|
|
69
79
|
if (parsed.success && parsed.data.previewAvailable === true) {
|
|
70
|
-
|
|
80
|
+
items.push(parsed.data);
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
|
-
return
|
|
83
|
+
return {
|
|
84
|
+
items,
|
|
85
|
+
// `hasMore` is sourced from the backend's pagination over the FULL result
|
|
86
|
+
// set; the client-side preview filter above may shrink `items` below
|
|
87
|
+
// `limit`, but must NOT corrupt `hasMore` (else a page whose tail was
|
|
88
|
+
// filtered out would falsely report the end of the catalog).
|
|
89
|
+
hasMore: json?.hasMore === true,
|
|
90
|
+
limit: numberOr(json?.limit, searchOptions.limit ?? rawTracks.length),
|
|
91
|
+
offset: numberOr(json?.offset, searchOptions.offset ?? 0),
|
|
92
|
+
};
|
|
74
93
|
},
|
|
75
94
|
async getTrack(id) {
|
|
76
95
|
const json = await getJson(`/api/tracks/${encodeURIComponent(id)}`);
|
|
@@ -104,5 +123,59 @@ export function createSyraClient(options = {}) {
|
|
|
104
123
|
}
|
|
105
124
|
return undefined;
|
|
106
125
|
},
|
|
126
|
+
async searchPodcasts(query, searchOptions = {}) {
|
|
127
|
+
const params = new URLSearchParams({ q: query });
|
|
128
|
+
if (typeof searchOptions.limit === 'number') {
|
|
129
|
+
params.set('limit', String(searchOptions.limit));
|
|
130
|
+
}
|
|
131
|
+
if (typeof searchOptions.offset === 'number') {
|
|
132
|
+
params.set('offset', String(searchOptions.offset));
|
|
133
|
+
}
|
|
134
|
+
const json = (await getJson(`/api/podcasts/search?${params.toString()}`));
|
|
135
|
+
const rawPodcasts = Array.isArray(json?.data) ? json.data : [];
|
|
136
|
+
const items = [];
|
|
137
|
+
for (const raw of rawPodcasts) {
|
|
138
|
+
// A single malformed catalog row must not fail the whole search.
|
|
139
|
+
const parsed = podcastSummarySchema.safeParse(raw);
|
|
140
|
+
if (parsed.success) {
|
|
141
|
+
items.push(parsed.data);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
items,
|
|
146
|
+
// `hasMore` reflects the backend's pagination over the full result set.
|
|
147
|
+
hasMore: json?.hasMore === true,
|
|
148
|
+
limit: numberOr(json?.limit, searchOptions.limit ?? rawPodcasts.length),
|
|
149
|
+
offset: numberOr(json?.offset, searchOptions.offset ?? 0),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
async getPodcast(id) {
|
|
153
|
+
const json = (await getJson(`/api/podcasts/${encodeURIComponent(id)}`));
|
|
154
|
+
return podcastSummarySchema.parse(json?.data?.podcast);
|
|
155
|
+
},
|
|
156
|
+
podcastUrl(id) {
|
|
157
|
+
return `${webBaseURL}/podcasts/${encodeURIComponent(id)}`;
|
|
158
|
+
},
|
|
159
|
+
podcastArtworkUrl(source, size) {
|
|
160
|
+
if (size && source.imageSizes) {
|
|
161
|
+
const resolved = resolveImageRef(source.imageSizes[size]?.url);
|
|
162
|
+
if (resolved) {
|
|
163
|
+
return resolved;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const fromImage = resolveImageRef(source.image);
|
|
167
|
+
if (fromImage) {
|
|
168
|
+
return fromImage;
|
|
169
|
+
}
|
|
170
|
+
if (source.imageSizes) {
|
|
171
|
+
for (const key of ARTWORK_FALLBACK_ORDER) {
|
|
172
|
+
const resolved = resolveImageRef(source.imageSizes[key]?.url);
|
|
173
|
+
if (resolved) {
|
|
174
|
+
return resolved;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return resolveImageRef(source.imageSourceUrl);
|
|
179
|
+
},
|
|
107
180
|
};
|
|
108
181
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createSyraClient, DEFAULT_SYRA_BASE_URL, } from './client.js';
|
|
2
|
-
export { trackSummarySchema, coverArtSizesSchema, coverArtVariantSchema, } from './schema.js';
|
|
1
|
+
export { createSyraClient, DEFAULT_SYRA_BASE_URL, DEFAULT_SYRA_WEB_BASE_URL, } from './client.js';
|
|
2
|
+
export { trackSummarySchema, podcastSummarySchema, coverArtSizesSchema, coverArtVariantSchema, } from './schema.js';
|
|
3
3
|
export { SyraApiError } from './errors.js';
|
package/dist/esm/schema.js
CHANGED
|
@@ -38,3 +38,22 @@ export const trackSummarySchema = z.object({
|
|
|
38
38
|
coverArtSizes: coverArtSizesSchema.optional(),
|
|
39
39
|
previewAvailable: z.boolean().optional(),
|
|
40
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* The summary view of a podcast SHOW returned by the public podcast endpoints
|
|
43
|
+
* (`GET /api/podcasts/search`, `GET /api/podcasts/:id`) — just enough to render
|
|
44
|
+
* a show card and deep-link into the Syra app.
|
|
45
|
+
*
|
|
46
|
+
* Artwork mirrors tracks: `image` is the re-hosted Syra image id (resolved via
|
|
47
|
+
* `/api/images/:id`); `imageSizes` is the multi-resolution variant set (each
|
|
48
|
+
* variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
|
|
49
|
+
* external artwork URL as an absolute fallback when re-hosting has not run yet.
|
|
50
|
+
*/
|
|
51
|
+
export const podcastSummarySchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
title: z.string(),
|
|
54
|
+
author: z.string().optional(),
|
|
55
|
+
description: z.string().optional(),
|
|
56
|
+
image: z.string().optional(),
|
|
57
|
+
imageSizes: coverArtSizesSchema.optional(),
|
|
58
|
+
imageSourceUrl: z.string().optional(),
|
|
59
|
+
});
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { type TrackSummary, type CoverArtSizes, type ArtworkSize } from './schema';
|
|
1
|
+
import { type TrackSummary, type PodcastSummary, type CoverArtSizes, type ArtworkSize } from './schema';
|
|
2
2
|
/** Default base URL of the public Syra API. */
|
|
3
3
|
export declare const DEFAULT_SYRA_BASE_URL = "https://api.syra.fm";
|
|
4
|
+
/** Default base URL of the Syra web app, used for deep links. */
|
|
5
|
+
export declare const DEFAULT_SYRA_WEB_BASE_URL = "https://syra.fm";
|
|
4
6
|
export interface SyraClientOptions {
|
|
5
7
|
/** Base URL of the Syra API. Defaults to {@link DEFAULT_SYRA_BASE_URL}. */
|
|
6
8
|
baseURL?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Base URL of the Syra WEB app (not the API host), used to build deep links
|
|
11
|
+
* such as {@link SyraClient.podcastUrl}. Defaults to
|
|
12
|
+
* {@link DEFAULT_SYRA_WEB_BASE_URL}.
|
|
13
|
+
*/
|
|
14
|
+
webBaseURL?: string;
|
|
7
15
|
/**
|
|
8
16
|
* `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
|
|
9
17
|
* React Native). Inject one (e.g. `node-fetch`) when no global is available.
|
|
@@ -12,20 +20,55 @@ export interface SyraClientOptions {
|
|
|
12
20
|
fetch?: typeof fetch;
|
|
13
21
|
}
|
|
14
22
|
export interface SearchTracksOptions {
|
|
15
|
-
/** Maximum number of tracks to request from the API. */
|
|
23
|
+
/** Maximum number of tracks to request from the API (the page size). */
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** Zero-based offset of the first track to return (for infinite scroll). */
|
|
26
|
+
offset?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SearchPodcastsOptions {
|
|
29
|
+
/** Maximum number of podcast shows to request from the API (the page size). */
|
|
16
30
|
limit?: number;
|
|
31
|
+
/** Zero-based offset of the first show to return (for infinite scroll). */
|
|
32
|
+
offset?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One page of paginated catalog search results.
|
|
36
|
+
*
|
|
37
|
+
* `hasMore` reflects the BACKEND's pagination over the full matching set — NOT
|
|
38
|
+
* `items.length`. {@link SyraClient.searchTracks} additionally filters its page
|
|
39
|
+
* client-side to preview-available tracks, so `items.length` can be smaller than
|
|
40
|
+
* `limit` while `hasMore` is still `true`; callers must paginate by advancing
|
|
41
|
+
* `offset` by `limit` (the page size), never by `items.length`.
|
|
42
|
+
*/
|
|
43
|
+
export interface SearchPage<T> {
|
|
44
|
+
/** The validated rows for this page. */
|
|
45
|
+
items: T[];
|
|
46
|
+
/** Whether the backend has results beyond this page. */
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
/** The page size the backend applied. */
|
|
49
|
+
limit: number;
|
|
50
|
+
/** The zero-based offset of this page. */
|
|
51
|
+
offset: number;
|
|
17
52
|
}
|
|
18
|
-
/** Minimal shape from which artwork URLs can be derived. */
|
|
53
|
+
/** Minimal shape from which track artwork URLs can be derived. */
|
|
19
54
|
export interface ArtworkSource {
|
|
20
55
|
coverArt?: string | null;
|
|
21
56
|
coverArtSizes?: CoverArtSizes | null;
|
|
22
57
|
}
|
|
58
|
+
/** Minimal shape from which podcast-show artwork URLs can be derived. */
|
|
59
|
+
export interface PodcastArtworkSource {
|
|
60
|
+
image?: string | null;
|
|
61
|
+
imageSizes?: CoverArtSizes | null;
|
|
62
|
+
imageSourceUrl?: string | null;
|
|
63
|
+
}
|
|
23
64
|
export interface SyraClient {
|
|
24
65
|
/**
|
|
25
|
-
* Search the public catalog for tracks.
|
|
26
|
-
* track-summary schema and filtered to those that expose
|
|
66
|
+
* Search the public catalog for tracks. Returns one paginated page: rows are
|
|
67
|
+
* validated against the track-summary schema and filtered to those that expose
|
|
68
|
+
* a public preview. `hasMore` comes from the backend's pagination, so it is
|
|
69
|
+
* unaffected by the client-side preview filter (see {@link SearchPage}).
|
|
27
70
|
*/
|
|
28
|
-
searchTracks(query: string, options?: SearchTracksOptions): Promise<TrackSummary
|
|
71
|
+
searchTracks(query: string, options?: SearchTracksOptions): Promise<SearchPage<TrackSummary>>;
|
|
29
72
|
/** Fetch a single track by id, validated against the track-summary schema. */
|
|
30
73
|
getTrack(id: string): Promise<TrackSummary>;
|
|
31
74
|
/** Build the public 30s preview URL for a track at the given start offset. */
|
|
@@ -35,6 +78,26 @@ export interface SyraClient {
|
|
|
35
78
|
* `undefined` when no artwork can be derived.
|
|
36
79
|
*/
|
|
37
80
|
artworkUrl(source: string | ArtworkSource, size?: ArtworkSize): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Search the public catalog for podcast SHOWS (not episodes). Returns one
|
|
83
|
+
* paginated page: rows are validated against the podcast-summary schema and
|
|
84
|
+
* malformed rows are dropped. `hasMore` comes from the backend's pagination.
|
|
85
|
+
*/
|
|
86
|
+
searchPodcasts(query: string, options?: SearchPodcastsOptions): Promise<SearchPage<PodcastSummary>>;
|
|
87
|
+
/**
|
|
88
|
+
* Fetch a single podcast show by id, validated against the podcast-summary
|
|
89
|
+
* schema. The by-id endpoint also returns episodes and resolved persons; this
|
|
90
|
+
* returns just the show summary needed to render a card.
|
|
91
|
+
*/
|
|
92
|
+
getPodcast(id: string): Promise<PodcastSummary>;
|
|
93
|
+
/** Build the Syra web app deep link for a podcast show (`/podcasts/:id`). */
|
|
94
|
+
podcastUrl(id: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Resolve an absolute artwork URL from a podcast show reference. Prefers the
|
|
97
|
+
* re-hosted Syra image, then the requested/fallback variant, then the original
|
|
98
|
+
* external artwork URL. Returns `undefined` when no artwork can be derived.
|
|
99
|
+
*/
|
|
100
|
+
podcastArtworkUrl(source: PodcastArtworkSource, size?: ArtworkSize): string | undefined;
|
|
38
101
|
}
|
|
39
102
|
/**
|
|
40
103
|
* Create a headless client for the public Syra API. Public reads only — there
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createSyraClient, DEFAULT_SYRA_BASE_URL, } from './client';
|
|
2
|
-
export type { SyraClient, SyraClientOptions, SearchTracksOptions, ArtworkSource, } from './client';
|
|
3
|
-
export { trackSummarySchema, coverArtSizesSchema, coverArtVariantSchema, } from './schema';
|
|
4
|
-
export type { TrackSummary, CoverArtSizes, CoverArtVariant, ArtworkSize, } from './schema';
|
|
1
|
+
export { createSyraClient, DEFAULT_SYRA_BASE_URL, DEFAULT_SYRA_WEB_BASE_URL, } from './client';
|
|
2
|
+
export type { SyraClient, SyraClientOptions, SearchTracksOptions, SearchPodcastsOptions, SearchPage, ArtworkSource, PodcastArtworkSource, } from './client';
|
|
3
|
+
export { trackSummarySchema, podcastSummarySchema, coverArtSizesSchema, coverArtVariantSchema, } from './schema';
|
|
4
|
+
export type { TrackSummary, PodcastSummary, CoverArtSizes, CoverArtVariant, ArtworkSize, } from './schema';
|
|
5
5
|
export { SyraApiError } from './errors';
|