wsper-js 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1616 +1,679 @@
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
- ```
82
-
83
- Dependency eksternal yang dibutuhkan untuk fitur tertentu:
84
-
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` |
2
+ <h1 align="center">
3
+ <img alt="ShikanokoBail banner" src="https://i.pinimg.com/736x/0c/ff/62/0cff624a04a81495f4b8e69bcedd34aa.jpg" width="100%"/>
4
+ </h1>
89
5
 
90
- ---
6
+ <div align="center">
91
7
 
92
- ## Quick Start
93
-
94
- ```ts
95
- import { WsperScraper, BratGenerator, ChartGenerator } from "wsper";
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-314%20passed-brightgreen)
12
+ ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)
13
+ </div>
96
14
 
97
- // Semua scraper dalam satu kelas
98
- const wsper = new WsperScraper();
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.
99
17
 
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/...");
18
+ The package is ESM-only and targets Node.js 18 or newer.
103
19
 
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
- });
118
- ```
119
-
120
- ---
121
-
122
- ## WsperScraper — Kelas Utama
123
-
124
- ```ts
125
- import { WsperScraper } from "wsper";
126
-
127
- const wsper = new WsperScraper(config?: WsperScraperConfig);
128
- ```
129
-
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
- }
151
- ```
152
-
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 |
163
-
164
- ---
165
-
166
- ## Response Structure
167
-
168
- Semua scraper mengembalikan `WsperResponse<T>`:
169
-
170
- ```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
- ```
20
+ ## Features
183
21
 
184
- **Pattern penggunaan:**
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.
185
32
 
186
- ```ts
187
- const res = await wsper.instagram.getProfile("username");
33
+ ## Installation
188
34
 
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);
194
- }
35
+ ```bash
36
+ npm install wsper-js
195
37
  ```
196
38
 
197
- ---
198
-
199
- ## Scrapers
200
-
201
- ---
202
-
203
- ### Instagram
204
-
205
- ```ts
206
- import { InstagramScraper } from "wsper";
207
- const ig = new InstagramScraper(options?: InstagramScraperOptions);
39
+ ```bash
40
+ pnpm add wsper-js
208
41
  ```
209
42
 
210
- #### `getProfile(username)` → `WsperResponse<InstagramProfile>`
211
-
212
- ```ts
213
- await ig.getProfile("charli_xcx")
43
+ ```bash
44
+ yarn add wsper-js
214
45
  ```
215
46
 
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
- ```
47
+ Some optional capabilities need external binaries:
232
48
 
233
- #### `getFeed(username, options?)` `WsperResponse<InstagramFeed>`
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 |
234
53
 
235
- ```ts
236
- await ig.getFeed("charli_xcx", { count: 12, maxId: "cursor" })
237
- ```
54
+ ## Quick Start
238
55
 
239
56
  ```ts
240
- interface InstagramFeed {
241
- items: InstagramMediaItem[];
242
- nextMaxId: string | null;
243
- hasMore: boolean;
244
- }
57
+ import { LyricsScraper } from "wsper-js";
245
58
 
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
- }
59
+ const lyrics = new LyricsScraper();
60
+ const result = await lyrics.search("after hours the weeknd");
259
61
 
260
- interface InstagramCarouselItem {
261
- id: string;
262
- mediaType: number;
263
- thumbnailUrl: string | null;
264
- videoUrl: string | null;
62
+ if (!result.ok) {
63
+ throw new Error(`${result.error?.code}: ${result.error?.message}`);
265
64
  }
266
- ```
267
-
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
- ```
274
65
 
275
- #### `getProfileWithFeed(username, options?)` → `WsperResponse<InstagramProfileWithFeed>`
276
-
277
- ```ts
278
- await ig.getProfileWithFeed("charli_xcx", { count: 6 })
279
- ```
280
-
281
- ```ts
282
- interface InstagramProfileWithFeed {
283
- profile: InstagramProfile;
284
- items: InstagramMediaItem[];
285
- nextMaxId: string | null;
286
- hasMore: boolean;
287
- }
66
+ console.log(result.statusCode);
67
+ console.log(result.data?.title);
68
+ console.log(result.data?.lyrics);
288
69
  ```
289
70
 
290
- #### `downloadPost(input)` `WsperResponse<InstagramDownloadResult>`
71
+ You can also use `WsperScraper` for the aggregate social/media scraper entrypoint:
291
72
 
292
73
  ```ts
293
- await ig.downloadPost({
294
- postUrl: "https://www.instagram.com/p/AbCdEfG/",
295
- outputDir: "./downloads/instagram",
296
- includeMetadata: true,
297
- })
298
- ```
74
+ import { WsperScraper } from "wsper-js";
299
75
 
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
- }>;
311
- }
312
- ```
76
+ const wsper = new WsperScraper({
77
+ queue: { concurrency: 1, minDelayMs: 500, maxDelayMs: 1500 },
78
+ });
313
79
 
314
- #### `downloadProfile(input)` `WsperResponse<InstagramDownloadResult>`
80
+ const track = await wsper.spotify.search("never gonna give you up", { limit: 3 });
81
+ const video = await wsper.youtube.getVideo("dQw4w9WgXcQ");
315
82
 
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
- })
83
+ console.log(track.ok, video.ok);
325
84
  ```
326
85
 
327
- ---
86
+ `WsperScraper` currently exposes `spotify`, `twitter`, `threads`, `instagram`, `pinterest`, and `youtube`. Other scrapers are exported as named classes.
328
87
 
329
- ### Spotify
88
+ ## Response Shape
330
89
 
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
- ```
90
+ Scraper methods return `WsperResponse<TData>`:
342
91
 
343
92
  ```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;
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;
362
103
  }
363
104
 
364
- // Meta tambahan untuk track
365
- interface SpotifyTrackMeta extends WsperResponseMeta {
366
- externalSourceUrl: string | null;
105
+ export interface WsperResponseMeta {
106
+ statusCode: number;
107
+ sourceUrl: string;
108
+ fetchedAt: string;
109
+ durationMs: number;
367
110
  }
368
111
  ```
369
112
 
370
- #### `getAlbum(input, options?)` → `WsperResponse<SpotifyAlbumWithTracks>`
371
-
372
- ```ts
373
- await spotify.getAlbum("https://open.spotify.com/album/...")
374
- ```
113
+ Recommended handling:
375
114
 
376
115
  ```ts
377
- interface SpotifyAlbumWithTracks {
378
- album: NormalizedSpotifyAlbum;
379
- tracks: NormalizedSpotifyTrack[];
380
- }
116
+ const response = await scraper.search("query");
381
117
 
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;
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);
395
122
  }
396
123
  ```
397
124
 
398
- #### `getPlaylist(input)` → `WsperResponse<NormalizedSpotifyPlaylist>`
399
-
400
- ```ts
401
- await spotify.getPlaylist("https://open.spotify.com/playlist/...")
402
- ```
403
-
404
- #### `search(query, options?)` → `WsperResponse<SpotifySearchResult>`
405
-
406
- ```ts
407
- await spotify.search("charli xcx brat", { limit: 10, type: ["track", "album"] })
408
- ```
125
+ ## Usage Examples
409
126
 
410
- #### `downloadPost(input)` / `downloadProfile(input)`
127
+ The examples below use public exports from `src/index.ts` and representative response fields from implementation and tests.
411
128
 
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
- ```
129
+ ### LyricsScraper
420
130
 
421
- #### OAuth helpers
131
+ Source: LRCLIB JSON API at `https://lrclib.net/api/search`.
422
132
 
423
133
  ```ts
424
- spotify.createAuthUrl({ state: "xyz", scopes: ["user-read-private"] })
425
- // → { state: string; url: string }
134
+ import { LyricsScraper } from "wsper-js";
426
135
 
427
- await spotify.exchangeCode(code) // → SpotifyOAuthToken
428
- await spotify.refreshToken(token) // SpotifyOAuthToken
429
- await spotify.getAccessToken() // → string
430
- spotify.invalidateToken() // → void
431
- ```
432
-
433
- ---
136
+ const scraper = new LyricsScraper();
137
+ const result = await scraper.search("after hours the weeknd");
434
138
 
435
- ### YouTube
436
-
437
- ```ts
438
- import { YouTubeScraper } from "wsper";
439
- const yt = new YouTubeScraper(options?: YouTubeScraperOptions);
139
+ console.log(result.data);
440
140
  ```
441
141
 
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
- }
451
- ```
452
-
453
- #### `getVideo(input)` → `WsperResponse<YoutubeVideoMetadata>`
454
-
455
- ```ts
456
- await yt.getVideo("https://youtu.be/dQw4w9WgXcQ")
457
- ```
142
+ Representative output:
458
143
 
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;
144
+ ```json
145
+ {
146
+ "title": "After Hours",
147
+ "lyrics": "Oh, baby, where are you now?",
148
+ "link": "https://lrclib.net/api/get/98765"
490
149
  }
491
150
  ```
492
151
 
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
- ---
152
+ ### WallpaperScraper
723
153
 
724
- ### Minecraft Addon
154
+ Source: Wallhaven search API at `https://wallhaven.cc/api/v1/search`.
725
155
 
726
156
  ```ts
727
- import { McAddonScraper } from "wsper";
728
- const mc = new McAddonScraper();
157
+ import { WallpaperScraper } from "wsper-js";
729
158
 
730
- await mc.search("texture pack")
731
- // WsperResponse<McAddonSearchItem[]>
159
+ const scraper = new WallpaperScraper();
160
+ const result = await scraper.search("cyberpunk");
732
161
 
733
- await mc.getDetail("https://mcpedl.com/addon/...")
734
- // → WsperResponse<McAddonDetail>
735
- // data: { title, description, downloadLinks[], images[], author }
162
+ console.log(result.data?.total);
163
+ console.log(result.data?.results[0]);
736
164
  ```
737
165
 
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>
166
+ Representative output:
773
167
 
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
- ---
799
-
800
- ### Drakor
801
-
802
- ```ts
803
- import { DrakorScraper } from "wsper";
804
- const drakor = new DrakorScraper();
805
-
806
- await drakor.search("undercover")
807
- // -> WsperResponse<DrakorList>
808
-
809
- await drakor.detail("undercover-miss-hong")
810
- // -> WsperResponse<DrakorDetail>
811
-
812
- await drakor.ongoing()
813
- // -> WsperResponse<DrakorList>
814
-
815
- await drakor.getAll(1)
816
- // -> WsperResponse<DrakorList>
817
- ```
818
-
819
- ---
820
-
821
- ### Dramabox
822
-
823
- ```ts
824
- import { DramaboxScraper } from "wsper";
825
- const dramabox = new DramaboxScraper();
826
-
827
- await dramabox.search("romance")
828
- // -> WsperResponse<DramaboxResult>
829
- ```
830
-
831
- ---
832
-
833
- ### SakuraNovel
834
-
835
- ```ts
836
- import { SakuraNovelScraper } from "wsper";
837
- const sakura = new SakuraNovelScraper();
838
-
839
- await sakura.search("isekai")
840
- // -> WsperResponse<SakuraNovelSearchItem[]>
841
-
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 }
860
- ```
861
-
862
- ---
863
-
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>
872
- ```
873
-
874
- ---
875
-
876
- ### IkiruManga
877
-
878
- ```ts
879
- import { IkiruMangaScraper } from "wsper";
880
- const ikiru = new IkiruMangaScraper();
881
-
882
- await ikiru.search("solo leveling")
883
- // -> WsperResponse<IkiruMangaResult>
884
- ```
885
-
886
- ---
887
-
888
- ### Wallpaper
889
-
890
- ```ts
891
- import { WallpaperScraper } from "wsper";
892
- const wallpaper = new WallpaperScraper();
893
-
894
- await wallpaper.search("blue sky")
895
- // -> WsperResponse<WallpaperResult>
896
- ```
897
-
898
- ---
899
-
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>
908
- ```
909
-
910
- ---
911
-
912
- ### Videy
913
-
914
- ```ts
915
- import { VideyScraper } from "wsper";
916
- const videy = new VideyScraper();
917
-
918
- await videy.upload("./video.mp4")
919
- // -> WsperResponse<VideyUploadResult>
920
- ```
921
-
922
- ---
923
-
924
- ### OCR
925
-
926
- ```ts
927
- import { OcrScraper } from "wsper";
928
- const ocr = new OcrScraper();
929
-
930
- await ocr.scan(imageBuffer)
931
- // -> WsperResponse<OcrScanResult>
932
- ```
933
-
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>
944
-
945
- await converter.toPng("https://example.com/image.webp")
946
- // -> WsperResponse<Webp2Mp4Result>
947
- ```
948
-
949
- ---
950
-
951
- ### MConverter
952
-
953
- ```ts
954
- import { MConverterScraper } from "wsper";
955
- const converter = new MConverterScraper();
956
-
957
- await converter.getTargets("image.webp")
958
- // -> WsperResponse<MConverterTargetsResult>
959
-
960
- await converter.convertBuffer(imageBuffer, "image.webp", "png")
961
- // -> WsperResponse<MConverterConvertResult>
168
+ ```json
169
+ {
170
+ "total": 1,
171
+ "results": [
172
+ {
173
+ "title": "Wallpaper sky123",
174
+ "resolution": "1920x1080",
175
+ "image": "https://w.wallhaven.cc/full/sky123.jpg",
176
+ "page": "https://wallhaven.cc/w/sky123"
177
+ }
178
+ ]
179
+ }
962
180
  ```
963
181
 
964
- ---
182
+ ### WwCharScraper
965
183
 
966
- ### HtmlToJpg
184
+ Source: Wuthering Waves Fandom MediaWiki parse API.
967
185
 
968
186
  ```ts
969
- import { HtmlToJpgScraper } from "wsper";
970
- const converter = new HtmlToJpgScraper();
971
-
972
- await converter.convertBuffer(htmlBuffer, "page.html")
973
- // -> WsperResponse<HtmlToJpgResult>
974
- ```
187
+ import { WwCharScraper } from "wsper-js";
975
188
 
976
- ---
189
+ const scraper = new WwCharScraper();
190
+ const result = await scraper.getCharacter("Jin Hsi");
977
191
 
978
- ### Stalk
979
-
980
- ```ts
981
- import { StalkScraper } from "wsper";
982
- const stalk = new StalkScraper();
983
-
984
- await stalk.getNpmPackage("wsper")
985
- // -> WsperResponse<NpmPackage>
192
+ console.log(result.data);
986
193
  ```
987
194
 
988
- ---
989
-
990
- ### ModAndroid
195
+ Representative output:
991
196
 
992
- ```ts
993
- import { ModAndroidScraper } from "wsper";
994
- const modAndroid = new ModAndroidScraper();
995
-
996
- await modAndroid.aptoide("maps")
997
- // -> WsperResponse<AptoideResult>
197
+ ```json
198
+ {
199
+ "title": "Jiyan",
200
+ "slug": "Jin_Hsi",
201
+ "url": "https://wutheringwaves.fandom.com/wiki/Jin_Hsi",
202
+ "bio": "Bio karakter.",
203
+ "profile": {},
204
+ "images": []
205
+ }
998
206
  ```
999
207
 
1000
- ---
208
+ ### HokInfoScraper
1001
209
 
1002
- ### Upscaler
210
+ Source: Honor of Kings Fandom MediaWiki parse API.
1003
211
 
1004
212
  ```ts
1005
- import { UpscalerScraper } from "wsper";
1006
- const upscaler = new UpscalerScraper();
213
+ import { HokInfoScraper } from "wsper-js";
1007
214
 
1008
- await upscaler.upscaleBuffer(imageBuffer, "image/jpeg")
1009
- // -> WsperResponse<UpscalerResult>
1010
- ```
215
+ const scraper = new HokInfoScraper();
216
+ const result = await scraper.getCharacter("Angela");
1011
217
 
1012
- ---
1013
-
1014
- ### Image
1015
-
1016
- ```ts
1017
- import { ImageScraper } from "wsper";
1018
- const image = new ImageScraper();
1019
-
1020
- await image.safebooru("blue_sky", 0, 10)
1021
- // -> WsperResponse<ImageSearchResult>
218
+ console.log(result.data?.profile);
1022
219
  ```
1023
220
 
1024
- ---
1025
-
1026
- ### ImgUpscaler
1027
-
1028
- ```ts
1029
- import { ImgUpscalerScraper } from "wsper";
1030
- const upscaler = new ImgUpscalerScraper();
221
+ Representative output:
1031
222
 
1032
- await upscaler.upscaleBuffer(imageBuffer, "image.jpg", 2)
1033
- // -> WsperResponse<ImgUpscalerResult>
223
+ ```json
224
+ {
225
+ "title": "Sun Wukong",
226
+ "image": null,
227
+ "profile": {
228
+ "Role": "Fighter"
229
+ },
230
+ "bio": "Bio singkat karakter.",
231
+ "skills": [],
232
+ "lore": null,
233
+ "url": "https://honor-of-kings.fandom.com/wiki/Sun%20Wukong"
234
+ }
1034
235
  ```
1035
236
 
1036
- ---
237
+ ### CapCutScraper
1037
238
 
1038
- ### Faceswap
239
+ Source: WordPress-style resolver endpoint, defaulting to `https://capdownloader.com/wp-json/aio-dl/video-data/`.
1039
240
 
1040
241
  ```ts
1041
- import { FaceswapScraper } from "wsper";
1042
- const faceswap = new FaceswapScraper();
1043
-
1044
- await faceswap.process(sourceImageBuffer, targetImageBuffer)
1045
- // -> WsperResponse<FaceswapResult>
1046
- ```
1047
-
1048
- ---
242
+ import { CapCutScraper } from "wsper-js";
1049
243
 
1050
- ### PhotoAi
1051
-
1052
- ```ts
1053
- import { PhotoAiScraper } from "wsper";
1054
- const photoAi = new PhotoAiScraper();
244
+ const scraper = new CapCutScraper();
245
+ const result = await scraper.download("https://www.capcut.com/t/Zs82gHj1a/");
1055
246
 
1056
- await photoAi.uploadBuffer(imageBuffer, "image.jpg")
1057
- // -> WsperResponse<PhotoAiUploadResult>
247
+ console.log(result.data);
1058
248
  ```
1059
249
 
1060
- ---
250
+ Representative output:
1061
251
 
1062
- ### Lyrics
1063
-
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 }
252
+ ```json
253
+ {
254
+ "videoUrl": "https://cdn.example/video.mp4"
255
+ }
1071
256
  ```
1072
257
 
1073
- ---
258
+ ### ImgUpscalerScraper
1074
259
 
1075
- ### Resep
260
+ Source: `https://get1.imglarger.com` upload and status endpoints.
1076
261
 
1077
262
  ```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[] }
1084
- ```
263
+ import { readFile } from "node:fs/promises";
264
+ import { ImgUpscalerScraper } from "wsper-js";
1085
265
 
1086
- ---
266
+ const image = await readFile("./photo.jpg");
267
+ const scraper = new ImgUpscalerScraper();
268
+ const result = await scraper.upscaleBuffer(image, "photo.jpg", 4);
1087
269
 
1088
- ### Top Anime
1089
-
1090
- ```ts
1091
- import { TopAnimeScraper } from "wsper";
1092
- const anime = new TopAnimeScraper();
1093
-
1094
- await anime.getTopAnime(25)
1095
- // → WsperResponse<TopAnimeItem[]>
1096
- // data[]: { rank, title, url, imageUrl, score, type, episodes, members }
270
+ console.log(result.data);
1097
271
  ```
1098
272
 
1099
- ---
1100
-
1101
- ### Anime Quote
273
+ Representative output:
1102
274
 
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 }
1110
- ```
1111
-
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 }
1123
-
1124
- console.log(ANIME_CHARACTERS);
1125
- // ["nezuko", "zero-two", "rem", "miku", ...]
275
+ ```json
276
+ {
277
+ "originalPath": null,
278
+ "outputPath": null,
279
+ "resultUrl": "https://cdn.test/out.jpg",
280
+ "scale": 4
281
+ }
1126
282
  ```
1127
283
 
1128
- ---
1129
-
1130
- ## Brat Generator
284
+ ### PhotoAiScraper
1131
285
 
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.
286
+ Source: `https://photoai.imglarger.com`.
1133
287
 
1134
288
  ```ts
1135
- import { BratGenerator, generateBrat, bratGenerator } from "wsper";
1136
-
1137
- const brat = new BratGenerator();
1138
-
1139
- // Atau gunakan singleton global:
1140
- // bratGenerator.generate(...)
1141
-
1142
- // Atau shorthand function:
1143
- // await generateBrat(config)
1144
- ```
1145
-
1146
- ---
289
+ import { readFile } from "node:fs/promises";
290
+ import { PhotoAiScraper } from "wsper-js";
1147
291
 
1148
- ### generate Image
292
+ const image = await readFile("./portrait.jpg");
293
+ const scraper = new PhotoAiScraper();
1149
294
 
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
- });
1160
-
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
295
+ const upload = await scraper.uploadBuffer(image, "portrait.jpg");
296
+ if (upload.ok && upload.data !== null) {
297
+ const status = await scraper.checkStatus(upload.data.code);
298
+ console.log(status.data);
1166
299
  }
1167
300
  ```
1168
301
 
1169
- **Format yang didukung:** `png`, `jpg`, `webp`
1170
-
1171
- ---
1172
-
1173
- ### generate — GIF
1174
-
1175
- ```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
- });
1192
-
1193
- // result.buffer → Buffer GIF
1194
- // result.format → "gif"
1195
- ```
1196
-
1197
- ---
1198
-
1199
- ### generate — Video
302
+ Representative output:
1200
303
 
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",
1210
- },
1211
- });
1212
-
1213
- // result.buffer → Buffer MP4/WebM
1214
- // result.format → "mp4" | "webm"
304
+ ```json
305
+ {
306
+ "status": "success",
307
+ "downloadUrl": "https://cdn.test/out.jpg",
308
+ "raw": {
309
+ "status": "success",
310
+ "downloadUrl": "https://cdn.test/out.jpg"
311
+ }
312
+ }
1215
313
  ```
1216
314
 
1217
- > Membutuhkan FFmpeg (`C:\ffmpeg\ffmpeg.exe` atau di PATH).
315
+ ### FaceswapScraper
1218
316
 
1219
- ---
1220
-
1221
- ### withPreset
1222
-
1223
- Terapkan style preset ke config:
317
+ Source: `https://api.lovefaceswap.com`.
1224
318
 
1225
319
  ```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
- ---
320
+ import { readFile } from "node:fs/promises";
321
+ import { FaceswapScraper } from "wsper-js";
1242
322
 
1243
- ### imageToSticker
323
+ const [source, target] = await Promise.all([
324
+ readFile("./source-face.jpg"),
325
+ readFile("./target.jpg"),
326
+ ]);
1244
327
 
1245
- Konversi gambar apa pun menjadi sticker format Telegram/WhatsApp (512×512 WebP):
328
+ const scraper = new FaceswapScraper();
329
+ const result = await scraper.process(source, target);
1246
330
 
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
331
+ console.log(result.data);
1268
332
  ```
1269
333
 
1270
- ---
1271
-
1272
- ### MP4 ↔ GIF Conversion
1273
-
1274
- #### GIF → MP4
334
+ Representative output:
1275
335
 
1276
- ```ts
1277
- const mp4Buffer = await brat.gifToMp4("./brat.gif", "./brat.mp4", {
1278
- fps: 24,
1279
- width: 720, // optional resize
1280
- });
1281
- // → Buffer MP4
336
+ ```json
337
+ {
338
+ "job_id": "job-1",
339
+ "image": "https://cdn.test/out.jpg"
340
+ }
1282
341
  ```
1283
342
 
1284
- #### GIF → WebM
1285
-
1286
- ```ts
1287
- const webmBuffer = await brat.gifToWebm("./brat.gif", "./brat.webm", { fps: 24 });
1288
- // → Buffer WebM
1289
- ```
343
+ ### UpscalerScraper
1290
344
 
1291
- #### MP4 → GIF
345
+ Source: `https://aienhancer.ai`.
1292
346
 
1293
347
  ```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
1300
- });
1301
- // → Buffer GIF (two-pass palette encoding untuk kualitas warna lebih baik)
1302
- ```
348
+ import { readFile } from "node:fs/promises";
349
+ import { UpscalerScraper } from "wsper-js";
1303
350
 
1304
- ---
351
+ const image = await readFile("./photo.jpg");
352
+ const scraper = new UpscalerScraper();
353
+ const result = await scraper.upscaleBuffer(image, "image/jpeg");
1305
354
 
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
355
+ console.log(result.data);
1314
356
  ```
1315
357
 
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
- }
358
+ Representative output:
1333
359
 
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)
360
+ ```json
361
+ {
362
+ "id": "task-1",
363
+ "input": "https://cdn.test/in.jpg",
364
+ "output": "https://cdn.test/out.jpg"
1342
365
  }
1343
366
  ```
1344
367
 
1345
- ---
368
+ ### StalkScraper
1346
369
 
1347
- ### Text Config
370
+ Source: npm registry API at `https://registry.npmjs.org`.
1348
371
 
1349
372
  ```ts
1350
- text: {
1351
- value: "teks yang ingin ditampilkan",
373
+ import { StalkScraper } from "wsper-js";
1352
374
 
1353
- align: "justify", // "left" | "right" | "center" | "justify"
375
+ const scraper = new StalkScraper();
376
+ const result = await scraper.getNpmPackage("axios");
1354
377
 
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"
378
+ console.log(result.data?.title);
379
+ console.log(result.data?.install);
380
+ ```
1361
381
 
1362
- lineHeight: 1.05, // default 1.05 — rapat khas brat style
1363
- color: "#111111",
382
+ Representative output:
1364
383
 
1365
- blur: 2, // blur teks 0–8 (0 = tidak ada, default 0)
384
+ ```json
385
+ {
386
+ "title": "@scope/pkg",
387
+ "language": "",
388
+ "publish": "2026-05-01T00:00:00.000Z",
389
+ "readme": "",
390
+ "explore": "https://www.npmjs.com/package/@scope/pkg",
391
+ "dependencies": "0",
392
+ "dependents": "0",
393
+ "version_count": "1",
394
+ "keywords": [],
395
+ "install": "npm install @scope/pkg",
396
+ "info": [],
397
+ "collaborator": []
1366
398
  }
1367
399
  ```
1368
400
 
1369
- ---
401
+ ### MediafireScraper
1370
402
 
1371
- ### Canvas Config
403
+ Source: Mediafire public file page HTML.
1372
404
 
1373
405
  ```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
- ```
406
+ import { MediafireScraper } from "wsper-js";
1393
407
 
1394
- **Padding:**
408
+ const scraper = new MediafireScraper();
409
+ const result = await scraper.getLink(
410
+ "https://www.mediafire.com/file/ipnyzofjcwri357/test-10mb.bin/file",
411
+ );
1395
412
 
1396
- ```ts
1397
- layout: { padding: 64 }
1398
- layout: { padding: { top: 80, right: 64, bottom: 80, left: 64 } }
413
+ console.log(result.data);
1399
414
  ```
1400
415
 
1401
- ---
416
+ Representative output:
1402
417
 
1403
- ### Animation Config
1404
-
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
418
+ ```json
419
+ {
420
+ "downloadUrl": "https://download.example/myfile.zip",
421
+ "fileName": "My_File.zip",
422
+ "fileSize": "50 MB",
423
+ "fileType": "ZIP"
1415
424
  }
1416
425
  ```
1417
426
 
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
- ---
427
+ ## Available Scrapers
428
+
429
+ | Scraper | Purpose | Source/API | Auth/Cookie required? | Example file | Notes |
430
+ | --- | --- | --- | --- | --- | --- |
431
+ | `AlkitabScraper` | Bible verse search | `alkitab.me` | No | `examples/alkitab.example.ts` | `search(query)` |
432
+ | `AnimeQuoteScraper` | Random anime quote | `otakotaku.com` | No | `examples/anime-quote.example.ts` | `getRandom()` |
433
+ | `AnimeRandomScraper` | Random anime character image | GitHub raw anime dataset | No | `examples/anime-random.example.ts` | `getImage(character)`, `random()` |
434
+ | `BiliBiliScraper` | BiliBili search and video info | `api.bilibili.com` | Optional cookie | `examples/bilibili.example.ts` | Cookie may unlock authenticated stream access |
435
+ | `BMKGScraper` | Indonesian earthquake and weather feeds | `data.bmkg.go.id`, `nowcasting.bmkg.go.id` | No | `examples/bmkg.example.ts` | Autogempa, gempa dirasakan, nowcasting, forecast |
436
+ | `CapCutScraper` | Resolve CapCut template video URL | `capdownloader.com/wp-json/aio-dl/video-data/` | No | `examples/capcut.example.ts` | Mocked in example runner |
437
+ | `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 |
438
+ | `DrakorScraper` | Korean drama search/list/detail | `drakorkita30.kita.baby` | No | `examples/drakor.example.ts` | `search`, `detail`, `ongoing`, `getAll` |
439
+ | `DramaboxScraper` | Dramabox search | `dramabox.com` | No | `examples/dramabox.example.ts` | `search(query)` |
440
+ | `FaceswapScraper` | Face-swap image processing | `api.lovefaceswap.com` | No | `examples/faceswap.example.ts` | Mocked in example runner |
441
+ | `HokInfoScraper` | Honor of Kings character info | Fandom MediaWiki parse API | No | `examples/hok-info.example.ts` | Uses `api.php?action=parse` |
442
+ | `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 |
443
+ | `IkiruMangaScraper` | Manga search | `02.ikiru.wtf` | No | `examples/ikiru-manga.example.ts` | Mock server fallback available |
444
+ | `ImageScraper` | Safebooru image search | `safebooru.org` | No | `examples/image.example.ts` | Current site type: `safebooru` |
445
+ | `ImgUpscalerScraper` | Image upscaling | `get1.imglarger.com` | No | `examples/img-upscaler.example.ts` | Mocked in example runner |
446
+ | `InstagramScraper` | Profile, feed, post, download | `instagram.com` web/API endpoints | Internal defaults; custom cookie supported | `examples/instagram.example.ts` | Use only legitimate session cookies |
447
+ | `KomikindoScraper` | Manga search/detail | `komikindo.ch` | No | `examples/komikindo.example.ts` | `search`, `getDetail` |
448
+ | `LyricsScraper` | Lyrics search | `lrclib.net` JSON API | No | `examples/lyrics.example.ts` | Replaced blocked HTML scraping with API integration |
449
+ | `MConverterScraper` | File conversion helpers | `mconverter.eu` | No | `examples/mconverter.example.ts` | `getTargets`, `convert`, `convertBuffer` |
450
+ | `McAddonScraper` | Minecraft addon search/detail | `mmcreviews.com` | No | `examples/mcaddon.example.ts` | `search`, `getDetail`, `getAddon` |
451
+ | `MediafireScraper` | Resolve Mediafire download link | Mediafire public HTML page | No | `examples/mediafire.example.ts` | Default example uses active 10MB test file |
452
+ | `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` |
453
+ | `OcrScraper` | OCR image scan | `newocr.com` | No | `examples/ocr.example.ts` | File/buffer based |
454
+ | `PhotoAiScraper` | Photo AI upload/status | `photoai.imglarger.com` | No | `examples/photo-ai.example.ts` | Mocked in example runner |
455
+ | `PinterestScraper` | Pin search, detail, download | `pinterest.com` | Internal defaults; custom cookie supported | `examples/pinterest.example.ts` | Supports `credentials` option |
456
+ | `PlayStoreScraper` | Google Play app search | `play.google.com` | No | `examples/playstore.example.ts` | `search(query, limit)` |
457
+ | `ResepScraper` | Recipe search | `cookpad.com` | No | `examples/resep.example.ts` | Returns recipe items |
458
+ | `SakuraNovelScraper` | Novel search/detail/chapter | `sakuranovel.id` | No | `examples/sakura-novel.example.ts` | Mock server fallback available |
459
+ | `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 |
460
+ | `StalkScraper` | npm package metadata lookup | `registry.npmjs.org` | No | `examples/stalk.example.ts` | Default example query is `axios` |
461
+ | `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 |
462
+ | `TopAnimeScraper` | MyAnimeList top anime list | `myanimelist.net` | No | `examples/top-anime.example.ts` | `getTopAnime(limit)` |
463
+ | `TwitterScraper` | Tweet, profile, search, timeline, downloads | `x.com/i/api/graphql` | Cookie/CSRF commonly required | `examples/twitter.example.ts` | Supports `credentials` option |
464
+ | `UguuScraper` | Temporary file upload | `uguu.se/upload` | No | `examples/uguu.example.ts` | `upload(buffer, filename)` |
465
+ | `UpscalerScraper` | Image enhancement | `aienhancer.ai` | No | `examples/upscaler.example.ts` | Rejects remote URL string input |
466
+ | `VideyScraper` | Video upload | `videy.co/api/upload` | No | `examples/videy.example.ts` | `upload`, `uploadBuffer` |
467
+ | `WallpaperScraper` | Wallpaper search | `wallhaven.cc/api/v1/search` | No | `examples/wallpaper.example.ts` | Replaced blocked HTML scraping with API integration |
468
+ | `Webp2Mp4Scraper` | WebP to MP4/PNG conversion | `ezgif.com` | No | `examples/webp2mp4.example.ts` | `toMp4`, `toPng` |
469
+ | `WwCharScraper` | Wuthering Waves character info | Fandom MediaWiki parse API | No | `examples/ww-char.example.ts` | Uses `api.php?action=parse` |
470
+ | `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 |
471
+
472
+ ## What's New
473
+
474
+ The latest scraper reliability pass fixed 12 failing scraper functions and their corresponding tests.
475
+
476
+ - `LyricsScraper` now uses LRCLIB's public JSON API instead of a Cloudflare-protected Musixmatch HTML page.
477
+ - `WallpaperScraper` now uses Wallhaven's public search API instead of a Cloudflare-protected Wallpaperflare HTML page.
478
+ - `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.
479
+ - `examples/mock-server.ts` now covers WordPress resolver routes, AI tool polling/upload routes, and Fandom API responses.
480
+ - `examples/alllexamp.ts` starts and stops the mock server during the runner lifecycle.
481
+ - `WSPER_MOCK_BASE_URL` is injected into spawned example subprocesses so individual examples can use the local mock endpoint automatically.
482
+ - Default example inputs were updated: `StalkScraper` uses `axios`, `MediafireScraper` uses an active 10MB test file URL, and `HokInfoScraper` uses `Angela`.
483
+ - Verification recorded in `walkthrough.md`: 82 test files passed, 314 tests passed, and the examples runner reported 83 OK, 0 FAIL, 2 SKIP.
484
+
485
+ ## Mock Server and Testing
486
+
487
+ `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.
488
+
489
+ Currently mocked routes include:
490
+
491
+ | Area | Routes |
492
+ | --- | --- |
493
+ | CapCut | `/wp-json/aio-dl/video-data/` |
494
+ | ImgUpscaler | `/api/UpscalerNew/UploadNew`, `/api/UpscalerNew/CheckStatusNew` |
495
+ | PhotoAi | `/api/PhoAi/Upload`, `/api/PhoAi/CheckStatus` |
496
+ | Faceswap | `/api/face-swap/create-poll`, `/api/common/get` |
497
+ | Upscaler | `/api/v1/r/image-enhance/create`, `/api/v1/r/image-enhance/result` |
498
+ | Fandom Wiki | `/api.php` |
499
+ | WordPress search fixtures | `/wp-admin/admin-ajax.php` |
500
+
501
+ `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.
502
+
503
+ 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.
504
+
505
+ ## Running Examples
506
+
507
+ Run all direct scraper demos and individual example files:
1425
508
 
1426
- ### Output Config
1427
-
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" }
509
+ ```bash
510
+ npx tsx examples/alllexamp.ts
1441
511
  ```
1442
512
 
1443
- > `path` opsional. Jika tidak diisi, file tidak disimpan ke disk hanya `buffer` yang dikembalikan di `BratResult`.
1444
-
1445
- ---
513
+ The runner writes scraper JSON results to `downloads/output/scrapers/` and subprocess logs to `downloads/output/examples/`.
1446
514
 
1447
- ## Chart Image Generator
515
+ Summary fields:
1448
516
 
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
- });
517
+ | Field | Meaning |
518
+ | --- | --- |
519
+ | `OK` | The scraper/example returned a successful result. |
520
+ | `FAIL` | The scraper/example returned a failed response or subprocess exit. Auth-required scrapers may fail without valid credentials. |
521
+ | `SKIP` | The runner intentionally skipped an entry, usually because a local fixture is unavailable. |
1476
522
 
1477
- await generateAnalyticsStatsImage({ model: "compact-card" });
523
+ Recorded walkthrough result:
1478
524
 
1479
- console.log(getAnalyticsChartModels());
525
+ ```txt
526
+ OK : 83
527
+ FAIL : 0
528
+ SKIP : 2
529
+ Total: 85
1480
530
  ```
1481
531
 
1482
- Model bawaan:
532
+ Individual examples can also be run directly:
1483
533
 
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 |
1491
-
1492
- Kustomisasi fleksibel:
1493
-
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
- });
1508
- ```
1509
-
1510
- `monthly` dan `weekly` opsional. Jika tidak diisi, generator memakai data demo internal agar output tetap valid.
1511
-
1512
- ---
1513
-
1514
- ## Credentials
1515
-
1516
- Credential tidak dibaca dari `.env`. Dua cara konfigurasi:
534
+ ```bash
535
+ npx tsx examples/lyrics.example.ts "after hours the weeknd"
536
+ npx tsx examples/wallpaper.example.ts cyberpunk
537
+ npx tsx examples/stalk.example.ts axios
538
+ npx tsx examples/mediafire.example.ts "https://www.mediafire.com/file/ipnyzofjcwri357/test-10mb.bin/file"
539
+ npx tsx examples/upscaler.example.ts testassets/photo.jpg
540
+ ```
541
+
542
+ 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.
543
+
544
+ ## Running Tests and Validation
545
+
546
+ Package scripts from `package.json`:
547
+
548
+ | Command | Purpose |
549
+ | --- | --- |
550
+ | `npm run test` | Run all Vitest tests once. |
551
+ | `npm run test:watch` | Run Vitest in watch mode. |
552
+ | `npm run typecheck` | Run TypeScript with `--noEmit`. |
553
+ | `npm run build` | Build production ESM output through `script/build.mjs`. |
554
+ | `npm run build:dev` | Build development output. |
555
+ | `npm run build:prod` | Build production output. |
556
+ | `npm run build:bytecode` | Build bytecode output with `script/build-bytecode.mjs`. |
557
+ | `npm run build:all` | Build production output and bytecode. |
558
+ | `npm run test:instagram` | Run Instagram tests only. |
559
+ | `npm run test:spotify` | Run Spotify tests only. |
560
+ | `npm run test:youtube` | Run YouTube tests only. |
561
+ | `npm run test:threads` | Run Threads tests only. |
562
+ | `npm run test:pinterest` | Run Pinterest tests only. |
563
+ | `npm run test:brat` | Run Brat tests only. |
564
+
565
+ Recommended validation before publishing or changing behavior:
1517
566
 
1518
- ```ts
1519
- // 1. Tanpa credential → library pakai internal defaults
1520
- const wsper = new WsperScraper();
567
+ ```bash
568
+ npm run typecheck
569
+ npm run test
570
+ npm run build
571
+ npx tsx examples/alllexamp.ts
572
+ ```
573
+
574
+ ## Project Structure
575
+
576
+ ```txt
577
+ src/
578
+ index.ts Public package exports
579
+ WsperScraper.ts Aggregate scraper entrypoint
580
+ core/
581
+ credentials/ Credential normalization and platform headers
582
+ error/ WsperError, ValidationError, HttpError, ParseError, DownloadError, ScraperError
583
+ http/ HTTP client, retries, timeouts, safe URL validation
584
+ parser/ Shared HTML and JSON parser helpers
585
+ queue/ Request pacing and concurrency control
586
+ modules/
587
+ brat/ Brat image/GIF/video generator and converters
588
+ chart/ Analytics image generator
589
+ download/ Safe downloader primitives
590
+ scrapers/ Platform-specific scraper implementations
591
+ types/ Shared response, option, and common types
592
+ utils/ Sleep, URL, validation, browser-profile, and helper utilities
593
+ examples/
594
+ alllexamp.ts Full example runner
595
+ mock-server.ts Local deterministic mock server
596
+ *.example.ts Individual runnable examples
597
+ tests/
598
+ */*.test.ts Unit and parser tests
599
+ dist/ Build output only; do not edit manually
600
+ ```
601
+
602
+ ## Environment Variables
603
+
604
+ These variables appear in the repository:
605
+
606
+ | Variable | Used by | Required? | Notes |
607
+ | --- | --- | --- | --- |
608
+ | `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. |
609
+ | `BMKG_WARNING_API_KEY` | `examples/cuaca.example.ts` | No | Optional warning API key passed to `CuacaScraper({ warningApiKey })`. |
610
+ | `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. |
611
+ | `INSTAGRAM_CSRF_TOKEN` | `examples/instagram.example.ts` comments | No | Optional example input for constructor credentials. |
612
+ | `BILI_COOKIE` | `examples/bilibili.example.ts` | No | Optional BiliBili cookie for authenticated stream access. |
613
+ | `WSPER_COOKIE` | `tests/core/credentials.test.ts` | No | Test-only variable proving runtime credential resolution does not read env credentials automatically. |
614
+
615
+ Credential configuration is constructor-based. The library does not rely on `.env` files for runtime credentials.
616
+
617
+ ```ts
618
+ import { WsperScraper } from "wsper-js";
1521
619
 
1522
- // 2. Custom credential per platform
1523
620
  const wsper = new WsperScraper({
1524
621
  spotifyCredentials: {
1525
- clientId: "your-client-id",
622
+ clientId: "your-client-id",
1526
623
  clientSecret: "your-client-secret",
1527
624
  },
1528
625
  credentials: {
1529
626
  twitter: {
1530
- cookie: "auth_token=xxx; ct0=yyy",
1531
- csrfToken: "yyy",
627
+ cookie: "<YOUR_COOKIE_HERE>",
628
+ csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
1532
629
  },
1533
- threads: {
1534
- cookie: "sessionid=xxx",
1535
- csrfToken: "zzz",
630
+ instagram: {
631
+ cookie: "<YOUR_COOKIE_HERE>",
632
+ csrfToken: "<YOUR_CSRF_TOKEN_HERE>",
1536
633
  },
1537
634
  },
1538
635
  });
1539
636
  ```
1540
637
 
1541
- > Jangan pernah commit cookie, token, atau secret ke repository.
638
+ Spotify custom credentials must include both `clientId` and `clientSecret`. Partial custom Spotify credentials are rejected with `ValidationError`.
1542
639
 
1543
- ---
640
+ ## Cloudflare, Rate Limits, and Reliability
1544
641
 
1545
- ## HTTP & Queue Options
642
+ 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
643
 
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
- }
644
+ 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
645
 
1558
- interface QueueOptions {
1559
- concurrency?: number; // default 1
1560
- intervalMs?: number;
1561
- intervalCap?: number;
1562
- minDelayMs?: number;
1563
- maxDelayMs?: number;
1564
- }
1565
- ```
646
+ 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
647
 
1567
- Contoh konfigurasi rate-limit:
648
+ ## Contributing
1568
649
 
1569
- ```ts
1570
- const wsper = new WsperScraper({
1571
- http: { timeoutMs: 20000, retries: 2 },
1572
- queue: { concurrency: 1, minDelayMs: 3000, maxDelayMs: 7000 },
1573
- });
650
+ ```bash
651
+ git clone <repository-url>
652
+ cd wsper
653
+ npm install
654
+ npm run typecheck
655
+ npm run test
1574
656
  ```
1575
657
 
1576
- ---
1577
-
1578
- ## Download Outputs
658
+ When adding or changing a scraper:
1579
659
 
1580
- File yang didownload disimpan ke direktori yang ditentukan di tiap method:
660
+ 1. Keep scraper-specific logic under `src/scrapers/<name>/`.
661
+ 2. Export the scraper from `src/scrapers/<name>/index.ts` and `src/scrapers/index.ts`.
662
+ 3. Return typed `WsperResponse<T>` results.
663
+ 4. Keep HTTP, parsing, queueing, and file download responsibilities separated.
664
+ 5. Add or update parser and scraper tests under `tests/`.
665
+ 6. Add or update a runnable example under `examples/`.
666
+ 7. Use the mock server for flows that should not depend on live third-party behavior in default tests.
667
+ 8. Update this README when public API, usage, behavior, examples, or validation results change.
1581
668
 
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
- ```
669
+ ## Security
1602
670
 
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
- ```
671
+ - Validate and normalize user-provided URLs before requesting them.
672
+ - Use only `http:` and `https:` unless a module explicitly supports something else.
673
+ - Keep SSRF protections enabled; private network requests require explicit `allowPrivateNetwork: true` and should be reserved for local mocks or trusted endpoints.
674
+ - Do not log secrets, cookies, client secrets, authorization headers, access tokens, refresh tokens, or raw credential objects.
675
+ - Do not commit credentials, cookies, tokens, private fixtures, or real session material.
1612
676
 
1613
- ## READ THIS INFORMATION
677
+ ## License
1614
678
 
1615
- baca rek!!
1616
- kalo ada bug gabung aja ke trus bilang ke ke yang buat lib
679
+ GPL-3.0-or-later. See `LICENSE` for the full license text.