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/README.md +2 -2
- package/dist/index.d.mts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +101 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
24
|
-
if (
|
|
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",
|
|
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
|
|
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:
|
|
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) =>
|
|
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
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
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.
|
|
1341
|
-
endOfWeek.
|
|
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.
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|