wsper-js 0.1.0 → 0.1.1-wc2
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 +520 -1395
- package/dist/index.d.ts +1030 -14
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,1616 +1,741 @@
|
|
|
1
|
-
<img src="https://i.pinimg.com/736x/9a/d3/8c/9ad38cbf49e39e599102b3021b2e88d5.jpg" alt="" align="center" width="1000">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# wsper
|
|
5
|
-
|
|
6
|
-
library berikut saya buat untuk mempermudah kalian membuat sebuah bot whatsapp, dengan ini anda tidak perlu lagi untuk menambah code yang tidak jelas dan sering error awookawoka
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Daftar Isi
|
|
11
|
-
|
|
12
|
-
- [Instalasi](#instalasi)
|
|
13
|
-
- [Quick Start](#quick-start)
|
|
14
|
-
- [WsperScraper — Kelas Utama](#wsperscraper--kelas-utama)
|
|
15
|
-
- [Response Structure](#response-structure)
|
|
16
|
-
- [Scrapers](#scrapers)
|
|
17
|
-
- [Instagram](#instagram)
|
|
18
|
-
- [Spotify](#spotify)
|
|
19
|
-
- [YouTube](#youtube)
|
|
20
|
-
- [Threads](#threads)
|
|
21
|
-
- [Twitter / X](#twitter--x)
|
|
22
|
-
- [Pinterest](#pinterest)
|
|
23
|
-
- [BiliBili](#bilibili)
|
|
24
|
-
- [CapCut](#capcut)
|
|
25
|
-
- [Komikindo](#komikindo)
|
|
26
|
-
- [Mediafire](#mediafire)
|
|
27
|
-
- [Play Store](#play-store)
|
|
28
|
-
- [Minecraft Addon](#minecraft-addon)
|
|
29
|
-
- [Alkitab](#alkitab)
|
|
30
|
-
- [BMKG](#bmkg)
|
|
31
|
-
- [Cuaca](#cuaca)
|
|
32
|
-
- [Drakor](#drakor)
|
|
33
|
-
- [Dramabox](#dramabox)
|
|
34
|
-
- [SakuraNovel](#sakuranovel)
|
|
35
|
-
- [Uguu](#uguu)
|
|
36
|
-
- [HokInfo](#hokinfo)
|
|
37
|
-
- [IkiruManga](#ikirumanga)
|
|
38
|
-
- [Wallpaper](#wallpaper)
|
|
39
|
-
- [WwChar](#wwchar)
|
|
40
|
-
- [Videy](#videy)
|
|
41
|
-
- [OCR](#ocr)
|
|
42
|
-
- [Webp2Mp4](#webp2mp4)
|
|
43
|
-
- [MConverter](#mconverter)
|
|
44
|
-
- [HtmlToJpg](#htmltojpg)
|
|
45
|
-
- [Stalk](#stalk)
|
|
46
|
-
- [ModAndroid](#modandroid)
|
|
47
|
-
- [Upscaler](#upscaler)
|
|
48
|
-
- [Image](#image)
|
|
49
|
-
- [ImgUpscaler](#imgupscaler)
|
|
50
|
-
- [Faceswap](#faceswap)
|
|
51
|
-
- [PhotoAi](#photoai)
|
|
52
|
-
- [Lyrics](#lyrics)
|
|
53
|
-
- [Resep](#resep)
|
|
54
|
-
- [Top Anime](#top-anime)
|
|
55
|
-
- [Anime Quote](#anime-quote)
|
|
56
|
-
- [Anime Random](#anime-random)
|
|
57
|
-
- [Brat Generator](#brat-generator)
|
|
58
|
-
- [generate — Image](#generate--image)
|
|
59
|
-
- [generate — GIF](#generate--gif)
|
|
60
|
-
- [generate — Video](#generate--video)
|
|
61
|
-
- [withPreset](#withpreset)
|
|
62
|
-
- [imageToSticker](#imagetosticker)
|
|
63
|
-
- [MP4 ↔ GIF Conversion](#mp4--gif-conversion)
|
|
64
|
-
- [convertImage](#convertimage)
|
|
65
|
-
- [Background Config](#background-config)
|
|
66
|
-
- [Text Config](#text-config)
|
|
67
|
-
- [Canvas Config](#canvas-config)
|
|
68
|
-
- [Animation Config](#animation-config)
|
|
69
|
-
- [Output Config](#output-config)
|
|
70
|
-
- [Chart Image Generator](#chart-image-generator)
|
|
71
|
-
- [Credentials](#credentials)
|
|
72
|
-
- [HTTP & Queue Options](#http--queue-options)
|
|
73
|
-
- [Download Outputs](#download-outputs)
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Instalasi
|
|
78
1
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
2
|
+
<h1 align="center">
|
|
3
|
+
<img alt="ShikanokoBail banner" src="https://i.pinimg.com/736x/0c/ff/62/0cff624a04a81495f4b8e69bcedd34aa.jpg" width="100%"/>
|
|
4
|
+
</h1>
|
|
82
5
|
|
|
83
|
-
|
|
6
|
+
<div align="center">
|
|
84
7
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
8
|
+
[](https://www.npmjs.com/package/wsper-js)
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
</div>
|
|
89
14
|
|
|
90
|
-
|
|
15
|
+
# wsper-js
|
|
16
|
+
`wsper-js` is a TypeScript-first scraper toolkit for building reusable, typed, and testable scrapers across multiple public platforms. It provides platform-specific scraper classes, shared HTTP and queue primitives, safe downloader utilities, parser helpers, media utilities, and runnable examples for controlled and ethical data access.
|
|
91
17
|
|
|
92
|
-
|
|
18
|
+
The package is ESM-only and targets Node.js 18 or newer.
|
|
93
19
|
|
|
94
|
-
|
|
95
|
-
import { WsperScraper, BratGenerator, ChartGenerator } from "wsper";
|
|
20
|
+
## Features
|
|
96
21
|
|
|
97
|
-
|
|
98
|
-
|
|
22
|
+
- Modular scraper architecture under `src/scrapers/`.
|
|
23
|
+
- TypeScript strict mode with exported response, option, and scraper data types.
|
|
24
|
+
- Standard `WsperResponse<T>` shape for scraper results.
|
|
25
|
+
- Shared HTTP client with timeouts, bounded retries, redirects, URL safety checks, browser-compatible public headers, and optional request queue pacing.
|
|
26
|
+
- Unit-tested scraper behavior and parser behavior.
|
|
27
|
+
- `examples/alllexamp.ts` runner for direct scraper demos and subprocess example checks.
|
|
28
|
+
- Local mock server support for testing WordPress-style endpoints, AI tools, and Fandom API responses without depending on live external services.
|
|
29
|
+
- Public API integrations where available, including LRCLIB, Wallhaven, Fandom MediaWiki, Spotify Web API, BMKG, npm registry, and BiliBili APIs.
|
|
30
|
+
- Cookie or credential support for platforms that legitimately require authenticated sessions, including Instagram, Twitter/X, Threads, Pinterest, BiliBili, and Spotify credentials.
|
|
31
|
+
- Additional modules for downloads, Brat image/GIF/video generation, and analytics chart image generation.
|
|
99
32
|
|
|
100
|
-
|
|
101
|
-
const track = await wsper.spotify.getTrack("https://open.spotify.com/track/...");
|
|
102
|
-
const video = await wsper.youtube.getVideo("https://youtu.be/...");
|
|
33
|
+
## Installation
|
|
103
34
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const result = await brat.generate({
|
|
107
|
-
text: { value: "brat summer" },
|
|
108
|
-
background: { type: "solid", color: "#8ace00" },
|
|
109
|
-
output: { type: "image", format: "png", path: "./brat.png" },
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Chart Generator
|
|
113
|
-
const chart = new ChartGenerator();
|
|
114
|
-
await chart.generateStatsImage({
|
|
115
|
-
model: "modern-dashboard",
|
|
116
|
-
output: "./analytics.png",
|
|
117
|
-
});
|
|
35
|
+
```bash
|
|
36
|
+
npm install wsper-js
|
|
118
37
|
```
|
|
119
38
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
## WsperScraper — Kelas Utama
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { WsperScraper } from "wsper";
|
|
126
|
-
|
|
127
|
-
const wsper = new WsperScraper(config?: WsperScraperConfig);
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add wsper-js
|
|
128
41
|
```
|
|
129
42
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
interface WsperScraperConfig {
|
|
134
|
-
debug?: boolean;
|
|
135
|
-
spotifyCredentials?: {
|
|
136
|
-
clientId: string;
|
|
137
|
-
clientSecret: string;
|
|
138
|
-
callbackUrl?: string;
|
|
139
|
-
market?: string;
|
|
140
|
-
};
|
|
141
|
-
credentials?: {
|
|
142
|
-
instagram?: { cookie?: string; csrfToken?: string };
|
|
143
|
-
threads?: { cookie?: string; csrfToken?: string };
|
|
144
|
-
twitter?: { cookie?: string; csrfToken?: string };
|
|
145
|
-
pinterest?: { cookie?: string; csrfToken?: string };
|
|
146
|
-
};
|
|
147
|
-
http?: HttpOptions;
|
|
148
|
-
queue?: QueueOptions;
|
|
149
|
-
youtube?: YouTubeScraperOptions;
|
|
150
|
-
}
|
|
43
|
+
```bash
|
|
44
|
+
yarn add wsper-js
|
|
151
45
|
```
|
|
152
46
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
| Property | Type | Deskripsi |
|
|
156
|
-
|---|---|---|
|
|
157
|
-
| `wsper.instagram` | `InstagramScraper` | Instagram scraper |
|
|
158
|
-
| `wsper.spotify` | `SpotifyScraper` | Spotify scraper |
|
|
159
|
-
| `wsper.youtube` | `YouTubeScraper` | YouTube scraper |
|
|
160
|
-
| `wsper.threads` | `ThreadsScraper` | Threads scraper |
|
|
161
|
-
| `wsper.twitter` | `TwitterScraper` | Twitter/X scraper |
|
|
162
|
-
| `wsper.pinterest` | `PinterestScraper` | Pinterest scraper |
|
|
47
|
+
Some optional capabilities need external binaries:
|
|
163
48
|
|
|
164
|
-
|
|
49
|
+
| Feature | External tool |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| YouTube and Spotify media enrichment/download helpers | `yt-dlp` |
|
|
52
|
+
| Video export and GIF/MP4 conversion | `ffmpeg` in `PATH` or configured path |
|
|
165
53
|
|
|
166
|
-
##
|
|
167
|
-
|
|
168
|
-
Semua scraper mengembalikan `WsperResponse<T>`:
|
|
54
|
+
## Quick Start
|
|
169
55
|
|
|
170
56
|
```ts
|
|
171
|
-
|
|
172
|
-
ok: boolean;
|
|
173
|
-
data: T | null;
|
|
174
|
-
error: { code: string; message: string; details?: Record<string, unknown> } | null;
|
|
175
|
-
meta: Meta & {
|
|
176
|
-
statusCode: number;
|
|
177
|
-
sourceUrl: string;
|
|
178
|
-
fetchedAt: string; // ISO timestamp
|
|
179
|
-
durationMs: number;
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
**Pattern penggunaan:**
|
|
57
|
+
import { LyricsScraper } from "wsper-js";
|
|
185
58
|
|
|
186
|
-
|
|
187
|
-
const
|
|
59
|
+
const lyrics = new LyricsScraper();
|
|
60
|
+
const result = await lyrics.search("after hours the weeknd");
|
|
188
61
|
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
console.log(res.meta.durationMs, "ms");
|
|
192
|
-
} else {
|
|
193
|
-
console.error(res.error?.code, res.error?.message);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
throw new Error(`${result.error?.code}: ${result.error?.message}`);
|
|
194
64
|
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
---
|
|
198
65
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
### Instagram
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
import { InstagramScraper } from "wsper";
|
|
207
|
-
const ig = new InstagramScraper(options?: InstagramScraperOptions);
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
#### `getProfile(username)` → `WsperResponse<InstagramProfile>`
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
await ig.getProfile("charli_xcx")
|
|
66
|
+
console.log(result.statusCode);
|
|
67
|
+
console.log(result.data?.title);
|
|
68
|
+
console.log(result.data?.lyrics);
|
|
214
69
|
```
|
|
215
70
|
|
|
216
|
-
|
|
217
|
-
interface InstagramProfile {
|
|
218
|
-
id: string;
|
|
219
|
-
username: string;
|
|
220
|
-
fullName: string;
|
|
221
|
-
biography: string;
|
|
222
|
-
profilePicUrl: string;
|
|
223
|
-
profilePicUrlHd: string | null;
|
|
224
|
-
followersCount: number;
|
|
225
|
-
followingCount: number;
|
|
226
|
-
mediaCount: number;
|
|
227
|
-
isPrivate: boolean;
|
|
228
|
-
isVerified: boolean;
|
|
229
|
-
externalUrl: string | null;
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
#### `getFeed(username, options?)` → `WsperResponse<InstagramFeed>`
|
|
71
|
+
You can also use `WsperScraper` for the aggregate social/media scraper entrypoint:
|
|
234
72
|
|
|
235
73
|
```ts
|
|
236
|
-
|
|
237
|
-
```
|
|
74
|
+
import { WsperScraper } from "wsper-js";
|
|
238
75
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
nextMaxId: string | null;
|
|
243
|
-
hasMore: boolean;
|
|
244
|
-
}
|
|
76
|
+
const wsper = new WsperScraper({
|
|
77
|
+
queue: { concurrency: 1, minDelayMs: 500, maxDelayMs: 1500 },
|
|
78
|
+
});
|
|
245
79
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
shortcode: string;
|
|
249
|
-
mediaType: number; // 1=image 2=video 8=carousel
|
|
250
|
-
productType: string | null;
|
|
251
|
-
caption: string | null;
|
|
252
|
-
takenAt: number | null;
|
|
253
|
-
likeCount: number | null;
|
|
254
|
-
commentCount: number | null;
|
|
255
|
-
thumbnailUrl: string | null;
|
|
256
|
-
videoUrl: string | null;
|
|
257
|
-
carousel: InstagramCarouselItem[] | null;
|
|
258
|
-
}
|
|
80
|
+
const track = await wsper.spotify.search("never gonna give you up", { limit: 3 });
|
|
81
|
+
const video = await wsper.youtube.getVideo("dQw4w9WgXcQ");
|
|
259
82
|
|
|
260
|
-
|
|
261
|
-
id: string;
|
|
262
|
-
mediaType: number;
|
|
263
|
-
thumbnailUrl: string | null;
|
|
264
|
-
videoUrl: string | null;
|
|
265
|
-
}
|
|
83
|
+
console.log(track.ok, video.ok);
|
|
266
84
|
```
|
|
267
85
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
await ig.getPost("https://www.instagram.com/p/AbCdEfG/")
|
|
272
|
-
await ig.getPost("AbCdEfG") // shortcode langsung
|
|
273
|
-
```
|
|
86
|
+
`WsperScraper` currently exposes `spotify`, `twitter`, `threads`, `instagram`, `pinterest`, `youtube`, and `cai`. Other scrapers are exported as named classes.
|
|
274
87
|
|
|
275
|
-
|
|
88
|
+
## Response Shape
|
|
276
89
|
|
|
277
|
-
|
|
278
|
-
await ig.getProfileWithFeed("charli_xcx", { count: 6 })
|
|
279
|
-
```
|
|
90
|
+
Scraper methods return `WsperResponse<TData>`:
|
|
280
91
|
|
|
281
92
|
```ts
|
|
282
|
-
interface
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
93
|
+
export interface WsperResponse<TData, TMeta extends WsperResponseMeta = WsperResponseMeta> {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
statusCode: number;
|
|
96
|
+
data: TData | null;
|
|
97
|
+
error: {
|
|
98
|
+
code: string;
|
|
99
|
+
message: string;
|
|
100
|
+
details?: Record<string, unknown>;
|
|
101
|
+
} | null;
|
|
102
|
+
meta: TMeta;
|
|
287
103
|
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
#### `downloadPost(input)` → `WsperResponse<InstagramDownloadResult>`
|
|
291
|
-
|
|
292
|
-
```ts
|
|
293
|
-
await ig.downloadPost({
|
|
294
|
-
postUrl: "https://www.instagram.com/p/AbCdEfG/",
|
|
295
|
-
outputDir: "./downloads/instagram",
|
|
296
|
-
includeMetadata: true,
|
|
297
|
-
})
|
|
298
|
-
```
|
|
299
104
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
sourceUrl:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
assets: Array<{
|
|
306
|
-
url: string;
|
|
307
|
-
outputPath: string;
|
|
308
|
-
type: "image" | "video";
|
|
309
|
-
index: number;
|
|
310
|
-
}>;
|
|
105
|
+
export interface WsperResponseMeta {
|
|
106
|
+
statusCode: number;
|
|
107
|
+
sourceUrl: string;
|
|
108
|
+
fetchedAt: string;
|
|
109
|
+
durationMs: number;
|
|
311
110
|
}
|
|
312
111
|
```
|
|
313
112
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
await ig.downloadProfile({
|
|
318
|
-
usernameOrUrl: "charli_xcx",
|
|
319
|
-
outputDir: "./downloads/instagram",
|
|
320
|
-
feedCount: 9,
|
|
321
|
-
includeProfilePicture: true,
|
|
322
|
-
includeInitialPosts: true,
|
|
323
|
-
includeMetadata: true,
|
|
324
|
-
})
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
### Spotify
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
import { SpotifyScraper } from "wsper";
|
|
333
|
-
const spotify = new SpotifyScraper(options?: SpotifyScraperOptions);
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
#### `getTrack(input, options?)` → `WsperResponse<NormalizedSpotifyTrack, SpotifyTrackMeta>`
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
await spotify.getTrack("https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh")
|
|
340
|
-
await spotify.getTrack("4iV5W9uYEdYUVa79Axb7Rh", { enrichYtDlp: true })
|
|
341
|
-
```
|
|
113
|
+
Recommended handling:
|
|
342
114
|
|
|
343
115
|
```ts
|
|
344
|
-
|
|
345
|
-
id: string;
|
|
346
|
-
name: string;
|
|
347
|
-
uri: string;
|
|
348
|
-
url: string | null;
|
|
349
|
-
durationMs: number;
|
|
350
|
-
durationString: string | null;
|
|
351
|
-
explicit: boolean;
|
|
352
|
-
trackNumber: number | null;
|
|
353
|
-
discNumber: number | null;
|
|
354
|
-
popularity: number | null;
|
|
355
|
-
isPlayable: boolean | null;
|
|
356
|
-
previewUrl: string | null;
|
|
357
|
-
artists: NormalizedSpotifyArtist[];
|
|
358
|
-
album: NormalizedSpotifyAlbum;
|
|
359
|
-
externalIds: Record<string, string>;
|
|
360
|
-
externalSource: ExternalSource | null; // yt-dlp enrichment (jika enrichYtDlp: true)
|
|
361
|
-
download: DownloadInfo | null;
|
|
362
|
-
}
|
|
116
|
+
const response = await scraper.search("query");
|
|
363
117
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
118
|
+
if (response.ok && response.data !== null) {
|
|
119
|
+
console.log(response.data);
|
|
120
|
+
} else {
|
|
121
|
+
console.error(response.statusCode, response.error?.code, response.error?.message);
|
|
367
122
|
}
|
|
368
123
|
```
|
|
369
124
|
|
|
370
|
-
|
|
125
|
+
## Usage Examples
|
|
371
126
|
|
|
372
|
-
|
|
373
|
-
await spotify.getAlbum("https://open.spotify.com/album/...")
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
```ts
|
|
377
|
-
interface SpotifyAlbumWithTracks {
|
|
378
|
-
album: NormalizedSpotifyAlbum;
|
|
379
|
-
tracks: NormalizedSpotifyTrack[];
|
|
380
|
-
}
|
|
127
|
+
The examples below use public exports from `src/index.ts` and representative response fields from implementation and tests.
|
|
381
128
|
|
|
382
|
-
|
|
383
|
-
id: string;
|
|
384
|
-
name: string;
|
|
385
|
-
albumType: string | null;
|
|
386
|
-
uri: string;
|
|
387
|
-
url: string | null;
|
|
388
|
-
releaseDate: string | null;
|
|
389
|
-
totalTracks: number | null;
|
|
390
|
-
images: SpotifyImage[];
|
|
391
|
-
artists: NormalizedSpotifyArtist[];
|
|
392
|
-
copyrights: Array<{ text: string; type: string }>;
|
|
393
|
-
label: string | null;
|
|
394
|
-
popularity: number | null;
|
|
395
|
-
}
|
|
396
|
-
```
|
|
129
|
+
### Public-Link Examples
|
|
397
130
|
|
|
398
|
-
|
|
131
|
+
Some scrapers need live public targets. The bundled examples avoid dummy URLs:
|
|
399
132
|
|
|
400
|
-
```
|
|
401
|
-
|
|
133
|
+
```bash
|
|
134
|
+
npx tsx examples/gdrive.example.ts
|
|
135
|
+
npx tsx examples/foreign-news.example.ts
|
|
136
|
+
npx tsx examples/pubgmobile.example.ts
|
|
137
|
+
npx tsx examples/soundcloud.example.ts
|
|
402
138
|
```
|
|
403
139
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
await spotify.search("charli xcx brat", { limit: 10, type: ["track", "album"] })
|
|
408
|
-
```
|
|
140
|
+
`GDriveScraper` defaults to a verified public Google Drive PDF in the example. `ForeignNewsScraper` uses the BBC World RSS feed. `PubgMobileScraper` uses the official server-rendered announcement list because the public `news.shtml` page is a client-rendered shell. Scrapers that commonly require a valid session or cookie, such as TeraBox, Pixiv, and Xiaohongshu, require an explicit URL/ID in their examples instead of using placeholder links.
|
|
409
141
|
|
|
410
|
-
|
|
142
|
+
When a live endpoint returns HTTP 200 but no parseable data, these scrapers return `ok: false` with a parser error code instead of returning `ok: true` with empty data.
|
|
411
143
|
|
|
412
|
-
|
|
413
|
-
await spotify.downloadPost({
|
|
414
|
-
trackUrl: "https://open.spotify.com/track/...",
|
|
415
|
-
outputDir: "./downloads/spotify",
|
|
416
|
-
format: "mp3",
|
|
417
|
-
})
|
|
418
|
-
// → WsperResponse<SpotifyDownloadResult>
|
|
419
|
-
```
|
|
144
|
+
### LyricsScraper
|
|
420
145
|
|
|
421
|
-
|
|
146
|
+
Source: LRCLIB JSON API at `https://lrclib.net/api/search`.
|
|
422
147
|
|
|
423
148
|
```ts
|
|
424
|
-
|
|
425
|
-
// → { state: string; url: string }
|
|
426
|
-
|
|
427
|
-
await spotify.exchangeCode(code) // → SpotifyOAuthToken
|
|
428
|
-
await spotify.refreshToken(token) // → SpotifyOAuthToken
|
|
429
|
-
await spotify.getAccessToken() // → string
|
|
430
|
-
spotify.invalidateToken() // → void
|
|
431
|
-
```
|
|
149
|
+
import { LyricsScraper } from "wsper-js";
|
|
432
150
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
### YouTube
|
|
436
|
-
|
|
437
|
-
```ts
|
|
438
|
-
import { YouTubeScraper } from "wsper";
|
|
439
|
-
const yt = new YouTubeScraper(options?: YouTubeScraperOptions);
|
|
440
|
-
```
|
|
151
|
+
const scraper = new LyricsScraper();
|
|
152
|
+
const result = await scraper.search("after hours the weeknd");
|
|
441
153
|
|
|
442
|
-
|
|
443
|
-
interface YouTubeScraperOptions {
|
|
444
|
-
debug?: boolean;
|
|
445
|
-
ytdlpPath?: string; // path ke yt-dlp binary
|
|
446
|
-
ffmpegPath?: string;
|
|
447
|
-
ffprobePath?: string;
|
|
448
|
-
outputDir?: string;
|
|
449
|
-
searchLimit?: number;
|
|
450
|
-
}
|
|
154
|
+
console.log(result.data);
|
|
451
155
|
```
|
|
452
156
|
|
|
453
|
-
|
|
157
|
+
Representative output:
|
|
454
158
|
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
interface YoutubeVideoMetadata {
|
|
461
|
-
provider: "youtube";
|
|
462
|
-
id: string;
|
|
463
|
-
title: string;
|
|
464
|
-
url: string;
|
|
465
|
-
durationSeconds: number | null;
|
|
466
|
-
durationString: string | null;
|
|
467
|
-
isLive: boolean;
|
|
468
|
-
uploadDate: string | null;
|
|
469
|
-
description: string | null;
|
|
470
|
-
thumbnail: string | null;
|
|
471
|
-
thumbnails: Array<{ url: string; width: number | null; height: number | null }>;
|
|
472
|
-
channel: {
|
|
473
|
-
id: string | null;
|
|
474
|
-
name: string | null;
|
|
475
|
-
url: string | null;
|
|
476
|
-
handle: string | null;
|
|
477
|
-
isVerified: boolean | null;
|
|
478
|
-
followerCount: number | null;
|
|
479
|
-
};
|
|
480
|
-
stats: {
|
|
481
|
-
viewCount: number | null;
|
|
482
|
-
likeCount: number | null;
|
|
483
|
-
commentCount: number | null;
|
|
484
|
-
};
|
|
485
|
-
formats: {
|
|
486
|
-
audio: YoutubeAudioFormat[];
|
|
487
|
-
video: YoutubeVideoFormat[];
|
|
488
|
-
};
|
|
489
|
-
download: YoutubeDownloadInfo;
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"title": "After Hours",
|
|
162
|
+
"lyrics": "Oh, baby, where are you now?",
|
|
163
|
+
"link": "https://lrclib.net/api/get/98765"
|
|
490
164
|
}
|
|
491
165
|
```
|
|
492
166
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
```ts
|
|
496
|
-
await yt.searchVideos("charli xcx brat", { limit: 5 })
|
|
497
|
-
// data: { query, total, items: YoutubeVideoMetadata[] }
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
#### `downloadVideo(input, options?)` → `WsperResponse<YoutubeDownloadResult>`
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
await yt.downloadVideo("https://youtu.be/...", {
|
|
504
|
-
outputDir: "./downloads",
|
|
505
|
-
fileName: "video",
|
|
506
|
-
format: "mp4",
|
|
507
|
-
})
|
|
508
|
-
// data: { sourceUrl, outputDir, format, type: "video", command }
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
#### `downloadAudio(input, options?)` → `WsperResponse<YoutubeDownloadResult>`
|
|
512
|
-
|
|
513
|
-
```ts
|
|
514
|
-
await yt.downloadAudio("https://youtu.be/...", {
|
|
515
|
-
outputDir: "./downloads",
|
|
516
|
-
audioFormat: "mp3", // "mp3" | "m4a" | "opus" | "flac" | "wav"
|
|
517
|
-
})
|
|
518
|
-
// data: { sourceUrl, outputDir, format, type: "audio", command }
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
#### `extractAudio(inputPath, options?)` → `WsperResponse<YoutubeAudioExtractResult>`
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
await yt.extractAudio("./video.mp4", {
|
|
525
|
-
outputPath: "./audio.mp3",
|
|
526
|
-
audioFormat: "mp3",
|
|
527
|
-
audioBitrate: "192k",
|
|
528
|
-
})
|
|
529
|
-
// data: { inputPath, outputPath, format, bitrate }
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
#### `getPlaylist(input)` → `WsperResponse<YoutubePlaylistData>`
|
|
533
|
-
|
|
534
|
-
```ts
|
|
535
|
-
await yt.getPlaylist("https://www.youtube.com/playlist?list=...")
|
|
536
|
-
// data: { id, title, url, uploader, channel, entriesTotal, entries[] }
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
#### `getChannel(input)` → `WsperResponse<YoutubeChannelData>`
|
|
540
|
-
|
|
541
|
-
```ts
|
|
542
|
-
await yt.getChannel("https://www.youtube.com/@channelname")
|
|
543
|
-
// data: { id, title, channel, channelId, entriesTotal, entries[] }
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
---
|
|
547
|
-
|
|
548
|
-
### Threads
|
|
549
|
-
|
|
550
|
-
```ts
|
|
551
|
-
import { ThreadsScraper } from "wsper";
|
|
552
|
-
const threads = new ThreadsScraper(options?: ScraperOptions);
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
#### `getProfile(usernameOrUrl)` → `WsperResponse<ThreadsProfile>`
|
|
556
|
-
|
|
557
|
-
```ts
|
|
558
|
-
await threads.getProfile("zuck")
|
|
559
|
-
await threads.getProfile("https://www.threads.net/@zuck")
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
#### `getPost(input)` → `WsperResponse<ThreadsPost>`
|
|
563
|
-
|
|
564
|
-
```ts
|
|
565
|
-
await threads.getPost("https://www.threads.net/@user/post/CgXxXxXxX")
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
#### Search methods → `WsperResponse<ThreadsSearchResult>`
|
|
569
|
-
|
|
570
|
-
```ts
|
|
571
|
-
await threads.search("brat summer")
|
|
572
|
-
await threads.searchByTag("bratstyle")
|
|
573
|
-
await threads.searchUsers("charli")
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
#### `downloadPost(input)` / `downloadProfile(input)` → `WsperResponse<ThreadsDownloadResult>`
|
|
577
|
-
|
|
578
|
-
```ts
|
|
579
|
-
await threads.downloadPost({
|
|
580
|
-
postUrl: "https://www.threads.net/@user/post/...",
|
|
581
|
-
outputDir: "./downloads/threads",
|
|
582
|
-
})
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
---
|
|
586
|
-
|
|
587
|
-
### Twitter / X
|
|
588
|
-
|
|
589
|
-
```ts
|
|
590
|
-
import { TwitterScraper } from "wsper";
|
|
591
|
-
const twitter = new TwitterScraper(options?: ScraperOptions);
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
> Sebagian besar endpoint membutuhkan `cookie` + `csrfToken` karena API Twitter memerlukan auth.
|
|
595
|
-
|
|
596
|
-
#### `getTweet(input)` → `WsperResponse<TwitterTweetDetail>`
|
|
597
|
-
|
|
598
|
-
```ts
|
|
599
|
-
await twitter.getTweet({ tweetId: "1234567890" })
|
|
600
|
-
await twitter.getTweet({ url: "https://x.com/user/status/1234567890" })
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
#### `searchTweets(query, options?)` → `WsperResponse<TwitterSearchResult>`
|
|
604
|
-
|
|
605
|
-
```ts
|
|
606
|
-
await twitter.searchTweets("brat summer", { limit: 20 })
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
#### Other search methods
|
|
610
|
-
|
|
611
|
-
```ts
|
|
612
|
-
await twitter.searchByTag("bratstyle") // → WsperResponse<TwitterSearchResult>
|
|
613
|
-
await twitter.searchTrend("brat") // → WsperResponse<TwitterSearchResult>
|
|
614
|
-
await twitter.searchUsers("charli xcx") // → WsperResponse<TwitterUserSearchResult>
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
#### `getProfile(usernameOrUrl)` → `WsperResponse<TwitterUser>`
|
|
618
|
-
|
|
619
|
-
```ts
|
|
620
|
-
await twitter.getProfile("charli_xcx")
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
---
|
|
624
|
-
|
|
625
|
-
### Pinterest
|
|
626
|
-
|
|
627
|
-
```ts
|
|
628
|
-
import { PinterestScraper } from "wsper";
|
|
629
|
-
const pinterest = new PinterestScraper(options?: ScraperOptions);
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
#### `getPin(input)` → `WsperResponse<PinterestPin>`
|
|
633
|
-
|
|
634
|
-
```ts
|
|
635
|
-
await pinterest.getPin("https://www.pinterest.com/pin/1234567890/")
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
#### `searchPins(query, options?)` → `WsperResponse<PinterestSearchResult>`
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
await pinterest.searchPins("brat aesthetic", { limit: 20 })
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
#### `downloadPin(input)` → `WsperResponse<PinterestDownloadResult>`
|
|
645
|
-
|
|
646
|
-
```ts
|
|
647
|
-
await pinterest.downloadPin({
|
|
648
|
-
pinUrl: "https://www.pinterest.com/pin/...",
|
|
649
|
-
outputDir: "./downloads/pinterest",
|
|
650
|
-
})
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
---
|
|
654
|
-
|
|
655
|
-
### BiliBili
|
|
656
|
-
|
|
657
|
-
```ts
|
|
658
|
-
import { BiliBiliScraper } from "wsper";
|
|
659
|
-
const bili = new BiliBiliScraper();
|
|
660
|
-
|
|
661
|
-
await bili.getVideoInfo("https://www.bilibili.com/video/BV...")
|
|
662
|
-
// → WsperResponse<BiliBiliResult>
|
|
663
|
-
// data: { title, cover, duration, author, views, plays, streams[] }
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
### CapCut
|
|
669
|
-
|
|
670
|
-
```ts
|
|
671
|
-
import { CapCutScraper } from "wsper";
|
|
672
|
-
const capcut = new CapCutScraper();
|
|
673
|
-
|
|
674
|
-
await capcut.download("https://www.capcut.com/t/...")
|
|
675
|
-
// → WsperResponse<CapCutDownloadResult>
|
|
676
|
-
// data: { title, coverUrl, videoUrl, authorName }
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
---
|
|
680
|
-
|
|
681
|
-
### Komikindo
|
|
682
|
-
|
|
683
|
-
```ts
|
|
684
|
-
import { KomikindoScraper } from "wsper";
|
|
685
|
-
const komik = new KomikindoScraper();
|
|
686
|
-
|
|
687
|
-
await komik.search("one piece")
|
|
688
|
-
// → WsperResponse<KomikindoSearchItem[]>
|
|
689
|
-
// data[]: { title, url, cover, type, status, rating, latestChapter }
|
|
690
|
-
|
|
691
|
-
await komik.getDetail("https://komikindo.tv/manga/...")
|
|
692
|
-
// → WsperResponse<KomikindoDetail>
|
|
693
|
-
// data: { title, cover, synopsis, chapters[], genres[] }
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
---
|
|
697
|
-
|
|
698
|
-
### Mediafire
|
|
699
|
-
|
|
700
|
-
```ts
|
|
701
|
-
import { MediafireScraper } from "wsper";
|
|
702
|
-
const mf = new MediafireScraper();
|
|
703
|
-
|
|
704
|
-
await mf.getLink("https://www.mediafire.com/file/.../file")
|
|
705
|
-
// → WsperResponse<MediafireResult>
|
|
706
|
-
// data: { fileName, fileSize, downloadUrl, uploadDate }
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
---
|
|
710
|
-
|
|
711
|
-
### Play Store
|
|
712
|
-
|
|
713
|
-
```ts
|
|
714
|
-
import { PlayStoreScraper } from "wsper";
|
|
715
|
-
const store = new PlayStoreScraper();
|
|
716
|
-
|
|
717
|
-
await store.search("spotify", 5)
|
|
718
|
-
// → WsperResponse<PlayStoreApp[]>
|
|
719
|
-
// data[]: { name, packageId, developer, rating, downloads, iconUrl, url }
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
---
|
|
723
|
-
|
|
724
|
-
### Minecraft Addon
|
|
725
|
-
|
|
726
|
-
```ts
|
|
727
|
-
import { McAddonScraper } from "wsper";
|
|
728
|
-
const mc = new McAddonScraper();
|
|
729
|
-
|
|
730
|
-
await mc.search("texture pack")
|
|
731
|
-
// → WsperResponse<McAddonSearchItem[]>
|
|
732
|
-
|
|
733
|
-
await mc.getDetail("https://mcpedl.com/addon/...")
|
|
734
|
-
// → WsperResponse<McAddonDetail>
|
|
735
|
-
// data: { title, description, downloadLinks[], images[], author }
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
---
|
|
739
|
-
|
|
740
|
-
### Alkitab
|
|
741
|
-
|
|
742
|
-
```ts
|
|
743
|
-
import { AlkitabScraper } from "wsper";
|
|
744
|
-
const alkitab = new AlkitabScraper();
|
|
745
|
-
|
|
746
|
-
await alkitab.search("yohanes 3:16")
|
|
747
|
-
// → WsperResponse<AlkitabVerse[]>
|
|
748
|
-
// data[]: { reference, text, book, chapter, verse }
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
---
|
|
752
|
-
|
|
753
|
-
### BMKG
|
|
754
|
-
|
|
755
|
-
```ts
|
|
756
|
-
import { BMKGScraper } from "wsper";
|
|
757
|
-
const bmkg = new BMKGScraper();
|
|
758
|
-
|
|
759
|
-
await bmkg.getAutogempa()
|
|
760
|
-
// -> WsperResponse<BMKGEarthquakeFeed>
|
|
761
|
-
|
|
762
|
-
await bmkg.getGempaDirasakan()
|
|
763
|
-
// -> WsperResponse<BMKGEarthquakeFeed>
|
|
764
|
-
|
|
765
|
-
await bmkg.getCuacaNowcasting(0, 100)
|
|
766
|
-
// -> WsperResponse<BMKGNowcasting>
|
|
767
|
-
|
|
768
|
-
await bmkg.getPrakiraanCuaca("31")
|
|
769
|
-
// -> WsperResponse<BMKGForecast>
|
|
770
|
-
|
|
771
|
-
await bmkg.downloadShakemap("kode-shakemap", "./downloads")
|
|
772
|
-
// -> WsperResponse<BMKGShakemapDownload>
|
|
773
|
-
|
|
774
|
-
bmkg.getKodeProvinsi()
|
|
775
|
-
// -> WsperResponse<Record<string, string>>
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
---
|
|
779
|
-
|
|
780
|
-
### Cuaca
|
|
781
|
-
|
|
782
|
-
```ts
|
|
783
|
-
import { CuacaScraper } from "wsper";
|
|
784
|
-
const cuaca = new CuacaScraper({
|
|
785
|
-
warningApiKey: process.env.BMKG_WARNING_API_KEY,
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
await cuaca.searchLocation("bandung")
|
|
789
|
-
// -> WsperResponse<CuacaSearchResult[]>
|
|
790
|
-
|
|
791
|
-
await cuaca.getWeather("bandung")
|
|
792
|
-
// -> WsperResponse<CuacaResult>
|
|
793
|
-
|
|
794
|
-
await cuaca.getWeatherByCoord(-6.9175, 107.6191)
|
|
795
|
-
// -> WsperResponse<CuacaResult>
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
---
|
|
167
|
+
### WallpaperScraper
|
|
799
168
|
|
|
800
|
-
|
|
169
|
+
Source: Wallhaven search API at `https://wallhaven.cc/api/v1/search`.
|
|
801
170
|
|
|
802
171
|
```ts
|
|
803
|
-
import {
|
|
804
|
-
const drakor = new DrakorScraper();
|
|
805
|
-
|
|
806
|
-
await drakor.search("undercover")
|
|
807
|
-
// -> WsperResponse<DrakorList>
|
|
172
|
+
import { WallpaperScraper } from "wsper-js";
|
|
808
173
|
|
|
809
|
-
|
|
810
|
-
|
|
174
|
+
const scraper = new WallpaperScraper();
|
|
175
|
+
const result = await scraper.search("cyberpunk");
|
|
811
176
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
await drakor.getAll(1)
|
|
816
|
-
// -> WsperResponse<DrakorList>
|
|
177
|
+
console.log(result.data?.total);
|
|
178
|
+
console.log(result.data?.results[0]);
|
|
817
179
|
```
|
|
818
180
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
### Dramabox
|
|
181
|
+
Representative output:
|
|
822
182
|
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"total": 1,
|
|
186
|
+
"results": [
|
|
187
|
+
{
|
|
188
|
+
"title": "Wallpaper sky123",
|
|
189
|
+
"resolution": "1920x1080",
|
|
190
|
+
"image": "https://w.wallhaven.cc/full/sky123.jpg",
|
|
191
|
+
"page": "https://wallhaven.cc/w/sky123"
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
829
195
|
```
|
|
830
196
|
|
|
831
|
-
|
|
197
|
+
### WwCharScraper
|
|
832
198
|
|
|
833
|
-
|
|
199
|
+
Source: Wuthering Waves Fandom MediaWiki parse API.
|
|
834
200
|
|
|
835
201
|
```ts
|
|
836
|
-
import {
|
|
837
|
-
const sakura = new SakuraNovelScraper();
|
|
202
|
+
import { WwCharScraper } from "wsper-js";
|
|
838
203
|
|
|
839
|
-
|
|
840
|
-
|
|
204
|
+
const scraper = new WwCharScraper();
|
|
205
|
+
const result = await scraper.getCharacter("Jin Hsi");
|
|
841
206
|
|
|
842
|
-
|
|
843
|
-
// -> WsperResponse<SakuraNovelDetail>
|
|
844
|
-
|
|
845
|
-
await sakura.getChapter("/chapter/example-1/")
|
|
846
|
-
// -> WsperResponse<SakuraNovelChapterContent>
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
---
|
|
850
|
-
|
|
851
|
-
### Uguu
|
|
852
|
-
|
|
853
|
-
```ts
|
|
854
|
-
import { UguuScraper } from "wsper";
|
|
855
|
-
const uguu = new UguuScraper();
|
|
856
|
-
|
|
857
|
-
await uguu.upload(Buffer.from("hello"), "hello.txt")
|
|
858
|
-
// -> WsperResponse<UguuUploadResult>
|
|
859
|
-
// data: { url }
|
|
207
|
+
console.log(result.data);
|
|
860
208
|
```
|
|
861
209
|
|
|
862
|
-
|
|
210
|
+
Representative output:
|
|
863
211
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"title": "Jiyan",
|
|
215
|
+
"slug": "Jin_Hsi",
|
|
216
|
+
"url": "https://wutheringwaves.fandom.com/wiki/Jin_Hsi",
|
|
217
|
+
"bio": "Bio karakter.",
|
|
218
|
+
"profile": {},
|
|
219
|
+
"images": []
|
|
220
|
+
}
|
|
872
221
|
```
|
|
873
222
|
|
|
874
|
-
|
|
223
|
+
### HokInfoScraper
|
|
875
224
|
|
|
876
|
-
|
|
225
|
+
Source: Honor of Kings Fandom MediaWiki parse API.
|
|
877
226
|
|
|
878
227
|
```ts
|
|
879
|
-
import {
|
|
880
|
-
const ikiru = new IkiruMangaScraper();
|
|
881
|
-
|
|
882
|
-
await ikiru.search("solo leveling")
|
|
883
|
-
// -> WsperResponse<IkiruMangaResult>
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
---
|
|
228
|
+
import { HokInfoScraper } from "wsper-js";
|
|
887
229
|
|
|
888
|
-
|
|
230
|
+
const scraper = new HokInfoScraper();
|
|
231
|
+
const result = await scraper.getCharacter("Angela");
|
|
889
232
|
|
|
890
|
-
|
|
891
|
-
import { WallpaperScraper } from "wsper";
|
|
892
|
-
const wallpaper = new WallpaperScraper();
|
|
893
|
-
|
|
894
|
-
await wallpaper.search("blue sky")
|
|
895
|
-
// -> WsperResponse<WallpaperResult>
|
|
233
|
+
console.log(result.data?.profile);
|
|
896
234
|
```
|
|
897
235
|
|
|
898
|
-
|
|
236
|
+
Representative output:
|
|
899
237
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"title": "Sun Wukong",
|
|
241
|
+
"image": null,
|
|
242
|
+
"profile": {
|
|
243
|
+
"Role": "Fighter"
|
|
244
|
+
},
|
|
245
|
+
"bio": "Bio singkat karakter.",
|
|
246
|
+
"skills": [],
|
|
247
|
+
"lore": null,
|
|
248
|
+
"url": "https://honor-of-kings.fandom.com/wiki/Sun%20Wukong"
|
|
249
|
+
}
|
|
908
250
|
```
|
|
909
251
|
|
|
910
|
-
|
|
252
|
+
### CapCutScraper
|
|
911
253
|
|
|
912
|
-
|
|
254
|
+
Source: WordPress-style resolver endpoint, defaulting to `https://capdownloader.com/wp-json/aio-dl/video-data/`.
|
|
913
255
|
|
|
914
256
|
```ts
|
|
915
|
-
import {
|
|
916
|
-
const videy = new VideyScraper();
|
|
257
|
+
import { CapCutScraper } from "wsper-js";
|
|
917
258
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
---
|
|
259
|
+
const scraper = new CapCutScraper();
|
|
260
|
+
const result = await scraper.download("https://www.capcut.com/t/Zs82gHj1a/");
|
|
923
261
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
```ts
|
|
927
|
-
import { OcrScraper } from "wsper";
|
|
928
|
-
const ocr = new OcrScraper();
|
|
929
|
-
|
|
930
|
-
await ocr.scan(imageBuffer)
|
|
931
|
-
// -> WsperResponse<OcrScanResult>
|
|
262
|
+
console.log(result.data);
|
|
932
263
|
```
|
|
933
264
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
### Webp2Mp4
|
|
937
|
-
|
|
938
|
-
```ts
|
|
939
|
-
import { Webp2Mp4Scraper } from "wsper";
|
|
940
|
-
const converter = new Webp2Mp4Scraper();
|
|
941
|
-
|
|
942
|
-
await converter.toMp4(webpBuffer)
|
|
943
|
-
// -> WsperResponse<Webp2Mp4Result>
|
|
265
|
+
Representative output:
|
|
944
266
|
|
|
945
|
-
|
|
946
|
-
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"videoUrl": "https://cdn.example/video.mp4"
|
|
270
|
+
}
|
|
947
271
|
```
|
|
948
272
|
|
|
949
|
-
|
|
273
|
+
### ImgUpscalerScraper
|
|
950
274
|
|
|
951
|
-
|
|
275
|
+
Source: `https://get1.imglarger.com` upload and status endpoints.
|
|
952
276
|
|
|
953
277
|
```ts
|
|
954
|
-
import {
|
|
955
|
-
|
|
278
|
+
import { readFile } from "node:fs/promises";
|
|
279
|
+
import { ImgUpscalerScraper } from "wsper-js";
|
|
956
280
|
|
|
957
|
-
await
|
|
958
|
-
|
|
281
|
+
const image = await readFile("./photo.jpg");
|
|
282
|
+
const scraper = new ImgUpscalerScraper();
|
|
283
|
+
const result = await scraper.upscaleBuffer(image, "photo.jpg", 4);
|
|
959
284
|
|
|
960
|
-
|
|
961
|
-
// -> WsperResponse<MConverterConvertResult>
|
|
285
|
+
console.log(result.data);
|
|
962
286
|
```
|
|
963
287
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
### HtmlToJpg
|
|
288
|
+
Representative output:
|
|
967
289
|
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"originalPath": null,
|
|
293
|
+
"outputPath": null,
|
|
294
|
+
"resultUrl": "https://cdn.test/out.jpg",
|
|
295
|
+
"scale": 4
|
|
296
|
+
}
|
|
974
297
|
```
|
|
975
298
|
|
|
976
|
-
|
|
299
|
+
### PhotoAiScraper
|
|
977
300
|
|
|
978
|
-
|
|
301
|
+
Source: `https://photoai.imglarger.com`.
|
|
979
302
|
|
|
980
303
|
```ts
|
|
981
|
-
import {
|
|
982
|
-
|
|
304
|
+
import { readFile } from "node:fs/promises";
|
|
305
|
+
import { PhotoAiScraper } from "wsper-js";
|
|
983
306
|
|
|
984
|
-
await
|
|
985
|
-
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
---
|
|
307
|
+
const image = await readFile("./portrait.jpg");
|
|
308
|
+
const scraper = new PhotoAiScraper();
|
|
989
309
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
await modAndroid.aptoide("maps")
|
|
997
|
-
// -> WsperResponse<AptoideResult>
|
|
310
|
+
const upload = await scraper.uploadBuffer(image, "portrait.jpg");
|
|
311
|
+
if (upload.ok && upload.data !== null) {
|
|
312
|
+
const status = await scraper.checkStatus(upload.data.code);
|
|
313
|
+
console.log(status.data);
|
|
314
|
+
}
|
|
998
315
|
```
|
|
999
316
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
### Upscaler
|
|
317
|
+
Representative output:
|
|
1003
318
|
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
319
|
+
```json
|
|
320
|
+
{
|
|
321
|
+
"status": "success",
|
|
322
|
+
"downloadUrl": "https://cdn.test/out.jpg",
|
|
323
|
+
"raw": {
|
|
324
|
+
"status": "success",
|
|
325
|
+
"downloadUrl": "https://cdn.test/out.jpg"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
1010
328
|
```
|
|
1011
329
|
|
|
1012
|
-
|
|
330
|
+
### FaceswapScraper
|
|
1013
331
|
|
|
1014
|
-
|
|
332
|
+
Source: `https://api.lovefaceswap.com`.
|
|
1015
333
|
|
|
1016
334
|
```ts
|
|
1017
|
-
import {
|
|
1018
|
-
|
|
335
|
+
import { readFile } from "node:fs/promises";
|
|
336
|
+
import { FaceswapScraper } from "wsper-js";
|
|
1019
337
|
|
|
1020
|
-
await
|
|
1021
|
-
|
|
1022
|
-
|
|
338
|
+
const [source, target] = await Promise.all([
|
|
339
|
+
readFile("./source-face.jpg"),
|
|
340
|
+
readFile("./target.jpg"),
|
|
341
|
+
]);
|
|
1023
342
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
### ImgUpscaler
|
|
1027
|
-
|
|
1028
|
-
```ts
|
|
1029
|
-
import { ImgUpscalerScraper } from "wsper";
|
|
1030
|
-
const upscaler = new ImgUpscalerScraper();
|
|
343
|
+
const scraper = new FaceswapScraper();
|
|
344
|
+
const result = await scraper.process(source, target);
|
|
1031
345
|
|
|
1032
|
-
|
|
1033
|
-
// -> WsperResponse<ImgUpscalerResult>
|
|
346
|
+
console.log(result.data);
|
|
1034
347
|
```
|
|
1035
348
|
|
|
1036
|
-
|
|
349
|
+
Representative output:
|
|
1037
350
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
await faceswap.process(sourceImageBuffer, targetImageBuffer)
|
|
1045
|
-
// -> WsperResponse<FaceswapResult>
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"job_id": "job-1",
|
|
354
|
+
"image": "https://cdn.test/out.jpg"
|
|
355
|
+
}
|
|
1046
356
|
```
|
|
1047
357
|
|
|
1048
|
-
|
|
358
|
+
### UpscalerScraper
|
|
1049
359
|
|
|
1050
|
-
|
|
360
|
+
Source: `https://aienhancer.ai`.
|
|
1051
361
|
|
|
1052
362
|
```ts
|
|
1053
|
-
import {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
await photoAi.uploadBuffer(imageBuffer, "image.jpg")
|
|
1057
|
-
// -> WsperResponse<PhotoAiUploadResult>
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
---
|
|
363
|
+
import { readFile } from "node:fs/promises";
|
|
364
|
+
import { UpscalerScraper } from "wsper-js";
|
|
1061
365
|
|
|
1062
|
-
|
|
366
|
+
const image = await readFile("./photo.jpg");
|
|
367
|
+
const scraper = new UpscalerScraper();
|
|
368
|
+
const result = await scraper.upscaleBuffer(image, "image/jpeg");
|
|
1063
369
|
|
|
1064
|
-
|
|
1065
|
-
import { LyricsScraper } from "wsper";
|
|
1066
|
-
const lyrics = new LyricsScraper();
|
|
1067
|
-
|
|
1068
|
-
await lyrics.search("brat charli xcx")
|
|
1069
|
-
// → WsperResponse<LyricsResult>
|
|
1070
|
-
// data: { title, artist, lyrics, sourceUrl }
|
|
370
|
+
console.log(result.data);
|
|
1071
371
|
```
|
|
1072
372
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
### Resep
|
|
373
|
+
Representative output:
|
|
1076
374
|
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
// data[]: { title, url, imageUrl, author, servings, cookTime, ingredients[], steps[] }
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"id": "task-1",
|
|
378
|
+
"input": "https://cdn.test/in.jpg",
|
|
379
|
+
"output": "https://cdn.test/out.jpg"
|
|
380
|
+
}
|
|
1084
381
|
```
|
|
1085
382
|
|
|
1086
|
-
|
|
383
|
+
### StalkScraper
|
|
1087
384
|
|
|
1088
|
-
|
|
385
|
+
Source: npm registry API at `https://registry.npmjs.org`.
|
|
1089
386
|
|
|
1090
387
|
```ts
|
|
1091
|
-
import {
|
|
1092
|
-
const anime = new TopAnimeScraper();
|
|
388
|
+
import { StalkScraper } from "wsper-js";
|
|
1093
389
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
// data[]: { rank, title, url, imageUrl, score, type, episodes, members }
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
---
|
|
390
|
+
const scraper = new StalkScraper();
|
|
391
|
+
const result = await scraper.getNpmPackage("axios");
|
|
1100
392
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
```ts
|
|
1104
|
-
import { AnimeQuoteScraper } from "wsper";
|
|
1105
|
-
const quote = new AnimeQuoteScraper();
|
|
1106
|
-
|
|
1107
|
-
await quote.getRandom()
|
|
1108
|
-
// → WsperResponse<AnimeQuoteItem>
|
|
1109
|
-
// data: { quote, character, anime, imageUrl }
|
|
393
|
+
console.log(result.data?.title);
|
|
394
|
+
console.log(result.data?.install);
|
|
1110
395
|
```
|
|
1111
396
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
### Anime Random
|
|
1115
|
-
|
|
1116
|
-
```ts
|
|
1117
|
-
import { AnimeRandomScraper, ANIME_CHARACTERS } from "wsper";
|
|
1118
|
-
const random = new AnimeRandomScraper();
|
|
1119
|
-
|
|
1120
|
-
await random.getImage("nezuko")
|
|
1121
|
-
// → WsperResponse<AnimeRandomResult>
|
|
1122
|
-
// data: { character, imageUrl, sourceUrl }
|
|
397
|
+
Representative output:
|
|
1123
398
|
|
|
1124
|
-
|
|
1125
|
-
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"title": "@scope/pkg",
|
|
402
|
+
"language": "",
|
|
403
|
+
"publish": "2026-05-01T00:00:00.000Z",
|
|
404
|
+
"readme": "",
|
|
405
|
+
"explore": "https://www.npmjs.com/package/@scope/pkg",
|
|
406
|
+
"dependencies": "0",
|
|
407
|
+
"dependents": "0",
|
|
408
|
+
"version_count": "1",
|
|
409
|
+
"keywords": [],
|
|
410
|
+
"install": "npm install @scope/pkg",
|
|
411
|
+
"info": [],
|
|
412
|
+
"collaborator": []
|
|
413
|
+
}
|
|
1126
414
|
```
|
|
1127
415
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
## Brat Generator
|
|
416
|
+
### MediafireScraper
|
|
1131
417
|
|
|
1132
|
-
|
|
418
|
+
Source: Mediafire public file page HTML.
|
|
1133
419
|
|
|
1134
420
|
```ts
|
|
1135
|
-
import {
|
|
421
|
+
import { MediafireScraper } from "wsper-js";
|
|
1136
422
|
|
|
1137
|
-
const
|
|
423
|
+
const scraper = new MediafireScraper();
|
|
424
|
+
const result = await scraper.getLink(
|
|
425
|
+
"https://www.mediafire.com/file/ipnyzofjcwri357/test-10mb.bin/file",
|
|
426
|
+
);
|
|
1138
427
|
|
|
1139
|
-
|
|
1140
|
-
// bratGenerator.generate(...)
|
|
1141
|
-
|
|
1142
|
-
// Atau shorthand function:
|
|
1143
|
-
// await generateBrat(config)
|
|
428
|
+
console.log(result.data);
|
|
1144
429
|
```
|
|
1145
430
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
### generate — Image
|
|
1149
|
-
|
|
1150
|
-
```ts
|
|
1151
|
-
const result = await brat.generate({
|
|
1152
|
-
canvas: { preset: "1:1" },
|
|
1153
|
-
background: { type: "solid", color: "#ffffff" },
|
|
1154
|
-
text: {
|
|
1155
|
-
value: "kamu pas kecil pernah nelen magnet ya? menarik banget soalnya",
|
|
1156
|
-
align: "justify",
|
|
1157
|
-
},
|
|
1158
|
-
output: { type: "image", format: "png", path: "./brat.png" },
|
|
1159
|
-
});
|
|
431
|
+
Representative output:
|
|
1160
432
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"downloadUrl": "https://download.example/myfile.zip",
|
|
436
|
+
"fileName": "My_File.zip",
|
|
437
|
+
"fileSize": "50 MB",
|
|
438
|
+
"fileType": "ZIP"
|
|
1166
439
|
}
|
|
1167
440
|
```
|
|
1168
441
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
---
|
|
442
|
+
### CaiScraper
|
|
1172
443
|
|
|
1173
|
-
|
|
444
|
+
Source: Character.AI WebSocket & API integrations.
|
|
1174
445
|
|
|
1175
446
|
```ts
|
|
1176
|
-
|
|
1177
|
-
text: {
|
|
1178
|
-
value: "charli xcx is so brat i can't even",
|
|
1179
|
-
align: "justify",
|
|
1180
|
-
},
|
|
1181
|
-
background: { type: "solid", color: "#8ace00" },
|
|
1182
|
-
animation: {
|
|
1183
|
-
enabled: true,
|
|
1184
|
-
direction: "left-to-right",
|
|
1185
|
-
mode: "word",
|
|
1186
|
-
fps: 12,
|
|
1187
|
-
textSpeed: 150, // ms per kata — prioritas di atas duration
|
|
1188
|
-
duration: 3000, // ms total — fallback jika textSpeed tidak diset
|
|
1189
|
-
},
|
|
1190
|
-
output: { type: "gif", path: "./brat.gif" },
|
|
1191
|
-
});
|
|
447
|
+
import { CaiScraper } from "wsper-js";
|
|
1192
448
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
---
|
|
1198
|
-
|
|
1199
|
-
### generate — Video
|
|
1200
|
-
|
|
1201
|
-
```ts
|
|
1202
|
-
const result = await brat.generate({
|
|
1203
|
-
text: { value: "brat video" },
|
|
1204
|
-
animation: { enabled: true, mode: "word", fps: 30 },
|
|
1205
|
-
output: {
|
|
1206
|
-
type: "video",
|
|
1207
|
-
format: "mp4", // "mp4" | "webm"
|
|
1208
|
-
codec: "libx264", // optional
|
|
1209
|
-
path: "./brat.mp4",
|
|
449
|
+
const scraper = new CaiScraper({
|
|
450
|
+
credentials: {
|
|
451
|
+
bearerToken: "YOUR_CHARACTER_AI_TOKEN",
|
|
1210
452
|
},
|
|
1211
453
|
});
|
|
1212
454
|
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
> Membutuhkan FFmpeg (`C:\ffmpeg\ffmpeg.exe` atau di PATH).
|
|
1218
|
-
|
|
1219
|
-
---
|
|
1220
|
-
|
|
1221
|
-
### withPreset
|
|
1222
|
-
|
|
1223
|
-
Terapkan style preset ke config:
|
|
1224
|
-
|
|
1225
|
-
```ts
|
|
1226
|
-
const cfg = brat.withPreset("brat-green", {
|
|
1227
|
-
text: { value: "brat summer" },
|
|
1228
|
-
output: { type: "image", format: "png", path: "./out.png" },
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
await brat.generate(cfg);
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
| Preset | Background | Text Color | Text Blur |
|
|
1235
|
-
|---|---|---|---|
|
|
1236
|
-
| `classic` | `#ffffff` | `#111111` | 0 |
|
|
1237
|
-
| `blurred` | `#ffffff` | `#111111` | 2 |
|
|
1238
|
-
| `brat-green` | `#8ace00` | `#111111` | 1 |
|
|
1239
|
-
| `clean` | `#f5f5f5` | `#000000` | 0 |
|
|
1240
|
-
|
|
1241
|
-
---
|
|
1242
|
-
|
|
1243
|
-
### imageToSticker
|
|
1244
|
-
|
|
1245
|
-
Konversi gambar apa pun menjadi sticker format Telegram/WhatsApp (512×512 WebP):
|
|
1246
|
-
|
|
1247
|
-
```ts
|
|
1248
|
-
// Dari file path
|
|
1249
|
-
const buf = await brat.imageToSticker("./photo.jpg", {
|
|
1250
|
-
size: 512, // ukuran sticker px (default 512)
|
|
1251
|
-
format: "webp", // "webp" | "png"
|
|
1252
|
-
quality: 90, // kualitas WebP 1-100
|
|
1253
|
-
top: "BRAT", // caption atas (otomatis uppercase)
|
|
1254
|
-
bottom: "2025", // caption bawah
|
|
1255
|
-
color: "#ffffff",
|
|
1256
|
-
strokeColor: "#000000",
|
|
1257
|
-
});
|
|
1258
|
-
// → Buffer WebP/PNG
|
|
1259
|
-
|
|
1260
|
-
// Dari Buffer
|
|
1261
|
-
const buf2 = await brat.imageToSticker(pngBuffer, { size: 512, format: "webp" });
|
|
1262
|
-
|
|
1263
|
-
// Langsung simpan ke file
|
|
1264
|
-
await brat.saveSticker("./photo.jpg", "./sticker.webp", {
|
|
1265
|
-
size: 512, top: "TOP", bottom: "BOTTOM",
|
|
1266
|
-
});
|
|
1267
|
-
// → void
|
|
1268
|
-
```
|
|
1269
|
-
|
|
1270
|
-
---
|
|
1271
|
-
|
|
1272
|
-
### MP4 ↔ GIF Conversion
|
|
1273
|
-
|
|
1274
|
-
#### GIF → MP4
|
|
1275
|
-
|
|
1276
|
-
```ts
|
|
1277
|
-
const mp4Buffer = await brat.gifToMp4("./brat.gif", "./brat.mp4", {
|
|
1278
|
-
fps: 24,
|
|
1279
|
-
width: 720, // optional resize
|
|
1280
|
-
});
|
|
1281
|
-
// → Buffer MP4
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
#### GIF → WebM
|
|
455
|
+
// 1. Search for a character
|
|
456
|
+
const search = await scraper.searchCharacters("Mario");
|
|
457
|
+
console.log(search.data?.[0]);
|
|
1285
458
|
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
#### MP4 → GIF
|
|
1292
|
-
|
|
1293
|
-
```ts
|
|
1294
|
-
const gifBuffer = await brat.mp4ToGif("./clip.mp4", "./clip.gif", {
|
|
1295
|
-
fps: 10, // frame rate GIF (default 10)
|
|
1296
|
-
width: 480, // lebar output GIF (default 480)
|
|
1297
|
-
loop: 0, // 0 = infinite loop
|
|
1298
|
-
startTime: 5, // mulai dari detik ke-5
|
|
1299
|
-
duration: 3, // ambil 3 detik saja
|
|
459
|
+
// 2. Chat with a character (optionally using a custom voice ID)
|
|
460
|
+
const chat = await scraper.chat({
|
|
461
|
+
characterId: "rGKdvZewGUZEJFEQPEBMS5JLQOTOrxi-8ByLFsGmgQM",
|
|
462
|
+
message: "Hello Mario!",
|
|
463
|
+
voiceId: "4fdd6bc1-c659-4587-b462-53f569b39078",
|
|
1300
464
|
});
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
---
|
|
1305
|
-
|
|
1306
|
-
### convertImage
|
|
1307
|
-
|
|
1308
|
-
Konversi format gambar tanpa teks/canvas:
|
|
1309
|
-
|
|
1310
|
-
```ts
|
|
1311
|
-
const webpBuf = await brat.convertImage("./photo.png", "webp", "./photo.webp");
|
|
1312
|
-
const jpgBuf = await brat.convertImage(pngBuffer, "jpg");
|
|
1313
|
-
// → Buffer dalam format target
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
---
|
|
1317
|
-
|
|
1318
|
-
### Background Config
|
|
1319
|
-
|
|
1320
|
-
```ts
|
|
1321
|
-
// Solid color
|
|
1322
|
-
background: { type: "solid", color: "#8ace00" }
|
|
1323
|
-
background: { type: "solid", color: "white" }
|
|
1324
|
-
|
|
1325
|
-
// Linear gradient
|
|
1326
|
-
background: {
|
|
1327
|
-
type: "linear-gradient",
|
|
1328
|
-
colors: ["#b7ff00", "#ffffff"],
|
|
1329
|
-
direction: "to-bottom-right",
|
|
1330
|
-
// "to-top" | "to-bottom" | "to-left" | "to-right"
|
|
1331
|
-
// "to-bottom-right" | "to-bottom-left" | "to-top-right" | "to-top-left"
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// Radial gradient
|
|
1335
|
-
background: {
|
|
1336
|
-
type: "radial-gradient",
|
|
1337
|
-
colors: ["#ffffff", "#d9d9d9"],
|
|
1338
|
-
position: "center",
|
|
1339
|
-
// "center" | "top" | "bottom" | "left" | "right"
|
|
1340
|
-
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
|
|
1341
|
-
// { x: 50, y: 50 } ← posisi custom dalam persen (0-100)
|
|
1342
|
-
}
|
|
1343
|
-
```
|
|
1344
|
-
|
|
1345
|
-
---
|
|
465
|
+
console.log(chat.data?.text);
|
|
466
|
+
console.log(chat.data?.audioUrl); // Generated TTS audio URL
|
|
1346
467
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
align: "justify", // "left" | "right" | "center" | "justify"
|
|
1354
|
-
|
|
1355
|
-
fontSize: "auto", // atau angka: 48 — "auto" menyesuaikan ukuran canvas
|
|
1356
|
-
minFontSize: 20, // batas bawah auto fit (default 20)
|
|
1357
|
-
maxFontSize: 180, // batas atas auto fit (default 180)
|
|
1358
|
-
|
|
1359
|
-
fontFamily: '"Arial Black", sans-serif',
|
|
1360
|
-
fontWeight: 900, // atau string: "bold"
|
|
1361
|
-
|
|
1362
|
-
lineHeight: 1.05, // default 1.05 — rapat khas brat style
|
|
1363
|
-
color: "#111111",
|
|
1364
|
-
|
|
1365
|
-
blur: 2, // blur teks 0–8 (0 = tidak ada, default 0)
|
|
1366
|
-
}
|
|
1367
|
-
```
|
|
1368
|
-
|
|
1369
|
-
---
|
|
1370
|
-
|
|
1371
|
-
### Canvas Config
|
|
1372
|
-
|
|
1373
|
-
```ts
|
|
1374
|
-
// Preset aspect ratio
|
|
1375
|
-
canvas: { preset: "1:1" } // 1080 × 1080
|
|
1376
|
-
canvas: { preset: "9:16" } // 1080 × 1920 (portrait / story)
|
|
1377
|
-
canvas: { preset: "16:9" } // 1920 × 1080 (landscape / widescreen)
|
|
1378
|
-
|
|
1379
|
-
// Preset nama
|
|
1380
|
-
canvas: { preset: "square" } // 1024 × 1024
|
|
1381
|
-
canvas: { preset: "story" } // 1080 × 1920
|
|
1382
|
-
canvas: { preset: "post" } // 1080 × 1350
|
|
1383
|
-
canvas: { preset: "landscape" } // 1920 × 1080
|
|
1384
|
-
|
|
1385
|
-
// Ukuran manual
|
|
1386
|
-
canvas: { width: 1200, height: 800 }
|
|
1387
|
-
|
|
1388
|
-
// Aspect ratio + satu dimensi → dimensi lain dihitung otomatis
|
|
1389
|
-
canvas: { aspectRatio: "4:3", width: 1200 } // → height 900
|
|
1390
|
-
canvas: { aspectRatio: "16:9", height: 720 } // → width 1280
|
|
1391
|
-
canvas: { aspectRatio: "4:3" } // → 1080 × 810 (base 1080)
|
|
1392
|
-
```
|
|
1393
|
-
|
|
1394
|
-
**Padding:**
|
|
1395
|
-
|
|
1396
|
-
```ts
|
|
1397
|
-
layout: { padding: 64 }
|
|
1398
|
-
layout: { padding: { top: 80, right: 64, bottom: 80, left: 64 } }
|
|
468
|
+
// Chat sessions stay connected per character and are reused by later chat()
|
|
469
|
+
// calls. Disconnect them when your app is done with the character.
|
|
470
|
+
await scraper.disconnectCharacterSession("rGKdvZewGUZEJFEQPEBMS5JLQOTOrxi-8ByLFsGmgQM");
|
|
471
|
+
// Or:
|
|
472
|
+
await scraper.disconnectAllCharacterSessions();
|
|
1399
473
|
```
|
|
1400
474
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
### Animation Config
|
|
475
|
+
Representative output:
|
|
1404
476
|
|
|
1405
|
-
```
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
fps: 12,
|
|
1411
|
-
|
|
1412
|
-
// Pilih satu untuk kecepatan:
|
|
1413
|
-
textSpeed: 150, // ms per step — prioritas utama
|
|
1414
|
-
duration: 3000, // ms total dibagi rata ke semua step — fallback
|
|
477
|
+
```json
|
|
478
|
+
{
|
|
479
|
+
"text": "It's-a me, Mario!",
|
|
480
|
+
"audioUrl": "https://storage.googleapis.com/.../voice.mp3",
|
|
481
|
+
"raw": { ... }
|
|
1415
482
|
}
|
|
1416
483
|
```
|
|
1417
484
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
|
1421
|
-
|
|
|
1422
|
-
| `
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
485
|
+
## Available Scrapers
|
|
486
|
+
|
|
487
|
+
| Scraper | Purpose | Source/API | Auth/Cookie required? | Example file | Notes |
|
|
488
|
+
| --- | --- | --- | --- | --- | --- |
|
|
489
|
+
| `AlkitabScraper` | Bible verse search | `alkitab.me` | No | `examples/alkitab.example.ts` | `search(query)` |
|
|
490
|
+
| `AnimeQuoteScraper` | Random anime quote | `otakotaku.com` | No | `examples/anime-quote.example.ts` | `getRandom()` |
|
|
491
|
+
| `AnimeRandomScraper` | Random anime character image | GitHub raw anime dataset | No | `examples/anime-random.example.ts` | `getImage(character)`, `random()` |
|
|
492
|
+
| `BiliBiliScraper` | BiliBili search and video info | `api.bilibili.com` | Optional cookie | `examples/bilibili.example.ts` | Cookie may unlock authenticated stream access |
|
|
493
|
+
| `BMKGScraper` | Indonesian earthquake and weather feeds | `data.bmkg.go.id`, `nowcasting.bmkg.go.id` | No | `examples/bmkg.example.ts` | Autogempa, gempa dirasakan, nowcasting, forecast |
|
|
494
|
+
| `CapCutScraper` | Resolve CapCut template video URL | `capdownloader.com/wp-json/aio-dl/video-data/` | No | `examples/capcut.example.ts` | Mocked in example runner |
|
|
495
|
+
| `CaiScraper` | Character.AI search, chat, and voice details | `neo.character.ai` API & WebSockets | Yes (Token required) | `examples/cai.example.ts` | `searchCharacters`, `chat`, `getVoice` |
|
|
496
|
+
| `CuacaScraper` | Indonesian weather by location/coordinate | BMKG weather APIs | Optional API key for warnings | `examples/cuaca.example.ts` | Reads optional `BMKG_WARNING_API_KEY` in example |
|
|
497
|
+
| `DrakorScraper` | Korean drama search/list/detail | `drakorkita30.kita.baby` | No | `examples/drakor.example.ts` | `search`, `detail`, `ongoing`, `getAll` |
|
|
498
|
+
| `DramaboxScraper` | Dramabox search | `dramabox.com` | No | `examples/dramabox.example.ts` | `search(query)` |
|
|
499
|
+
| `FaceswapScraper` | Face-swap image processing | `api.lovefaceswap.com` | No | `examples/faceswap.example.ts` | Mocked in example runner |
|
|
500
|
+
| `HokInfoScraper` | Honor of Kings character info | Fandom MediaWiki parse API | No | `examples/hok-info.example.ts` | Uses `api.php?action=parse` |
|
|
501
|
+
| `HtmlToJpgScraper` | HTML file to JPG conversion | `api.freeconvert.com` | No credential in code | `examples/html-to-jpg.example.ts` | File-based conversion; skipped in runner without fixture |
|
|
502
|
+
| `IkiruMangaScraper` | Manga search | `02.ikiru.wtf` | No | `examples/ikiru-manga.example.ts` | Mock server fallback available |
|
|
503
|
+
| `ImageScraper` | Safebooru image search | `safebooru.org` | No | `examples/image.example.ts` | Current site type: `safebooru` |
|
|
504
|
+
| `ImgUpscalerScraper` | Image upscaling | `get1.imglarger.com` | No | `examples/img-upscaler.example.ts` | Mocked in example runner |
|
|
505
|
+
| `InstagramScraper` | Profile, feed, post, download | `instagram.com` web/API endpoints | Internal defaults; custom cookie supported | `examples/instagram.example.ts` | Use only legitimate session cookies |
|
|
506
|
+
| `KomikindoScraper` | Manga search/detail | `komikindo.ch` | No | `examples/komikindo.example.ts` | `search`, `getDetail` |
|
|
507
|
+
| `LyricsScraper` | Lyrics search | `lrclib.net` JSON API | No | `examples/lyrics.example.ts` | Replaced blocked HTML scraping with API integration |
|
|
508
|
+
| `MConverterScraper` | File conversion helpers | `mconverter.eu` | No | `examples/mconverter.example.ts` | `getTargets`, `convert`, `convertBuffer` |
|
|
509
|
+
| `McAddonScraper` | Minecraft addon search/detail | `mmcreviews.com` | No | `examples/mcaddon.example.ts` | `search`, `getDetail`, `getAddon` |
|
|
510
|
+
| `MediafireScraper` | Resolve Mediafire download link | Mediafire public HTML page | No | `examples/mediafire.example.ts` | Default example uses active 10MB test file |
|
|
511
|
+
| `ModAndroidScraper` | Android APK/mod search aggregations | `an1.com`, `modyolo.com`, `aptoide.com`, `uptodown.com` | No | `examples/mod-android.example.ts` | `android1`, `modyolo`, `aptoide`, `uptodown`, `searchAll` |
|
|
512
|
+
| `OcrScraper` | OCR image scan | `newocr.com` | No | `examples/ocr.example.ts` | File/buffer based |
|
|
513
|
+
| `PhotoAiScraper` | Photo AI upload/status | `photoai.imglarger.com` | No | `examples/photo-ai.example.ts` | Mocked in example runner |
|
|
514
|
+
| `PinterestScraper` | Pin search, detail, download | `pinterest.com` | Internal defaults; custom cookie supported | `examples/pinterest.example.ts` | Supports `credentials` option |
|
|
515
|
+
| `PlayStoreScraper` | Google Play app search | `play.google.com` | No | `examples/playstore.example.ts` | `search(query, limit)` |
|
|
516
|
+
| `ResepScraper` | Recipe search | `cookpad.com` | No | `examples/resep.example.ts` | Returns recipe items |
|
|
517
|
+
| `SakuraNovelScraper` | Novel search/detail/chapter | `sakuranovel.id` | No | `examples/sakura-novel.example.ts` | Mock server fallback available |
|
|
518
|
+
| `SpotifyScraper` | Spotify track, album, playlist, search, downloads | Spotify Web API and Accounts API | Internal defaults; custom client credentials supported | `examples/spotify.example.ts` | Partial custom credentials are rejected |
|
|
519
|
+
| `StalkScraper` | npm package metadata lookup | `registry.npmjs.org` | No | `examples/stalk.example.ts` | Default example query is `axios` |
|
|
520
|
+
| `ThreadsScraper` | Threads profile, post, search, download | `threads.net` web/API endpoints | Internal defaults; custom cookie supported | `examples/threads.example.ts` | Use only legitimate session cookies |
|
|
521
|
+
| `TopAnimeScraper` | MyAnimeList top anime list | `myanimelist.net` | No | `examples/top-anime.example.ts` | `getTopAnime(limit)` |
|
|
522
|
+
| `TwitterScraper` | Tweet, profile, search, timeline, downloads | `x.com/i/api/graphql` | Cookie/CSRF commonly required | `examples/twitter.example.ts` | Supports `credentials` option |
|
|
523
|
+
| `UguuScraper` | Temporary file upload | `uguu.se/upload` | No | `examples/uguu.example.ts` | `upload(buffer, filename)` |
|
|
524
|
+
| `UpscalerScraper` | Image enhancement | `aienhancer.ai` | No | `examples/upscaler.example.ts` | Rejects remote URL string input |
|
|
525
|
+
| `VideyScraper` | Video upload | `videy.co/api/upload` | No | `examples/videy.example.ts` | `upload`, `uploadBuffer` |
|
|
526
|
+
| `WallpaperScraper` | Wallpaper search | `wallhaven.cc/api/v1/search` | No | `examples/wallpaper.example.ts` | Replaced blocked HTML scraping with API integration |
|
|
527
|
+
| `Webp2Mp4Scraper` | WebP to MP4/PNG conversion | `ezgif.com` | No | `examples/webp2mp4.example.ts` | `toMp4`, `toPng` |
|
|
528
|
+
| `WwCharScraper` | Wuthering Waves character info | Fandom MediaWiki parse API | No | `examples/ww-char.example.ts` | Uses `api.php?action=parse` |
|
|
529
|
+
| `YouTubeScraper` | YouTube metadata, search, playlist, channel, downloads | `yt-dlp`, `play-dl`, YouTube pages | No cookie option exposed in current scraper options | `examples/youtube.example.ts` | Media download features need external tools |
|
|
530
|
+
|
|
531
|
+
## What's New
|
|
532
|
+
|
|
533
|
+
The latest scraper reliability pass fixed 12 failing scraper functions and their corresponding tests.
|
|
534
|
+
|
|
535
|
+
- `LyricsScraper` now uses LRCLIB's public JSON API instead of a Cloudflare-protected Musixmatch HTML page.
|
|
536
|
+
- `WallpaperScraper` now uses Wallhaven's public search API instead of a Cloudflare-protected Wallpaperflare HTML page.
|
|
537
|
+
- `WwCharScraper` and `HokInfoScraper` now use Fandom's MediaWiki parse API (`api.php?action=parse`) and prepend a synthetic `#firstHeading` node before running the existing parsers.
|
|
538
|
+
- `examples/mock-server.ts` now covers WordPress resolver routes, AI tool polling/upload routes, and Fandom API responses.
|
|
539
|
+
- `examples/alllexamp.ts` starts and stops the mock server during the runner lifecycle.
|
|
540
|
+
- `WSPER_MOCK_BASE_URL` is injected into spawned example subprocesses so individual examples can use the local mock endpoint automatically.
|
|
541
|
+
- Default example inputs were updated: `StalkScraper` uses `axios`, `MediafireScraper` uses an active 10MB test file URL, and `HokInfoScraper` uses `Angela`.
|
|
542
|
+
- Verification recorded in `walkthrough.md`: 82 test files passed, 314 tests passed, and the examples runner reported 83 OK, 0 FAIL, 2 SKIP.
|
|
543
|
+
|
|
544
|
+
## Mock Server and Testing
|
|
545
|
+
|
|
546
|
+
`examples/mock-server.ts` is a local HTTP server used by the examples runner to provide deterministic responses for endpoints that are rate-limited, depend on third-party availability, or are inconvenient to call during automated checks.
|
|
547
|
+
|
|
548
|
+
Currently mocked routes include:
|
|
549
|
+
|
|
550
|
+
| Area | Routes |
|
|
551
|
+
| --- | --- |
|
|
552
|
+
| CapCut | `/wp-json/aio-dl/video-data/` |
|
|
553
|
+
| ImgUpscaler | `/api/UpscalerNew/UploadNew`, `/api/UpscalerNew/CheckStatusNew` |
|
|
554
|
+
| PhotoAi | `/api/PhoAi/Upload`, `/api/PhoAi/CheckStatus` |
|
|
555
|
+
| Faceswap | `/api/face-swap/create-poll`, `/api/common/get` |
|
|
556
|
+
| Upscaler | `/api/v1/r/image-enhance/create`, `/api/v1/r/image-enhance/result` |
|
|
557
|
+
| Fandom Wiki | `/api.php` |
|
|
558
|
+
| WordPress search fixtures | `/wp-admin/admin-ajax.php` |
|
|
559
|
+
|
|
560
|
+
`examples/alllexamp.ts` starts the mock server, sets `process.env.WSPER_MOCK_BASE_URL`, routes direct scraper demos to the mock server with `http: { allowPrivateNetwork: true }` where needed, runs individual example files as subprocesses, then closes the mock server.
|
|
561
|
+
|
|
562
|
+
This keeps example validation safer and more repeatable. It avoids making every test depend on live third-party services, Cloudflare-protected HTML pages, or rate-limited AI tool endpoints.
|
|
563
|
+
|
|
564
|
+
## Running Examples
|
|
565
|
+
|
|
566
|
+
Run all direct scraper demos and individual example files:
|
|
1427
567
|
|
|
1428
|
-
```
|
|
1429
|
-
|
|
1430
|
-
output: { type: "image", format: "png", path: "./out.png" }
|
|
1431
|
-
output: { type: "image", format: "jpg" }
|
|
1432
|
-
output: { type: "image", format: "webp" }
|
|
1433
|
-
|
|
1434
|
-
// GIF
|
|
1435
|
-
output: { type: "gif", path: "./out.gif" }
|
|
1436
|
-
|
|
1437
|
-
// Video
|
|
1438
|
-
output: { type: "video", format: "mp4", path: "./out.mp4" }
|
|
1439
|
-
output: { type: "video", format: "webm", path: "./out.webm" }
|
|
1440
|
-
output: { type: "video", format: "mp4", codec: "libx264", path: "./out.mp4" }
|
|
568
|
+
```bash
|
|
569
|
+
npx tsx examples/alllexamp.ts
|
|
1441
570
|
```
|
|
1442
571
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
---
|
|
1446
|
-
|
|
1447
|
-
## Chart Image Generator
|
|
1448
|
-
|
|
1449
|
-
Generator chart analytics berbasis canvas. Default-nya memakai model poster vintage dari `analytics-stat-image`, tetapi tersedia beberapa model dan bisa di-override per render.
|
|
1450
|
-
|
|
1451
|
-
```ts
|
|
1452
|
-
import {
|
|
1453
|
-
ChartGenerator,
|
|
1454
|
-
generateAnalyticsStatsImage,
|
|
1455
|
-
getAnalyticsChartModels,
|
|
1456
|
-
} from "wsper";
|
|
1457
|
-
|
|
1458
|
-
const chart = new ChartGenerator();
|
|
1459
|
-
|
|
1460
|
-
await chart.generateStatsImage({
|
|
1461
|
-
model: "modern-dashboard",
|
|
1462
|
-
title: "Group Analytics Report",
|
|
1463
|
-
subtitle: "Monthly and weekly usage overview",
|
|
1464
|
-
monthly: [
|
|
1465
|
-
{ label: "JAN", group: 1200, user: 810 },
|
|
1466
|
-
{ label: "FEB", group: 980, user: 760 },
|
|
1467
|
-
{ label: "MAR", group: 1600, user: 920 },
|
|
1468
|
-
],
|
|
1469
|
-
weekly: [
|
|
1470
|
-
{ label: "MON", group: 320, user: 120 },
|
|
1471
|
-
{ label: "TUE", group: 210, user: 90 },
|
|
1472
|
-
{ label: "WED", group: 450, user: 170 },
|
|
1473
|
-
],
|
|
1474
|
-
output: "./analytics.png",
|
|
1475
|
-
});
|
|
1476
|
-
|
|
1477
|
-
await generateAnalyticsStatsImage({ model: "compact-card" });
|
|
1478
|
-
|
|
1479
|
-
console.log(getAnalyticsChartModels());
|
|
1480
|
-
```
|
|
572
|
+
The runner writes scraper JSON results to `downloads/output/scrapers/` and subprocess logs to `downloads/output/examples/`.
|
|
1481
573
|
|
|
1482
|
-
|
|
574
|
+
Summary fields:
|
|
1483
575
|
|
|
1484
|
-
|
|
|
1485
|
-
|
|
1486
|
-
| `
|
|
1487
|
-
| `
|
|
1488
|
-
| `
|
|
1489
|
-
| `dark-neon` | dark analytics | preview sosial atau tema gelap |
|
|
1490
|
-
| `compact-card` | kartu ringkas | thumbnail dan summary kecil |
|
|
576
|
+
| Field | Meaning |
|
|
577
|
+
| --- | --- |
|
|
578
|
+
| `OK` | The scraper/example returned a successful result. |
|
|
579
|
+
| `FAIL` | The scraper/example returned a failed response or subprocess exit. Auth-required scrapers may fail without valid credentials. |
|
|
580
|
+
| `SKIP` | The runner intentionally skipped an entry, usually because a local fixture is unavailable. |
|
|
1491
581
|
|
|
1492
|
-
|
|
582
|
+
Recorded walkthrough result:
|
|
1493
583
|
|
|
1494
|
-
```
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
weeklyChart: "radial-bars",
|
|
1500
|
-
showAnnotations: true,
|
|
1501
|
-
theme: {
|
|
1502
|
-
background: "#ffffff",
|
|
1503
|
-
groupLine: "#111827",
|
|
1504
|
-
userLine: "#0f766e",
|
|
1505
|
-
},
|
|
1506
|
-
},
|
|
1507
|
-
});
|
|
584
|
+
```txt
|
|
585
|
+
OK : 83
|
|
586
|
+
FAIL : 0
|
|
587
|
+
SKIP : 2
|
|
588
|
+
Total: 85
|
|
1508
589
|
```
|
|
1509
590
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
---
|
|
591
|
+
Individual examples can also be run directly:
|
|
1513
592
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
593
|
+
```bash
|
|
594
|
+
npx tsx examples/lyrics.example.ts "after hours the weeknd"
|
|
595
|
+
npx tsx examples/wallpaper.example.ts cyberpunk
|
|
596
|
+
npx tsx examples/stalk.example.ts axios
|
|
597
|
+
npx tsx examples/mediafire.example.ts "https://www.mediafire.com/file/ipnyzofjcwri357/test-10mb.bin/file"
|
|
598
|
+
npx tsx examples/upscaler.example.ts testassets/photo.jpg
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
For scrapers that support cookies or credentials, use only accounts and sessions you are authorized to access. Do not hardcode real cookies, tokens, client secrets, or API keys in source files.
|
|
602
|
+
|
|
603
|
+
## Running Tests and Validation
|
|
604
|
+
|
|
605
|
+
Package scripts from `package.json`:
|
|
606
|
+
|
|
607
|
+
| Command | Purpose |
|
|
608
|
+
| --- | --- |
|
|
609
|
+
| `npm run test` | Run all Vitest tests once. |
|
|
610
|
+
| `npm run test:watch` | Run Vitest in watch mode. |
|
|
611
|
+
| `npm run typecheck` | Run TypeScript with `--noEmit`. |
|
|
612
|
+
| `npm run build` | Build production ESM output through `script/build.mjs`. |
|
|
613
|
+
| `npm run build:dev` | Build development output. |
|
|
614
|
+
| `npm run build:prod` | Build production output. |
|
|
615
|
+
| `npm run build:bytecode` | Build bytecode output with `script/build-bytecode.mjs`. |
|
|
616
|
+
| `npm run build:all` | Build production output and bytecode. |
|
|
617
|
+
| `npm run test:instagram` | Run Instagram tests only. |
|
|
618
|
+
| `npm run test:spotify` | Run Spotify tests only. |
|
|
619
|
+
| `npm run test:youtube` | Run YouTube tests only. |
|
|
620
|
+
| `npm run test:threads` | Run Threads tests only. |
|
|
621
|
+
| `npm run test:pinterest` | Run Pinterest tests only. |
|
|
622
|
+
| `npm run test:brat` | Run Brat tests only. |
|
|
623
|
+
|
|
624
|
+
Recommended validation before publishing or changing behavior:
|
|
1517
625
|
|
|
1518
|
-
```
|
|
1519
|
-
|
|
1520
|
-
|
|
626
|
+
```bash
|
|
627
|
+
npm run typecheck
|
|
628
|
+
npm run test
|
|
629
|
+
npm run build
|
|
630
|
+
npx tsx examples/alllexamp.ts
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Project Structure
|
|
634
|
+
|
|
635
|
+
```txt
|
|
636
|
+
src/
|
|
637
|
+
index.ts Public package exports
|
|
638
|
+
WsperScraper.ts Aggregate scraper entrypoint
|
|
639
|
+
core/
|
|
640
|
+
credentials/ Credential normalization and platform headers
|
|
641
|
+
error/ WsperError, ValidationError, HttpError, ParseError, DownloadError, ScraperError
|
|
642
|
+
http/ HTTP client, retries, timeouts, safe URL validation
|
|
643
|
+
parser/ Shared HTML and JSON parser helpers
|
|
644
|
+
queue/ Request pacing and concurrency control
|
|
645
|
+
modules/
|
|
646
|
+
brat/ Brat image/GIF/video generator and converters
|
|
647
|
+
chart/ Analytics image generator
|
|
648
|
+
download/ Safe downloader primitives
|
|
649
|
+
scrapers/ Platform-specific scraper implementations
|
|
650
|
+
types/ Shared response, option, and common types
|
|
651
|
+
utils/ Sleep, URL, validation, browser-profile, and helper utilities
|
|
652
|
+
examples/
|
|
653
|
+
alllexamp.ts Full example runner
|
|
654
|
+
mock-server.ts Local deterministic mock server
|
|
655
|
+
*.example.ts Individual runnable examples
|
|
656
|
+
tests/
|
|
657
|
+
*/*.test.ts Unit and parser tests
|
|
658
|
+
dist/ Build output only; do not edit manually
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Environment Variables
|
|
662
|
+
|
|
663
|
+
These variables appear in the repository:
|
|
664
|
+
|
|
665
|
+
| Variable | Used by | Required? | Notes |
|
|
666
|
+
| --- | --- | --- | --- |
|
|
667
|
+
| `WSPER_MOCK_BASE_URL` | `examples/alllexamp.ts`, AI tool examples | No | Set by the all-examples runner for subprocesses. Points examples to the local mock server. |
|
|
668
|
+
| `BMKG_WARNING_API_KEY` | `examples/cuaca.example.ts` | No | Optional warning API key passed to `CuacaScraper({ warningApiKey })`. |
|
|
669
|
+
| `INSTAGRAM_COOKIE` | `examples/instagram.example.ts` comments | No | Optional example input for constructor credentials. Use `<YOUR_COOKIE_HERE>` style placeholders in docs and never commit real cookies. |
|
|
670
|
+
| `INSTAGRAM_CSRF_TOKEN` | `examples/instagram.example.ts` comments | No | Optional example input for constructor credentials. |
|
|
671
|
+
| `BILI_COOKIE` | `examples/bilibili.example.ts` | No | Optional BiliBili cookie for authenticated stream access. |
|
|
672
|
+
| `WSPER_COOKIE` | `tests/core/credentials.test.ts` | No | Test-only variable proving runtime credential resolution does not read env credentials automatically. |
|
|
673
|
+
|
|
674
|
+
Credential configuration is constructor-based. The library does not rely on `.env` files for runtime credentials.
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
import { WsperScraper } from "wsper-js";
|
|
1521
678
|
|
|
1522
|
-
// 2. Custom credential per platform
|
|
1523
679
|
const wsper = new WsperScraper({
|
|
1524
680
|
spotifyCredentials: {
|
|
1525
|
-
clientId:
|
|
681
|
+
clientId: "your-client-id",
|
|
1526
682
|
clientSecret: "your-client-secret",
|
|
1527
683
|
},
|
|
1528
684
|
credentials: {
|
|
685
|
+
cai: {
|
|
686
|
+
bearerToken: "<YOUR_TOKEN_HERE>",
|
|
687
|
+
},
|
|
1529
688
|
twitter: {
|
|
1530
|
-
cookie:
|
|
1531
|
-
csrfToken: "
|
|
689
|
+
cookie: "<YOUR_COOKIE_HERE>",
|
|
690
|
+
csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
|
|
1532
691
|
},
|
|
1533
|
-
|
|
1534
|
-
cookie:
|
|
1535
|
-
csrfToken: "
|
|
692
|
+
instagram: {
|
|
693
|
+
cookie: "<YOUR_COOKIE_HERE>",
|
|
694
|
+
csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
|
|
1536
695
|
},
|
|
1537
696
|
},
|
|
1538
697
|
});
|
|
1539
698
|
```
|
|
1540
699
|
|
|
1541
|
-
|
|
700
|
+
Spotify custom credentials must include both `clientId` and `clientSecret`. Partial custom Spotify credentials are rejected with `ValidationError`.
|
|
1542
701
|
|
|
1543
|
-
|
|
702
|
+
## Cloudflare, Rate Limits, and Reliability
|
|
1544
703
|
|
|
1545
|
-
|
|
704
|
+
Some websites protect HTML pages with Cloudflare, require active authenticated sessions, or apply strict rate limits. `wsper-js` favors public APIs when they are available and documented by implementation, such as LRCLIB, Wallhaven, Fandom MediaWiki, Spotify, BMKG, BiliBili, and npm registry endpoints.
|
|
1546
705
|
|
|
1547
|
-
|
|
1548
|
-
interface HttpOptions {
|
|
1549
|
-
timeoutMs?: number; // default 30000
|
|
1550
|
-
retries?: number; // default 3
|
|
1551
|
-
retryDelayMs?: number;
|
|
1552
|
-
maxRetryAfterMs?: number;
|
|
1553
|
-
maxRedirects?: number;
|
|
1554
|
-
headers?: Record<string, string>;
|
|
1555
|
-
userAgent?: string;
|
|
1556
|
-
}
|
|
706
|
+
The mock server exists to make tests and examples deterministic. It should be used for local validation of rate-limited, Cloudflare-protected, or external-service-dependent flows instead of repeatedly hitting live services.
|
|
1557
707
|
|
|
1558
|
-
|
|
1559
|
-
concurrency?: number; // default 1
|
|
1560
|
-
intervalMs?: number;
|
|
1561
|
-
intervalCap?: number;
|
|
1562
|
-
minDelayMs?: number;
|
|
1563
|
-
maxDelayMs?: number;
|
|
1564
|
-
}
|
|
1565
|
-
```
|
|
708
|
+
This project does not provide CAPTCHA bypass logic, credential harvesting, session theft, anti-bot evasion, or rate-limit abuse. Use cookies only for accounts and sessions you legitimately control, respect platform terms, and keep request pacing conservative.
|
|
1566
709
|
|
|
1567
|
-
|
|
710
|
+
## Contributing
|
|
1568
711
|
|
|
1569
|
-
```
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
712
|
+
```bash
|
|
713
|
+
git clone <repository-url>
|
|
714
|
+
cd wsper
|
|
715
|
+
npm install
|
|
716
|
+
npm run typecheck
|
|
717
|
+
npm run test
|
|
1574
718
|
```
|
|
1575
719
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
## Download Outputs
|
|
720
|
+
When adding or changing a scraper:
|
|
1579
721
|
|
|
1580
|
-
|
|
722
|
+
1. Keep scraper-specific logic under `src/scrapers/<name>/`.
|
|
723
|
+
2. Export the scraper from `src/scrapers/<name>/index.ts` and `src/scrapers/index.ts`.
|
|
724
|
+
3. Return typed `WsperResponse<T>` results.
|
|
725
|
+
4. Keep HTTP, parsing, queueing, and file download responsibilities separated.
|
|
726
|
+
5. Add or update parser and scraper tests under `tests/`.
|
|
727
|
+
6. Add or update a runnable example under `examples/`.
|
|
728
|
+
7. Use the mock server for flows that should not depend on live third-party behavior in default tests.
|
|
729
|
+
8. Update this README when public API, usage, behavior, examples, or validation results change.
|
|
1581
730
|
|
|
1582
|
-
|
|
1583
|
-
downloads/
|
|
1584
|
-
├── instagram/
|
|
1585
|
-
│ └── charli_xcx/
|
|
1586
|
-
│ ├── post_AbCdEfG_0.mp4
|
|
1587
|
-
│ ├── post_AbCdEfG_1.jpg
|
|
1588
|
-
│ └── metadata.json
|
|
1589
|
-
├── spotify/
|
|
1590
|
-
│ └── track_4iV5W9.mp3
|
|
1591
|
-
├── youtube/
|
|
1592
|
-
│ └── video_dQw4w9.mp4
|
|
1593
|
-
├── threads/
|
|
1594
|
-
│ └── post_CgXxXxX_0.mp4
|
|
1595
|
-
└── brat/
|
|
1596
|
-
├── brat.png
|
|
1597
|
-
├── brat-green.png
|
|
1598
|
-
├── brat.gif
|
|
1599
|
-
├── brat.mp4
|
|
1600
|
-
└── brat.sticker.webp
|
|
1601
|
-
```
|
|
731
|
+
## Security
|
|
1602
732
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
npx tsx examples/threads.example.ts <username-or-url> <post-url>
|
|
1609
|
-
npx tsx examples/twitter.example.ts <tweet-url> <username-or-url>
|
|
1610
|
-
npx tsx examples/brat.example.ts
|
|
1611
|
-
```
|
|
733
|
+
- Validate and normalize user-provided URLs before requesting them.
|
|
734
|
+
- Use only `http:` and `https:` unless a module explicitly supports something else.
|
|
735
|
+
- Keep SSRF protections enabled; private network requests require explicit `allowPrivateNetwork: true` and should be reserved for local mocks or trusted endpoints.
|
|
736
|
+
- Do not log secrets, cookies, client secrets, authorization headers, access tokens, refresh tokens, or raw credential objects.
|
|
737
|
+
- Do not commit credentials, cookies, tokens, private fixtures, or real session material.
|
|
1612
738
|
|
|
1613
|
-
##
|
|
739
|
+
## License
|
|
1614
740
|
|
|
1615
|
-
|
|
1616
|
-
kalo ada bug gabung aja ke trus bilang ke ke yang buat lib
|
|
741
|
+
GPL-3.0-or-later. See `LICENSE` for the full license text.
|