ani-client 1.6.0 → 1.6.1

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.mjs CHANGED
@@ -2,6 +2,11 @@
2
2
  function parseAniListMarkdown(text) {
3
3
  if (!text) return "";
4
4
  let html = text;
5
+ html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6
+ html = html.replace(/```([\s\S]*?)```/g, (_match, code) => {
7
+ return `<pre><code>${code.trim()}</code></pre>`;
8
+ });
9
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
5
10
  html = html.replace(/~!(.*?)!~/gs, '<span class="anilist-spoiler">$1</span>');
6
11
  html = html.replace(/~~~(.*?)~~~/gs, '<div class="anilist-center">$1</div>');
7
12
  html = html.replace(/img(\d+)\((.*?)\)/gi, '<img src="$2" width="$1" alt="" class="anilist-image" />');
@@ -11,6 +16,12 @@ function parseAniListMarkdown(text) {
11
16
  '<iframe src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen class="anilist-youtube"></iframe>'
12
17
  );
13
18
  html = html.replace(/webm\((.*?)\)/gi, '<video src="$1" controls class="anilist-webm"></video>');
19
+ html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>");
20
+ html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>");
21
+ html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>");
22
+ html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
23
+ html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>");
24
+ html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>");
14
25
  html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
15
26
  html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
16
27
  html = html.replace(/_(.*?)_/g, "<em>$1</em>");
@@ -18,14 +29,46 @@ function parseAniListMarkdown(text) {
18
29
  html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
19
30
  html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
20
31
  html = html.replace(/\r\n/g, "\n");
32
+ const lines = html.split("\n");
33
+ const processed = [];
34
+ let listType = null;
35
+ for (const line of lines) {
36
+ const ulMatch = line.match(/^[\s]*[-*]\s+(.*)/);
37
+ const olMatch = line.match(/^[\s]*\d+\.\s+(.*)/);
38
+ if (ulMatch) {
39
+ if (listType !== "ul") {
40
+ if (listType) processed.push(`</${listType}>`);
41
+ processed.push("<ul>");
42
+ listType = "ul";
43
+ }
44
+ processed.push(`<li>${ulMatch[1]}</li>`);
45
+ } else if (olMatch) {
46
+ if (listType !== "ol") {
47
+ if (listType) processed.push(`</${listType}>`);
48
+ processed.push("<ol>");
49
+ listType = "ol";
50
+ }
51
+ processed.push(`<li>${olMatch[1]}</li>`);
52
+ } else {
53
+ if (listType) {
54
+ processed.push(`</${listType}>`);
55
+ listType = null;
56
+ }
57
+ processed.push(line);
58
+ }
59
+ }
60
+ if (listType) processed.push(`</${listType}>`);
61
+ html = processed.join("\n");
21
62
  const paragraphs = html.split(/\n{2,}/);
22
63
  html = paragraphs.map((p) => {
23
- const withBr = p.replace(/\n/g, "<br />");
24
- if (withBr.match(/^(<div|<iframe|<video|<img)/)) {
64
+ const trimmed = p.trim();
65
+ if (!trimmed) return "";
66
+ const withBr = trimmed.replace(/\n/g, "<br />");
67
+ if (withBr.match(/^(<div|<iframe|<video|<img|<h[1-6]|<ul|<ol|<pre)/)) {
25
68
  return withBr;
26
69
  }
27
70
  return `<p>${withBr}</p>`;
28
- }).join("\n");
71
+ }).filter(Boolean).join("\n");
29
72
  return html;
30
73
  }
31
74
 
@@ -800,7 +843,7 @@ function buildBatchQuery(ids, typeName, fields, prefix) {
800
843
  ${aliases}
801
844
  }`;
802
845
  }
803
- var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS_BASE, "m");
846
+ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS, "m");
804
847
  var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
805
848
  var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
806
849
 
@@ -868,6 +911,8 @@ var RateLimiter = class {
868
911
  constructor(options = {}) {
869
912
  this.head = 0;
870
913
  this.count = 0;
914
+ /** @internal — active sleep timers for cleanup */
915
+ this.activeTimers = /* @__PURE__ */ new Set();
871
916
  this.maxRequests = options.maxRequests ?? 85;
872
917
  this.windowMs = options.windowMs ?? 6e4;
873
918
  this.maxRetries = options.maxRetries ?? 3;
@@ -885,8 +930,7 @@ var RateLimiter = class {
885
930
  if (!this.enabled) return;
886
931
  if (this.count >= this.maxRequests) {
887
932
  const oldest = this.timestamps[this.head];
888
- const now2 = Date.now();
889
- const elapsed = now2 - oldest;
933
+ const elapsed = Date.now() - oldest;
890
934
  if (elapsed < this.windowMs) {
891
935
  const waitMs = this.windowMs - elapsed + 50;
892
936
  await this.sleep(waitMs);
@@ -950,14 +994,32 @@ var RateLimiter = class {
950
994
  if (this.timeoutMs <= 0) return fetch(url, init);
951
995
  const controller = new AbortController();
952
996
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
997
+ const signals = [controller.signal, init.signal].filter(Boolean);
998
+ const combinedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
953
999
  try {
954
- return await fetch(url, { ...init, signal: controller.signal });
1000
+ return await fetch(url, { ...init, signal: combinedSignal });
955
1001
  } finally {
956
1002
  clearTimeout(timer);
957
1003
  }
958
1004
  }
959
1005
  sleep(ms) {
960
- return new Promise((resolve) => setTimeout(resolve, ms));
1006
+ return new Promise((resolve) => {
1007
+ const timer = setTimeout(() => {
1008
+ this.activeTimers.delete(timer);
1009
+ resolve();
1010
+ }, ms);
1011
+ this.activeTimers.add(timer);
1012
+ });
1013
+ }
1014
+ /** Cancel all pending sleep timers and reset internal state. */
1015
+ dispose() {
1016
+ for (const timer of this.activeTimers) {
1017
+ clearTimeout(timer);
1018
+ }
1019
+ this.activeTimers.clear();
1020
+ this.head = 0;
1021
+ this.count = 0;
1022
+ this.timestamps.fill(0);
961
1023
  }
962
1024
  };
963
1025
  var RETRYABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
@@ -1331,14 +1393,12 @@ async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
1331
1393
  Saturday: [],
1332
1394
  Sunday: []
1333
1395
  };
1334
- const startOfWeek = new Date(date);
1335
- const day = startOfWeek.getDay();
1336
- const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
1337
- startOfWeek.setDate(diff);
1338
- startOfWeek.setHours(0, 0, 0, 0);
1396
+ const utcDay = date.getUTCDay();
1397
+ const diff = utcDay === 0 ? -6 : 1 - utcDay;
1398
+ const startOfWeek = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + diff, 0, 0, 0));
1339
1399
  const endOfWeek = new Date(startOfWeek);
1340
- endOfWeek.setDate(startOfWeek.getDate() + 6);
1341
- endOfWeek.setHours(23, 59, 59, 999);
1400
+ endOfWeek.setUTCDate(startOfWeek.getUTCDate() + 6);
1401
+ endOfWeek.setUTCHours(23, 59, 59, 999);
1342
1402
  const startTimestamp = Math.floor(startOfWeek.getTime() / 1e3);
1343
1403
  const endTimestamp = Math.floor(endOfWeek.getTime() / 1e3);
1344
1404
  const iterator = client.paginate(
@@ -1352,7 +1412,7 @@ async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
1352
1412
  const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1353
1413
  for await (const episode of iterator) {
1354
1414
  const epDate = new Date(episode.airingAt * 1e3);
1355
- const dayName = names[epDate.getDay()];
1415
+ const dayName = names[epDate.getUTCDay()];
1356
1416
  schedule[dayName].push(episode);
1357
1417
  }
1358
1418
  return schedule;
@@ -1475,13 +1535,15 @@ function mapFavorites(fav) {
1475
1535
 
1476
1536
  // src/client/index.ts
1477
1537
  var DEFAULT_API_URL = "https://graphql.anilist.co";
1538
+ var LIB_VERSION = "1.6.1" ;
1478
1539
  var AniListClient = class {
1479
1540
  constructor(options = {}) {
1480
1541
  this.inFlight = /* @__PURE__ */ new Map();
1481
1542
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
1482
1543
  this.headers = {
1483
1544
  "Content-Type": "application/json",
1484
- Accept: "application/json"
1545
+ Accept: "application/json",
1546
+ "User-Agent": `ani-client/${LIB_VERSION}`
1485
1547
  };
1486
1548
  if (options.token) {
1487
1549
  this.headers.Authorization = `Bearer ${options.token}`;
@@ -1505,7 +1567,6 @@ var AniListClient = class {
1505
1567
  get lastRequestMeta() {
1506
1568
  return this._lastRequestMeta;
1507
1569
  }
1508
- // ── Core infrastructure (internal) ──
1509
1570
  /** @internal */
1510
1571
  async request(query, variables = {}) {
1511
1572
  const cacheKey = MemoryCache.key(query, variables);
@@ -1532,20 +1593,29 @@ var AniListClient = class {
1532
1593
  const start = Date.now();
1533
1594
  this.hooks.onRequest?.(query, variables);
1534
1595
  const minifiedQuery = normalizeQuery(query);
1535
- const res = await this.rateLimiter.fetchWithRetry(
1536
- this.apiUrl,
1537
- {
1538
- method: "POST",
1539
- headers: this.headers,
1540
- body: JSON.stringify({ query: minifiedQuery, variables }),
1541
- signal: this.signal
1542
- },
1543
- { onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
1544
- );
1596
+ let res;
1597
+ try {
1598
+ res = await this.rateLimiter.fetchWithRetry(
1599
+ this.apiUrl,
1600
+ {
1601
+ method: "POST",
1602
+ headers: this.headers,
1603
+ body: JSON.stringify({ query: minifiedQuery, variables }),
1604
+ signal: this.signal
1605
+ },
1606
+ { onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
1607
+ );
1608
+ } catch (err) {
1609
+ const error = err instanceof AniListError ? err : new AniListError(err.message ?? "Network request failed", 0, [err]);
1610
+ this.hooks.onError?.(error, query, variables);
1611
+ throw error;
1612
+ }
1545
1613
  const json = await res.json();
1546
1614
  if (!res.ok || json.errors) {
1547
1615
  const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
1548
- throw new AniListError(message, res.status, json.errors ?? []);
1616
+ const error = new AniListError(message, res.status, json.errors ?? []);
1617
+ this.hooks.onError?.(error, query, variables);
1618
+ throw error;
1549
1619
  }
1550
1620
  const rlLimit = res.headers.get("X-RateLimit-Limit");
1551
1621
  const rlRemaining = res.headers.get("X-RateLimit-Remaining");
@@ -1574,7 +1644,6 @@ var AniListClient = class {
1574
1644
  }
1575
1645
  return { pageInfo: data.Page.pageInfo, results };
1576
1646
  }
1577
- // ── Media ──
1578
1647
  /**
1579
1648
  * Fetch a single media entry by its AniList ID.
1580
1649
  *
@@ -1631,7 +1700,6 @@ var AniListClient = class {
1631
1700
  async getMediaBySeason(options) {
1632
1701
  return getMediaBySeason(this, options);
1633
1702
  }
1634
- // ── Characters ──
1635
1703
  /** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
1636
1704
  async getCharacter(id, include) {
1637
1705
  return getCharacter(this, id, include);
@@ -1640,7 +1708,6 @@ var AniListClient = class {
1640
1708
  async searchCharacters(options = {}) {
1641
1709
  return searchCharacters(this, options);
1642
1710
  }
1643
- // ── Staff ──
1644
1711
  /** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
1645
1712
  async getStaff(id, include) {
1646
1713
  return getStaff(this, id, include);
@@ -1649,7 +1716,6 @@ var AniListClient = class {
1649
1716
  async searchStaff(options = {}) {
1650
1717
  return searchStaff(this, options);
1651
1718
  }
1652
- // ── Users ──
1653
1719
  /**
1654
1720
  * Fetch a user by AniList ID or username.
1655
1721
  *
@@ -1681,7 +1747,6 @@ var AniListClient = class {
1681
1747
  async getUserFavorites(idOrName) {
1682
1748
  return getUserFavorites(this, idOrName);
1683
1749
  }
1684
- // ── Studios ──
1685
1750
  /** Fetch a studio by its AniList ID. */
1686
1751
  async getStudio(id) {
1687
1752
  return getStudio(this, id);
@@ -1690,8 +1755,6 @@ var AniListClient = class {
1690
1755
  async searchStudios(options = {}) {
1691
1756
  return searchStudios(this, options);
1692
1757
  }
1693
- // ── Metadata ──
1694
- // ── Threads ──
1695
1758
  /** Fetch a forum thread by its AniList ID. */
1696
1759
  async getThread(id) {
1697
1760
  return getThread(this, id);
@@ -1710,12 +1773,10 @@ var AniListClient = class {
1710
1773
  const data = await this.request(QUERY_TAGS);
1711
1774
  return data.MediaTagCollection;
1712
1775
  }
1713
- // ── Raw query ──
1714
1776
  /** Execute an arbitrary GraphQL query against the AniList API. */
1715
1777
  async raw(query, variables) {
1716
1778
  return this.request(query, variables ?? {});
1717
1779
  }
1718
- // ── Pagination ──
1719
1780
  /**
1720
1781
  * Auto-paginating async iterator. Yields individual items across all pages.
1721
1782
  *
@@ -1734,7 +1795,6 @@ var AniListClient = class {
1734
1795
  page++;
1735
1796
  }
1736
1797
  }
1737
- // ── Batch queries ──
1738
1798
  /** Fetch multiple media entries in a single API request. */
1739
1799
  async getMediaBatch(ids) {
1740
1800
  if (ids.length === 0) return [];
@@ -1768,13 +1828,12 @@ var AniListClient = class {
1768
1828
  );
1769
1829
  return chunkResults.flat();
1770
1830
  }
1771
- // ── Cache management ──
1772
1831
  /** Clear the entire response cache. */
1773
1832
  async clearCache() {
1774
1833
  await this.cacheAdapter.clear();
1775
1834
  }
1776
1835
  /** Number of entries currently in the cache. */
1777
- get cacheSize() {
1836
+ async cacheSize() {
1778
1837
  return this.cacheAdapter.size;
1779
1838
  }
1780
1839
  /** Remove cache entries whose key matches the given pattern. */
@@ -1797,6 +1856,7 @@ var AniListClient = class {
1797
1856
  async destroy() {
1798
1857
  await this.cacheAdapter.clear();
1799
1858
  this.inFlight.clear();
1859
+ this.rateLimiter.dispose();
1800
1860
  }
1801
1861
  };
1802
1862