@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +25 -0
- package/README.md +162 -0
- package/dist/index.cjs +183 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +212 -0
- package/dist/index.d.ts +212 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/openverb.d.ts +26 -0
- package/dist/openverb.js +299 -0
- package/dist/openverb.js.map +1 -0
- package/openverb.music_atlas.json +220 -0
- package/package.json +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release of `@openverb/music-atlas`, for the Open Music Atlas API v1.
|
|
6
|
+
|
|
7
|
+
- `OpenMusicAtlas` client and the ready-made `atlas` instance
|
|
8
|
+
- `places()` with continent, region and kind filters; `place()`; `editions()`; `edition()`; `geojson()`
|
|
9
|
+
- `find()` / `country()` by slug, ISO code or name; `search()`
|
|
10
|
+
- `embedUrl()`, `embedHtml()` and, in the browser, `createEmbed()`
|
|
11
|
+
- `OpenMusicAtlasError` with `status` and `retryAfter`
|
|
12
|
+
- TypeScript types for every API shape; ES modules and CommonJS
|
|
13
|
+
- OpenVerb: the `openverb.music_atlas` verb library (eight read-only verbs), `createMusicAtlasExecutor()` and `registerMusicAtlasVerbs()` at `@openverb/music-atlas/openverb`, with `openverb` as an optional peer dependency
|
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Music Atlas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
This license covers the software in this package only. The World Music Atlas
|
|
24
|
+
data and music served by Open Music Atlas are licensed separately; see
|
|
25
|
+
https://openmusicatlas.org/developers.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @openverb/music-atlas
|
|
2
|
+
|
|
3
|
+
The official SDK for [Open Music Atlas](https://openmusicatlas.org) — the world, mapped in music. Every place in the **World Music Atlas**, its song, editions, GeoJSON and embeddable players, in your code. Part of the [OpenVerb](https://openverb.org) developer ecosystem.
|
|
4
|
+
|
|
5
|
+
- A thin, typed wrapper around the [Open Music Atlas API v1](https://openmusicatlas.org/developers)
|
|
6
|
+
- No dependencies. Works in Node 18+, browsers, Deno and Bun
|
|
7
|
+
- ES modules and CommonJS, with TypeScript types
|
|
8
|
+
- OpenVerb verbs, so AI agents can explore the atlas
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @openverb/music-atlas
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { atlas } from "@openverb/music-atlas"
|
|
18
|
+
|
|
19
|
+
const jamaica = await atlas.country("Jamaica")
|
|
20
|
+
// { slug: "jamaica", name: "Jamaica", isoCode: "JM", continent: "Americas",
|
|
21
|
+
// region: "Caribbean", coordinates: [-77.32, 18.14], ... }
|
|
22
|
+
|
|
23
|
+
const { entries } = await atlas.place("jamaica")
|
|
24
|
+
console.log(entries[0].entry.title) // "Island of One People"
|
|
25
|
+
console.log(entries[0].entry.embedUrl) // the embeddable player
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
CommonJS works too:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { atlas } = require("@openverb/music-atlas")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Places
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const all = await atlas.places() // every place, in listening order
|
|
38
|
+
const caribbean = await atlas.places({ region: "Caribbean" })
|
|
39
|
+
const africa = await atlas.places({ continent: "Africa" })
|
|
40
|
+
|
|
41
|
+
await atlas.find("JM") // by ISO code
|
|
42
|
+
await atlas.find("cote d'ivoire") // by name — case- and accent-insensitive
|
|
43
|
+
await atlas.search("guinea") // Guinea, Guinea-Bissau, Equatorial Guinea, Papua New Guinea
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`places()` is fetched once per client and served from memory afterwards, so `find()` and `search()` are cheap.
|
|
47
|
+
|
|
48
|
+
Regions and continents follow the UN M49 scheme. Where a place's classification is contested, its `kind` is `"place"` and `isDisputed` is `true`; inclusion in the atlas is not a statement about sovereignty. See the [curatorial policy](https://openmusicatlas.org/curatorial-policy).
|
|
49
|
+
|
|
50
|
+
## Editions
|
|
51
|
+
|
|
52
|
+
An edition gives every place a song. Once published, an edition is frozen: its songs never change.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
const editions = await atlas.editions()
|
|
56
|
+
const founding = await atlas.edition() // defaults to the 2026 Founding Edition
|
|
57
|
+
founding.entries.forEach(({ place, entry }) => console.log(place.name, "—", entry.title))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## GeoJSON
|
|
61
|
+
|
|
62
|
+
One point per place, with the place and its song as properties.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const geo = await atlas.geojson() // FeatureCollection
|
|
66
|
+
|
|
67
|
+
// Leaflet: every place a marker, every popup a player
|
|
68
|
+
L.geoJSON(geo, {
|
|
69
|
+
onEachFeature: (feature, layer) => {
|
|
70
|
+
const p = feature.properties
|
|
71
|
+
layer.bindPopup(`<b>${p.name}</b><br>${p.title}<br>` + atlas.embedHtml(p.slug, p.edition, { width: 300 }))
|
|
72
|
+
},
|
|
73
|
+
}).addTo(map)
|
|
74
|
+
|
|
75
|
+
// MapLibre / Mapbox
|
|
76
|
+
map.addSource("atlas", { type: "geojson", data: geo })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Embedding the player
|
|
80
|
+
|
|
81
|
+
Songs are heard through the Open Music Atlas player. It runs in its own protected frame and links back to the place's page.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
atlas.embedUrl("jamaica") // "https://openmusicatlas.org/embed/jamaica/2026-founding"
|
|
85
|
+
atlas.embedHtml("jamaica") // an <iframe> string, for servers, templates and popups
|
|
86
|
+
|
|
87
|
+
// In the browser
|
|
88
|
+
document.querySelector("#player").append(atlas.createEmbed("jamaica"))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The player fits a frame 212px tall (`EMBED_HEIGHT`) without scrolling.
|
|
92
|
+
|
|
93
|
+
## For AI agents: OpenVerb
|
|
94
|
+
|
|
95
|
+
The package ships an [OpenVerb](https://openverb.org) verb library, `openverb.music_atlas`, that describes what an AI may do with the atlas — find places, get a place's song or its player, list editions, get GeoJSON. Every verb is read-only.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install openverb
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { createMusicAtlasExecutor } from "@openverb/music-atlas/openverb"
|
|
103
|
+
|
|
104
|
+
const executor = createMusicAtlasExecutor()
|
|
105
|
+
|
|
106
|
+
await executor.execute({ verb: "get_place_song", params: { place: "Jamaica" } })
|
|
107
|
+
// { verb: "get_place_song", status: "success",
|
|
108
|
+
// data: { place: {...}, edition: {...}, entry: { title: "Island of One People", embedUrl: "..." } } }
|
|
109
|
+
|
|
110
|
+
await executor.execute({ verb: "list_places", params: { region: "Caribbean" } })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Verb | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `find_place` | One place by name, ISO code or slug |
|
|
116
|
+
| `search_places` | Places whose name contains the query |
|
|
117
|
+
| `list_places` | Places, optionally by continent, region or kind |
|
|
118
|
+
| `get_place_song` | A place's song in an edition |
|
|
119
|
+
| `get_embed_player` | A place's player URL and iframe |
|
|
120
|
+
| `list_editions` | Every published edition |
|
|
121
|
+
| `get_edition` | An edition and all its songs |
|
|
122
|
+
| `get_geojson` | An edition as GeoJSON |
|
|
123
|
+
|
|
124
|
+
The library itself is `@openverb/music-atlas/openverb.music_atlas.json`, or `musicAtlasLibrary` in code, ready to hand to a model as its list of available actions. Already have an executor? `registerMusicAtlasVerbs(executor)` adds the handlers to it.
|
|
125
|
+
|
|
126
|
+
The OpenVerb integration is ES-module only, because `openverb` is. Everything else also works with `require()`.
|
|
127
|
+
|
|
128
|
+
## Errors and limits
|
|
129
|
+
|
|
130
|
+
`place()`, `edition()` and `geojson()` return `null` for something that doesn't exist. Other failures throw an `OpenMusicAtlasError`, which carries the HTTP `status` and, when the API gives one, `retryAfter` in seconds. (OpenVerb verbs report failures as `status: "error"` results instead.)
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { OpenMusicAtlasError } from "@openverb/music-atlas"
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await atlas.editions()
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e instanceof OpenMusicAtlasError && e.status === 429) console.log(`Wait ${e.retryAfter}s`)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The API allows each caller 300 requests every 10 minutes, and responses are cached for an hour. Keep one client and reuse it.
|
|
143
|
+
|
|
144
|
+
## Options
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { OpenMusicAtlas } from "@openverb/music-atlas"
|
|
148
|
+
|
|
149
|
+
const client = new OpenMusicAtlas({
|
|
150
|
+
baseUrl: "https://openmusicatlas.org", // the default
|
|
151
|
+
fetch: customFetch, // optional; defaults to the global fetch
|
|
152
|
+
headers: { "X-App": "my-museum-kiosk" },
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## What's included — and what isn't
|
|
157
|
+
|
|
158
|
+
The API and this package give you places, coordinates, songs' titles, descriptions and styles, editions, page links and embeddable players. They give you **no audio files, no downloadable audio links and no audio-provider identifiers**: songs are always heard through the player.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
The code in this package is MIT-licensed. The atlas's data and its music are licensed separately: the music is not licensed for download or reuse, and the terms for the data are published at [openmusicatlas.org/developers](https://openmusicatlas.org/developers). Please credit "World Music Atlas, openmusicatlas.org" and link to the place pages.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
|
+
EMBED_HEIGHT: () => EMBED_HEIGHT,
|
|
25
|
+
FOUNDING_EDITION: () => FOUNDING_EDITION,
|
|
26
|
+
OpenMusicAtlas: () => OpenMusicAtlas,
|
|
27
|
+
OpenMusicAtlasError: () => OpenMusicAtlasError,
|
|
28
|
+
atlas: () => atlas
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(src_exports);
|
|
31
|
+
var DEFAULT_BASE_URL = "https://openmusicatlas.org";
|
|
32
|
+
var FOUNDING_EDITION = "2026-founding";
|
|
33
|
+
var EMBED_HEIGHT = 212;
|
|
34
|
+
var OpenMusicAtlasError = class extends Error {
|
|
35
|
+
constructor(message, status, retryAfter = null) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "OpenMusicAtlasError";
|
|
38
|
+
this.status = status;
|
|
39
|
+
this.retryAfter = retryAfter;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var fold = (s) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
43
|
+
var escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
44
|
+
var slugOf = (place) => typeof place === "string" ? place : place.slug;
|
|
45
|
+
var OpenMusicAtlas = class {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.placesRequest = null;
|
|
48
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
49
|
+
this.customFetch = options.fetch;
|
|
50
|
+
this.headers = { Accept: "application/json", ...options.headers };
|
|
51
|
+
}
|
|
52
|
+
/* ----------------------------------------------------------------- data */
|
|
53
|
+
/**
|
|
54
|
+
* Every place in the atlas, in listening order (continent, region, name).
|
|
55
|
+
* Fetched once per client and then served from memory; pass a filter to
|
|
56
|
+
* narrow it.
|
|
57
|
+
*/
|
|
58
|
+
async places(filter = {}) {
|
|
59
|
+
if (!this.placesRequest) {
|
|
60
|
+
this.placesRequest = this.request("/api/v1/places").then((d) => d.places);
|
|
61
|
+
this.placesRequest.catch(() => this.placesRequest = null);
|
|
62
|
+
}
|
|
63
|
+
const all = await this.placesRequest;
|
|
64
|
+
const continent = filter.continent ? fold(filter.continent) : null;
|
|
65
|
+
const region = filter.region ? fold(filter.region) : null;
|
|
66
|
+
return all.filter(
|
|
67
|
+
(p) => (!continent || fold(p.continent ?? "") === continent) && (!region || fold(p.region ?? "") === region) && (!filter.kind || p.kind === filter.kind)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
/** A place and its song in every published edition, or null if there is no such place. */
|
|
71
|
+
async place(place) {
|
|
72
|
+
return this.request(`/api/v1/places/${encodeURIComponent(slugOf(place))}`, { nullOn404: true });
|
|
73
|
+
}
|
|
74
|
+
/** Every published edition, newest first. */
|
|
75
|
+
async editions() {
|
|
76
|
+
return (await this.request("/api/v1/editions")).editions;
|
|
77
|
+
}
|
|
78
|
+
/** An edition and all its songs in listening order, or null if there is no such edition. */
|
|
79
|
+
async edition(slug = FOUNDING_EDITION) {
|
|
80
|
+
return this.request(`/api/v1/editions/${encodeURIComponent(slug)}`, { nullOn404: true });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* An edition as a GeoJSON FeatureCollection — one point per place, with the
|
|
84
|
+
* place and its song as properties. Loads straight into Leaflet, MapLibre,
|
|
85
|
+
* Mapbox, OpenLayers or QGIS.
|
|
86
|
+
*/
|
|
87
|
+
async geojson(slug = FOUNDING_EDITION) {
|
|
88
|
+
return this.request(`/api/v1/editions/${encodeURIComponent(slug)}.geojson`, {
|
|
89
|
+
nullOn404: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/* -------------------------------------------------------------- finding */
|
|
93
|
+
/**
|
|
94
|
+
* One place by slug ("jamaica"), ISO code ("JM") or name ("Jamaica",
|
|
95
|
+
* "cote d'ivoire"), or null. Case- and accent-insensitive; an exact match
|
|
96
|
+
* wins over a name that merely starts with the query.
|
|
97
|
+
*/
|
|
98
|
+
async find(query) {
|
|
99
|
+
const q = fold(query);
|
|
100
|
+
if (!q) return null;
|
|
101
|
+
const all = await this.places();
|
|
102
|
+
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;
|
|
103
|
+
}
|
|
104
|
+
/** The same as find(), named for the common case: `atlas.country("Jamaica")`. */
|
|
105
|
+
country(query) {
|
|
106
|
+
return this.find(query);
|
|
107
|
+
}
|
|
108
|
+
/** Places whose name contains the query, names starting with it first. */
|
|
109
|
+
async search(query) {
|
|
110
|
+
const q = fold(query);
|
|
111
|
+
if (!q) return [];
|
|
112
|
+
const all = await this.places();
|
|
113
|
+
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));
|
|
114
|
+
}
|
|
115
|
+
/* --------------------------------------------------------------- embeds */
|
|
116
|
+
/** The embeddable player's address for a place's song. No network request. */
|
|
117
|
+
embedUrl(place, edition = FOUNDING_EDITION) {
|
|
118
|
+
return `${this.baseUrl}/embed/${encodeURIComponent(slugOf(place))}/${encodeURIComponent(edition)}`;
|
|
119
|
+
}
|
|
120
|
+
/** An `<iframe>` for the embeddable player, as HTML — for server rendering, templates and map popups. */
|
|
121
|
+
embedHtml(place, edition = FOUNDING_EDITION, options = {}) {
|
|
122
|
+
const a = this.embedAttributes(place, edition, options);
|
|
123
|
+
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>`;
|
|
124
|
+
}
|
|
125
|
+
/** An `<iframe>` element for the embeddable player, ready to append. Browser only. */
|
|
126
|
+
createEmbed(place, edition = FOUNDING_EDITION, options = {}) {
|
|
127
|
+
if (typeof document === "undefined") {
|
|
128
|
+
throw new Error("createEmbed() needs a browser. On the server, use embedHtml().");
|
|
129
|
+
}
|
|
130
|
+
const a = this.embedAttributes(place, edition, options);
|
|
131
|
+
const frame = document.createElement("iframe");
|
|
132
|
+
frame.src = a.src;
|
|
133
|
+
frame.width = a.width;
|
|
134
|
+
frame.height = a.height;
|
|
135
|
+
frame.loading = a.loading;
|
|
136
|
+
frame.setAttribute("style", a.style);
|
|
137
|
+
frame.allow = a.allow;
|
|
138
|
+
frame.title = a.title;
|
|
139
|
+
return frame;
|
|
140
|
+
}
|
|
141
|
+
embedAttributes(place, edition, options) {
|
|
142
|
+
const name = typeof place === "string" ? place : "name" in place ? String(place.name) : place.slug;
|
|
143
|
+
return {
|
|
144
|
+
src: this.embedUrl(place, edition),
|
|
145
|
+
width: String(options.width ?? "100%"),
|
|
146
|
+
height: String(options.height ?? EMBED_HEIGHT),
|
|
147
|
+
loading: options.loading ?? "lazy",
|
|
148
|
+
style: "border:0;border-radius:12px",
|
|
149
|
+
allow: "autoplay; encrypted-media",
|
|
150
|
+
title: options.title ?? `${name} \u2014 World Music Atlas`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async request(path, options = {}) {
|
|
154
|
+
const fetcher = this.customFetch ?? globalThis.fetch;
|
|
155
|
+
if (typeof fetcher !== "function") {
|
|
156
|
+
throw new Error("No fetch is available. Use Node 18 or later, or pass `fetch` in the client options.");
|
|
157
|
+
}
|
|
158
|
+
const res = await fetcher(`${this.baseUrl}${path}`, { headers: this.headers });
|
|
159
|
+
if (res.status === 404 && options.nullOn404) return null;
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
let message = `Open Music Atlas API responded with ${res.status}`;
|
|
162
|
+
try {
|
|
163
|
+
const body = await res.json();
|
|
164
|
+
if (body?.error) message = body.error;
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
const retry = res.headers.get("retry-after");
|
|
168
|
+
throw new OpenMusicAtlasError(message, res.status, retry !== null && retry !== "" ? Number(retry) : null);
|
|
169
|
+
}
|
|
170
|
+
return await res.json();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var atlas = new OpenMusicAtlas();
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
DEFAULT_BASE_URL,
|
|
177
|
+
EMBED_HEIGHT,
|
|
178
|
+
FOUNDING_EDITION,
|
|
179
|
+
OpenMusicAtlas,
|
|
180
|
+
OpenMusicAtlasError,
|
|
181
|
+
atlas
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=index.cjs.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, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\")\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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|