@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 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 (only tracks with a public preview are returned)
22
- const tracks = await syra.searchTracks('lofi beats', { limit: 10 });
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(tracks[0].id);
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 })` | Preview-available `TrackSummary[]` matching `query`. |
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.
@@ -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 tracks = [];
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
- tracks.push(parsed.data);
84
+ items.push(parsed.data);
75
85
  }
76
86
  }
77
- return tracks;
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");
@@ -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
+ });
@@ -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 tracks = [];
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
- tracks.push(parsed.data);
80
+ items.push(parsed.data);
71
81
  }
72
82
  }
73
- return tracks;
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';
@@ -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
+ });
@@ -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. Results are validated against the
26
- * track-summary schema and filtered to those that expose a public preview.
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
@@ -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';