@syra.fm/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 +58 -0
- package/dist/cjs/client.js +112 -0
- package/dist/cjs/errors.js +16 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/schema.js +43 -0
- package/dist/esm/client.js +108 -0
- package/dist/esm/errors.js +12 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/schema.js +40 -0
- package/dist/types/client.d.ts +43 -0
- package/dist/types/errors.d.ts +8 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/schema.d.ts +486 -0
- package/package.json +61 -0
- package/src/client.test.ts +196 -0
- package/src/client.ts +178 -0
- package/src/errors.ts +15 -0
- package/src/index.ts +22 -0
- package/src/schema.ts +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @syra.fm/sdk
|
|
2
|
+
|
|
3
|
+
Headless, isomorphic client for the public [Syra](https://syra.fm) API. Runs on
|
|
4
|
+
Node 18+, browsers, and React Native — no React, React Native, or DOM
|
|
5
|
+
dependencies; the only runtime dependency is [`zod`](https://zod.dev).
|
|
6
|
+
**Public reads only** (no authentication in this version).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add @syra.fm/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createSyraClient } from '@syra.fm/sdk';
|
|
18
|
+
|
|
19
|
+
const syra = createSyraClient(); // defaults to https://api.syra.fm
|
|
20
|
+
|
|
21
|
+
// Search the catalog (only tracks with a public preview are returned)
|
|
22
|
+
const tracks = await syra.searchTracks('lofi beats', { limit: 10 });
|
|
23
|
+
|
|
24
|
+
// Fetch a single track
|
|
25
|
+
const track = await syra.getTrack(tracks[0].id);
|
|
26
|
+
|
|
27
|
+
// Build a public 30s preview URL (directly playable MP3)
|
|
28
|
+
const url = syra.previewUrl(track.id); // .../api/preview/<id>.mp3?start=0
|
|
29
|
+
const hook = syra.previewUrl(track.id, 45); // start 45s in
|
|
30
|
+
|
|
31
|
+
// Resolve artwork to an absolute URL
|
|
32
|
+
const cover = syra.artworkUrl(track, 'large');
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
createSyraClient({
|
|
39
|
+
baseURL: 'https://api.syra.fm', // override the API origin
|
|
40
|
+
fetch, // inject a fetch implementation (e.g. node-fetch)
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`fetch` defaults to the global `fetch`. It is the seam through which an
|
|
45
|
+
authenticated transport can be layered in a future version.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| Method | Description |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `searchTracks(query, { limit })` | Preview-available `TrackSummary[]` matching `query`. |
|
|
52
|
+
| `getTrack(id)` | A single `TrackSummary`, schema-validated. |
|
|
53
|
+
| `previewUrl(id, startSec = 0)` | Public 30s preview URL. |
|
|
54
|
+
| `artworkUrl(trackOrCoverArt, size?)` | Absolute artwork URL, or `undefined`. |
|
|
55
|
+
|
|
56
|
+
Responses are validated at runtime with the package's own self-contained Zod
|
|
57
|
+
schemas (`trackSummarySchema`), so there are no shared internal dependencies.
|
|
58
|
+
`SyraApiError` (with a `status`) is thrown on non-2xx responses.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_SYRA_BASE_URL = void 0;
|
|
4
|
+
exports.createSyraClient = createSyraClient;
|
|
5
|
+
const schema_1 = require("./schema");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
/** Default base URL of the public Syra API. */
|
|
8
|
+
exports.DEFAULT_SYRA_BASE_URL = 'https://api.syra.fm';
|
|
9
|
+
/** Order used to pick the best available artwork variant when none is named. */
|
|
10
|
+
const ARTWORK_FALLBACK_ORDER = [
|
|
11
|
+
'original',
|
|
12
|
+
'xxlarge',
|
|
13
|
+
'xlarge',
|
|
14
|
+
'large',
|
|
15
|
+
'medium',
|
|
16
|
+
'small',
|
|
17
|
+
];
|
|
18
|
+
const OBJECT_ID_PATTERN = /^[a-f\d]{24}$/i;
|
|
19
|
+
/**
|
|
20
|
+
* Create a headless client for the public Syra API. Public reads only — there
|
|
21
|
+
* is no authentication in this version.
|
|
22
|
+
*/
|
|
23
|
+
function createSyraClient(options = {}) {
|
|
24
|
+
const baseURL = (options.baseURL ?? exports.DEFAULT_SYRA_BASE_URL).replace(/\/+$/, '');
|
|
25
|
+
function resolveFetch() {
|
|
26
|
+
if (options.fetch) {
|
|
27
|
+
return options.fetch;
|
|
28
|
+
}
|
|
29
|
+
const globalFetch = globalThis.fetch;
|
|
30
|
+
if (typeof globalFetch === 'function') {
|
|
31
|
+
return globalFetch.bind(globalThis);
|
|
32
|
+
}
|
|
33
|
+
throw new Error('@syra.fm/sdk: no global fetch is available. Pass `fetch` in createSyraClient options, ' +
|
|
34
|
+
'or run on Node 18+, a browser, or React Native.');
|
|
35
|
+
}
|
|
36
|
+
async function getJson(path) {
|
|
37
|
+
const doFetch = resolveFetch();
|
|
38
|
+
const response = await doFetch(`${baseURL}${path}`, {
|
|
39
|
+
headers: { Accept: 'application/json' },
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new errors_1.SyraApiError(response.status, `Syra API request failed: ${response.status} ${response.statusText} (${path})`);
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
function resolveImageRef(ref) {
|
|
47
|
+
if (!ref) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
51
|
+
return ref;
|
|
52
|
+
}
|
|
53
|
+
if (ref.startsWith('/api/images/')) {
|
|
54
|
+
return `${baseURL}${ref}`;
|
|
55
|
+
}
|
|
56
|
+
if (OBJECT_ID_PATTERN.test(ref)) {
|
|
57
|
+
return `${baseURL}/api/images/${ref}`;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async searchTracks(query, searchOptions = {}) {
|
|
63
|
+
const params = new URLSearchParams({ q: query, category: 'tracks' });
|
|
64
|
+
if (typeof searchOptions.limit === 'number') {
|
|
65
|
+
params.set('limit', String(searchOptions.limit));
|
|
66
|
+
}
|
|
67
|
+
const json = (await getJson(`/api/search?${params.toString()}`));
|
|
68
|
+
const rawTracks = Array.isArray(json?.results?.tracks) ? json.results.tracks : [];
|
|
69
|
+
const tracks = [];
|
|
70
|
+
for (const raw of rawTracks) {
|
|
71
|
+
// A single malformed catalog row must not fail the whole search.
|
|
72
|
+
const parsed = schema_1.trackSummarySchema.safeParse(raw);
|
|
73
|
+
if (parsed.success && parsed.data.previewAvailable === true) {
|
|
74
|
+
tracks.push(parsed.data);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return tracks;
|
|
78
|
+
},
|
|
79
|
+
async getTrack(id) {
|
|
80
|
+
const json = await getJson(`/api/tracks/${encodeURIComponent(id)}`);
|
|
81
|
+
return schema_1.trackSummarySchema.parse(json);
|
|
82
|
+
},
|
|
83
|
+
previewUrl(id, startSec = 0) {
|
|
84
|
+
const safeStart = Number.isFinite(startSec) ? Math.max(0, Math.trunc(startSec)) : 0;
|
|
85
|
+
return `${baseURL}/api/preview/${encodeURIComponent(id)}.mp3?start=${safeStart}`;
|
|
86
|
+
},
|
|
87
|
+
artworkUrl(source, size) {
|
|
88
|
+
if (typeof source === 'string') {
|
|
89
|
+
return resolveImageRef(source);
|
|
90
|
+
}
|
|
91
|
+
if (size && source.coverArtSizes) {
|
|
92
|
+
const resolved = resolveImageRef(source.coverArtSizes[size]?.url);
|
|
93
|
+
if (resolved) {
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const fromCoverArt = resolveImageRef(source.coverArt);
|
|
98
|
+
if (fromCoverArt) {
|
|
99
|
+
return fromCoverArt;
|
|
100
|
+
}
|
|
101
|
+
if (source.coverArtSizes) {
|
|
102
|
+
for (const key of ARTWORK_FALLBACK_ORDER) {
|
|
103
|
+
const resolved = resolveImageRef(source.coverArtSizes[key]?.url);
|
|
104
|
+
if (resolved) {
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SyraApiError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when the Syra API returns a non-2xx response.
|
|
6
|
+
*/
|
|
7
|
+
class SyraApiError extends Error {
|
|
8
|
+
constructor(status, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'SyraApiError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
// Restore the prototype chain when targeting ES5-ish runtimes.
|
|
13
|
+
Object.setPrototypeOf(this, SyraApiError.prototype);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.SyraApiError = SyraApiError;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "createSyraClient", { enumerable: true, get: function () { return client_1.createSyraClient; } });
|
|
6
|
+
Object.defineProperty(exports, "DEFAULT_SYRA_BASE_URL", { enumerable: true, get: function () { return client_1.DEFAULT_SYRA_BASE_URL; } });
|
|
7
|
+
var schema_1 = require("./schema");
|
|
8
|
+
Object.defineProperty(exports, "trackSummarySchema", { enumerable: true, get: function () { return schema_1.trackSummarySchema; } });
|
|
9
|
+
Object.defineProperty(exports, "coverArtSizesSchema", { enumerable: true, get: function () { return schema_1.coverArtSizesSchema; } });
|
|
10
|
+
Object.defineProperty(exports, "coverArtVariantSchema", { enumerable: true, get: function () { return schema_1.coverArtVariantSchema; } });
|
|
11
|
+
var errors_1 = require("./errors");
|
|
12
|
+
Object.defineProperty(exports, "SyraApiError", { enumerable: true, get: function () { return errors_1.SyraApiError; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackSummarySchema = exports.coverArtSizesSchema = exports.coverArtVariantSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* Minimal, self-contained schemas for the public Syra API response shapes this
|
|
7
|
+
* SDK consumes. Intentionally NOT shared with the Syra backend's internal
|
|
8
|
+
* types — the SDK validates only the fields it returns, and tolerantly strips
|
|
9
|
+
* everything else (Zod object schemas drop unknown keys by default), so the API
|
|
10
|
+
* can evolve without breaking external consumers.
|
|
11
|
+
*/
|
|
12
|
+
/** A single artwork variant. The backend serializes `url` as `/api/images/:id`. */
|
|
13
|
+
exports.coverArtVariantSchema = zod_1.z.object({
|
|
14
|
+
id: zod_1.z.string().optional(),
|
|
15
|
+
url: zod_1.z.string(),
|
|
16
|
+
width: zod_1.z.number().optional(),
|
|
17
|
+
height: zod_1.z.number().optional(),
|
|
18
|
+
});
|
|
19
|
+
/** Named artwork variants keyed by size. */
|
|
20
|
+
exports.coverArtSizesSchema = zod_1.z.object({
|
|
21
|
+
small: exports.coverArtVariantSchema.optional(),
|
|
22
|
+
medium: exports.coverArtVariantSchema.optional(),
|
|
23
|
+
large: exports.coverArtVariantSchema.optional(),
|
|
24
|
+
xlarge: exports.coverArtVariantSchema.optional(),
|
|
25
|
+
xxlarge: exports.coverArtVariantSchema.optional(),
|
|
26
|
+
original: exports.coverArtVariantSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* The summary view of a track returned by the public catalog endpoints — just
|
|
30
|
+
* enough to render a song row and play its preview.
|
|
31
|
+
*/
|
|
32
|
+
exports.trackSummarySchema = zod_1.z.object({
|
|
33
|
+
id: zod_1.z.string(),
|
|
34
|
+
title: zod_1.z.string(),
|
|
35
|
+
artistId: zod_1.z.string().optional(),
|
|
36
|
+
artistName: zod_1.z.string(),
|
|
37
|
+
albumId: zod_1.z.string().optional(),
|
|
38
|
+
albumName: zod_1.z.string().optional(),
|
|
39
|
+
duration: zod_1.z.number(),
|
|
40
|
+
coverArt: zod_1.z.string().optional(),
|
|
41
|
+
coverArtSizes: exports.coverArtSizesSchema.optional(),
|
|
42
|
+
previewAvailable: zod_1.z.boolean().optional(),
|
|
43
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { trackSummarySchema, } from './schema.js';
|
|
2
|
+
import { SyraApiError } from './errors.js';
|
|
3
|
+
/** Default base URL of the public Syra API. */
|
|
4
|
+
export const DEFAULT_SYRA_BASE_URL = 'https://api.syra.fm';
|
|
5
|
+
/** Order used to pick the best available artwork variant when none is named. */
|
|
6
|
+
const ARTWORK_FALLBACK_ORDER = [
|
|
7
|
+
'original',
|
|
8
|
+
'xxlarge',
|
|
9
|
+
'xlarge',
|
|
10
|
+
'large',
|
|
11
|
+
'medium',
|
|
12
|
+
'small',
|
|
13
|
+
];
|
|
14
|
+
const OBJECT_ID_PATTERN = /^[a-f\d]{24}$/i;
|
|
15
|
+
/**
|
|
16
|
+
* Create a headless client for the public Syra API. Public reads only — there
|
|
17
|
+
* is no authentication in this version.
|
|
18
|
+
*/
|
|
19
|
+
export function createSyraClient(options = {}) {
|
|
20
|
+
const baseURL = (options.baseURL ?? DEFAULT_SYRA_BASE_URL).replace(/\/+$/, '');
|
|
21
|
+
function resolveFetch() {
|
|
22
|
+
if (options.fetch) {
|
|
23
|
+
return options.fetch;
|
|
24
|
+
}
|
|
25
|
+
const globalFetch = globalThis.fetch;
|
|
26
|
+
if (typeof globalFetch === 'function') {
|
|
27
|
+
return globalFetch.bind(globalThis);
|
|
28
|
+
}
|
|
29
|
+
throw new Error('@syra.fm/sdk: no global fetch is available. Pass `fetch` in createSyraClient options, ' +
|
|
30
|
+
'or run on Node 18+, a browser, or React Native.');
|
|
31
|
+
}
|
|
32
|
+
async function getJson(path) {
|
|
33
|
+
const doFetch = resolveFetch();
|
|
34
|
+
const response = await doFetch(`${baseURL}${path}`, {
|
|
35
|
+
headers: { Accept: 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new SyraApiError(response.status, `Syra API request failed: ${response.status} ${response.statusText} (${path})`);
|
|
39
|
+
}
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
function resolveImageRef(ref) {
|
|
43
|
+
if (!ref) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
47
|
+
return ref;
|
|
48
|
+
}
|
|
49
|
+
if (ref.startsWith('/api/images/')) {
|
|
50
|
+
return `${baseURL}${ref}`;
|
|
51
|
+
}
|
|
52
|
+
if (OBJECT_ID_PATTERN.test(ref)) {
|
|
53
|
+
return `${baseURL}/api/images/${ref}`;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
async searchTracks(query, searchOptions = {}) {
|
|
59
|
+
const params = new URLSearchParams({ q: query, category: 'tracks' });
|
|
60
|
+
if (typeof searchOptions.limit === 'number') {
|
|
61
|
+
params.set('limit', String(searchOptions.limit));
|
|
62
|
+
}
|
|
63
|
+
const json = (await getJson(`/api/search?${params.toString()}`));
|
|
64
|
+
const rawTracks = Array.isArray(json?.results?.tracks) ? json.results.tracks : [];
|
|
65
|
+
const tracks = [];
|
|
66
|
+
for (const raw of rawTracks) {
|
|
67
|
+
// A single malformed catalog row must not fail the whole search.
|
|
68
|
+
const parsed = trackSummarySchema.safeParse(raw);
|
|
69
|
+
if (parsed.success && parsed.data.previewAvailable === true) {
|
|
70
|
+
tracks.push(parsed.data);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return tracks;
|
|
74
|
+
},
|
|
75
|
+
async getTrack(id) {
|
|
76
|
+
const json = await getJson(`/api/tracks/${encodeURIComponent(id)}`);
|
|
77
|
+
return trackSummarySchema.parse(json);
|
|
78
|
+
},
|
|
79
|
+
previewUrl(id, startSec = 0) {
|
|
80
|
+
const safeStart = Number.isFinite(startSec) ? Math.max(0, Math.trunc(startSec)) : 0;
|
|
81
|
+
return `${baseURL}/api/preview/${encodeURIComponent(id)}.mp3?start=${safeStart}`;
|
|
82
|
+
},
|
|
83
|
+
artworkUrl(source, size) {
|
|
84
|
+
if (typeof source === 'string') {
|
|
85
|
+
return resolveImageRef(source);
|
|
86
|
+
}
|
|
87
|
+
if (size && source.coverArtSizes) {
|
|
88
|
+
const resolved = resolveImageRef(source.coverArtSizes[size]?.url);
|
|
89
|
+
if (resolved) {
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const fromCoverArt = resolveImageRef(source.coverArt);
|
|
94
|
+
if (fromCoverArt) {
|
|
95
|
+
return fromCoverArt;
|
|
96
|
+
}
|
|
97
|
+
if (source.coverArtSizes) {
|
|
98
|
+
for (const key of ARTWORK_FALLBACK_ORDER) {
|
|
99
|
+
const resolved = resolveImageRef(source.coverArtSizes[key]?.url);
|
|
100
|
+
if (resolved) {
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when the Syra API returns a non-2xx response.
|
|
3
|
+
*/
|
|
4
|
+
export class SyraApiError extends Error {
|
|
5
|
+
constructor(status, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'SyraApiError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
// Restore the prototype chain when targeting ES5-ish runtimes.
|
|
10
|
+
Object.setPrototypeOf(this, SyraApiError.prototype);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal, self-contained schemas for the public Syra API response shapes this
|
|
4
|
+
* SDK consumes. Intentionally NOT shared with the Syra backend's internal
|
|
5
|
+
* types — the SDK validates only the fields it returns, and tolerantly strips
|
|
6
|
+
* everything else (Zod object schemas drop unknown keys by default), so the API
|
|
7
|
+
* can evolve without breaking external consumers.
|
|
8
|
+
*/
|
|
9
|
+
/** A single artwork variant. The backend serializes `url` as `/api/images/:id`. */
|
|
10
|
+
export const coverArtVariantSchema = z.object({
|
|
11
|
+
id: z.string().optional(),
|
|
12
|
+
url: z.string(),
|
|
13
|
+
width: z.number().optional(),
|
|
14
|
+
height: z.number().optional(),
|
|
15
|
+
});
|
|
16
|
+
/** Named artwork variants keyed by size. */
|
|
17
|
+
export const coverArtSizesSchema = z.object({
|
|
18
|
+
small: coverArtVariantSchema.optional(),
|
|
19
|
+
medium: coverArtVariantSchema.optional(),
|
|
20
|
+
large: coverArtVariantSchema.optional(),
|
|
21
|
+
xlarge: coverArtVariantSchema.optional(),
|
|
22
|
+
xxlarge: coverArtVariantSchema.optional(),
|
|
23
|
+
original: coverArtVariantSchema.optional(),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The summary view of a track returned by the public catalog endpoints — just
|
|
27
|
+
* enough to render a song row and play its preview.
|
|
28
|
+
*/
|
|
29
|
+
export const trackSummarySchema = z.object({
|
|
30
|
+
id: z.string(),
|
|
31
|
+
title: z.string(),
|
|
32
|
+
artistId: z.string().optional(),
|
|
33
|
+
artistName: z.string(),
|
|
34
|
+
albumId: z.string().optional(),
|
|
35
|
+
albumName: z.string().optional(),
|
|
36
|
+
duration: z.number(),
|
|
37
|
+
coverArt: z.string().optional(),
|
|
38
|
+
coverArtSizes: coverArtSizesSchema.optional(),
|
|
39
|
+
previewAvailable: z.boolean().optional(),
|
|
40
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type TrackSummary, type CoverArtSizes, type ArtworkSize } from './schema';
|
|
2
|
+
/** Default base URL of the public Syra API. */
|
|
3
|
+
export declare const DEFAULT_SYRA_BASE_URL = "https://api.syra.fm";
|
|
4
|
+
export interface SyraClientOptions {
|
|
5
|
+
/** Base URL of the Syra API. Defaults to {@link DEFAULT_SYRA_BASE_URL}. */
|
|
6
|
+
baseURL?: string;
|
|
7
|
+
/**
|
|
8
|
+
* `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
|
|
9
|
+
* React Native). Inject one (e.g. `node-fetch`) when no global is available.
|
|
10
|
+
* This is the seam where an authenticated transport can be layered in later.
|
|
11
|
+
*/
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchTracksOptions {
|
|
15
|
+
/** Maximum number of tracks to request from the API. */
|
|
16
|
+
limit?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Minimal shape from which artwork URLs can be derived. */
|
|
19
|
+
export interface ArtworkSource {
|
|
20
|
+
coverArt?: string | null;
|
|
21
|
+
coverArtSizes?: CoverArtSizes | null;
|
|
22
|
+
}
|
|
23
|
+
export interface SyraClient {
|
|
24
|
+
/**
|
|
25
|
+
* Search the public catalog for tracks. Results are validated against the
|
|
26
|
+
* track-summary schema and filtered to those that expose a public preview.
|
|
27
|
+
*/
|
|
28
|
+
searchTracks(query: string, options?: SearchTracksOptions): Promise<TrackSummary[]>;
|
|
29
|
+
/** Fetch a single track by id, validated against the track-summary schema. */
|
|
30
|
+
getTrack(id: string): Promise<TrackSummary>;
|
|
31
|
+
/** Build the public 30s preview URL for a track at the given start offset. */
|
|
32
|
+
previewUrl(id: string, startSec?: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve an absolute artwork URL from a track / cover-art reference. Returns
|
|
35
|
+
* `undefined` when no artwork can be derived.
|
|
36
|
+
*/
|
|
37
|
+
artworkUrl(source: string | ArtworkSource, size?: ArtworkSize): string | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a headless client for the public Syra API. Public reads only — there
|
|
41
|
+
* is no authentication in this version.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createSyraClient(options?: SyraClientOptions): SyraClient;
|
|
@@ -0,0 +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';
|
|
5
|
+
export { SyraApiError } from './errors';
|