musiciwant 1.0.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.
Files changed (4) hide show
  1. package/README.md +49 -0
  2. package/index.d.ts +86 -0
  3. package/index.js +103 -0
  4. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # musiciwant (JavaScript / TypeScript)
2
+
3
+ Tiny, zero-dependency client for the [Music I Want API](https://musiciwant.com/developers) — independent sensory + musical analysis for ~19,000 songs. BPM, dynamic range, sudden changes, texture, vocal style, a 0–100 **intensity** score, moods, use-case fits, and **misophonia** flags no other public API carries.
4
+
5
+ An honest alternative to the data that disappeared when Spotify deprecated its Audio Features API.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install musiciwant
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```js
16
+ const { MusicIWant } = require('musiciwant');
17
+
18
+ const miw = new MusicIWant(); // works key-less at 100 req/day/IP
19
+
20
+ const { song } = await miw.song({ title: 'Black', artist: 'Pearl Jam' });
21
+ console.log(song.bpm, song.dynamic_range, song.intensity, song.sensory_level);
22
+ // 76 7 47 "moderate"
23
+
24
+ const { results } = await miw.search('radiohead');
25
+ ```
26
+
27
+ ## A free key (2,000 requests/day)
28
+
29
+ ```js
30
+ const { key } = await MusicIWant.getKey('you@example.com');
31
+ const miw = new MusicIWant({ apiKey: key });
32
+ await miw.usage(); // { tier, requests_today, remaining_today, ... }
33
+ ```
34
+
35
+ Or set `MUSICIWANT_API_KEY` in your environment and the client picks it up.
36
+
37
+ ## Tiers
38
+
39
+ | Tier | Limit | How |
40
+ | --- | --- | --- |
41
+ | anonymous | 100 req/day/IP | nothing |
42
+ | free | 2,000 req/day | `MusicIWant.getKey(email)` |
43
+ | pro | 100,000 req/day | $19/mo — see [docs](https://musiciwant.com/developers) |
44
+
45
+ Bulk dataset licensing for apps, platforms, and research: [musiciwant.com/developers](https://musiciwant.com/developers#data).
46
+
47
+ ## License
48
+
49
+ MIT (client code). The song data is © Music I Want, free to use with attribution; bulk/commercial use is licensed separately.
package/index.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ export interface Song {
2
+ slug: string;
3
+ title: string;
4
+ artist: string;
5
+ album: string | null;
6
+ year: number | null;
7
+ bpm: number | null;
8
+ /** "safe" | "moderate" | "intense" */
9
+ sensory_level: string;
10
+ /** 1–10: distance between the quietest and loudest moments */
11
+ dynamic_range: number;
12
+ /** "none" | "mild" | "moderate" | "frequent" | "extreme" */
13
+ sudden_changes: string;
14
+ /** "smooth" | "layered" | "complex" | "harsh" | "abrasive" */
15
+ texture: string;
16
+ /** "high" | "medium" | "low" */
17
+ predictability: string;
18
+ /** "instrumental" | "soft vocals" | "spoken word" | "dynamic vocals" | "screaming" */
19
+ vocal_style: string;
20
+ sensory_notes: string | null;
21
+ /** Misophonia flags: "none" | "mild" | "present" */
22
+ miso_mouth: string;
23
+ miso_clicks: string;
24
+ miso_breathing: string;
25
+ miso_repetitive: string;
26
+ age_min: number | null;
27
+ age_max: number | null;
28
+ age_confidence: number | null;
29
+ lyrical_content: string | null;
30
+ /** 0–100 single sensory-load score. Unique to Music I Want. */
31
+ intensity: number | null;
32
+ spotify_id: string | null;
33
+ youtube_id: string | null;
34
+ spotify_url: string | null;
35
+ youtube_url: string | null;
36
+ /** Present on single-song lookups. */
37
+ moods?: string[];
38
+ /** Present on single-song lookups. */
39
+ recommended_for?: string[];
40
+ url: string;
41
+ }
42
+
43
+ export interface SongResponse { found: boolean; song?: Song; error?: string; }
44
+ export interface SearchResponse { count: number; results: Song[]; }
45
+ export interface UsageResponse {
46
+ tier: string; requests_today: number; limit_per_day: number;
47
+ remaining_today: number; total_requests: number; member_since: string;
48
+ }
49
+ export interface KeyResponse { key: string; tier: string; limit_per_day?: number; }
50
+
51
+ export interface ClientOptions { apiKey?: string; baseUrl?: string; fetch?: typeof fetch; }
52
+
53
+ export class MusicIWantError extends Error {
54
+ status: number;
55
+ body: any;
56
+ }
57
+
58
+ export interface GentlerResponse {
59
+ found: boolean;
60
+ already_gentle?: boolean;
61
+ original?: Song;
62
+ alternatives?: (Song & { shared_moods?: string[] })[];
63
+ error?: string;
64
+ }
65
+ export interface DecadeStat { decade: number; mean_intensity: number; n: number; }
66
+ export interface GenreStat { genre: string; mean_intensity: number; n: number; }
67
+ export interface StatsResponse {
68
+ total_songs: number;
69
+ mean_intensity: number;
70
+ sensory_level_split: { safe: number; moderate: number; intense: number };
71
+ by_decade: DecadeStat[];
72
+ gentlest_genres: GenreStat[];
73
+ most_intense_genres: GenreStat[];
74
+ }
75
+
76
+ export class MusicIWant {
77
+ constructor(opts?: ClientOptions);
78
+ song(params: { title: string; artist?: string } | { slug: string }): Promise<SongResponse>;
79
+ search(q: string): Promise<SearchResponse>;
80
+ gentler(params: { title: string; artist?: string } | { slug: string }): Promise<GentlerResponse>;
81
+ stats(): Promise<StatsResponse>;
82
+ usage(): Promise<UsageResponse>;
83
+ static getKey(email: string, opts?: ClientOptions): Promise<KeyResponse>;
84
+ }
85
+
86
+ export default MusicIWant;
package/index.js ADDED
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ /**
3
+ * musiciwant — tiny, zero-dependency client for the Music I Want API.
4
+ * Independent sensory + musical analysis for ~19,000 songs: BPM, dynamic range,
5
+ * sudden changes, texture, vocal style, a 0–100 intensity score, moods,
6
+ * use-case fits, and misophonia flags. An honest alternative to the music
7
+ * features that vanished when Spotify deprecated its Audio Features API.
8
+ *
9
+ * Docs: https://musiciwant.com/developers
10
+ * Free key (2,000 req/day): MusicIWant.getKey('you@example.com')
11
+ */
12
+
13
+ const BASE = 'https://musiciwant.com';
14
+
15
+ class MusicIWantError extends Error {
16
+ constructor(message, status, body) {
17
+ super(message);
18
+ this.name = 'MusicIWantError';
19
+ this.status = status;
20
+ this.body = body;
21
+ }
22
+ }
23
+
24
+ class MusicIWant {
25
+ /** @param {{apiKey?: string, baseUrl?: string, fetch?: Function}} [opts] */
26
+ constructor(opts = {}) {
27
+ this.apiKey = opts.apiKey || process.env.MUSICIWANT_API_KEY || null;
28
+ this.baseUrl = (opts.baseUrl || BASE).replace(/\/$/, '');
29
+ this._fetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
30
+ if (!this._fetch) throw new Error('No fetch available. Use Node 18+ or pass opts.fetch.');
31
+ }
32
+
33
+ async _get(path, params = {}) {
34
+ const url = new URL(this.baseUrl + path);
35
+ for (const [k, v] of Object.entries(params)) {
36
+ if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v));
37
+ }
38
+ const headers = { 'Accept': 'application/json' };
39
+ if (this.apiKey) headers['X-API-Key'] = this.apiKey;
40
+ const res = await this._fetch(url.toString(), { headers });
41
+ const body = await res.json().catch(() => ({}));
42
+ if (!res.ok) {
43
+ throw new MusicIWantError((body && body.error) || ('HTTP ' + res.status), res.status, body);
44
+ }
45
+ return body;
46
+ }
47
+
48
+ /**
49
+ * Full analysis for one song. Provide {title, artist} or {slug}.
50
+ * @returns {Promise<object>} { found, song }
51
+ */
52
+ song({ title, artist, slug } = {}) {
53
+ if (!title && !slug) throw new Error('song() needs { title, artist } or { slug }');
54
+ return this._get('/api/v1/song', { title, artist, slug });
55
+ }
56
+
57
+ /** Search by title or artist. @returns {Promise<object>} { count, results } */
58
+ search(q) {
59
+ if (!q || String(q).trim().length < 2) throw new Error('search(q) needs 2+ characters');
60
+ return this._get('/api/v1/search', { q });
61
+ }
62
+
63
+ /**
64
+ * Songs with the same emotional feel but a lower sensory intensity.
65
+ * Provide {title, artist} or {slug}. @returns {Promise<object>} { found, original, alternatives }
66
+ */
67
+ gentler({ title, artist, slug } = {}) {
68
+ if (!title && !slug) throw new Error('gentler() needs { title, artist } or { slug }');
69
+ return this._get('/api/v1/gentler', { title, artist, slug });
70
+ }
71
+
72
+ /** Aggregate catalog statistics (level split, intensity by decade, genre extremes). */
73
+ stats() {
74
+ return this._get('/api/v1/stats', {});
75
+ }
76
+
77
+ /** Current key's tier + remaining quota. Requires an apiKey. */
78
+ usage() {
79
+ if (!this.apiKey) throw new Error('usage() requires an apiKey');
80
+ return this._get('/api/v1/usage', {});
81
+ }
82
+
83
+ /**
84
+ * Mint a free key (2,000 req/day) for an email. Static — no instance needed.
85
+ * @returns {Promise<{key:string, tier:string}>}
86
+ */
87
+ static async getKey(email, opts = {}) {
88
+ const base = (opts.baseUrl || BASE).replace(/\/$/, '');
89
+ const f = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
90
+ if (!f) throw new Error('No fetch available. Use Node 18+ or pass opts.fetch.');
91
+ const res = await f(base + '/api/v1/keys', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ email })
95
+ });
96
+ const body = await res.json().catch(() => ({}));
97
+ if (!res.ok) throw new MusicIWantError((body && body.error) || ('HTTP ' + res.status), res.status, body);
98
+ return body;
99
+ }
100
+ }
101
+
102
+ module.exports = { MusicIWant, MusicIWantError, default: MusicIWant };
103
+ module.exports.MusicIWant = MusicIWant;
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "musiciwant",
3
+ "version": "1.0.0",
4
+ "description": "Independent music analysis API client — BPM, dynamic range, sensory intensity, moods, misophonia flags. A Spotify Audio Features alternative.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
8
+ "files": ["index.js", "index.d.ts", "README.md"],
9
+ "scripts": {
10
+ "test": "node test.js"
11
+ },
12
+ "keywords": [
13
+ "music", "bpm", "tempo", "audio-features", "spotify-alternative",
14
+ "music-analysis", "dynamic-range", "misophonia", "sensory", "playlist",
15
+ "music-data", "api-client"
16
+ ],
17
+ "homepage": "https://musiciwant.com/developers",
18
+ "repository": { "type": "git", "url": "git+https://github.com/musiciwant/musiciwant-api.git", "directory": "packages/js" },
19
+ "bugs": { "url": "https://github.com/musiciwant/musiciwant-api/issues" },
20
+ "author": "Music I Want",
21
+ "license": "MIT",
22
+ "engines": { "node": ">=18" }
23
+ }