@audd/sdk 1.4.7 → 1.5.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/dist/index.cjs CHANGED
@@ -355,6 +355,8 @@ var RECOGNITION_KEYS = [
355
355
  "release_date",
356
356
  "label",
357
357
  "song_link",
358
+ "isrc",
359
+ "upc",
358
360
  "apple_music",
359
361
  "spotify",
360
362
  "deezer",
@@ -381,6 +383,8 @@ function parseRecognitionResult(raw) {
381
383
  releaseDate: asString(r.release_date),
382
384
  label: asString(r.label),
383
385
  songLink,
386
+ isrc: asString(r.isrc),
387
+ upc: asString(r.upc),
384
388
  appleMusic: asObject(r.apple_music),
385
389
  spotify: asObject(r.spotify),
386
390
  deezer: asObject(r.deezer),
@@ -518,7 +522,7 @@ function parseStream(raw) {
518
522
  rawResponse: r
519
523
  };
520
524
  }
521
- var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
525
+ var STREAM_CALLBACK_SONG_KEYS = [
522
526
  "artist",
523
527
  "title",
524
528
  "score",
@@ -526,20 +530,22 @@ var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
526
530
  "release_date",
527
531
  "label",
528
532
  "song_link",
533
+ "isrc",
534
+ "upc",
529
535
  "apple_music",
530
536
  "spotify",
531
537
  "deezer",
532
538
  "napster",
533
539
  "musicbrainz"
534
540
  ];
535
- function parseStreamCallbackResultEntry(raw) {
536
- const r = requireObject(raw, "StreamCallbackResultEntry");
541
+ function parseStreamCallbackSong(raw) {
542
+ const r = requireObject(raw, "StreamCallbackSong");
537
543
  const artist = asString(r.artist);
538
544
  const title = asString(r.title);
539
545
  const score = asNumber(r.score);
540
- if (artist === void 0) throw new TypeError("StreamCallbackResultEntry: artist not string");
541
- if (title === void 0) throw new TypeError("StreamCallbackResultEntry: title not string");
542
- if (score === void 0) throw new TypeError("StreamCallbackResultEntry: score not number");
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");
543
549
  const musicbrainz = Array.isArray(r.musicbrainz) ? r.musicbrainz.filter((x) => asObject(x) !== void 0) : void 0;
544
550
  return {
545
551
  artist,
@@ -549,33 +555,39 @@ function parseStreamCallbackResultEntry(raw) {
549
555
  releaseDate: asString(r.release_date),
550
556
  label: asString(r.label),
551
557
  songLink: asString(r.song_link),
558
+ isrc: asString(r.isrc),
559
+ upc: asString(r.upc),
552
560
  appleMusic: asObject(r.apple_music),
553
561
  spotify: asObject(r.spotify),
554
562
  deezer: asObject(r.deezer),
555
563
  napster: asObject(r.napster),
556
564
  musicbrainz,
557
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_ENTRY_KEYS),
558
- rawResponse: r
565
+ extras: pickExtras(r, STREAM_CALLBACK_SONG_KEYS)
559
566
  };
560
567
  }
561
- var STREAM_CALLBACK_RESULT_KEYS = [
568
+ var STREAM_CALLBACK_MATCH_KEYS = [
562
569
  "radio_id",
563
570
  "timestamp",
564
571
  "play_length",
565
572
  "results"
566
573
  ];
567
- function parseStreamCallbackResult(raw) {
568
- const r = requireObject(raw, "StreamCallbackResult");
574
+ function parseStreamCallbackMatch(raw) {
575
+ const r = requireObject(raw, "StreamCallbackMatch");
569
576
  const radioId = asNumber(r.radio_id);
570
- if (radioId === void 0) throw new TypeError("StreamCallbackResult: radio_id not number");
577
+ if (radioId === void 0) throw new TypeError("StreamCallbackMatch: radio_id not number");
571
578
  const resultsRaw = r.results;
572
- if (!Array.isArray(resultsRaw)) throw new TypeError("StreamCallbackResult: results not array");
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);
582
+ const [first, ...rest] = songs;
583
+ if (first === void 0) throw new TypeError("StreamCallbackMatch: results is empty");
573
584
  return {
574
585
  radioId,
575
586
  timestamp: asString(r.timestamp),
576
587
  playLength: asNumber(r.play_length),
577
- results: resultsRaw.map(parseStreamCallbackResultEntry),
578
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_KEYS),
588
+ song: first,
589
+ alternatives: rest,
590
+ extras: pickExtras(r, STREAM_CALLBACK_MATCH_KEYS),
579
591
  rawResponse: r
580
592
  };
581
593
  }
@@ -605,39 +617,6 @@ function parseStreamCallbackNotification(raw) {
605
617
  rawResponse: r
606
618
  };
607
619
  }
608
- function parseStreamCallback(raw) {
609
- const r = requireObject(raw, "StreamCallbackPayload");
610
- if ("notification" in r) {
611
- const notification = parseStreamCallbackNotification(r.notification);
612
- const time = asNumber(r.time) ?? null;
613
- return {
614
- result: null,
615
- notification,
616
- time,
617
- rawPayload: r,
618
- get isResult() {
619
- return false;
620
- },
621
- get isNotification() {
622
- return true;
623
- }
624
- };
625
- }
626
- const inner = requireObject(r.result, "StreamCallbackPayload.result");
627
- const result = parseStreamCallbackResult(inner);
628
- return {
629
- result,
630
- notification: null,
631
- time: null,
632
- rawPayload: r,
633
- get isResult() {
634
- return true;
635
- },
636
- get isNotification() {
637
- return false;
638
- }
639
- };
640
- }
641
620
  var LYRICS_KEYS = [
642
621
  "artist",
643
622
  "title",
@@ -836,6 +815,246 @@ function addReturnToUrl(url, returnMetadata) {
836
815
  u.searchParams.set("return", value);
837
816
  return u.toString();
838
817
  }
818
+ function parseCallback(body) {
819
+ let raw = body;
820
+ if (typeof body === "string") {
821
+ try {
822
+ raw = JSON.parse(body);
823
+ } catch (err) {
824
+ throw new AudDSerializationError(
825
+ `callback body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
826
+ body
827
+ );
828
+ }
829
+ }
830
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
831
+ throw new AudDSerializationError(
832
+ "callback body is not a JSON object",
833
+ typeof body === "string" ? body : ""
834
+ );
835
+ }
836
+ const r = raw;
837
+ if ("notification" in r) {
838
+ let notification;
839
+ try {
840
+ notification = parseStreamCallbackNotification(r.notification);
841
+ } catch (err) {
842
+ throw new AudDSerializationError(
843
+ `callback notification: ${err instanceof Error ? err.message : String(err)}`,
844
+ typeof body === "string" ? body : JSON.stringify(r)
845
+ );
846
+ }
847
+ const t = r.time;
848
+ if (typeof t === "number") notification.time = t;
849
+ return { match: null, notification };
850
+ }
851
+ if ("result" in r) {
852
+ let match;
853
+ try {
854
+ match = parseStreamCallbackMatch(r.result);
855
+ } catch (err) {
856
+ throw new AudDSerializationError(
857
+ `callback result: ${err instanceof Error ? err.message : String(err)}`,
858
+ typeof body === "string" ? body : JSON.stringify(r)
859
+ );
860
+ }
861
+ return { match, notification: null };
862
+ }
863
+ throw new AudDSerializationError(
864
+ "callback body has neither result nor notification",
865
+ typeof body === "string" ? body : JSON.stringify(r)
866
+ );
867
+ }
868
+ async function handleCallback(req) {
869
+ if (typeof req.text === "function") {
870
+ const text = await req.text();
871
+ return parseCallback(text);
872
+ }
873
+ const reqRecord = req;
874
+ if ("body" in reqRecord && reqRecord.body !== void 0 && reqRecord.body !== null) {
875
+ const body = reqRecord.body;
876
+ if (typeof body === "object" && body !== null && Symbol.asyncIterator in body && // Plain Buffer/Uint8Array also has Symbol.iterator — accept those by value.
877
+ !(body instanceof Uint8Array)) {
878
+ return parseCallback(await drainAsyncIterable(body));
879
+ }
880
+ if (body instanceof Uint8Array) {
881
+ return parseCallback(new TextDecoder().decode(body));
882
+ }
883
+ return parseCallback(body);
884
+ }
885
+ if (typeof req === "object" && req !== null && Symbol.asyncIterator in req) {
886
+ return parseCallback(await drainAsyncIterable(req));
887
+ }
888
+ throw new AudDSerializationError(
889
+ "handleCallback: request has no body, no text() method, and is not async-iterable"
890
+ );
891
+ }
892
+ async function drainAsyncIterable(it) {
893
+ const chunks = [];
894
+ let stringBuf = "";
895
+ for await (const chunk of it) {
896
+ if (typeof chunk === "string") {
897
+ stringBuf += chunk;
898
+ } else if (chunk instanceof Uint8Array) {
899
+ chunks.push(chunk);
900
+ } else if (chunk !== null && chunk !== void 0) {
901
+ stringBuf += String(chunk);
902
+ }
903
+ }
904
+ if (chunks.length > 0) {
905
+ let total = 0;
906
+ for (const c of chunks) total += c.byteLength;
907
+ const merged = new Uint8Array(total);
908
+ let offset = 0;
909
+ for (const c of chunks) {
910
+ merged.set(c, offset);
911
+ offset += c.byteLength;
912
+ }
913
+ stringBuf = new TextDecoder().decode(merged) + stringBuf;
914
+ }
915
+ return stringBuf;
916
+ }
917
+
918
+ // src/longpollCore.ts
919
+ var HTTP_CLIENT_ERROR_FLOOR = 400;
920
+ var AsyncQueue = class {
921
+ items = [];
922
+ waiters = [];
923
+ done = false;
924
+ push(value) {
925
+ if (this.done) return;
926
+ const w = this.waiters.shift();
927
+ if (w !== void 0) {
928
+ w({ value, done: false });
929
+ return;
930
+ }
931
+ this.items.push(value);
932
+ }
933
+ finish() {
934
+ if (this.done) return;
935
+ this.done = true;
936
+ while (this.waiters.length > 0) {
937
+ const w = this.waiters.shift();
938
+ if (w !== void 0) w({ value: void 0, done: true });
939
+ }
940
+ }
941
+ next() {
942
+ const item = this.items.shift();
943
+ if (item !== void 0) return Promise.resolve({ value: item, done: false });
944
+ if (this.done) return Promise.resolve({ value: void 0, done: true });
945
+ return new Promise((resolve) => {
946
+ this.waiters.push(resolve);
947
+ });
948
+ }
949
+ toIterable() {
950
+ const next = () => this.next();
951
+ return {
952
+ [Symbol.asyncIterator]() {
953
+ return { next };
954
+ }
955
+ };
956
+ }
957
+ };
958
+ function decodeOne(resp) {
959
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
960
+ throw new AudDServerError({
961
+ errorCode: 0,
962
+ message: `Longpoll endpoint returned HTTP ${String(resp.httpStatus)}`,
963
+ httpStatus: resp.httpStatus,
964
+ requestId: resp.requestId,
965
+ rawResponse: resp.jsonBody ?? resp.rawText
966
+ });
967
+ }
968
+ const body = resp.jsonBody;
969
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
970
+ throw new AudDSerializationError(
971
+ "Longpoll response was not a JSON object",
972
+ resp.rawText
973
+ );
974
+ }
975
+ const dict = body;
976
+ if ("notification" in dict) {
977
+ const notification = parseStreamCallbackNotification(dict.notification);
978
+ const t = dict["time"];
979
+ if (typeof t === "number") notification.time = t;
980
+ return { kind: "notification", notification };
981
+ }
982
+ if ("result" in dict) {
983
+ return { kind: "match", match: parseStreamCallbackMatch(dict.result) };
984
+ }
985
+ return { kind: "keepalive" };
986
+ }
987
+ function startLongpoll(cfg) {
988
+ const matches = new AsyncQueue();
989
+ const notifications = new AsyncQueue();
990
+ const errors = new AsyncQueue();
991
+ const controller = new AbortController();
992
+ let closed = false;
993
+ const finishAll = () => {
994
+ matches.finish();
995
+ notifications.finish();
996
+ errors.finish();
997
+ };
998
+ const close = () => {
999
+ if (closed) return;
1000
+ closed = true;
1001
+ controller.abort();
1002
+ finishAll();
1003
+ };
1004
+ const run = async () => {
1005
+ let curSince = cfg.sinceTime;
1006
+ while (!closed) {
1007
+ const params = {
1008
+ category: cfg.category,
1009
+ timeout: String(cfg.timeout)
1010
+ };
1011
+ if (curSince !== void 0) params["since_time"] = String(curSince);
1012
+ let resp;
1013
+ try {
1014
+ resp = await cfg.fetchOnce(params, controller.signal);
1015
+ } catch (err) {
1016
+ if (closed) return;
1017
+ if (err instanceof TypeError) {
1018
+ errors.push(new AudDConnectionError(`Network error: ${err.message}`, err));
1019
+ } else if (err !== null && typeof err === "object" && err.name === "AbortError") {
1020
+ if (closed) return;
1021
+ errors.push(new AudDConnectionError("Request was aborted (timeout)", err));
1022
+ } else if (err instanceof Error) {
1023
+ errors.push(err);
1024
+ } else {
1025
+ errors.push(new Error(String(err)));
1026
+ }
1027
+ finishAll();
1028
+ return;
1029
+ }
1030
+ try {
1031
+ const decoded = decodeOne(resp);
1032
+ if (decoded.kind === "match") matches.push(decoded.match);
1033
+ else if (decoded.kind === "notification") notifications.push(decoded.notification);
1034
+ } catch (err) {
1035
+ errors.push(err instanceof Error ? err : new Error(String(err)));
1036
+ finishAll();
1037
+ return;
1038
+ }
1039
+ const dict = resp.jsonBody;
1040
+ if (typeof dict === "object" && dict !== null && !Array.isArray(dict)) {
1041
+ const ts = dict["timestamp"];
1042
+ if (typeof ts === "number") curSince = ts;
1043
+ }
1044
+ }
1045
+ };
1046
+ void run();
1047
+ return {
1048
+ matches: matches.toIterable(),
1049
+ notifications: notifications.toIterable(),
1050
+ errors: errors.toIterable(),
1051
+ close,
1052
+ async [Symbol.asyncDispose]() {
1053
+ close();
1054
+ await Promise.resolve();
1055
+ }
1056
+ };
1057
+ }
839
1058
 
840
1059
  // src/streams.ts
841
1060
  var API_BASE = "https://api.audd.io";
@@ -942,69 +1161,58 @@ var Streams = class {
942
1161
  deriveLongpollCategory(radioId) {
943
1162
  return deriveLongpollCategory(this.apiToken, radioId);
944
1163
  }
945
- /** Parse a callback POST body into a typed `StreamCallbackPayload`. */
1164
+ /**
1165
+ * Parse a callback POST body into `{ match, notification }`. Pass an
1166
+ * already-parsed JSON value or a string. Exactly one field is non-null on
1167
+ * success. See {@link parseCallback} for details.
1168
+ */
946
1169
  parseCallback(body) {
947
- return parseStreamCallback(body);
1170
+ return parseCallback(body);
948
1171
  }
949
1172
  /**
950
- * Longpoll the AudD subscription endpoint, yielding parsed JSON dicts
951
- * (recognition events, notifications, or `{ timeout: ... }` markers).
1173
+ * Long-poll the AudD subscription endpoint.
952
1174
  *
953
- * By default, performs a one-time `getCallbackUrl` preflight. If the
954
- * server returns code 19 (no callback URL configured), throws
955
- * `AudDInvalidRequestError` with an actionable hint. Pass
956
- * `skipCallbackCheck: true` to bypass.
1175
+ * Returns a {@link LongpollPoll} handle with three async-iterables —
1176
+ * `matches`, `notifications`, `errors` — that are filled by a background
1177
+ * loop. Iterate them independently or in parallel via `Promise.all([...])`.
1178
+ *
1179
+ * Before the first request the SDK runs a one-time `getCallbackUrl`
1180
+ * preflight: AudD silently discards events for accounts that haven't set a
1181
+ * callback URL, and the preflight surfaces that misconfiguration as an
1182
+ * actionable {@link AudDInvalidRequestError}. Pass `skipCallbackCheck: true`
1183
+ * to bypass.
957
1184
  */
958
- longpoll(category, opts = {}) {
959
- const skipCheck = opts.skipCallbackCheck ?? false;
1185
+ async longpoll(category, opts = {}) {
1186
+ if (opts.skipCallbackCheck !== true) {
1187
+ await this.preflightCallbackUrl();
1188
+ }
960
1189
  const timeoutSec = opts.timeout ?? 50;
961
- const initialSince = opts.sinceTime;
962
- const getCallbackUrl = () => this.getCallbackUrl();
963
- const httpGet = (params) => this.http.get(`${API_BASE}/longpoll/`, params);
1190
+ const httpClient = this.http;
964
1191
  const readPolicy = this.readPolicy;
965
- return {
966
- [Symbol.asyncIterator]() {
967
- let preflighted = skipCheck;
968
- let curSince = initialSince;
969
- return {
970
- async next() {
971
- if (!preflighted) {
972
- try {
973
- await getCallbackUrl();
974
- } catch (err) {
975
- if (err instanceof AudDAPIError && err.errorCode === NO_CALLBACK_ERROR_CODE) {
976
- throw new AudDInvalidRequestError({
977
- errorCode: 0,
978
- message: PREFLIGHT_NO_CALLBACK_HINT,
979
- httpStatus: err.httpStatus,
980
- requestId: err.requestId
981
- });
982
- }
983
- throw err;
984
- }
985
- preflighted = true;
986
- }
987
- const params = {
988
- category,
989
- timeout: String(timeoutSec)
990
- };
991
- if (curSince !== void 0) params["since_time"] = String(curSince);
992
- const resp = await runRetried(() => httpGet(params), readPolicy);
993
- const body = resp.jsonBody;
994
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
995
- throw new AudDSerializationError(
996
- "Unparseable longpoll response",
997
- resp.rawText
998
- );
999
- }
1000
- const dict = body;
1001
- const ts = dict["timestamp"];
1002
- if (typeof ts === "number") curSince = ts;
1003
- return { value: dict, done: false };
1004
- }
1005
- };
1192
+ return startLongpoll({
1193
+ category,
1194
+ timeout: timeoutSec,
1195
+ sinceTime: opts.sinceTime,
1196
+ fetchOnce: (params, signal) => runRetried(
1197
+ () => httpClient.get(`${API_BASE}/longpoll/`, params, { signal }),
1198
+ readPolicy
1199
+ )
1200
+ });
1201
+ }
1202
+ async preflightCallbackUrl() {
1203
+ try {
1204
+ await this.getCallbackUrl();
1205
+ } catch (err) {
1206
+ if (err instanceof AudDAPIError && err.errorCode === NO_CALLBACK_ERROR_CODE) {
1207
+ throw new AudDInvalidRequestError({
1208
+ errorCode: 0,
1209
+ message: PREFLIGHT_NO_CALLBACK_HINT,
1210
+ httpStatus: err.httpStatus,
1211
+ requestId: err.requestId
1212
+ });
1006
1213
  }
1007
- };
1214
+ throw err;
1215
+ }
1008
1216
  }
1009
1217
  };
1010
1218
 
@@ -1142,7 +1350,7 @@ var Advanced = class {
1142
1350
  var API_BASE3 = "https://api.audd.io";
1143
1351
  var ENTERPRISE_BASE = "https://enterprise.audd.io";
1144
1352
  var DEPRECATED_PARAMS_CODE = 51;
1145
- var HTTP_CLIENT_ERROR_FLOOR = 400;
1353
+ var HTTP_CLIENT_ERROR_FLOOR2 = 400;
1146
1354
  var TOKEN_ENV_VAR = "AUDD_API_TOKEN";
1147
1355
  function safeEmit(hook, event) {
1148
1356
  if (hook === void 0) return;
@@ -1190,7 +1398,7 @@ function maybeWarnAndStrip(body) {
1190
1398
  function decodeOrRaise(resp) {
1191
1399
  const body = resp.jsonBody;
1192
1400
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
1193
- if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
1401
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR2) {
1194
1402
  throw new AudDServerError({
1195
1403
  errorCode: 0,
1196
1404
  message: `HTTP ${String(resp.httpStatus)} with non-JSON response body`,
@@ -1437,6 +1645,7 @@ exports.VERSION = VERSION;
1437
1645
  exports.addReturnToUrl = addReturnToUrl;
1438
1646
  exports.deriveLongpollCategory = deriveLongpollCategory;
1439
1647
  exports.errorClassForCode = errorClassForCode;
1440
- exports.parseCallback = parseStreamCallback;
1648
+ exports.handleCallback = handleCallback;
1649
+ exports.parseCallback = parseCallback;
1441
1650
  //# sourceMappingURL=index.cjs.map
1442
1651
  //# sourceMappingURL=index.cjs.map