musiciwant 1.0.0 → 1.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.
Files changed (3) hide show
  1. package/index.d.ts +27 -0
  2. package/index.js +54 -0
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -73,12 +73,39 @@ export interface StatsResponse {
73
73
  most_intense_genres: GenreStat[];
74
74
  }
75
75
 
76
+ export interface FilterOptions {
77
+ minBpm?: number; maxBpm?: number; minIntensity?: number; maxIntensity?: number;
78
+ maxDynamicRange?: number; sensoryLevel?: 'safe' | 'moderate' | 'intense';
79
+ suddenChanges?: string | string[]; vocalStyle?: string; mood?: string;
80
+ misoSafe?: boolean; limit?: number; offset?: number;
81
+ }
82
+ export interface FilterResponse { count: number; results: Song[]; }
83
+ export type SongRef = { title: string; artist?: string } | { slug: string };
84
+ export interface BatchResponse { count: number; results: { query: SongRef; found: boolean; song: Song | null }[]; }
85
+ export interface PlaylistTrack { query: SongRef; matched: boolean; title?: string; artist?: string; slug?: string; intensity?: number; sensory_level?: string; }
86
+ export interface PlaylistResponse {
87
+ tracks: PlaylistTrack[];
88
+ stats: { avg: number; max_jump: number; ambushes: number[]; peak_index: number; calm_index: number } | null;
89
+ unmatched: number;
90
+ }
91
+ export interface FingerprintResponse {
92
+ found: boolean;
93
+ song?: { title: string; artist: string; slug: string };
94
+ vector?: Record<string, number>;
95
+ vector_array?: number[];
96
+ dims?: string[];
97
+ }
98
+
76
99
  export class MusicIWant {
77
100
  constructor(opts?: ClientOptions);
78
101
  song(params: { title: string; artist?: string } | { slug: string }): Promise<SongResponse>;
79
102
  search(q: string): Promise<SearchResponse>;
80
103
  gentler(params: { title: string; artist?: string } | { slug: string }): Promise<GentlerResponse>;
81
104
  stats(): Promise<StatsResponse>;
105
+ filter(opts?: FilterOptions): Promise<FilterResponse>;
106
+ batch(songs: SongRef[]): Promise<BatchResponse>;
107
+ playlist(songs: SongRef[]): Promise<PlaylistResponse>;
108
+ fingerprint(params: { title: string; artist?: string } | { slug: string }): Promise<FingerprintResponse>;
82
109
  usage(): Promise<UsageResponse>;
83
110
  static getKey(email: string, opts?: ClientOptions): Promise<KeyResponse>;
84
111
  }
package/index.js CHANGED
@@ -74,6 +74,60 @@ class MusicIWant {
74
74
  return this._get('/api/v1/stats', {});
75
75
  }
76
76
 
77
+ /**
78
+ * Query the catalog by feel. All options optional: { minBpm, maxBpm,
79
+ * minIntensity, maxIntensity, maxDynamicRange, sensoryLevel, suddenChanges
80
+ * (string or array), vocalStyle, mood, misoSafe (bool), limit, offset }.
81
+ * @returns {Promise<object>} { count, results }
82
+ */
83
+ filter(opts = {}) {
84
+ const sc = Array.isArray(opts.suddenChanges) ? opts.suddenChanges.join(',') : opts.suddenChanges;
85
+ return this._get('/api/v1/filter', {
86
+ min_bpm: opts.minBpm, max_bpm: opts.maxBpm,
87
+ min_intensity: opts.minIntensity, max_intensity: opts.maxIntensity,
88
+ max_dynamic_range: opts.maxDynamicRange, sensory_level: opts.sensoryLevel,
89
+ sudden_changes: sc, vocal_style: opts.vocalStyle, mood: opts.mood,
90
+ miso_safe: opts.misoSafe ? 'true' : undefined,
91
+ limit: opts.limit, offset: opts.offset
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Batch lookup (up to 100). songs: array of { title, artist } or { slug }.
97
+ * @returns {Promise<object>} { count, results: [{ query, found, song }] }
98
+ */
99
+ batch(songs) {
100
+ if (!Array.isArray(songs) || !songs.length) throw new Error('batch(songs) needs a non-empty array');
101
+ return this._post('/api/v1/songs', { songs });
102
+ }
103
+
104
+ /**
105
+ * Sensory shape of an ordered playlist (up to 60). songs: array of
106
+ * { title, artist } or { slug }. @returns {Promise<object>} { tracks, stats, unmatched }
107
+ */
108
+ playlist(songs) {
109
+ if (!Array.isArray(songs) || !songs.length) throw new Error('playlist(songs) needs a non-empty array');
110
+ return this._post('/api/v1/playlist', { songs });
111
+ }
112
+
113
+ /**
114
+ * Normalized 0–1 feature vector (the Spotify audio-features replacement).
115
+ * Provide {title, artist} or {slug}. @returns {Promise<object>} { found, song, vector, vector_array, dims }
116
+ */
117
+ fingerprint({ title, artist, slug } = {}) {
118
+ if (!title && !slug) throw new Error('fingerprint() needs { title, artist } or { slug }');
119
+ return this._get('/api/v1/fingerprint', { title, artist, slug });
120
+ }
121
+
122
+ async _post(path, body) {
123
+ const headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' };
124
+ if (this.apiKey) headers['X-API-Key'] = this.apiKey;
125
+ const res = await this._fetch(this.baseUrl + path, { method: 'POST', headers, body: JSON.stringify(body) });
126
+ const out = await res.json().catch(() => ({}));
127
+ if (!res.ok) throw new MusicIWantError((out && out.error) || ('HTTP ' + res.status), res.status, out);
128
+ return out;
129
+ }
130
+
77
131
  /** Current key's tier + remaining quota. Requires an apiKey. */
78
132
  usage() {
79
133
  if (!this.apiKey) throw new Error('usage() requires an apiKey');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musiciwant",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Independent music analysis API client — BPM, dynamic range, sensory intensity, moods, misophonia flags. A Spotify Audio Features alternative.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",