@openverb/music-atlas 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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The shapes of the Open Music Atlas API, v1.
3
+ *
4
+ * v1 is a stable contract: fields may be added, never removed or renamed.
5
+ * Responses carry Open Music Atlas URLs and descriptive metadata only — no
6
+ * internal database ids, no provider identifiers and no audio files. Songs
7
+ * are heard through the embeddable player (`embedUrl`).
8
+ */
9
+ /** What kind of place a record is. "place" is used where classification itself is contested. */
10
+ type PlaceKind = "country" | "territory" | "continent" | "place" | "city" | "region" | "landmark";
11
+ /** A place in the atlas. Places are permanent; each edition gives a place a song. */
12
+ interface Place {
13
+ /** Stable identifier, e.g. "jamaica". */
14
+ slug: string;
15
+ name: string;
16
+ /** ISO 3166-1 alpha-2 code where one applies, e.g. "JM". */
17
+ isoCode: string | null;
18
+ kind: PlaceKind;
19
+ /** UN M49 continent, e.g. "Americas". */
20
+ continent: string | null;
21
+ /** UN M49 region, e.g. "Caribbean". */
22
+ region: string | null;
23
+ /** A representative point, [longitude, latitude]. */
24
+ coordinates: [number, number] | null;
25
+ /** Disputed or partially recognised. Inclusion is not a statement about sovereignty. */
26
+ isDisputed: boolean;
27
+ /** The place's page on Open Music Atlas. */
28
+ pageUrl: string;
29
+ }
30
+ /** A published edition of the atlas, e.g. the 2026 Founding Edition. */
31
+ interface Edition {
32
+ slug: string;
33
+ name: string;
34
+ /** The collection it belongs to, e.g. "world-music-atlas". */
35
+ collection: string;
36
+ year: number;
37
+ version: number;
38
+ /** The artistic constraint for the whole edition, if any. */
39
+ styleConstraint: string | null;
40
+ /** When the edition was frozen (ISO 8601). A frozen edition's songs never change. */
41
+ frozenAt: string | null;
42
+ entryCount: number;
43
+ pageUrl: string;
44
+ }
45
+ /** The edition an entry belongs to, as it appears alongside a place. */
46
+ interface EditionRef {
47
+ slug: string;
48
+ name: string;
49
+ year: number;
50
+ version: number;
51
+ frozenAt: string | null;
52
+ }
53
+ /** A place's song in one edition. */
54
+ interface Entry {
55
+ title: string;
56
+ description: string | null;
57
+ /** The song's musical style. */
58
+ style: string | null;
59
+ curatorsSelection: boolean;
60
+ /** The place's page on Open Music Atlas. */
61
+ pageUrl: string;
62
+ /** The embeddable player for this song. */
63
+ embedUrl: string;
64
+ }
65
+ /** A place and its song in every published edition. */
66
+ interface PlaceDetail {
67
+ place: Place;
68
+ entries: {
69
+ edition: EditionRef;
70
+ entry: Entry;
71
+ }[];
72
+ }
73
+ /** An edition and every song in it, in listening order. */
74
+ interface EditionDetail {
75
+ edition: Edition;
76
+ entries: {
77
+ place: Place;
78
+ entry: Entry;
79
+ }[];
80
+ }
81
+ /** The properties of one point in an edition's GeoJSON. */
82
+ interface AtlasFeatureProperties {
83
+ slug: string;
84
+ name: string;
85
+ isoCode: string | null;
86
+ kind: PlaceKind;
87
+ continent: string | null;
88
+ region: string | null;
89
+ isDisputed: boolean;
90
+ /** The edition's slug. */
91
+ edition: string;
92
+ title: string;
93
+ description: string | null;
94
+ style: string | null;
95
+ curatorsSelection: boolean;
96
+ pageUrl: string;
97
+ embedUrl: string;
98
+ }
99
+ interface AtlasFeature {
100
+ type: "Feature";
101
+ id: string;
102
+ geometry: {
103
+ type: "Point";
104
+ coordinates: [number, number];
105
+ };
106
+ properties: AtlasFeatureProperties;
107
+ }
108
+ /** An edition as a GeoJSON FeatureCollection: one point per place. */
109
+ interface AtlasFeatureCollection {
110
+ type: "FeatureCollection";
111
+ name: string;
112
+ edition: Edition;
113
+ features: AtlasFeature[];
114
+ }
115
+ interface ClientOptions {
116
+ /** Defaults to https://openmusicatlas.org. */
117
+ baseUrl?: string;
118
+ /** A fetch implementation. Defaults to the global fetch (Node 18+, browsers, Deno, Bun). */
119
+ fetch?: typeof fetch;
120
+ /** Extra headers sent with every request. */
121
+ headers?: Record<string, string>;
122
+ }
123
+ interface PlaceFilter {
124
+ /** UN M49 continent, e.g. "Africa" (case-insensitive). */
125
+ continent?: string;
126
+ /** UN M49 region, e.g. "Caribbean" (case-insensitive). */
127
+ region?: string;
128
+ kind?: PlaceKind;
129
+ }
130
+ interface EmbedOptions {
131
+ /** Defaults to "100%". */
132
+ width?: number | string;
133
+ /** Defaults to 212, which fits the player without scrolling. */
134
+ height?: number | string;
135
+ /** The frame's accessible title. Defaults to "<place> — World Music Atlas". */
136
+ title?: string;
137
+ /** Defaults to "lazy". */
138
+ loading?: "lazy" | "eager";
139
+ }
140
+
141
+ /** The public home of the World Music Atlas. */
142
+ declare const DEFAULT_BASE_URL = "https://openmusicatlas.org";
143
+ /** The first edition: the World Music Atlas — 2026 Founding Edition. */
144
+ declare const FOUNDING_EDITION = "2026-founding";
145
+ /** The height, in pixels, at which the embeddable player fits without scrolling. */
146
+ declare const EMBED_HEIGHT = 212;
147
+ /** A failed request to the Open Music Atlas API. */
148
+ declare class OpenMusicAtlasError extends Error {
149
+ /** The HTTP status, e.g. 429 when rate-limited. */
150
+ readonly status: number;
151
+ /** Seconds to wait before retrying, when the API says so. */
152
+ readonly retryAfter: number | null;
153
+ constructor(message: string, status: number, retryAfter?: number | null);
154
+ }
155
+ type PlaceRef = string | Pick<Place, "slug">;
156
+ /**
157
+ * A client for the Open Music Atlas API (v1).
158
+ *
159
+ * ```ts
160
+ * import { atlas } from "@openverb/music-atlas"
161
+ *
162
+ * const jamaica = await atlas.country("Jamaica")
163
+ * const song = await atlas.place("jamaica")
164
+ * ```
165
+ */
166
+ declare class OpenMusicAtlas {
167
+ readonly baseUrl: string;
168
+ private readonly customFetch?;
169
+ private readonly headers;
170
+ private placesRequest;
171
+ constructor(options?: ClientOptions);
172
+ /**
173
+ * Every place in the atlas, in listening order (continent, region, name).
174
+ * Fetched once per client and then served from memory; pass a filter to
175
+ * narrow it.
176
+ */
177
+ places(filter?: PlaceFilter): Promise<Place[]>;
178
+ /** A place and its song in every published edition, or null if there is no such place. */
179
+ place(place: PlaceRef): Promise<PlaceDetail | null>;
180
+ /** Every published edition, newest first. */
181
+ editions(): Promise<Edition[]>;
182
+ /** An edition and all its songs in listening order, or null if there is no such edition. */
183
+ edition(slug?: string): Promise<EditionDetail | null>;
184
+ /**
185
+ * An edition as a GeoJSON FeatureCollection — one point per place, with the
186
+ * place and its song as properties. Loads straight into Leaflet, MapLibre,
187
+ * Mapbox, OpenLayers or QGIS.
188
+ */
189
+ geojson(slug?: string): Promise<AtlasFeatureCollection | null>;
190
+ /**
191
+ * One place by slug ("jamaica"), ISO code ("JM") or name ("Jamaica",
192
+ * "cote d'ivoire"), or null. Case- and accent-insensitive; an exact match
193
+ * wins over a name that merely starts with the query.
194
+ */
195
+ find(query: string): Promise<Place | null>;
196
+ /** The same as find(), named for the common case: `atlas.country("Jamaica")`. */
197
+ country(query: string): Promise<Place | null>;
198
+ /** Places whose name contains the query, names starting with it first. */
199
+ search(query: string): Promise<Place[]>;
200
+ /** The embeddable player's address for a place's song. No network request. */
201
+ embedUrl(place: PlaceRef, edition?: string): string;
202
+ /** An `<iframe>` for the embeddable player, as HTML — for server rendering, templates and map popups. */
203
+ embedHtml(place: PlaceRef, edition?: string, options?: EmbedOptions): string;
204
+ /** An `<iframe>` element for the embeddable player, ready to append. Browser only. */
205
+ createEmbed(place: PlaceRef, edition?: string, options?: EmbedOptions): HTMLIFrameElement;
206
+ private embedAttributes;
207
+ private request;
208
+ }
209
+ /** A ready-made client for https://openmusicatlas.org. */
210
+ declare const atlas: OpenMusicAtlas;
211
+
212
+ export { type AtlasFeature, type AtlasFeatureCollection, type AtlasFeatureProperties, type ClientOptions, DEFAULT_BASE_URL, EMBED_HEIGHT, type Edition, type EditionDetail, type EditionRef, type EmbedOptions, type Entry, FOUNDING_EDITION, OpenMusicAtlas, OpenMusicAtlasError, type Place, type PlaceDetail, type PlaceFilter, type PlaceKind, atlas };
package/dist/index.js ADDED
@@ -0,0 +1,153 @@
1
+ // src/index.ts
2
+ var DEFAULT_BASE_URL = "https://openmusicatlas.org";
3
+ var FOUNDING_EDITION = "2026-founding";
4
+ var EMBED_HEIGHT = 212;
5
+ var OpenMusicAtlasError = class extends Error {
6
+ constructor(message, status, retryAfter = null) {
7
+ super(message);
8
+ this.name = "OpenMusicAtlasError";
9
+ this.status = status;
10
+ this.retryAfter = retryAfter;
11
+ }
12
+ };
13
+ var fold = (s) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
14
+ var escapeAttr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15
+ var slugOf = (place) => typeof place === "string" ? place : place.slug;
16
+ var OpenMusicAtlas = class {
17
+ constructor(options = {}) {
18
+ this.placesRequest = null;
19
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
20
+ this.customFetch = options.fetch;
21
+ this.headers = { Accept: "application/json", ...options.headers };
22
+ }
23
+ /* ----------------------------------------------------------------- data */
24
+ /**
25
+ * Every place in the atlas, in listening order (continent, region, name).
26
+ * Fetched once per client and then served from memory; pass a filter to
27
+ * narrow it.
28
+ */
29
+ async places(filter = {}) {
30
+ if (!this.placesRequest) {
31
+ this.placesRequest = this.request("/api/v1/places").then((d) => d.places);
32
+ this.placesRequest.catch(() => this.placesRequest = null);
33
+ }
34
+ const all = await this.placesRequest;
35
+ const continent = filter.continent ? fold(filter.continent) : null;
36
+ const region = filter.region ? fold(filter.region) : null;
37
+ return all.filter(
38
+ (p) => (!continent || fold(p.continent ?? "") === continent) && (!region || fold(p.region ?? "") === region) && (!filter.kind || p.kind === filter.kind)
39
+ );
40
+ }
41
+ /** A place and its song in every published edition, or null if there is no such place. */
42
+ async place(place) {
43
+ return this.request(`/api/v1/places/${encodeURIComponent(slugOf(place))}`, { nullOn404: true });
44
+ }
45
+ /** Every published edition, newest first. */
46
+ async editions() {
47
+ return (await this.request("/api/v1/editions")).editions;
48
+ }
49
+ /** An edition and all its songs in listening order, or null if there is no such edition. */
50
+ async edition(slug = FOUNDING_EDITION) {
51
+ return this.request(`/api/v1/editions/${encodeURIComponent(slug)}`, { nullOn404: true });
52
+ }
53
+ /**
54
+ * An edition as a GeoJSON FeatureCollection — one point per place, with the
55
+ * place and its song as properties. Loads straight into Leaflet, MapLibre,
56
+ * Mapbox, OpenLayers or QGIS.
57
+ */
58
+ async geojson(slug = FOUNDING_EDITION) {
59
+ return this.request(`/api/v1/editions/${encodeURIComponent(slug)}.geojson`, {
60
+ nullOn404: true
61
+ });
62
+ }
63
+ /* -------------------------------------------------------------- finding */
64
+ /**
65
+ * One place by slug ("jamaica"), ISO code ("JM") or name ("Jamaica",
66
+ * "cote d'ivoire"), or null. Case- and accent-insensitive; an exact match
67
+ * wins over a name that merely starts with the query.
68
+ */
69
+ async find(query) {
70
+ const q = fold(query);
71
+ if (!q) return null;
72
+ const all = await this.places();
73
+ return all.find((p) => p.slug === q) ?? all.find((p) => p.isoCode !== null && p.isoCode.toLowerCase() === q) ?? all.find((p) => fold(p.name) === q) ?? all.find((p) => fold(p.name).startsWith(q)) ?? null;
74
+ }
75
+ /** The same as find(), named for the common case: `atlas.country("Jamaica")`. */
76
+ country(query) {
77
+ return this.find(query);
78
+ }
79
+ /** Places whose name contains the query, names starting with it first. */
80
+ async search(query) {
81
+ const q = fold(query);
82
+ if (!q) return [];
83
+ const all = await this.places();
84
+ return all.filter((p) => fold(p.name).includes(q)).sort((a, b) => Number(!fold(a.name).startsWith(q)) - Number(!fold(b.name).startsWith(q)) || a.name.localeCompare(b.name));
85
+ }
86
+ /* --------------------------------------------------------------- embeds */
87
+ /** The embeddable player's address for a place's song. No network request. */
88
+ embedUrl(place, edition = FOUNDING_EDITION) {
89
+ return `${this.baseUrl}/embed/${encodeURIComponent(slugOf(place))}/${encodeURIComponent(edition)}`;
90
+ }
91
+ /** An `<iframe>` for the embeddable player, as HTML — for server rendering, templates and map popups. */
92
+ embedHtml(place, edition = FOUNDING_EDITION, options = {}) {
93
+ const a = this.embedAttributes(place, edition, options);
94
+ return `<iframe src="${escapeAttr(a.src)}" width="${escapeAttr(a.width)}" height="${escapeAttr(a.height)}" loading="${a.loading}" style="${a.style}" allow="${a.allow}" title="${escapeAttr(a.title)}"></iframe>`;
95
+ }
96
+ /** An `<iframe>` element for the embeddable player, ready to append. Browser only. */
97
+ createEmbed(place, edition = FOUNDING_EDITION, options = {}) {
98
+ if (typeof document === "undefined") {
99
+ throw new Error("createEmbed() needs a browser. On the server, use embedHtml().");
100
+ }
101
+ const a = this.embedAttributes(place, edition, options);
102
+ const frame = document.createElement("iframe");
103
+ frame.src = a.src;
104
+ frame.width = a.width;
105
+ frame.height = a.height;
106
+ frame.loading = a.loading;
107
+ frame.setAttribute("style", a.style);
108
+ frame.allow = a.allow;
109
+ frame.title = a.title;
110
+ return frame;
111
+ }
112
+ embedAttributes(place, edition, options) {
113
+ const name = typeof place === "string" ? place : "name" in place ? String(place.name) : place.slug;
114
+ return {
115
+ src: this.embedUrl(place, edition),
116
+ width: String(options.width ?? "100%"),
117
+ height: String(options.height ?? EMBED_HEIGHT),
118
+ loading: options.loading ?? "lazy",
119
+ style: "border:0;border-radius:12px",
120
+ allow: "autoplay; encrypted-media",
121
+ title: options.title ?? `${name} \u2014 World Music Atlas`
122
+ };
123
+ }
124
+ async request(path, options = {}) {
125
+ const fetcher = this.customFetch ?? globalThis.fetch;
126
+ if (typeof fetcher !== "function") {
127
+ throw new Error("No fetch is available. Use Node 18 or later, or pass `fetch` in the client options.");
128
+ }
129
+ const res = await fetcher(`${this.baseUrl}${path}`, { headers: this.headers });
130
+ if (res.status === 404 && options.nullOn404) return null;
131
+ if (!res.ok) {
132
+ let message = `Open Music Atlas API responded with ${res.status}`;
133
+ try {
134
+ const body = await res.json();
135
+ if (body?.error) message = body.error;
136
+ } catch {
137
+ }
138
+ const retry = res.headers.get("retry-after");
139
+ throw new OpenMusicAtlasError(message, res.status, retry !== null && retry !== "" ? Number(retry) : null);
140
+ }
141
+ return await res.json();
142
+ }
143
+ };
144
+ var atlas = new OpenMusicAtlas();
145
+ export {
146
+ DEFAULT_BASE_URL,
147
+ EMBED_HEIGHT,
148
+ FOUNDING_EDITION,
149
+ OpenMusicAtlas,
150
+ OpenMusicAtlasError,
151
+ atlas
152
+ };
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n AtlasFeatureCollection,\n ClientOptions,\n Edition,\n EditionDetail,\n EmbedOptions,\n Place,\n PlaceDetail,\n PlaceFilter,\n} from \"./types\"\n\nexport type * from \"./types\"\n\n/** The public home of the World Music Atlas. */\nexport const DEFAULT_BASE_URL = \"https://openmusicatlas.org\"\n\n/** The first edition: the World Music Atlas — 2026 Founding Edition. */\nexport const FOUNDING_EDITION = \"2026-founding\"\n\n/** The height, in pixels, at which the embeddable player fits without scrolling. */\nexport const EMBED_HEIGHT = 212\n\n/** A failed request to the Open Music Atlas API. */\nexport class OpenMusicAtlasError extends Error {\n /** The HTTP status, e.g. 429 when rate-limited. */\n readonly status: number\n /** Seconds to wait before retrying, when the API says so. */\n readonly retryAfter: number | null\n\n constructor(message: string, status: number, retryAfter: number | null = null) {\n super(message)\n this.name = \"OpenMusicAtlasError\"\n this.status = status\n this.retryAfter = retryAfter\n }\n}\n\ntype PlaceRef = string | Pick<Place, \"slug\">\n\n/** Lower-case and strip accents, so \"Côte d'Ivoire\" matches \"cote d'ivoire\". */\nconst fold = (s: string) =>\n s\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .trim()\n\nconst escapeAttr = (s: string) =>\n s.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n\nconst slugOf = (place: PlaceRef) => (typeof place === \"string\" ? place : place.slug)\n\n/**\n * A client for the Open Music Atlas API (v1).\n *\n * ```ts\n * import { atlas } from \"@openverb/music-atlas\"\n *\n * const jamaica = await atlas.country(\"Jamaica\")\n * const song = await atlas.place(\"jamaica\")\n * ```\n */\nexport class OpenMusicAtlas {\n readonly baseUrl: string\n private readonly customFetch?: typeof fetch\n private readonly headers: Record<string, string>\n private placesRequest: Promise<Place[]> | null = null\n\n constructor(options: ClientOptions = {}) {\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\")\n this.customFetch = options.fetch\n this.headers = { Accept: \"application/json\", ...options.headers }\n }\n\n /* ----------------------------------------------------------------- data */\n\n /**\n * Every place in the atlas, in listening order (continent, region, name).\n * Fetched once per client and then served from memory; pass a filter to\n * narrow it.\n */\n async places(filter: PlaceFilter = {}): Promise<Place[]> {\n if (!this.placesRequest) {\n this.placesRequest = this.request<{ places: Place[] }>(\"/api/v1/places\").then((d) => d.places)\n // A failed load shouldn't be remembered.\n this.placesRequest.catch(() => (this.placesRequest = null))\n }\n const all = await this.placesRequest\n const continent = filter.continent ? fold(filter.continent) : null\n const region = filter.region ? fold(filter.region) : null\n return all.filter(\n (p) =>\n (!continent || fold(p.continent ?? \"\") === continent) &&\n (!region || fold(p.region ?? \"\") === region) &&\n (!filter.kind || p.kind === filter.kind)\n )\n }\n\n /** A place and its song in every published edition, or null if there is no such place. */\n async place(place: PlaceRef): Promise<PlaceDetail | null> {\n return this.request<PlaceDetail>(`/api/v1/places/${encodeURIComponent(slugOf(place))}`, { nullOn404: true })\n }\n\n /** Every published edition, newest first. */\n async editions(): Promise<Edition[]> {\n return (await this.request<{ editions: Edition[] }>(\"/api/v1/editions\")).editions\n }\n\n /** An edition and all its songs in listening order, or null if there is no such edition. */\n async edition(slug: string = FOUNDING_EDITION): Promise<EditionDetail | null> {\n return this.request<EditionDetail>(`/api/v1/editions/${encodeURIComponent(slug)}`, { nullOn404: true })\n }\n\n /**\n * An edition as a GeoJSON FeatureCollection — one point per place, with the\n * place and its song as properties. Loads straight into Leaflet, MapLibre,\n * Mapbox, OpenLayers or QGIS.\n */\n async geojson(slug: string = FOUNDING_EDITION): Promise<AtlasFeatureCollection | null> {\n return this.request<AtlasFeatureCollection>(`/api/v1/editions/${encodeURIComponent(slug)}.geojson`, {\n nullOn404: true,\n })\n }\n\n /* -------------------------------------------------------------- finding */\n\n /**\n * One place by slug (\"jamaica\"), ISO code (\"JM\") or name (\"Jamaica\",\n * \"cote d'ivoire\"), or null. Case- and accent-insensitive; an exact match\n * wins over a name that merely starts with the query.\n */\n async find(query: string): Promise<Place | null> {\n const q = fold(query)\n if (!q) return null\n const all = await this.places()\n return (\n all.find((p) => p.slug === q) ??\n all.find((p) => p.isoCode !== null && p.isoCode.toLowerCase() === q) ??\n all.find((p) => fold(p.name) === q) ??\n all.find((p) => fold(p.name).startsWith(q)) ??\n null\n )\n }\n\n /** The same as find(), named for the common case: `atlas.country(\"Jamaica\")`. */\n country(query: string): Promise<Place | null> {\n return this.find(query)\n }\n\n /** Places whose name contains the query, names starting with it first. */\n async search(query: string): Promise<Place[]> {\n const q = fold(query)\n if (!q) return []\n const all = await this.places()\n return all\n .filter((p) => fold(p.name).includes(q))\n .sort((a, b) => Number(!fold(a.name).startsWith(q)) - Number(!fold(b.name).startsWith(q)) || a.name.localeCompare(b.name))\n }\n\n /* --------------------------------------------------------------- embeds */\n\n /** The embeddable player's address for a place's song. No network request. */\n embedUrl(place: PlaceRef, edition: string = FOUNDING_EDITION): string {\n return `${this.baseUrl}/embed/${encodeURIComponent(slugOf(place))}/${encodeURIComponent(edition)}`\n }\n\n /** An `<iframe>` for the embeddable player, as HTML — for server rendering, templates and map popups. */\n embedHtml(place: PlaceRef, edition: string = FOUNDING_EDITION, options: EmbedOptions = {}): string {\n const a = this.embedAttributes(place, edition, options)\n return (\n `<iframe src=\"${escapeAttr(a.src)}\" width=\"${escapeAttr(a.width)}\" height=\"${escapeAttr(a.height)}\"` +\n ` loading=\"${a.loading}\" style=\"${a.style}\" allow=\"${a.allow}\" title=\"${escapeAttr(a.title)}\"></iframe>`\n )\n }\n\n /** An `<iframe>` element for the embeddable player, ready to append. Browser only. */\n createEmbed(place: PlaceRef, edition: string = FOUNDING_EDITION, options: EmbedOptions = {}): HTMLIFrameElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"createEmbed() needs a browser. On the server, use embedHtml().\")\n }\n const a = this.embedAttributes(place, edition, options)\n const frame = document.createElement(\"iframe\")\n frame.src = a.src\n frame.width = a.width\n frame.height = a.height\n frame.loading = a.loading\n frame.setAttribute(\"style\", a.style)\n frame.allow = a.allow\n frame.title = a.title\n return frame\n }\n\n private embedAttributes(place: PlaceRef, edition: string, options: EmbedOptions) {\n const name = typeof place === \"string\" ? place : \"name\" in place ? String((place as Place).name) : place.slug\n return {\n src: this.embedUrl(place, edition),\n width: String(options.width ?? \"100%\"),\n height: String(options.height ?? EMBED_HEIGHT),\n loading: options.loading ?? \"lazy\",\n style: \"border:0;border-radius:12px\",\n allow: \"autoplay; encrypted-media\",\n title: options.title ?? `${name} — World Music Atlas`,\n }\n }\n\n /* -------------------------------------------------------------- transport */\n\n private async request<T>(path: string): Promise<T>\n private async request<T>(path: string, options: { nullOn404: true }): Promise<T | null>\n private async request<T>(path: string, options: { nullOn404?: boolean } = {}): Promise<T | null> {\n const fetcher = this.customFetch ?? (globalThis as { fetch?: typeof fetch }).fetch\n if (typeof fetcher !== \"function\") {\n throw new Error(\"No fetch is available. Use Node 18 or later, or pass `fetch` in the client options.\")\n }\n\n const res = await fetcher(`${this.baseUrl}${path}`, { headers: this.headers })\n if (res.status === 404 && options.nullOn404) return null\n if (!res.ok) {\n let message = `Open Music Atlas API responded with ${res.status}`\n try {\n const body = (await res.json()) as { error?: string }\n if (body?.error) message = body.error\n } catch {\n // Not JSON; keep the generic message.\n }\n const retry = res.headers.get(\"retry-after\")\n throw new OpenMusicAtlasError(message, res.status, retry !== null && retry !== \"\" ? Number(retry) : null)\n }\n return (await res.json()) as T\n }\n}\n\n/** A ready-made client for https://openmusicatlas.org. */\nexport const atlas = new OpenMusicAtlas()\n"],"mappings":"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","names":[]}
@@ -0,0 +1,26 @@
1
+ import * as openverb from 'openverb';
2
+ import { VerbHandler, ActionResult, VerbLibrary } from 'openverb';
3
+ import { OpenMusicAtlas } from '@openverb/music-atlas';
4
+
5
+ declare const MUSIC_ATLAS_NAMESPACE = "openverb.music_atlas";
6
+ /** The verb library: everything an AI may do with the World Music Atlas. */
7
+ declare const musicAtlasLibrary: VerbLibrary;
8
+ /** Anything handlers can be registered on — an OpenVerb executor, or your own registry. */
9
+ interface VerbRegistrar {
10
+ register(verbName: string, handler: VerbHandler): void;
11
+ }
12
+ /**
13
+ * Register a handler for every Music Atlas verb on an executor, using the
14
+ * given client (by default, the one for https://openmusicatlas.org).
15
+ */
16
+ declare function registerMusicAtlasVerbs(executor: VerbRegistrar, client?: OpenMusicAtlas): void;
17
+ /** An OpenVerb executor with every Music Atlas verb registered and ready to run. */
18
+ declare function createMusicAtlasExecutor(client?: OpenMusicAtlas): {
19
+ register(verbName: string, handler: VerbHandler): void;
20
+ execute(action: openverb.Action): Promise<ActionResult>;
21
+ getRegistry(): openverb.VerbRegistry;
22
+ getVerbs(): string[];
23
+ getVerb(name: string): openverb.Verb | undefined;
24
+ };
25
+
26
+ export { MUSIC_ATLAS_NAMESPACE, type VerbRegistrar, createMusicAtlasExecutor, musicAtlasLibrary, registerMusicAtlasVerbs };