umedia 0.1.2 → 0.2.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 +72 -138
- package/bin/umedia.js +25 -51
- package/index.d.ts +59 -111
- package/index.js +45 -189
- package/lib/download.js +159 -0
- package/lib/errors.js +30 -0
- package/lib/providers/instagram.js +69 -0
- package/lib/providers/itunes.js +47 -0
- package/lib/providers/reddit.js +97 -0
- package/lib/providers/tiktok.js +165 -0
- package/lib/providers/x.js +71 -0
- package/lib/providers/youtube.js +146 -0
- package/lib/providers/ytdlp.js +97 -0
- package/lib/quality.js +49 -0
- package/lib/registry.js +123 -0
- package/lib/util.js +93 -0
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
# umedia
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**The standalone media engine — search, resolve and download, entirely on YOUR machine.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
No hosted API. No API keys. No rate limits. **No server bill.**
|
|
10
|
+
|
|
11
|
+
YouTube · TikTok · Instagram · Reddit · X · iTunes · 1800+ sites (via the optional yt-dlp tier)
|
|
10
12
|
|
|
11
13
|
[](https://www.npmjs.com/package/umedia)
|
|
12
14
|
[](https://nodejs.org)
|
|
13
|
-
[](#-zero-dependencies)
|
|
14
15
|
[](LICENSE)
|
|
15
16
|
|
|
16
17
|
</div>
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## 💸 Why this exists
|
|
22
|
+
|
|
23
|
+
Hosted media APIs cost money **every month** — per request, per user, per server.
|
|
24
|
+
`umedia` flips the model: the engine runs **inside your app**. Your users' machines do the work,
|
|
25
|
+
so your costs are… zero. Install it, call it, ship.
|
|
21
26
|
|
|
22
27
|
```bash
|
|
23
28
|
npm install umedia
|
|
@@ -26,139 +31,83 @@ npm install umedia
|
|
|
26
31
|
```js
|
|
27
32
|
import { UMedia } from "umedia";
|
|
28
33
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const { data } = await client.search({ q: "afrobeats", type: "music", limit: 3 });
|
|
32
|
-
for (const item of data.items) {
|
|
33
|
-
console.log(item.title, "—", item.previewKind, "→", item.previewUrl);
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
That's it. No SDK zoo, no six-service setup — one small client, the whole media web behind it.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 🎯 Why developers pick it
|
|
34
|
+
const media = new UMedia();
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
// 1) Search — music leads with REAL 30-second previews
|
|
37
|
+
const { data } = await media.search({ q: "afrobeats", type: "music", limit: 3 });
|
|
38
|
+
for (const item of data.items) console.log(item.title, "—", item.previewKind, "→", item.previewUrl);
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
// 2) Resolve a post → ALL media, in order
|
|
41
|
+
const { data: post } = await media.resolve("https://www.tiktok.com/@user/photo/7690630628697459990");
|
|
42
|
+
console.log(post.itemCount, post.counts); // 30 { images: 30, … } — never truncated silently
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
const { data:
|
|
49
|
-
//
|
|
50
|
-
const r = (await client.getDownload(handle.jobId)).data.result;
|
|
51
|
-
console.log(r.requestedQuality, "→", r.selectedQuality); // "best" → "240p" ← real, never invented
|
|
44
|
+
// 3) Download everything to disk
|
|
45
|
+
const { data: job } = await media.download({ url: post.sourceUrl, quality: "best", dir: "./out" });
|
|
46
|
+
console.log(job.requestedQuality, "→", job.selectedQuality); // "best" → "720p" — real, never invented
|
|
52
47
|
```
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### 🖼️ Galleries arrive complete — 100 photos means 100 photos
|
|
49
|
+
That's the whole model: **your code, your machine, your files.**
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
its sequence. If anything is dropped, it's named — `truncated: true`, `failedItems[]` — never silent.
|
|
60
|
-
|
|
61
|
-
### 🎧 Previews drop first
|
|
62
|
-
|
|
63
|
-
Music search leads with real **30-second clips** (`previewKind: "clip30s"`), movies with trailers,
|
|
64
|
-
social posts with official embeds. `previewKind: "none"` when nothing honest exists — we'd rather
|
|
65
|
-
say "none" than fake one.
|
|
51
|
+
---
|
|
66
52
|
|
|
67
|
-
|
|
53
|
+
## 🎯 The contract that keeps your app honest
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
await client.resolve(url);
|
|
74
|
-
} catch (e) {
|
|
75
|
-
if (e instanceof UMediaError) {
|
|
76
|
-
console.error(e.code, e.message, e.requestId); // e.g. PROVIDER_UNAVAILABLE, req_9f3a…
|
|
77
|
-
if (e.code === Codes.RATE_LIMITED) await sleep(60_000);
|
|
78
|
-
if (e.code === Codes.PROVIDER_UNAVAILABLE) await retryWithBackoff();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
```
|
|
55
|
+
### 🏷️ Quality is never manufactured
|
|
56
|
+
`requestedQuality` vs `selectedQuality` on every download. A 240p source never arrives wearing a
|
|
57
|
+
1080p sticker. `fallback: true` tells you exactly when reality differed from the request.
|
|
82
58
|
|
|
83
|
-
|
|
59
|
+
### 🖼️ Galleries arrive complete
|
|
60
|
+
100 photos → **100 files, in order** (`001 - …`, `002 - …`). Anything that failed is named in
|
|
61
|
+
`failedItems[]` with the reason — never silent, never partial-by-accident.
|
|
84
62
|
|
|
85
|
-
###
|
|
63
|
+
### 🎧 Previews drop first
|
|
64
|
+
Music search leads with real **30s iTunes clips** (`previewKind: "clip30s"`), movies with YouTube
|
|
65
|
+
trailers, social posts with official embeds. `previewKind: "none"` beats a fake preview.
|
|
86
66
|
|
|
87
|
-
|
|
67
|
+
### 🧯 Typed failures, always
|
|
68
|
+
`UMediaError` with a stable `code` (`RATE_LIMITED`, `MEDIA_NOT_FOUND`, `PROVIDER_UNAVAILABLE`,
|
|
69
|
+
`SSRF_BLOCKED`, …), plus `engine.attempts[]` — the real trail of which providers were tried and why.
|
|
88
70
|
|
|
89
71
|
---
|
|
90
72
|
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
### Resolve a post → every photo, every video, in order
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
const { data: post, engine } = await client.resolve(
|
|
97
|
-
"https://www.tiktok.com/@user/photo/7690630628697459990"
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
console.log(post.itemCount); // 30
|
|
101
|
-
console.log(post.counts); // { images: 30, videos: 0, audios: 0, other: 0 }
|
|
102
|
-
console.log(post.truncated); // false — we tell you, always
|
|
73
|
+
## 🗺️ What runs where (honest edition)
|
|
103
74
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
75
|
+
| Platform | Search | Resolve | Download | How |
|
|
76
|
+
|---|---|---|---|---|
|
|
77
|
+
| **YouTube** | ✅ | ✅ | ✅ * | InnerTube — search & metadata always; stream URLs depend on your network (see below) |
|
|
78
|
+
| **TikTok** | — | ✅ | ✅ | video posts = direct CDN mp4; photo galleries = every slide the platform publicly exposes, `truncated: true` when it degrades galleries |
|
|
79
|
+
| **Instagram** | — | ✅ best-effort | ✅ | public embeds (gated on some networks — the yt-dlp tier is the reliable path) |
|
|
80
|
+
| **Reddit** | — | ✅ best-effort | ✅ | public JSON across reddit edges (datacenter IPs often 403 — typed honest error) |
|
|
81
|
+
| **X** | — | ✅ best-effort | ✅ | public syndication (media login-gated on some networks — typed honest error) |
|
|
82
|
+
| **iTunes** | ✅ | — | ✅ previews | real 30s clips, rock solid |
|
|
83
|
+
| **1800+ sites** | — | ✅ | ✅ | optional tier: `pip install yt-dlp` — labeled `source: "ytdlp"` |
|
|
107
84
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
**\* About YouTube stream URLs:** YouTube bot-gates datacenter IPs and serves formats without URLs.
|
|
86
|
+
On typical home connections `download()` just works; on hard networks you'll get a typed
|
|
87
|
+
`PROVIDER_UNAVAILABLE` with the real reason — or run `download({ … })` via
|
|
88
|
+
`downloadWithYtdlp()` / `umedia download <url> --ytdlp` which handles PO tokens, cookies and muxing.
|
|
89
|
+
We'd rather tell you the truth than fake a download.
|
|
111
90
|
|
|
112
|
-
|
|
91
|
+
**Direct media URLs always work:** give `resolve()`/`download()` any direct `.mp4/.m4a/.jpg/…` link and it
|
|
92
|
+
goes straight to disk — no platform needed.
|
|
113
93
|
|
|
114
94
|
```js
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
type: "video",
|
|
118
|
-
quality: "best", // original | best | 2160p … 144p | audio
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// poll the job
|
|
122
|
-
let job;
|
|
123
|
-
do {
|
|
124
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
125
|
-
job = (await client.getDownload(handle.jobId)).data;
|
|
126
|
-
} while (!["done", "failed", "cancelled"].includes(job.status));
|
|
127
|
-
|
|
128
|
-
// honest outcome
|
|
129
|
-
const r = job.result;
|
|
130
|
-
console.log(r.requestedQuality, "→", r.selectedQuality, r.fallback ? "(fallback)" : "");
|
|
131
|
-
|
|
132
|
-
// stream or save
|
|
133
|
-
const res = await client.fetchFile(handle.jobId, 0);
|
|
134
|
-
// multi-item job? files come as an ordered zip: "001 - name.jpg", "002 - …"
|
|
135
|
-
for (const f of r.files) console.log(f.index, f.name, client.fileUrl(handle.jobId, f.index));
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Platform metadata — real, not marketing
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
const { data: caps } = await client.capabilities(); // live capability registry
|
|
142
|
-
const { data: st } = await client.status(); // real component + provider health
|
|
143
|
-
console.log(st.overall); // "operational" — checked, not hard-coded
|
|
95
|
+
// the power tier — one function, 1800+ sites, hard networks
|
|
96
|
+
await media.downloadWithYtdlp({ url: "https://example.com/anything", quality: "720p", dir: "./out" });
|
|
144
97
|
```
|
|
145
98
|
|
|
146
99
|
---
|
|
147
100
|
|
|
148
|
-
## 📚
|
|
149
|
-
|
|
150
|
-
|
|
|
151
|
-
|
|
152
|
-
| `search({q, type, limit, adult})` |
|
|
153
|
-
| `resolve(url)` |
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `fileUrl(jobId, index)` | `GET /api/v1/download/{id}/file?index=N` | single = file · multi = ordered zip |
|
|
159
|
-
| `fetchFile(jobId, index)` | same | returns a streamable `Response` |
|
|
160
|
-
| `capabilities()` | `GET /api/v1/capabilities` | live platform registry |
|
|
161
|
-
| `status()` | `GET /api/v1/status` | real health, refreshed continuously |
|
|
101
|
+
## 📚 API
|
|
102
|
+
|
|
103
|
+
| Call | What it does |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `media.search({q, type, limit, adult})` | Multi-source search — `type`: `video` \| `music` \| `movie` \| `short_video` |
|
|
106
|
+
| `media.resolve(url)` | Every media item of a post, in order + honest `engine.attempts` |
|
|
107
|
+
| `media.download({url, quality, dir, zip, onProgress})` | Saves files to disk — `quality`: `original` \| `best` \| `2160p` … `144p` \| `audio` |
|
|
108
|
+
| `media.downloadWithYtdlp({url, quality, dir})` | yt-dlp tier (requires `yt-dlp` on PATH) |
|
|
109
|
+
| `media.capabilities()` | What THIS machine can actually do — never overstated |
|
|
110
|
+
| `media.status()` | Real adapter status |
|
|
162
111
|
|
|
163
112
|
**Error codes** — `INVALID_REQUEST` · `INVALID_URL` · `SSRF_BLOCKED` · `AUTH_REQUIRED` ·
|
|
164
113
|
`AUTH_INVALID` · `RATE_LIMITED` · `MEDIA_NOT_FOUND` · `CONTENT_PRIVATE` · `ACCESS_RESTRICTED` ·
|
|
@@ -166,45 +115,30 @@ console.log(st.overall); // "operational" — checked, not hard-coded
|
|
|
166
115
|
|
|
167
116
|
---
|
|
168
117
|
|
|
169
|
-
## 🖥️
|
|
118
|
+
## 🖥️ CLI
|
|
170
119
|
|
|
171
120
|
```bash
|
|
172
121
|
npx umedia search "lofi beats" --type music --limit 5
|
|
173
122
|
npx umedia resolve "https://www.tiktok.com/@user/photo/7690630628697459990"
|
|
174
|
-
npx umedia download "https://youtu.be/jNQXAC9IVRw" --quality best
|
|
123
|
+
npx umedia download "https://youtu.be/jNQXAC9IVRw" --quality best -o ./out
|
|
124
|
+
npx umedia download "https://example.com/video" --ytdlp # power tier
|
|
175
125
|
npx umedia status
|
|
176
|
-
npx umedia capabilities
|
|
177
126
|
```
|
|
178
127
|
|
|
179
|
-
`--wait` polls the job and prints ready-to-use file URLs. Environment: `UMedia_API_KEY` (optional),
|
|
180
|
-
`UMedia_BASE_URL` (optional override).
|
|
181
|
-
|
|
182
128
|
---
|
|
183
129
|
|
|
184
|
-
##
|
|
185
|
-
|
|
186
|
-
Let's be straight about guarantees, because the industry isn't:
|
|
187
|
-
|
|
188
|
-
- **Our layer** (auth, search, resolve, downloads, files): designed and measured for high
|
|
189
|
-
availability — hundreds of requests per second sustained on this platform, 4-worker jobs,
|
|
190
|
-
typed errors with `requestId` on every failure. A correct integration **degrades gracefully;
|
|
191
|
-
it does not break**.
|
|
192
|
-
- **Their layer** (the upstream platforms): YouTube, TikTok and friends change, throttle and
|
|
193
|
-
geo-block. **No media API on earth can honestly promise zero upstream failures** — and this one
|
|
194
|
-
won't insult you by pretending.
|
|
195
|
-
- **What you get instead**: real data or a typed, honest error. Never fake quality, never silent
|
|
196
|
-
truncation, never a `200` with garbage. `requestedQuality` vs `selectedQuality`, `truncated`,
|
|
197
|
-
`failedItems[]`, `engine.attempts[]` — the truth is in every payload.
|
|
130
|
+
## 🧯 Safety, built in
|
|
198
131
|
|
|
199
|
-
|
|
200
|
-
|
|
132
|
+
- **SSRF hygiene** — private, loopback and internal hosts are refused (`SSRF_BLOCKED`).
|
|
133
|
+
- **No secrets to manage** — there are none. No keys, no tokens, no dashboards.
|
|
134
|
+
- **Zero telemetry.** Your users' requests never touch anyone's server — because there isn't one.
|
|
201
135
|
|
|
202
136
|
---
|
|
203
137
|
|
|
204
138
|
<div align="center">
|
|
205
139
|
|
|
206
|
-
|
|
140
|
+
**Built to save you the hosting bill.** Ship the engine, not the infrastructure.
|
|
207
141
|
|
|
208
|
-
MIT © 2026 · [Unified Media API](https://dev-pappy-api-downloader.duckdns.org)
|
|
142
|
+
MIT © 2026 · part of the [Unified Media API](https://dev-pappy-api-downloader.duckdns.org) project
|
|
209
143
|
|
|
210
144
|
</div>
|
package/bin/umedia.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* umedia CLI —
|
|
3
|
+
* umedia CLI — the standalone media engine, in your terminal.
|
|
4
4
|
*
|
|
5
5
|
* umedia search "afrobeats" --type music --limit 5
|
|
6
6
|
* umedia resolve "https://www.tiktok.com/@user/photo/123"
|
|
7
|
-
* umedia download "https://youtu.be/jNQXAC9IVRw" --quality best
|
|
7
|
+
* umedia download "https://youtu.be/jNQXAC9IVRw" --quality best -o ./out
|
|
8
8
|
* umedia status
|
|
9
9
|
* umedia capabilities
|
|
10
|
-
*
|
|
11
|
-
* Env: UMedia_API_KEY (optional), UMedia_BASE_URL (optional).
|
|
12
10
|
*/
|
|
13
11
|
import { UMedia, UMediaError } from "../index.js";
|
|
14
12
|
|
|
@@ -16,94 +14,70 @@ function parseArgs(argv) {
|
|
|
16
14
|
const out = { _: [] };
|
|
17
15
|
for (let i = 0; i < argv.length; i++) {
|
|
18
16
|
const a = argv[i];
|
|
19
|
-
if (a.startsWith("--")) {
|
|
20
|
-
const key = a.
|
|
17
|
+
if (a.startsWith("--") || a.startsWith("-")) {
|
|
18
|
+
const key = a.replace(/^-+/, "");
|
|
21
19
|
const next = argv[i + 1];
|
|
22
|
-
if (next !== undefined && !next.startsWith("
|
|
20
|
+
if (next !== undefined && !next.startsWith("-")) { out[key] = next; i++; }
|
|
23
21
|
else out[key] = true;
|
|
24
22
|
} else out._.push(a);
|
|
25
23
|
}
|
|
26
24
|
return out;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
const HELP = `umedia —
|
|
27
|
+
const HELP = `umedia — standalone media engine (runs on YOUR machine, no API needed)
|
|
30
28
|
|
|
31
|
-
umedia search "<query>" [--type video|music|movie|short_video] [--limit N]
|
|
29
|
+
umedia search "<query>" [--type video|music|movie|short_video] [--limit N]
|
|
32
30
|
umedia resolve <url>
|
|
33
|
-
umedia download <url> [--
|
|
31
|
+
umedia download <url> [--quality best|720p|audio] [-o <dir>] [--zip] [--ytdlp]
|
|
34
32
|
umedia status
|
|
35
|
-
umedia capabilities
|
|
36
|
-
|
|
37
|
-
Env: UMedia_API_KEY (optional key), UMedia_BASE_URL (optional)`;
|
|
33
|
+
umedia capabilities`;
|
|
38
34
|
|
|
39
35
|
const args = parseArgs(process.argv.slice(2));
|
|
40
36
|
const cmd = args._[0];
|
|
41
|
-
|
|
42
37
|
if (!cmd || cmd === "help" || args.help) {
|
|
43
38
|
console.log(HELP);
|
|
44
39
|
process.exit(0);
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
const client = new UMedia({
|
|
48
|
-
apiKey: process.env.UMedia_API_KEY,
|
|
49
|
-
baseUrl: process.env.UMedia_BASE_URL,
|
|
50
|
-
});
|
|
51
|
-
|
|
42
|
+
const client = new UMedia({ downloadDir: args.o || args.out || "./umedia-downloads" });
|
|
52
43
|
const print = (x) => console.log(JSON.stringify(x, null, 2));
|
|
53
44
|
|
|
54
45
|
async function main() {
|
|
55
46
|
switch (cmd) {
|
|
56
47
|
case "search": {
|
|
57
48
|
const q = args._.slice(1).join(" ");
|
|
58
|
-
if (!q) throw new UMediaError("INVALID_REQUEST",
|
|
59
|
-
const { data, meta } = await client.search({
|
|
60
|
-
q,
|
|
61
|
-
type: args.type || "video",
|
|
62
|
-
limit: args.limit ? Number(args.limit) : 12,
|
|
63
|
-
adult: Boolean(args.adult),
|
|
49
|
+
if (!q) throw new UMediaError("INVALID_REQUEST", 'usage: umedia search "<query>"');
|
|
50
|
+
const { data, meta, engine } = await client.search({
|
|
51
|
+
q, type: args.type || "video", limit: args.limit ? Number(args.limit) : 12, adult: Boolean(args.adult),
|
|
64
52
|
});
|
|
65
|
-
print({ items: data.items, meta });
|
|
53
|
+
print({ items: data.items, meta, engine });
|
|
66
54
|
break;
|
|
67
55
|
}
|
|
68
56
|
case "resolve": {
|
|
69
57
|
const url = args._[1];
|
|
70
58
|
if (!url) throw new UMediaError("INVALID_REQUEST", "usage: umedia resolve <url>");
|
|
71
|
-
const
|
|
72
|
-
print(
|
|
59
|
+
const r = await client.resolve(url);
|
|
60
|
+
print(r);
|
|
73
61
|
break;
|
|
74
62
|
}
|
|
75
63
|
case "download": {
|
|
76
64
|
const url = args._[1];
|
|
77
65
|
if (!url) throw new UMediaError("INVALID_REQUEST", "usage: umedia download <url>");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (;;) {
|
|
86
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
87
|
-
({ data } = await client.getDownload(jobId));
|
|
88
|
-
if (["done", "failed", "cancelled"].includes(data.status)) break;
|
|
89
|
-
process.stderr.write(` status: ${data.status}${data.stage ? " · " + data.stage : ""}\n`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const result = data.result || null;
|
|
93
|
-
if (result && result.files) {
|
|
94
|
-
result.files = result.files.map((f) => ({ ...f, url: client.fileUrl(jobId, f.index) }));
|
|
95
|
-
}
|
|
96
|
-
print({ ...data, result });
|
|
66
|
+
const onProgress = args.json ? undefined : (done, total) => {
|
|
67
|
+
if (total) process.stderr.write(`\r ${((done / total) * 100).toFixed(1)}%`);
|
|
68
|
+
};
|
|
69
|
+
const fn = args.ytdlp ? client.downloadWithYtdlp.bind(client) : client.download.bind(client);
|
|
70
|
+
const r = await fn({ url, quality: args.quality || "best", type: args.type || "video", zip: Boolean(args.zip), onProgress });
|
|
71
|
+
if (!args.json) process.stderr.write("\n");
|
|
72
|
+
print(r.data);
|
|
97
73
|
break;
|
|
98
74
|
}
|
|
99
75
|
case "status": {
|
|
100
|
-
|
|
101
|
-
print({ ...data, meta });
|
|
76
|
+
print((await client.status()).data);
|
|
102
77
|
break;
|
|
103
78
|
}
|
|
104
79
|
case "capabilities": {
|
|
105
|
-
|
|
106
|
-
print({ ...data, meta });
|
|
80
|
+
print((await client.capabilities()).data);
|
|
107
81
|
break;
|
|
108
82
|
}
|
|
109
83
|
default:
|