@audd/sdk 1.5.9 → 1.5.11

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
@@ -85,7 +85,7 @@ console.log(song.releaseDate, song.label, song.timecode);
85
85
  // AudD's universal song page — links into every provider
86
86
  console.log(song.songLink);
87
87
 
88
- // Helpers — driven off songLink, work without any `return` opt-in
88
+ // Helpers — driven off songLink, work without any `returnMetadata` opt-in
89
89
  console.log(song.thumbnailUrl); // cover-art image, or null
90
90
  console.log(song.streamingUrl("spotify")); // direct or lis.tn redirect
91
91
  console.log(song.streamingUrls()); // map of provider -> URL
@@ -107,21 +107,21 @@ Valid `returnMetadata` values: `apple_music`, `spotify`, `deezer`, `napster`,
107
107
  `musicbrainz`. Blocks are `undefined` when not requested.
108
108
 
109
109
  `streamingUrl(provider)` prefers the direct provider URL when you
110
- requested that block via `return`, then falls back to the lis.tn redirect
110
+ requested that block via `returnMetadata`, then falls back to the lis.tn redirect
111
111
  when `songLink` is on `lis.tn`. YouTube has only the redirect path.
112
112
 
113
113
  ## Reading additional metadata
114
114
 
115
115
  Every model carries an `extras` map with any server-side fields outside
116
116
  the typed surface, plus a `rawResponse` of the full unparsed JSON. Use
117
- `extras` to read undocumented or beta fields:
117
+ `extras` to read fields outside the typed surface:
118
118
 
119
119
  ```ts
120
120
  console.log(song.extras); // any non-typed top-level fields
121
121
  console.log(song.rawResponse); // the whole result object as the server returned it
122
122
  ```
123
123
 
124
- For the **request** side, every call accepts an `extraParameters` map for additional form fields the typed options don't cover — undocumented parameters or beta features:
124
+ For the **request** side, every call accepts an `extraParameters` map for additional form fields the typed options don't cover:
125
125
 
126
126
  ```ts
127
127
  await audd.recognize(url, {
@@ -145,10 +145,15 @@ for (const m of matches) {
145
145
  }
146
146
  ```
147
147
 
148
- `EnterpriseMatch` carries the same core tags plus `score`, `startOffset`,
149
- `endOffset`, `isrc`, `upc`. Access to `isrc`, `upc`, and `score` requires
150
- a Startup plan or higher — [contact us](mailto:api@audd.io) for enterprise
151
- features.
148
+ `EnterpriseMatch` carries the same core tags plus `score`, `startSeconds`,
149
+ `endSeconds`, `startOffset`, `endOffset`, `isrc`, `upc`. `startSeconds` and
150
+ `endSeconds` are where this song plays in your file, in seconds (e.g. 64.2 to
151
+ 71.8) — feed straight to a player or ffmpeg; computed while parsing the chunked
152
+ response, and `recognizeEnterprise` requests accurate offsets by default so
153
+ they're precise. `startOffset`/`endOffset` are the raw millisecond positions
154
+ within AudD's internal 12-second scan fragment behind them. Access to `isrc`,
155
+ `upc`, and `score` requires a Startup plan or higher — [contact
156
+ us](mailto:api@audd.io) for enterprise features.
152
157
 
153
158
  The default per-call timeout is **1 hour** for this endpoint (60s for
154
159
  standard recognition); override with `timeoutMs`.
package/dist/index.cjs CHANGED
@@ -285,6 +285,20 @@ function asNumber(v) {
285
285
  function asBoolean(v) {
286
286
  return typeof v === "boolean" ? v : void 0;
287
287
  }
288
+ function parseOffsetToSeconds(o) {
289
+ if (o === void 0 || o === null) return void 0;
290
+ if (typeof o === "number") return Number.isFinite(o) ? o : void 0;
291
+ const t = String(o).trim();
292
+ if (t === "") return void 0;
293
+ let total = 0;
294
+ for (const part of t.split(":")) {
295
+ if (part.trim() === "") return void 0;
296
+ const n = Number(part);
297
+ if (!Number.isFinite(n)) return void 0;
298
+ total = total * 60 + n;
299
+ }
300
+ return total;
301
+ }
288
302
  var STREAMING_PROVIDERS = [
289
303
  "spotify",
290
304
  "apple_music",
@@ -366,9 +380,6 @@ var RECOGNITION_KEYS = [
366
380
  function parseRecognitionResult(raw) {
367
381
  const r = requireObject(raw, "RecognitionResult");
368
382
  const timecode = asString(r.timecode);
369
- if (timecode === void 0) {
370
- throw new TypeError("RecognitionResult: timecode missing or not string");
371
- }
372
383
  const audioId = asNumber(r.audio_id);
373
384
  const artist = asString(r.artist);
374
385
  const title = asString(r.title);
@@ -437,14 +448,14 @@ var ENTERPRISE_MATCH_KEYS = [
437
448
  "upc",
438
449
  "song_link",
439
450
  "start_offset",
440
- "end_offset"
451
+ "end_offset",
452
+ "start_seconds",
453
+ "end_seconds"
441
454
  ];
442
455
  function parseEnterpriseMatch(raw) {
443
456
  const r = requireObject(raw, "EnterpriseMatch");
444
457
  const score = asNumber(r.score);
445
458
  const timecode = asString(r.timecode);
446
- if (score === void 0) throw new TypeError("EnterpriseMatch: score not number");
447
- if (timecode === void 0) throw new TypeError("EnterpriseMatch: timecode not string");
448
459
  const songLink = asString(r.song_link);
449
460
  return {
450
461
  score,
@@ -459,6 +470,8 @@ function parseEnterpriseMatch(raw) {
459
470
  songLink,
460
471
  startOffset: asNumber(r.start_offset),
461
472
  endOffset: asNumber(r.end_offset),
473
+ startSeconds: void 0,
474
+ endSeconds: void 0,
462
475
  extras: pickExtras(r, ENTERPRISE_MATCH_KEYS),
463
476
  rawResponse: r,
464
477
  streamingUrl(provider) {
@@ -491,14 +504,19 @@ function parseEnterpriseMatch(raw) {
491
504
  var ENTERPRISE_CHUNK_KEYS = ["songs", "offset"];
492
505
  function parseEnterpriseChunkResult(raw) {
493
506
  const r = requireObject(raw, "EnterpriseChunkResult");
494
- const songsRaw = r.songs;
495
- if (!Array.isArray(songsRaw)) {
496
- throw new TypeError("EnterpriseChunkResult: songs not array");
497
- }
507
+ const songsRaw = Array.isArray(r.songs) ? r.songs : [];
498
508
  const offset = asString(r.offset);
499
- if (offset === void 0) throw new TypeError("EnterpriseChunkResult: offset not string");
509
+ const base = parseOffsetToSeconds(r.offset);
510
+ const songs = songsRaw.map((s) => {
511
+ const match = parseEnterpriseMatch(s);
512
+ if (base !== void 0) {
513
+ match.startSeconds = base + (match.startOffset ?? 0) / 1e3;
514
+ match.endSeconds = base + (match.endOffset ?? 0) / 1e3;
515
+ }
516
+ return match;
517
+ });
500
518
  return {
501
- songs: songsRaw.map(parseEnterpriseMatch),
519
+ songs,
502
520
  offset,
503
521
  extras: pickExtras(r, ENTERPRISE_CHUNK_KEYS),
504
522
  rawResponse: r
@@ -510,9 +528,6 @@ function parseStream(raw) {
510
528
  const radioId = asNumber(r.radio_id);
511
529
  const url = asString(r.url);
512
530
  const streamRunning = asBoolean(r.stream_running);
513
- if (radioId === void 0) throw new TypeError("Stream: radio_id not number");
514
- if (url === void 0) throw new TypeError("Stream: url not string");
515
- if (streamRunning === void 0) throw new TypeError("Stream: stream_running not boolean");
516
531
  return {
517
532
  radioId,
518
533
  url,
@@ -543,9 +558,6 @@ function parseStreamCallbackSong(raw) {
543
558
  const artist = asString(r.artist);
544
559
  const title = asString(r.title);
545
560
  const score = asNumber(r.score);
546
- if (artist === void 0) throw new TypeError("StreamCallbackSong: artist not string");
547
- if (title === void 0) throw new TypeError("StreamCallbackSong: title not string");
548
- if (score === void 0) throw new TypeError("StreamCallbackSong: score not number");
549
561
  const musicbrainz = Array.isArray(r.musicbrainz) ? r.musicbrainz.filter((x) => asObject(x) !== void 0) : void 0;
550
562
  return {
551
563
  artist,
@@ -574,13 +586,9 @@ var STREAM_CALLBACK_MATCH_KEYS = [
574
586
  function parseStreamCallbackMatch(raw) {
575
587
  const r = requireObject(raw, "StreamCallbackMatch");
576
588
  const radioId = asNumber(r.radio_id);
577
- if (radioId === void 0) throw new TypeError("StreamCallbackMatch: radio_id not number");
578
- const resultsRaw = r.results;
579
- if (!Array.isArray(resultsRaw)) throw new TypeError("StreamCallbackMatch: results not array");
580
- if (resultsRaw.length === 0) throw new TypeError("StreamCallbackMatch: results is empty");
581
- const songs = resultsRaw.map(parseStreamCallbackSong);
589
+ const resultsRaw = Array.isArray(r.results) ? r.results : [];
590
+ const songs = resultsRaw.filter((x) => asObject(x) !== void 0).map(parseStreamCallbackSong);
582
591
  const [first, ...rest] = songs;
583
- if (first === void 0) throw new TypeError("StreamCallbackMatch: results is empty");
584
592
  return {
585
593
  radioId,
586
594
  timestamp: asString(r.timestamp),
@@ -600,14 +608,8 @@ var STREAM_CALLBACK_NOTIFICATION_KEYS = [
600
608
  function parseStreamCallbackNotification(raw) {
601
609
  const r = requireObject(raw, "StreamCallbackNotification");
602
610
  const radioId = asNumber(r.radio_id);
603
- if (radioId === void 0)
604
- throw new TypeError("StreamCallbackNotification: radio_id not number");
605
611
  const code = asNumber(r.notification_code);
606
- if (code === void 0)
607
- throw new TypeError("StreamCallbackNotification: notification_code not number");
608
612
  const message = asString(r.notification_message);
609
- if (message === void 0)
610
- throw new TypeError("StreamCallbackNotification: notification_message not string");
611
613
  return {
612
614
  radioId,
613
615
  streamRunning: asBoolean(r.stream_running),
@@ -631,8 +633,6 @@ function parseLyricsResult(raw) {
631
633
  const r = requireObject(raw, "LyricsResult");
632
634
  const artist = asString(r.artist);
633
635
  const title = asString(r.title);
634
- if (artist === void 0) throw new TypeError("LyricsResult: artist not string");
635
- if (title === void 0) throw new TypeError("LyricsResult: title not string");
636
636
  return {
637
637
  artist,
638
638
  title,
@@ -1403,8 +1403,8 @@ function buildEnterpriseFields(opts) {
1403
1403
  if (opts.skipFirstSeconds !== void 0)
1404
1404
  fields["skip_first_seconds"] = String(opts.skipFirstSeconds);
1405
1405
  if (opts.useTimecode !== void 0) fields["use_timecode"] = opts.useTimecode ? "true" : "false";
1406
- if (opts.accurateOffsets !== void 0)
1407
- fields["accurate_offsets"] = opts.accurateOffsets ? "true" : "false";
1406
+ const accurate = opts.accurateOffsets ?? true;
1407
+ fields["accurate_offsets"] = accurate ? "true" : "false";
1408
1408
  return fields;
1409
1409
  }
1410
1410
  function isDeprecationPassThrough(body) {