wsper-js 0.1.0

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 ADDED
@@ -0,0 +1,1616 @@
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
+
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` |
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ ```ts
95
+ import { WsperScraper, BratGenerator, ChartGenerator } from "wsper";
96
+
97
+ // Semua scraper dalam satu kelas
98
+ const wsper = new WsperScraper();
99
+
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/...");
103
+
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
+ ```
183
+
184
+ **Pattern penggunaan:**
185
+
186
+ ```ts
187
+ const res = await wsper.instagram.getProfile("username");
188
+
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
+ }
195
+ ```
196
+
197
+ ---
198
+
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")
214
+ ```
215
+
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>`
234
+
235
+ ```ts
236
+ await ig.getFeed("charli_xcx", { count: 12, maxId: "cursor" })
237
+ ```
238
+
239
+ ```ts
240
+ interface InstagramFeed {
241
+ items: InstagramMediaItem[];
242
+ nextMaxId: string | null;
243
+ hasMore: boolean;
244
+ }
245
+
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
+ }
259
+
260
+ interface InstagramCarouselItem {
261
+ id: string;
262
+ mediaType: number;
263
+ thumbnailUrl: string | null;
264
+ videoUrl: string | null;
265
+ }
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
+
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
+ }
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
+
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
+ ```
313
+
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
+ ```
342
+
343
+ ```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
+ }
363
+
364
+ // Meta tambahan untuk track
365
+ interface SpotifyTrackMeta extends WsperResponseMeta {
366
+ externalSourceUrl: string | null;
367
+ }
368
+ ```
369
+
370
+ #### `getAlbum(input, options?)` → `WsperResponse<SpotifyAlbumWithTracks>`
371
+
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
+ }
381
+
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
+ ```
397
+
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
+ ```
409
+
410
+ #### `downloadPost(input)` / `downloadProfile(input)`
411
+
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
+ ```
420
+
421
+ #### OAuth helpers
422
+
423
+ ```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
+ ```
432
+
433
+ ---
434
+
435
+ ### YouTube
436
+
437
+ ```ts
438
+ import { YouTubeScraper } from "wsper";
439
+ const yt = new YouTubeScraper(options?: YouTubeScraperOptions);
440
+ ```
441
+
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
+ ```
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;
490
+ }
491
+ ```
492
+
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
+ ---
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>
962
+ ```
963
+
964
+ ---
965
+
966
+ ### HtmlToJpg
967
+
968
+ ```ts
969
+ import { HtmlToJpgScraper } from "wsper";
970
+ const converter = new HtmlToJpgScraper();
971
+
972
+ await converter.convertBuffer(htmlBuffer, "page.html")
973
+ // -> WsperResponse<HtmlToJpgResult>
974
+ ```
975
+
976
+ ---
977
+
978
+ ### Stalk
979
+
980
+ ```ts
981
+ import { StalkScraper } from "wsper";
982
+ const stalk = new StalkScraper();
983
+
984
+ await stalk.getNpmPackage("wsper")
985
+ // -> WsperResponse<NpmPackage>
986
+ ```
987
+
988
+ ---
989
+
990
+ ### ModAndroid
991
+
992
+ ```ts
993
+ import { ModAndroidScraper } from "wsper";
994
+ const modAndroid = new ModAndroidScraper();
995
+
996
+ await modAndroid.aptoide("maps")
997
+ // -> WsperResponse<AptoideResult>
998
+ ```
999
+
1000
+ ---
1001
+
1002
+ ### Upscaler
1003
+
1004
+ ```ts
1005
+ import { UpscalerScraper } from "wsper";
1006
+ const upscaler = new UpscalerScraper();
1007
+
1008
+ await upscaler.upscaleBuffer(imageBuffer, "image/jpeg")
1009
+ // -> WsperResponse<UpscalerResult>
1010
+ ```
1011
+
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>
1022
+ ```
1023
+
1024
+ ---
1025
+
1026
+ ### ImgUpscaler
1027
+
1028
+ ```ts
1029
+ import { ImgUpscalerScraper } from "wsper";
1030
+ const upscaler = new ImgUpscalerScraper();
1031
+
1032
+ await upscaler.upscaleBuffer(imageBuffer, "image.jpg", 2)
1033
+ // -> WsperResponse<ImgUpscalerResult>
1034
+ ```
1035
+
1036
+ ---
1037
+
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>
1046
+ ```
1047
+
1048
+ ---
1049
+
1050
+ ### PhotoAi
1051
+
1052
+ ```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
+ ---
1061
+
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 }
1071
+ ```
1072
+
1073
+ ---
1074
+
1075
+ ### Resep
1076
+
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[] }
1084
+ ```
1085
+
1086
+ ---
1087
+
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 }
1097
+ ```
1098
+
1099
+ ---
1100
+
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 }
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", ...]
1126
+ ```
1127
+
1128
+ ---
1129
+
1130
+ ## Brat Generator
1131
+
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.
1133
+
1134
+ ```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
+ ---
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
+ });
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
1166
+ }
1167
+ ```
1168
+
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
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",
1210
+ },
1211
+ });
1212
+
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
1285
+
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
1300
+ });
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
+ ---
1346
+
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 } }
1399
+ ```
1400
+
1401
+ ---
1402
+
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
1415
+ }
1416
+ ```
1417
+
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
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" }
1441
+ ```
1442
+
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
+ ```
1481
+
1482
+ Model bawaan:
1483
+
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:
1517
+
1518
+ ```ts
1519
+ // 1. Tanpa credential → library pakai internal defaults
1520
+ const wsper = new WsperScraper();
1521
+
1522
+ // 2. Custom credential per platform
1523
+ const wsper = new WsperScraper({
1524
+ spotifyCredentials: {
1525
+ clientId: "your-client-id",
1526
+ clientSecret: "your-client-secret",
1527
+ },
1528
+ credentials: {
1529
+ twitter: {
1530
+ cookie: "auth_token=xxx; ct0=yyy",
1531
+ csrfToken: "yyy",
1532
+ },
1533
+ threads: {
1534
+ cookie: "sessionid=xxx",
1535
+ csrfToken: "zzz",
1536
+ },
1537
+ },
1538
+ });
1539
+ ```
1540
+
1541
+ > Jangan pernah commit cookie, token, atau secret ke repository.
1542
+
1543
+ ---
1544
+
1545
+ ## HTTP & Queue Options
1546
+
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
+ }
1557
+
1558
+ interface QueueOptions {
1559
+ concurrency?: number; // default 1
1560
+ intervalMs?: number;
1561
+ intervalCap?: number;
1562
+ minDelayMs?: number;
1563
+ maxDelayMs?: number;
1564
+ }
1565
+ ```
1566
+
1567
+ Contoh konfigurasi rate-limit:
1568
+
1569
+ ```ts
1570
+ const wsper = new WsperScraper({
1571
+ http: { timeoutMs: 20000, retries: 2 },
1572
+ queue: { concurrency: 1, minDelayMs: 3000, maxDelayMs: 7000 },
1573
+ });
1574
+ ```
1575
+
1576
+ ---
1577
+
1578
+ ## Download Outputs
1579
+
1580
+ File yang didownload disimpan ke direktori yang ditentukan di tiap method:
1581
+
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
+ ```
1602
+
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
+ ```
1612
+
1613
+ ## READ THIS INFORMATION
1614
+
1615
+ baca rek!!
1616
+ kalo ada bug gabung aja ke trus bilang ke ke yang buat lib