@audd/sdk 1.4.9 → 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
@@ -500,7 +500,7 @@ function parseStream(raw) {
500
500
  rawResponse: r
501
501
  };
502
502
  }
503
- var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
503
+ var STREAM_CALLBACK_SONG_KEYS = [
504
504
  "artist",
505
505
  "title",
506
506
  "score",
@@ -508,20 +508,22 @@ var STREAM_CALLBACK_RESULT_ENTRY_KEYS = [
508
508
  "release_date",
509
509
  "label",
510
510
  "song_link",
511
+ "isrc",
512
+ "upc",
511
513
  "apple_music",
512
514
  "spotify",
513
515
  "deezer",
514
516
  "napster",
515
517
  "musicbrainz"
516
518
  ];
517
- function parseStreamCallbackResultEntry(raw) {
518
- const r = requireObject(raw, "StreamCallbackResultEntry");
519
+ function parseStreamCallbackSong(raw) {
520
+ const r = requireObject(raw, "StreamCallbackSong");
519
521
  const artist = asString(r.artist);
520
522
  const title = asString(r.title);
521
523
  const score = asNumber(r.score);
522
- if (artist === void 0) throw new TypeError("StreamCallbackResultEntry: artist not string");
523
- if (title === void 0) throw new TypeError("StreamCallbackResultEntry: title not string");
524
- 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");
525
527
  const musicbrainz = Array.isArray(r.musicbrainz) ? r.musicbrainz.filter((x) => asObject(x) !== void 0) : void 0;
526
528
  return {
527
529
  artist,
@@ -531,33 +533,39 @@ function parseStreamCallbackResultEntry(raw) {
531
533
  releaseDate: asString(r.release_date),
532
534
  label: asString(r.label),
533
535
  songLink: asString(r.song_link),
536
+ isrc: asString(r.isrc),
537
+ upc: asString(r.upc),
534
538
  appleMusic: asObject(r.apple_music),
535
539
  spotify: asObject(r.spotify),
536
540
  deezer: asObject(r.deezer),
537
541
  napster: asObject(r.napster),
538
542
  musicbrainz,
539
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_ENTRY_KEYS),
540
- rawResponse: r
543
+ extras: pickExtras(r, STREAM_CALLBACK_SONG_KEYS)
541
544
  };
542
545
  }
543
- var STREAM_CALLBACK_RESULT_KEYS = [
546
+ var STREAM_CALLBACK_MATCH_KEYS = [
544
547
  "radio_id",
545
548
  "timestamp",
546
549
  "play_length",
547
550
  "results"
548
551
  ];
549
- function parseStreamCallbackResult(raw) {
550
- const r = requireObject(raw, "StreamCallbackResult");
552
+ function parseStreamCallbackMatch(raw) {
553
+ const r = requireObject(raw, "StreamCallbackMatch");
551
554
  const radioId = asNumber(r.radio_id);
552
- 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");
553
556
  const resultsRaw = r.results;
554
- 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");
555
562
  return {
556
563
  radioId,
557
564
  timestamp: asString(r.timestamp),
558
565
  playLength: asNumber(r.play_length),
559
- results: resultsRaw.map(parseStreamCallbackResultEntry),
560
- extras: pickExtras(r, STREAM_CALLBACK_RESULT_KEYS),
566
+ song: first,
567
+ alternatives: rest,
568
+ extras: pickExtras(r, STREAM_CALLBACK_MATCH_KEYS),
561
569
  rawResponse: r
562
570
  };
563
571
  }
@@ -587,39 +595,6 @@ function parseStreamCallbackNotification(raw) {
587
595
  rawResponse: r
588
596
  };
589
597
  }
590
- function parseStreamCallback(raw) {
591
- const r = requireObject(raw, "StreamCallbackPayload");
592
- if ("notification" in r) {
593
- const notification = parseStreamCallbackNotification(r.notification);
594
- const time = asNumber(r.time) ?? null;
595
- return {
596
- result: null,
597
- notification,
598
- time,
599
- rawPayload: r,
600
- get isResult() {
601
- return false;
602
- },
603
- get isNotification() {
604
- return true;
605
- }
606
- };
607
- }
608
- const inner = requireObject(r.result, "StreamCallbackPayload.result");
609
- const result = parseStreamCallbackResult(inner);
610
- return {
611
- result,
612
- notification: null,
613
- time: null,
614
- rawPayload: r,
615
- get isResult() {
616
- return true;
617
- },
618
- get isNotification() {
619
- return false;
620
- }
621
- };
622
- }
623
598
  var LYRICS_KEYS = [
624
599
  "artist",
625
600
  "title",
@@ -818,6 +793,246 @@ function addReturnToUrl(url, returnMetadata) {
818
793
  u.searchParams.set("return", value);
819
794
  return u.toString();
820
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
+ }
821
1036
 
822
1037
  // src/streams.ts
823
1038
  var API_BASE = "https://api.audd.io";
@@ -924,69 +1139,58 @@ var Streams = class {
924
1139
  deriveLongpollCategory(radioId) {
925
1140
  return deriveLongpollCategory(this.apiToken, radioId);
926
1141
  }
927
- /** 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
+ */
928
1147
  parseCallback(body) {
929
- return parseStreamCallback(body);
1148
+ return parseCallback(body);
930
1149
  }
931
1150
  /**
932
- * Longpoll the AudD subscription endpoint, yielding parsed JSON dicts
933
- * (recognition events, notifications, or `{ timeout: ... }` markers).
1151
+ * Long-poll the AudD subscription endpoint.
1152
+ *
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([...])`.
934
1156
  *
935
- * By default, performs a one-time `getCallbackUrl` preflight. If the
936
- * server returns code 19 (no callback URL configured), throws
937
- * `AudDInvalidRequestError` with an actionable hint. Pass
938
- * `skipCallbackCheck: true` to bypass.
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.
939
1162
  */
940
- longpoll(category, opts = {}) {
941
- const skipCheck = opts.skipCallbackCheck ?? false;
1163
+ async longpoll(category, opts = {}) {
1164
+ if (opts.skipCallbackCheck !== true) {
1165
+ await this.preflightCallbackUrl();
1166
+ }
942
1167
  const timeoutSec = opts.timeout ?? 50;
943
- const initialSince = opts.sinceTime;
944
- const getCallbackUrl = () => this.getCallbackUrl();
945
- const httpGet = (params) => this.http.get(`${API_BASE}/longpoll/`, params);
1168
+ const httpClient = this.http;
946
1169
  const readPolicy = this.readPolicy;
947
- return {
948
- [Symbol.asyncIterator]() {
949
- let preflighted = skipCheck;
950
- let curSince = initialSince;
951
- return {
952
- async next() {
953
- if (!preflighted) {
954
- try {
955
- await getCallbackUrl();
956
- } catch (err) {
957
- if (err instanceof AudDAPIError && err.errorCode === NO_CALLBACK_ERROR_CODE) {
958
- throw new AudDInvalidRequestError({
959
- errorCode: 0,
960
- message: PREFLIGHT_NO_CALLBACK_HINT,
961
- httpStatus: err.httpStatus,
962
- requestId: err.requestId
963
- });
964
- }
965
- throw err;
966
- }
967
- preflighted = true;
968
- }
969
- const params = {
970
- category,
971
- timeout: String(timeoutSec)
972
- };
973
- if (curSince !== void 0) params["since_time"] = String(curSince);
974
- const resp = await runRetried(() => httpGet(params), readPolicy);
975
- const body = resp.jsonBody;
976
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
977
- throw new AudDSerializationError(
978
- "Unparseable longpoll response",
979
- resp.rawText
980
- );
981
- }
982
- const dict = body;
983
- const ts = dict["timestamp"];
984
- if (typeof ts === "number") curSince = ts;
985
- return { value: dict, done: false };
986
- }
987
- };
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
+ });
988
1191
  }
989
- };
1192
+ throw err;
1193
+ }
990
1194
  }
991
1195
  };
992
1196
 
@@ -1124,7 +1328,7 @@ var Advanced = class {
1124
1328
  var API_BASE3 = "https://api.audd.io";
1125
1329
  var ENTERPRISE_BASE = "https://enterprise.audd.io";
1126
1330
  var DEPRECATED_PARAMS_CODE = 51;
1127
- var HTTP_CLIENT_ERROR_FLOOR = 400;
1331
+ var HTTP_CLIENT_ERROR_FLOOR2 = 400;
1128
1332
  var TOKEN_ENV_VAR = "AUDD_API_TOKEN";
1129
1333
  function safeEmit(hook, event) {
1130
1334
  if (hook === void 0) return;
@@ -1172,7 +1376,7 @@ function maybeWarnAndStrip(body) {
1172
1376
  function decodeOrRaise(resp) {
1173
1377
  const body = resp.jsonBody;
1174
1378
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
1175
- if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
1379
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR2) {
1176
1380
  throw new AudDServerError({
1177
1381
  errorCode: 0,
1178
1382
  message: `HTTP ${String(resp.httpStatus)} with non-JSON response body`,
@@ -1397,6 +1601,6 @@ var AudD = class {
1397
1601
  }
1398
1602
  };
1399
1603
 
1400
- 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 };
1401
1605
  //# sourceMappingURL=index.js.map
1402
1606
  //# sourceMappingURL=index.js.map