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 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
- ```bash
80
- npm install wsper
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
- Dependency eksternal yang dibutuhkan untuk fitur tertentu:
6
+ <div align="center">
84
7
 
85
- | Fitur | Dependency |
86
- |---|---|
87
- | YouTube download / audio | `yt-dlp` (install terpisah) |
88
- | Video export, GIF ↔ MP4 | `ffmpeg` di PATH atau `C:\ffmpeg\ffmpeg.exe` |
8
+ [![npm](https://img.shields.io/npm/v/wsper-js?label=npm)](https://www.npmjs.com/package/wsper-js)
9
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue)
10
+ ![Node](https://img.shields.io/badge/Node-%3E%3D18.0.0-green)
11
+ ![Tests](https://img.shields.io/badge/tests-383%20passed-brightgreen)
12
+ ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)
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
- ## Quick Start
18
+ The package is ESM-only and targets Node.js 18 or newer.
93
19
 
94
- ```ts
95
- import { WsperScraper, BratGenerator, ChartGenerator } from "wsper";
20
+ ## Features
96
21
 
97
- // Semua scraper dalam satu kelas
98
- const wsper = new WsperScraper();
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
- const profile = await wsper.instagram.getProfile("charli_xcx");
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
- // Brat Generator
105
- const brat = new BratGenerator();
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
- ### WsperScraperConfig
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
- ### Properties
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
- ## Response Structure
167
-
168
- Semua scraper mengembalikan `WsperResponse<T>`:
54
+ ## Quick Start
169
55
 
170
56
  ```ts
171
- interface WsperResponse<T, Meta = WsperResponseMeta> {
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
- ```ts
187
- const res = await wsper.instagram.getProfile("username");
59
+ const lyrics = new LyricsScraper();
60
+ const result = await lyrics.search("after hours the weeknd");
188
61
 
189
- if (res.ok) {
190
- console.log(res.data.username);
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
- ## Scrapers
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
- ```ts
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
- await ig.getFeed("charli_xcx", { count: 12, maxId: "cursor" })
237
- ```
74
+ import { WsperScraper } from "wsper-js";
238
75
 
239
- ```ts
240
- interface InstagramFeed {
241
- items: InstagramMediaItem[];
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
- interface InstagramMediaItem {
247
- id: string;
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
- interface InstagramCarouselItem {
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
- #### `getPost(input)` `WsperResponse<InstagramMediaItem>`
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
- #### `getProfileWithFeed(username, options?)` → `WsperResponse<InstagramProfileWithFeed>`
88
+ ## Response Shape
276
89
 
277
- ```ts
278
- await ig.getProfileWithFeed("charli_xcx", { count: 6 })
279
- ```
90
+ Scraper methods return `WsperResponse<TData>`:
280
91
 
281
92
  ```ts
282
- interface InstagramProfileWithFeed {
283
- profile: InstagramProfile;
284
- items: InstagramMediaItem[];
285
- nextMaxId: string | null;
286
- hasMore: boolean;
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
- ```ts
301
- interface InstagramDownloadResult {
302
- sourceUrl: string;
303
- outputDir: string;
304
- metadataPath: string | null;
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
- #### `downloadProfile(input)` → `WsperResponse<InstagramDownloadResult>`
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
- interface NormalizedSpotifyTrack {
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
- // Meta tambahan untuk track
365
- interface SpotifyTrackMeta extends WsperResponseMeta {
366
- externalSourceUrl: string | null;
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
- #### `getAlbum(input, options?)` → `WsperResponse<SpotifyAlbumWithTracks>`
125
+ ## Usage Examples
371
126
 
372
- ```ts
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
- interface NormalizedSpotifyAlbum {
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
- #### `getPlaylist(input)` `WsperResponse<NormalizedSpotifyPlaylist>`
131
+ Some scrapers need live public targets. The bundled examples avoid dummy URLs:
399
132
 
400
- ```ts
401
- await spotify.getPlaylist("https://open.spotify.com/playlist/...")
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
- #### `search(query, options?)` `WsperResponse<SpotifySearchResult>`
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
- #### `downloadPost(input)` / `downloadProfile(input)`
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
- ```ts
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
- #### OAuth helpers
146
+ Source: LRCLIB JSON API at `https://lrclib.net/api/search`.
422
147
 
423
148
  ```ts
424
- spotify.createAuthUrl({ state: "xyz", scopes: ["user-read-private"] })
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
- ```ts
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
- #### `getVideo(input)` → `WsperResponse<YoutubeVideoMetadata>`
157
+ Representative output:
454
158
 
455
- ```ts
456
- await yt.getVideo("https://youtu.be/dQw4w9WgXcQ")
457
- ```
458
-
459
- ```ts
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
- #### `searchVideos(query, options?)` → `WsperResponse<YoutubeSearchResult>`
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
- ### Drakor
169
+ Source: Wallhaven search API at `https://wallhaven.cc/api/v1/search`.
801
170
 
802
171
  ```ts
803
- import { DrakorScraper } from "wsper";
804
- const drakor = new DrakorScraper();
805
-
806
- await drakor.search("undercover")
807
- // -> WsperResponse<DrakorList>
172
+ import { WallpaperScraper } from "wsper-js";
808
173
 
809
- await drakor.detail("undercover-miss-hong")
810
- // -> WsperResponse<DrakorDetail>
174
+ const scraper = new WallpaperScraper();
175
+ const result = await scraper.search("cyberpunk");
811
176
 
812
- await drakor.ongoing()
813
- // -> WsperResponse<DrakorList>
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
- ```ts
824
- import { DramaboxScraper } from "wsper";
825
- const dramabox = new DramaboxScraper();
826
-
827
- await dramabox.search("romance")
828
- // -> WsperResponse<DramaboxResult>
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
- ### SakuraNovel
199
+ Source: Wuthering Waves Fandom MediaWiki parse API.
834
200
 
835
201
  ```ts
836
- import { SakuraNovelScraper } from "wsper";
837
- const sakura = new SakuraNovelScraper();
202
+ import { WwCharScraper } from "wsper-js";
838
203
 
839
- await sakura.search("isekai")
840
- // -> WsperResponse<SakuraNovelSearchItem[]>
204
+ const scraper = new WwCharScraper();
205
+ const result = await scraper.getCharacter("Jin Hsi");
841
206
 
842
- await sakura.getDetail("/series/example/")
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
- ### HokInfo
865
-
866
- ```ts
867
- import { HokInfoScraper } from "wsper";
868
- const hok = new HokInfoScraper();
869
-
870
- await hok.getCharacter("Sun Wukong")
871
- // -> WsperResponse<HokInfoResult>
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
- ### IkiruManga
225
+ Source: Honor of Kings Fandom MediaWiki parse API.
877
226
 
878
227
  ```ts
879
- import { IkiruMangaScraper } from "wsper";
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
- ### Wallpaper
230
+ const scraper = new HokInfoScraper();
231
+ const result = await scraper.getCharacter("Angela");
889
232
 
890
- ```ts
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
- ### WwChar
901
-
902
- ```ts
903
- import { WwCharScraper } from "wsper";
904
- const ww = new WwCharScraper();
905
-
906
- await ww.getCharacter("Jin Hsi")
907
- // -> WsperResponse<WwCharResult>
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
- ### Videy
254
+ Source: WordPress-style resolver endpoint, defaulting to `https://capdownloader.com/wp-json/aio-dl/video-data/`.
913
255
 
914
256
  ```ts
915
- import { VideyScraper } from "wsper";
916
- const videy = new VideyScraper();
257
+ import { CapCutScraper } from "wsper-js";
917
258
 
918
- await videy.upload("./video.mp4")
919
- // -> WsperResponse<VideyUploadResult>
920
- ```
921
-
922
- ---
259
+ const scraper = new CapCutScraper();
260
+ const result = await scraper.download("https://www.capcut.com/t/Zs82gHj1a/");
923
261
 
924
- ### OCR
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
- await converter.toPng("https://example.com/image.webp")
946
- // -> WsperResponse<Webp2Mp4Result>
267
+ ```json
268
+ {
269
+ "videoUrl": "https://cdn.example/video.mp4"
270
+ }
947
271
  ```
948
272
 
949
- ---
273
+ ### ImgUpscalerScraper
950
274
 
951
- ### MConverter
275
+ Source: `https://get1.imglarger.com` upload and status endpoints.
952
276
 
953
277
  ```ts
954
- import { MConverterScraper } from "wsper";
955
- const converter = new MConverterScraper();
278
+ import { readFile } from "node:fs/promises";
279
+ import { ImgUpscalerScraper } from "wsper-js";
956
280
 
957
- await converter.getTargets("image.webp")
958
- // -> WsperResponse<MConverterTargetsResult>
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
- await converter.convertBuffer(imageBuffer, "image.webp", "png")
961
- // -> WsperResponse<MConverterConvertResult>
285
+ console.log(result.data);
962
286
  ```
963
287
 
964
- ---
965
-
966
- ### HtmlToJpg
288
+ Representative output:
967
289
 
968
- ```ts
969
- import { HtmlToJpgScraper } from "wsper";
970
- const converter = new HtmlToJpgScraper();
971
-
972
- await converter.convertBuffer(htmlBuffer, "page.html")
973
- // -> WsperResponse<HtmlToJpgResult>
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
- ### Stalk
301
+ Source: `https://photoai.imglarger.com`.
979
302
 
980
303
  ```ts
981
- import { StalkScraper } from "wsper";
982
- const stalk = new StalkScraper();
304
+ import { readFile } from "node:fs/promises";
305
+ import { PhotoAiScraper } from "wsper-js";
983
306
 
984
- await stalk.getNpmPackage("wsper")
985
- // -> WsperResponse<NpmPackage>
986
- ```
987
-
988
- ---
307
+ const image = await readFile("./portrait.jpg");
308
+ const scraper = new PhotoAiScraper();
989
309
 
990
- ### ModAndroid
991
-
992
- ```ts
993
- import { ModAndroidScraper } from "wsper";
994
- const modAndroid = new ModAndroidScraper();
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
- ```ts
1005
- import { UpscalerScraper } from "wsper";
1006
- const upscaler = new UpscalerScraper();
1007
-
1008
- await upscaler.upscaleBuffer(imageBuffer, "image/jpeg")
1009
- // -> WsperResponse<UpscalerResult>
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
- ### Image
332
+ Source: `https://api.lovefaceswap.com`.
1015
333
 
1016
334
  ```ts
1017
- import { ImageScraper } from "wsper";
1018
- const image = new ImageScraper();
335
+ import { readFile } from "node:fs/promises";
336
+ import { FaceswapScraper } from "wsper-js";
1019
337
 
1020
- await image.safebooru("blue_sky", 0, 10)
1021
- // -> WsperResponse<ImageSearchResult>
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
- await upscaler.upscaleBuffer(imageBuffer, "image.jpg", 2)
1033
- // -> WsperResponse<ImgUpscalerResult>
346
+ console.log(result.data);
1034
347
  ```
1035
348
 
1036
- ---
349
+ Representative output:
1037
350
 
1038
- ### Faceswap
1039
-
1040
- ```ts
1041
- import { FaceswapScraper } from "wsper";
1042
- const faceswap = new FaceswapScraper();
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
- ### PhotoAi
360
+ Source: `https://aienhancer.ai`.
1051
361
 
1052
362
  ```ts
1053
- import { PhotoAiScraper } from "wsper";
1054
- const photoAi = new PhotoAiScraper();
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
- ### Lyrics
366
+ const image = await readFile("./photo.jpg");
367
+ const scraper = new UpscalerScraper();
368
+ const result = await scraper.upscaleBuffer(image, "image/jpeg");
1063
369
 
1064
- ```ts
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
- ```ts
1078
- import { ResepScraper } from "wsper";
1079
- const resep = new ResepScraper();
1080
-
1081
- await resep.search("nasi goreng")
1082
- // → WsperResponse<ResepItem[]>
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
- ### Top Anime
385
+ Source: npm registry API at `https://registry.npmjs.org`.
1089
386
 
1090
387
  ```ts
1091
- import { TopAnimeScraper } from "wsper";
1092
- const anime = new TopAnimeScraper();
388
+ import { StalkScraper } from "wsper-js";
1093
389
 
1094
- await anime.getTopAnime(25)
1095
- // WsperResponse<TopAnimeItem[]>
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
- ### Anime Quote
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
- console.log(ANIME_CHARACTERS);
1125
- // ["nezuko", "zero-two", "rem", "miku", ...]
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
- Generator konten visual bergaya **brat** — teks besar di atas background minimalis. Bisa diekspor sebagai gambar statis, GIF animasi kata per kata, atau video MP4/WebM.
418
+ Source: Mediafire public file page HTML.
1133
419
 
1134
420
  ```ts
1135
- import { BratGenerator, generateBrat, bratGenerator } from "wsper";
421
+ import { MediafireScraper } from "wsper-js";
1136
422
 
1137
- const brat = new BratGenerator();
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
- // Atau gunakan singleton global:
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
- // → BratResult
1162
- interface BratResult {
1163
- buffer: Buffer; // konten file dalam memory
1164
- format: string; // "png" | "jpg" | "webp"
1165
- path?: string; // path file jika output.path diisi
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
- **Format yang didukung:** `png`, `jpg`, `webp`
1170
-
1171
- ---
442
+ ### CaiScraper
1172
443
 
1173
- ### generate GIF
444
+ Source: Character.AI WebSocket & API integrations.
1174
445
 
1175
446
  ```ts
1176
- const result = await brat.generate({
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
- // result.buffer Buffer GIF
1194
- // result.format → "gif"
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
- // result.buffer Buffer MP4/WebM
1214
- // result.format "mp4" | "webm"
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
- ```ts
1287
- const webmBuffer = await brat.gifToWebm("./brat.gif", "./brat.webm", { fps: 24 });
1288
- // → Buffer WebM
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
- // → Buffer GIF (two-pass palette encoding untuk kualitas warna lebih baik)
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
- ### Text Config
1348
-
1349
- ```ts
1350
- text: {
1351
- value: "teks yang ingin ditampilkan",
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
- ```ts
1406
- animation: {
1407
- enabled: true,
1408
- direction: "left-to-right", // "left-to-right" | "right-to-left"
1409
- mode: "word", // "word" | "line" | "character"
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
- | Mode | Deskripsi |
1419
- |---|---|
1420
- | `word` | Kata muncul satu per satu cocok untuk brat GIF |
1421
- | `line` | Baris muncul satu per satu lebih dramatis |
1422
- | `character` | Huruf muncul satu per satu paling smooth |
1423
-
1424
- ---
1425
-
1426
- ### Output Config
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
- ```ts
1429
- // Image
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
- > `path` opsional. Jika tidak diisi, file tidak disimpan ke disk hanya `buffer` yang dikembalikan di `BratResult`.
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
- Model bawaan:
574
+ Summary fields:
1483
575
 
1484
- | Model | Layout | Cocok untuk |
1485
- |---|---|---|
1486
- | `vintage-poster` | poster statistik vintage | report visual penuh |
1487
- | `modern-dashboard` | dashboard KPI | ringkasan operasional |
1488
- | `minimal-report` | report bersih | dokumen atau lampiran |
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
- Kustomisasi fleksibel:
582
+ Recorded walkthrough result:
1493
583
 
1494
- ```ts
1495
- await generateAnalyticsStatsImage({
1496
- model: "modern-dashboard",
1497
- modelOverrides: {
1498
- monthlyChart: "grouped-bars",
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
- `monthly` dan `weekly` opsional. Jika tidak diisi, generator memakai data demo internal agar output tetap valid.
1511
-
1512
- ---
591
+ Individual examples can also be run directly:
1513
592
 
1514
- ## Credentials
1515
-
1516
- Credential tidak dibaca dari `.env`. Dua cara konfigurasi:
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
- ```ts
1519
- // 1. Tanpa credential → library pakai internal defaults
1520
- const wsper = new WsperScraper();
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: "your-client-id",
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: "auth_token=xxx; ct0=yyy",
1531
- csrfToken: "yyy",
689
+ cookie: "<YOUR_COOKIE_HERE>",
690
+ csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
1532
691
  },
1533
- threads: {
1534
- cookie: "sessionid=xxx",
1535
- csrfToken: "zzz",
692
+ instagram: {
693
+ cookie: "<YOUR_COOKIE_HERE>",
694
+ csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
1536
695
  },
1537
696
  },
1538
697
  });
1539
698
  ```
1540
699
 
1541
- > Jangan pernah commit cookie, token, atau secret ke repository.
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
- ## HTTP & Queue Options
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
- ```ts
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
- interface QueueOptions {
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
- Contoh konfigurasi rate-limit:
710
+ ## Contributing
1568
711
 
1569
- ```ts
1570
- const wsper = new WsperScraper({
1571
- http: { timeoutMs: 20000, retries: 2 },
1572
- queue: { concurrency: 1, minDelayMs: 3000, maxDelayMs: 7000 },
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
- File yang didownload disimpan ke direktori yang ditentukan di tiap method:
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
- **CLI examples:**
1604
-
1605
- ```bash
1606
- npx tsx examples/instagram.example.ts <username> 6 <post-url>
1607
- npx tsx examples/spotify.example.ts <track-url> "" "" "" downloads/spotify
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
- ## READ THIS INFORMATION
739
+ ## License
1614
740
 
1615
- baca rek!!
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.