@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.js CHANGED
@@ -333,6 +333,8 @@ var RECOGNITION_KEYS = [
333
333
  "release_date",
334
334
  "label",
335
335
  "song_link",
336
+ "isrc",
337
+ "upc",
336
338
  "apple_music",
337
339
  "spotify",
338
340
  "deezer",
@@ -359,6 +361,8 @@ function parseRecognitionResult(raw) {
359
361
  releaseDate: asString(r.release_date),
360
362
  label: asString(r.label),
361
363
  songLink,
364
+ isrc: asString(r.isrc),
365
+ upc: asString(r.upc),
362
366
  appleMusic: asObject(r.apple_music),
363
367
  spotify: asObject(r.spotify),
364
368
  deezer: asObject(r.deezer),
@@ -496,7 +500,7 @@ function parseStream(raw) {
496
500
  rawResponse: r
497
501
  };
498
502
  }
499
- var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
503
+ var STREAM_CALLBACK_SONG_KEYS = [
500
504
  "artist",
501
505
  "title",
502
506
  "score",
@@ -504,20 +508,22 @@ var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
504
508
  "release_date",
505
509
  "label",
506
510
  "song_link",
511
+ "isrc",
512
+ "upc",
507
513
  "apple_music",
508
514
  "spotify",
509
515
  "deezer",
510
516
  "napster",
511
517
  "musicbrainz"
512
518
  ];
513
- function parseStreamCallbackResultEntry(raw) {
514
- const r = requireObject(raw, "StreamCallbackResultEntry");
519
+ function parseStreamCallbackSong(raw) {
520
+ const r = requireObject(raw, "StreamCallbackSong");
515
521
  const artist = asString(r.artist);
516
522
  const title = asString(r.title);
517
523
  const score = asNumber(r.score);
518
- if (artist === void 0) throw new TypeError("StreamCallbackResultEntry: artist not string");
519
- if (title === void 0) throw new TypeError("StreamCallbackResultEntry: title not string");
520
- if (score === void 0) throw new TypeError("StreamCallbackResultEntry: score not number");
524
+ if (artist === void 0) throw new TypeError("StreamCallbackSong: artist not string");
525
+ if (title === void 0) throw new TypeError("StreamCallbackSong: title not string");
526
+ if (score === void 0) throw new TypeError("StreamCallbackSong: score not number");
521
527
  const musicbrainz = Array.isArray(r.musicbrainz) ? r.musicbrainz.filter((x) => asObject(x) !== void 0) : void 0;
522
528
  return {
523
529
  artist,
@@ -527,33 +533,39 @@ function parseStreamCallbackResultEntry(raw) {
527
533
  releaseDate: asString(r.release_date),
528
534
  label: asString(r.label),
529
535
  songLink: asString(r.song_link),
536
+ isrc: asString(r.isrc),
537
+ upc: asString(r.upc),
530
538
  appleMusic: asObject(r.apple_music),
531
539
  spotify: asObject(r.spotify),
532
540
  deezer: asObject(r.deezer),
533
541
  napster: asObject(r.napster),
534
542
  musicbrainz,
535
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_ENTRY_KEYS),
536
- rawResponse: r
543
+ extras: pickExtras(r, STREAM_CALLBACK_SONG_KEYS)
537
544
  };
538
545
  }
539
- var STREAM_CALLBACK_RESULT_KEYS = [
546
+ var STREAM_CALLBACK_MATCH_KEYS = [
540
547
  "radio_id",
541
548
  "timestamp",
542
549
  "play_length",
543
550
  "results"
544
551
  ];
545
- function parseStreamCallbackResult(raw) {
546
- const r = requireObject(raw, "StreamCallbackResult");
552
+ function parseStreamCallbackMatch(raw) {
553
+ const r = requireObject(raw, "StreamCallbackMatch");
547
554
  const radioId = asNumber(r.radio_id);
548
- if (radioId === void 0) throw new TypeError("StreamCallbackResult: radio_id not number");
555
+ if (radioId === void 0) throw new TypeError("StreamCallbackMatch: radio_id not number");
549
556
  const resultsRaw = r.results;
550
- if (!Array.isArray(resultsRaw)) throw new TypeError("StreamCallbackResult: results not array");
557
+ if (!Array.isArray(resultsRaw)) throw new TypeError("StreamCallbackMatch: results not array");
558
+ if (resultsRaw.length === 0) throw new TypeError("StreamCallbackMatch: results is empty");
559
+ const songs = resultsRaw.map(parseStreamCallbackSong);
560
+ const [first, ...rest] = songs;
561
+ if (first === void 0) throw new TypeError("StreamCallbackMatch: results is empty");
551
562
  return {
552
563
  radioId,
553
564
  timestamp: asString(r.timestamp),
554
565
  playLength: asNumber(r.play_length),
555
- results: resultsRaw.map(parseStreamCallbackResultEntry),
556
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_KEYS),
566
+ song: first,
567
+ alternatives: rest,
568
+ extras: pickExtras(r, STREAM_CALLBACK_MATCH_KEYS),
557
569
  rawResponse: r
558
570
  };
559
571
  }
@@ -583,39 +595,6 @@ function parseStreamCallbackNotification(raw) {
583
595
  rawResponse: r
584
596
  };
585
597
  }
586
- function parseStreamCallback(raw) {
587
- const r = requireObject(raw, "StreamCallbackPayload");
588
- if ("notification" in r) {
589
- const notification = parseStreamCallbackNotification(r.notification);
590
- const time = asNumber(r.time) ?? null;
591
- return {
592
- result: null,
593
- notification,
594
- time,
595
- rawPayload: r,
596
- get isResult() {
597
- return false;
598
- },
599
- get isNotification() {
600
- return true;
601
- }
602
- };
603
- }
604
- const inner = requireObject(r.result, "StreamCallbackPayload.result");
605
- const result = parseStreamCallbackResult(inner);
606
- return {
607
- result,
608
- notification: null,
609
- time: null,
610
- rawPayload: r,
611
- get isResult() {
612
- return true;
613
- },
614
- get isNotification() {
615
- return false;
616
- }
617
- };
618
- }
619
598
  var LYRICS_KEYS = [
620
599
  "artist",
621
600
  "title",
@@ -814,6 +793,246 @@ function addReturnToUrl(url, returnMetadata) {
814
793
  u.searchParams.set("return", value);
815
794
  return u.toString();
816
795
  }
796
+ function parseCallback(body) {
797
+ let raw = body;
798
+ if (typeof body === "string") {
799
+ try {
800
+ raw = JSON.parse(body);
801
+ } catch (err) {
802
+ throw new AudDSerializationError(
803
+ `callback body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
804
+ body
805
+ );
806
+ }
807
+ }
808
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
809
+ throw new AudDSerializationError(
810
+ "callback body is not a JSON object",
811
+ typeof body === "string" ? body : ""
812
+ );
813
+ }
814
+ const r = raw;
815
+ if ("notification" in r) {
816
+ let notification;
817
+ try {
818
+ notification = parseStreamCallbackNotification(r.notification);
819
+ } catch (err) {
820
+ throw new AudDSerializationError(
821
+ `callback notification: ${err instanceof Error ? err.message : String(err)}`,
822
+ typeof body === "string" ? body : JSON.stringify(r)
823
+ );
824
+ }
825
+ const t = r.time;
826
+ if (typeof t === "number") notification.time = t;
827
+ return { match: null, notification };
828
+ }
829
+ if ("result" in r) {
830
+ let match;
831
+ try {
832
+ match = parseStreamCallbackMatch(r.result);
833
+ } catch (err) {
834
+ throw new AudDSerializationError(
835
+ `callback result: ${err instanceof Error ? err.message : String(err)}`,
836
+ typeof body === "string" ? body : JSON.stringify(r)
837
+ );
838
+ }
839
+ return { match, notification: null };
840
+ }
841
+ throw new AudDSerializationError(
842
+ "callback body has neither result nor notification",
843
+ typeof body === "string" ? body : JSON.stringify(r)
844
+ );
845
+ }
846
+ async function handleCallback(req) {
847
+ if (typeof req.text === "function") {
848
+ const text = await req.text();
849
+ return parseCallback(text);
850
+ }
851
+ const reqRecord = req;
852
+ if ("body" in reqRecord && reqRecord.body !== void 0 && reqRecord.body !== null) {
853
+ const body = reqRecord.body;
854
+ if (typeof body === "object" && body !== null && Symbol.asyncIterator in body && // Plain Buffer/Uint8Array also has Symbol.iterator — accept those by value.
855
+ !(body instanceof Uint8Array)) {
856
+ return parseCallback(await drainAsyncIterable(body));
857
+ }
858
+ if (body instanceof Uint8Array) {
859
+ return parseCallback(new TextDecoder().decode(body));
860
+ }
861
+ return parseCallback(body);
862
+ }
863
+ if (typeof req === "object" && req !== null && Symbol.asyncIterator in req) {
864
+ return parseCallback(await drainAsyncIterable(req));
865
+ }
866
+ throw new AudDSerializationError(
867
+ "handleCallback: request has no body, no text() method, and is not async-iterable"
868
+ );
869
+ }
870
+ async function drainAsyncIterable(it) {
871
+ const chunks = [];
872
+ let stringBuf = "";
873
+ for await (const chunk of it) {
874
+ if (typeof chunk === "string") {
875
+ stringBuf += chunk;
876
+ } else if (chunk instanceof Uint8Array) {
877
+ chunks.push(chunk);
878
+ } else if (chunk !== null && chunk !== void 0) {
879
+ stringBuf += String(chunk);
880
+ }
881
+ }
882
+ if (chunks.length > 0) {
883
+ let total = 0;
884
+ for (const c of chunks) total += c.byteLength;
885
+ const merged = new Uint8Array(total);
886
+ let offset = 0;
887
+ for (const c of chunks) {
888
+ merged.set(c, offset);
889
+ offset += c.byteLength;
890
+ }
891
+ stringBuf = new TextDecoder().decode(merged) + stringBuf;
892
+ }
893
+ return stringBuf;
894
+ }
895
+
896
+ // src/longpollCore.ts
897
+ var HTTP_CLIENT_ERROR_FLOOR = 400;
898
+ var AsyncQueue = class {
899
+ items = [];
900
+ waiters = [];
901
+ done = false;
902
+ push(value) {
903
+ if (this.done) return;
904
+ const w = this.waiters.shift();
905
+ if (w !== void 0) {
906
+ w({ value, done: false });
907
+ return;
908
+ }
909
+ this.items.push(value);
910
+ }
911
+ finish() {
912
+ if (this.done) return;
913
+ this.done = true;
914
+ while (this.waiters.length > 0) {
915
+ const w = this.waiters.shift();
916
+ if (w !== void 0) w({ value: void 0, done: true });
917
+ }
918
+ }
919
+ next() {
920
+ const item = this.items.shift();
921
+ if (item !== void 0) return Promise.resolve({ value: item, done: false });
922
+ if (this.done) return Promise.resolve({ value: void 0, done: true });
923
+ return new Promise((resolve) => {
924
+ this.waiters.push(resolve);
925
+ });
926
+ }
927
+ toIterable() {
928
+ const next = () => this.next();
929
+ return {
930
+ [Symbol.asyncIterator]() {
931
+ return { next };
932
+ }
933
+ };
934
+ }
935
+ };
936
+ function decodeOne(resp) {
937
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
938
+ throw new AudDServerError({
939
+ errorCode: 0,
940
+ message: `Longpoll endpoint returned HTTP ${String(resp.httpStatus)}`,
941
+ httpStatus: resp.httpStatus,
942
+ requestId: resp.requestId,
943
+ rawResponse: resp.jsonBody ?? resp.rawText
944
+ });
945
+ }
946
+ const body = resp.jsonBody;
947
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
948
+ throw new AudDSerializationError(
949
+ "Longpoll response was not a JSON object",
950
+ resp.rawText
951
+ );
952
+ }
953
+ const dict = body;
954
+ if ("notification" in dict) {
955
+ const notification = parseStreamCallbackNotification(dict.notification);
956
+ const t = dict["time"];
957
+ if (typeof t === "number") notification.time = t;
958
+ return { kind: "notification", notification };
959
+ }
960
+ if ("result" in dict) {
961
+ return { kind: "match", match: parseStreamCallbackMatch(dict.result) };
962
+ }
963
+ return { kind: "keepalive" };
964
+ }
965
+ function startLongpoll(cfg) {
966
+ const matches = new AsyncQueue();
967
+ const notifications = new AsyncQueue();
968
+ const errors = new AsyncQueue();
969
+ const controller = new AbortController();
970
+ let closed = false;
971
+ const finishAll = () => {
972
+ matches.finish();
973
+ notifications.finish();
974
+ errors.finish();
975
+ };
976
+ const close = () => {
977
+ if (closed) return;
978
+ closed = true;
979
+ controller.abort();
980
+ finishAll();
981
+ };
982
+ const run = async () => {
983
+ let curSince = cfg.sinceTime;
984
+ while (!closed) {
985
+ const params = {
986
+ category: cfg.category,
987
+ timeout: String(cfg.timeout)
988
+ };
989
+ if (curSince !== void 0) params["since_time"] = String(curSince);
990
+ let resp;
991
+ try {
992
+ resp = await cfg.fetchOnce(params, controller.signal);
993
+ } catch (err) {
994
+ if (closed) return;
995
+ if (err instanceof TypeError) {
996
+ errors.push(new AudDConnectionError(`Network error: ${err.message}`, err));
997
+ } else if (err !== null && typeof err === "object" && err.name === "AbortError") {
998
+ if (closed) return;
999
+ errors.push(new AudDConnectionError("Request was aborted (timeout)", err));
1000
+ } else if (err instanceof Error) {
1001
+ errors.push(err);
1002
+ } else {
1003
+ errors.push(new Error(String(err)));
1004
+ }
1005
+ finishAll();
1006
+ return;
1007
+ }
1008
+ try {
1009
+ const decoded = decodeOne(resp);
1010
+ if (decoded.kind === "match") matches.push(decoded.match);
1011
+ else if (decoded.kind === "notification") notifications.push(decoded.notification);
1012
+ } catch (err) {
1013
+ errors.push(err instanceof Error ? err : new Error(String(err)));
1014
+ finishAll();
1015
+ return;
1016
+ }
1017
+ const dict = resp.jsonBody;
1018
+ if (typeof dict === "object" && dict !== null && !Array.isArray(dict)) {
1019
+ const ts = dict["timestamp"];
1020
+ if (typeof ts === "number") curSince = ts;
1021
+ }
1022
+ }
1023
+ };
1024
+ void run();
1025
+ return {
1026
+ matches: matches.toIterable(),
1027
+ notifications: notifications.toIterable(),
1028
+ errors: errors.toIterable(),
1029
+ close,
1030
+ async [Symbol.asyncDispose]() {
1031
+ close();
1032
+ await Promise.resolve();
1033
+ }
1034
+ };
1035
+ }
817
1036
 
818
1037
  // src/streams.ts
819
1038
  var API_BASE = "https://api.audd.io";
@@ -920,69 +1139,58 @@ var Streams = class {
920
1139
  deriveLongpollCategory(radioId) {
921
1140
  return deriveLongpollCategory(this.apiToken, radioId);
922
1141
  }
923
- /** Parse a callback POST body into a typed `StreamCallbackPayload`. */
1142
+ /**
1143
+ * Parse a callback POST body into `{ match, notification }`. Pass an
1144
+ * already-parsed JSON value or a string. Exactly one field is non-null on
1145
+ * success. See {@link parseCallback} for details.
1146
+ */
924
1147
  parseCallback(body) {
925
- return parseStreamCallback(body);
1148
+ return parseCallback(body);
926
1149
  }
927
1150
  /**
928
- * Longpoll the AudD subscription endpoint, yielding parsed JSON dicts
929
- * (recognition events, notifications, or `{ timeout: ... }` markers).
1151
+ * Long-poll the AudD subscription endpoint.
930
1152
  *
931
- * By default, performs a one-time `getCallbackUrl` preflight. If the
932
- * server returns code 19 (no callback URL configured), throws
933
- * `AudDInvalidRequestError` with an actionable hint. Pass
934
- * `skipCallbackCheck: true` to bypass.
1153
+ * Returns a {@link LongpollPoll} handle with three async-iterables —
1154
+ * `matches`, `notifications`, `errors` — that are filled by a background
1155
+ * loop. Iterate them independently or in parallel via `Promise.all([...])`.
1156
+ *
1157
+ * Before the first request the SDK runs a one-time `getCallbackUrl`
1158
+ * preflight: AudD silently discards events for accounts that haven't set a
1159
+ * callback URL, and the preflight surfaces that misconfiguration as an
1160
+ * actionable {@link AudDInvalidRequestError}. Pass `skipCallbackCheck: true`
1161
+ * to bypass.
935
1162
  */
936
- longpoll(category, opts = {}) {
937
- const skipCheck = opts.skipCallbackCheck ?? false;
1163
+ async longpoll(category, opts = {}) {
1164
+ if (opts.skipCallbackCheck !== true) {
1165
+ await this.preflightCallbackUrl();
1166
+ }
938
1167
  const timeoutSec = opts.timeout ?? 50;
939
- const initialSince = opts.sinceTime;
940
- const getCallbackUrl = () => this.getCallbackUrl();
941
- const httpGet = (params) => this.http.get(`${API_BASE}/longpoll/`, params);
1168
+ const httpClient = this.http;
942
1169
  const readPolicy = this.readPolicy;
943
- return {
944
- [Symbol.asyncIterator]() {
945
- let preflighted = skipCheck;
946
- let curSince = initialSince;
947
- return {
948
- async next() {
949
- if (!preflighted) {
950
- try {
951
- await getCallbackUrl();
952
- } catch (err) {
953
- if (err instanceof AudDAPIError && err.errorCode === NO_CALLBACK_ERROR_CODE) {
954
- throw new AudDInvalidRequestError({
955
- errorCode: 0,
956
- message: PREFLIGHT_NO_CALLBACK_HINT,
957
- httpStatus: err.httpStatus,
958
- requestId: err.requestId
959
- });
960
- }
961
- throw err;
962
- }
963
- preflighted = true;
964
- }
965
- const params = {
966
- category,
967
- timeout: String(timeoutSec)
968
- };
969
- if (curSince !== void 0) params["since_time"] = String(curSince);
970
- const resp = await runRetried(() => httpGet(params), readPolicy);
971
- const body = resp.jsonBody;
972
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
973
- throw new AudDSerializationError(
974
- "Unparseable longpoll response",
975
- resp.rawText
976
- );
977
- }
978
- const dict = body;
979
- const ts = dict["timestamp"];
980
- if (typeof ts === "number") curSince = ts;
981
- return { value: dict, done: false };
982
- }
983
- };
1170
+ return startLongpoll({
1171
+ category,
1172
+ timeout: timeoutSec,
1173
+ sinceTime: opts.sinceTime,
1174
+ fetchOnce: (params, signal) => runRetried(
1175
+ () => httpClient.get(`${API_BASE}/longpoll/`, params, { signal }),
1176
+ readPolicy
1177
+ )
1178
+ });
1179
+ }
1180
+ async preflightCallbackUrl() {
1181
+ try {
1182
+ await this.getCallbackUrl();
1183
+ } catch (err) {
1184
+ if (err instanceof AudDAPIError && err.errorCode === NO_CALLBACK_ERROR_CODE) {
1185
+ throw new AudDInvalidRequestError({
1186
+ errorCode: 0,
1187
+ message: PREFLIGHT_NO_CALLBACK_HINT,
1188
+ httpStatus: err.httpStatus,
1189
+ requestId: err.requestId
1190
+ });
984
1191
  }
985
- };
1192
+ throw err;
1193
+ }
986
1194
  }
987
1195
  };
988
1196
 
@@ -1120,7 +1328,7 @@ var Advanced = class {
1120
1328
  var API_BASE3 = "https://api.audd.io";
1121
1329
  var ENTERPRISE_BASE = "https://enterprise.audd.io";
1122
1330
  var DEPRECATED_PARAMS_CODE = 51;
1123
- var HTTP_CLIENT_ERROR_FLOOR = 400;
1331
+ var HTTP_CLIENT_ERROR_FLOOR2 = 400;
1124
1332
  var TOKEN_ENV_VAR = "AUDD_API_TOKEN";
1125
1333
  function safeEmit(hook, event) {
1126
1334
  if (hook === void 0) return;
@@ -1168,7 +1376,7 @@ function maybeWarnAndStrip(body) {
1168
1376
  function decodeOrRaise(resp) {
1169
1377
  const body = resp.jsonBody;
1170
1378
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
1171
- if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
1379
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR2) {
1172
1380
  throw new AudDServerError({
1173
1381
  errorCode: 0,
1174
1382
  message: `HTTP ${String(resp.httpStatus)} with non-JSON response body`,
@@ -1393,6 +1601,6 @@ var AudD = class {
1393
1601
  }
1394
1602
  };
1395
1603
 
1396
- export { AudD, AudDAPIError, AudDAuthenticationError, AudDBlockedError, AudDConnectionError, AudDCustomCatalogAccessError, AudDError, AudDInvalidAudioError, AudDInvalidRequestError, AudDNeedsUpdateError, AudDNotReleasedError, AudDQuotaError, AudDRateLimitError, AudDSerializationError, AudDServerError, AudDStreamLimitError, AudDSubscriptionError, DuplicateReturnParameterError, VERSION, addReturnToUrl, deriveLongpollCategory, errorClassForCode, parseStreamCallback as parseCallback };
1604
+ export { AudD, AudDAPIError, AudDAuthenticationError, AudDBlockedError, AudDConnectionError, AudDCustomCatalogAccessError, AudDError, AudDInvalidAudioError, AudDInvalidRequestError, AudDNeedsUpdateError, AudDNotReleasedError, AudDQuotaError, AudDRateLimitError, AudDSerializationError, AudDServerError, AudDStreamLimitError, AudDSubscriptionError, DuplicateReturnParameterError, VERSION, addReturnToUrl, deriveLongpollCategory, errorClassForCode, handleCallback, parseCallback };
1397
1605
  //# sourceMappingURL=index.js.map
1398
1606
  //# sourceMappingURL=index.js.map