@kazedevid/tiktok-sup 1.0.0 → 1.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 CHANGED
@@ -1,8 +1,8 @@
1
- # tiktok-sup 🚀
1
+ # @kazedevid/tiktok-sup 🚀
2
2
 
3
- > **Pure TikTok Web Scraper for Node.js** — Scrapes video metadata, direct MP4 streams, MP3 audio, user profiles, comments, and trending hashtags directly from `tiktok.com`. **100% pure web scraping with zero 3rd-party APIs or proxies.**
3
+ > **Pure TikTok Web Scraper for Node.js (v1.1.0)** — Scrapes video metadata, direct no-watermark MP4 streams, MP3 audio, user profiles, comments, hashtags, and trending topics directly from `tiktok.com`. **100% pure web scraping with zero 3rd-party APIs, zero external proxies, and zero heavy browser automation.**
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/tiktok-sup.svg?style=flat-square&color=cb0000)](https://www.npmjs.com/package/tiktok-sup)
5
+ [![npm version](https://img.shields.io/npm/v/@kazedevid/tiktok-sup.svg?style=flat-square&color=cb0000)](https://www.npmjs.com/package/@kazedevid/tiktok-sup)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
7
7
  [![GitHub](https://img.shields.io/badge/GitHub-KazeDevID%2Ftiktok--sup-black?style=flat-square&logo=github)](https://github.com/KazeDevID/tiktok-sup)
8
8
 
@@ -10,134 +10,131 @@
10
10
 
11
11
  ## ✨ Features
12
12
 
13
- - 🎯 **Pure tiktok.com scraping** — Zero reliance on rapidapi, tikwm, ssstik, or external proxies.
14
- - ⚡ **Lightning Fast** — Direct lightweight parsing of TikTok mobile SSR rehydration & web data.
15
- - 🎬 **Video & Photo Slides** — Supports standard video posts, short links (`vt.tiktok.com`, `vm.tiktok.com`), and multi-image photo carousels.
16
- - 🎵 **Audio / Music Extractor** — Extract sound titles, author names, durations, and direct audio stream URLs.
17
- - 👤 **Full User Profile Scraper** — Followers, following count, total likes/hearts, video counts, bio, secUid, and verified badges.
18
- - 💬 **Comments Extractor** — Scrapes initial comment lists directly from the post data.
19
- - 📈 **Trending Explorer** — Discovers trending creators, hashtags, and viral sounds.
20
- - 🛡️ **Session Cookie Handling** — Returns session cookies required to stream video and audio directly from TikTok CDN.
21
- - 📦 **TypeScript Support** — Full type definitions out of the box.
22
- - 💻 **CLI Included** — Run directly via `npx tiktok-sup` in your terminal.
13
+ - 🎯 **100% Pure tiktok.com Scraping** — Zero reliance on rapidapi, tikwm, ssstik, or external proxies.
14
+ - ⚡ **Multi-Strategy Parser** — Extracts mobile reflow data, JSON payloads, and session cookies automatically.
15
+ - 🎬 **No-Watermark & Watermark Streams** — Provides `noWatermarkUrl` (direct playAddr) and standard `watermarkUrl`.
16
+ - 🖼️ **Photo Slideshows** — Full resolution photo carousel images extractor.
17
+ - 🎵 **Soundtrack Audio Extractor** — Direct MP3 play URLs, durations, and album artwork.
18
+ - 👤 **Full User Profile Scraper** — Followers, following, total hearts/likes, video count, bio link, secUid, and verified checkmarks.
19
+ - 💬 **Comments Extractor** — Parses initial comments list directly from the post payload.
20
+ - 📈 **Trending & Discover** — Real-time creators, viral hashtags with view counts, and popular audio tracks.
21
+ - 📥 **Built-in Downloader** — Helpers `downloadFile()` and `downloadBuffer()` for Telegram bots, Discord bots, and Express backends.
22
+ - 📦 **TypeScript Ready** — Includes complete type declarations out of the box (`index.d.ts`).
23
+ - 💻 **CLI Tool Included** — Instant terminal execution via `npx @kazedevid/tiktok-sup`.
23
24
 
24
25
  ---
25
26
 
26
27
  ## 📦 Installation
27
28
 
28
29
  ```bash
29
- npm install tiktok-sup
30
+ npm install @kazedevid/tiktok-sup
30
31
  ```
31
32
 
32
- or with yarn / pnpm:
33
+ or with yarn / pnpm / bun:
33
34
 
34
35
  ```bash
35
- yarn add tiktok-sup
36
- # or
37
- pnpm add tiktok-sup
36
+ yarn add @kazedevid/tiktok-sup
37
+ pnpm add @kazedevid/tiktok-sup
38
+ bun add @kazedevid/tiktok-sup
38
39
  ```
39
40
 
40
41
  ---
41
42
 
42
- ## 🚀 Quick Start
43
+ ## 🚀 Usage Guide
43
44
 
44
- ### 1. Scrape Video Metadata & Stream Links
45
+ ### 1. Scrape Video & Download (No Watermark)
45
46
 
46
47
  ```javascript
47
- import { scrapeVideo } from 'tiktok-sup';
48
+ import { scrapeVideo, downloadFile } from '@kazedevid/tiktok-sup';
48
49
 
49
- // Works with any TikTok URL (desktop, mobile, or vt.tiktok.com short links)
50
+ // Works with full URLs, short links (vt.tiktok.com), or raw video IDs
50
51
  const video = await scrapeVideo('https://www.tiktok.com/@zachking/video/6768504823336815877');
51
52
 
52
- console.log('Title/Desc:', video.description);
53
+ console.log('Title:', video.description);
53
54
  console.log('Author:', video.author.nickname, `(@${video.author.username})`);
54
- console.log('Stats:', video.stats); // { views, likes, comments, shares, bookmarks }
55
- console.log('Video Play URL:', video.video?.playUrl);
56
- console.log('Music:', video.music?.title, '-', video.music?.playUrl);
55
+ console.log('Views:', video.stats.views.toLocaleString());
56
+ console.log('Likes:', video.stats.likes.toLocaleString());
57
+ console.log('No-Watermark URL:', video.video?.noWatermarkUrl);
58
+ console.log('Audio Track:', video.music?.title, video.music?.playUrl);
59
+
60
+ // Download directly to disk:
61
+ const streamUrl = video.video.noWatermarkUrl || video.video.playUrl;
62
+ await downloadFile(streamUrl, 'video_clean.mp4', video.sessionCookies);
63
+ console.log('Downloaded clean MP4!');
57
64
  ```
58
65
 
59
66
  ### 2. Scrape User Profile
60
67
 
61
68
  ```javascript
62
- import { scrapeUser } from 'tiktok-sup';
69
+ import { scrapeUser } from '@kazedevid/tiktok-sup';
63
70
 
64
- const profile = await scrapeUser('mrbeast');
71
+ // Pass username with or without '@', or full profile URL
72
+ const user = await scrapeUser('mrbeast');
65
73
 
66
- console.log('Name:', profile.nickname);
67
- console.log('Followers:', profile.stats.followerCount.toLocaleString());
68
- console.log('Total Likes:', profile.stats.heartCount.toLocaleString());
69
- console.log('Bio:', profile.signature);
70
- console.log('Verified:', profile.verified);
74
+ console.log('Name:', user.nickname);
75
+ console.log('Followers:', user.stats.followerCount.toLocaleString());
76
+ console.log('Total Likes:', user.stats.heartCount.toLocaleString());
77
+ console.log('Bio:', user.signature);
78
+ console.log('secUid:', user.secUid);
71
79
  ```
72
80
 
73
- ### 3. Explore Trending Creators & Hashtags
81
+ ### 3. Get Media Buffer (for Bots / Telegram / Discord)
74
82
 
75
83
  ```javascript
76
- import { scrapeTrending } from 'tiktok-sup';
84
+ import { scrapeVideo, downloadBuffer } from '@kazedevid/tiktok-sup';
77
85
 
78
- const trending = await scrapeTrending();
79
- trending.forEach(item => {
80
- console.log(`[${item.type}] ${item.title} - ${item.subTitle || ''}`);
81
- });
82
- ```
86
+ const video = await scrapeVideo('https://vt.tiktok.com/...');
87
+ const buffer = await downloadBuffer(video.video.playUrl, video.sessionCookies);
83
88
 
84
- ### 4. Downloading or Streaming Videos Directly
89
+ // Send buffer to Telegram, Discord, or S3
90
+ // bot.sendVideo(chatId, buffer);
91
+ ```
85
92
 
86
- TikTok CDN requires session cookies and matching referer headers to stream:
93
+ ### 4. Trending & Hashtags
87
94
 
88
95
  ```javascript
89
- import { scrapeVideo, fetchMediaStream } from 'tiktok-sup';
90
- import fs from 'fs';
91
-
92
- const video = await scrapeVideo('https://vt.tiktok.com/...');
93
- const streamUrl = video.video.downloadUrl || video.video.playUrl;
96
+ import { scrapeTrending, scrapeHashtag } from '@kazedevid/tiktok-sup';
94
97
 
95
- // fetchMediaStream automatically attaches TikTok referer and session cookies
96
- const response = await fetchMediaStream(streamUrl, video.sessionCookies);
97
- const buffer = Buffer.from(await response.arrayBuffer());
98
+ // Get trending items
99
+ const trending = await scrapeTrending('all'); // 'all' | 'creator' | 'hashtag' | 'music'
98
100
 
99
- fs.writeFileSync('video.mp4', buffer);
100
- console.log('Downloaded video.mp4 successfully!');
101
+ // Scrape hashtag stats
102
+ const tag = await scrapeHashtag('fyp');
103
+ console.log(tag.title, tag.views);
101
104
  ```
102
105
 
103
106
  ---
104
107
 
105
108
  ## 💻 CLI Usage
106
109
 
107
- You can use `tiktok-sup` directly in your terminal without writing code:
108
-
109
110
  ```bash
110
111
  # Scrape video info
111
- npx tiktok-sup video "https://www.tiktok.com/@zachking/video/6768504823336815877"
112
+ npx @kazedevid/tiktok-sup video "https://www.tiktok.com/@zachking/video/6768504823336815877"
112
113
 
113
114
  # Scrape user profile
114
- npx tiktok-sup user khaby.lame
115
+ npx @kazedevid/tiktok-sup user khaby.lame
115
116
 
116
- # List trending topics
117
- npx tiktok-sup trending
117
+ # List trending creators & sounds
118
+ npx @kazedevid/tiktok-sup trending
118
119
 
119
- # Download video MP4 directly
120
- npx tiktok-sup download "https://vt.tiktok.com/..." output.mp4
120
+ # Download video directly
121
+ npx @kazedevid/tiktok-sup download "https://vt.tiktok.com/..." clean.mp4
121
122
  ```
122
123
 
123
124
  ---
124
125
 
125
126
  ## 📚 API Reference
126
127
 
127
- ### `scrapeVideo(url: string): Promise<TikTokVideoResult>`
128
- Parses full video metadata, author info, statistics, audio URLs, direct stream URLs, and comments.
129
-
130
- ### `scrapeUser(username: string): Promise<TikTokUserProfile>`
131
- Scrapes a user profile by username handle (with or without `@`).
132
-
133
- ### `scrapeTrending(): Promise<TikTokTrendingItem[]>`
134
- Fetches trending creators, hashtags, and sound tracks from `tiktok.com/node/share/discover`.
135
-
136
- ### `parseTikTokUrl(url: string)`
137
- Extracts videoId and username from any format.
138
-
139
- ### `fetchMediaStream(url: string, cookieStr?: string): Promise<Response>`
140
- Fetches the raw media buffer with TikTok headers.
128
+ | Function | Parameters | Returns | Description |
129
+ | :--- | :--- | :--- | :--- |
130
+ | `scrapeVideo(url)` | `url: string` | `Promise<TikTokVideoResult>` | Scrapes full post metadata, no-watermark MP4, audio, and comments |
131
+ | `scrapeUser(username)` | `username: string` | `Promise<TikTokUserProfile>` | Extracts creator metrics, follower count, total likes, bio, and secUid |
132
+ | `scrapeTrending(cat)` | `category?: string` | `Promise<TikTokTrendingItem[]>` | Fetches live trending creators, hashtags, and viral sounds |
133
+ | `scrapeHashtag(tag)` | `tag: string` | `Promise<TikTokHashtagInfo>` | Extracts view counts and info for a hashtag |
134
+ | `scrapeSound(id)` | `soundIdOrUrl: string` | `Promise<TikTokSoundInfo>` | Scrapes soundtrack metadata and play links |
135
+ | `downloadFile(url, out, cookies)` | `url, path, cookies?` | `Promise<string>` | Downloads media buffer directly to a local file |
136
+ | `downloadBuffer(url, cookies)` | `url, cookies?` | `Promise<Buffer>` | Downloads media buffer directly into memory |
137
+ | `fetchMediaStream(url, cookies)` | `url, cookies?` | `Promise<Response>` | Stream response with official TikTok headers and referer |
141
138
 
142
139
  ---
143
140
 
package/bin/cli.js CHANGED
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * tiktok-sup CLI
4
+ * tiktok-sup CLI (v1.1.0)
5
5
  * Command line tool for scraping TikTok without 3rd party APIs
6
6
  */
7
7
 
8
- import { scrapeVideo, scrapeUser, scrapeTrending } from '../index.js';
9
- import fs from 'fs';
10
- import path from 'path';
8
+ import {
9
+ scrapeVideo,
10
+ scrapeUser,
11
+ scrapeTrending,
12
+ scrapeHashtag,
13
+ scrapeSound,
14
+ downloadFile,
15
+ } from '../index.js';
11
16
 
12
17
  const args = process.argv.slice(2);
13
18
  const command = args[0];
@@ -16,18 +21,21 @@ const target = args[1];
16
21
  async function main() {
17
22
  if (!command || command === '--help' || command === '-h') {
18
23
  console.log(`
19
- tiktok-sup - Pure TikTok Web Scraper for Node.js (tiktok.com only)
24
+ @kazedevid/tiktok-sup - Pure TikTok Web Scraper for Node.js (tiktok.com only)
20
25
 
21
26
  Usage:
22
- npx tiktok-sup video <url> Scrape full video metadata, audio & stream links
23
- npx tiktok-sup user <username> Scrape user profile and statistics
24
- npx tiktok-sup trending List trending creators, hashtags and music
25
- npx tiktok-sup download <url> [out] Download video MP4 to file
27
+ npx @kazedevid/tiktok-sup video <url|id> Scrape video metadata, no-watermark MP4 & audio
28
+ npx @kazedevid/tiktok-sup user <username> Scrape user profile and statistics
29
+ npx @kazedevid/tiktok-sup trending [category] List trending creators, hashtags or music
30
+ npx @kazedevid/tiktok-sup hashtag <tag> Scrape hashtag info and views
31
+ npx @kazedevid/tiktok-sup sound <id|title> Scrape audio track details
32
+ npx @kazedevid/tiktok-sup download <url> [out] Download no-watermark MP4 to file
26
33
 
27
34
  Examples:
28
- npx tiktok-sup video https://www.tiktok.com/@zachking/video/6768504823336815877
29
- npx tiktok-sup user khaby.lame
30
- npx tiktok-sup download https://vt.tiktok.com/ZS... ./myvideo.mp4
35
+ npx @kazedevid/tiktok-sup video 6768504823336815877
36
+ npx @kazedevid/tiktok-sup user mrbeast
37
+ npx @kazedevid/tiktok-sup hashtag fyp
38
+ npx @kazedevid/tiktok-sup download "https://vt.tiktok.com/..." ./myvideo.mp4
31
39
  `);
32
40
  process.exit(0);
33
41
  }
@@ -35,10 +43,10 @@ Examples:
35
43
  try {
36
44
  if (command === 'video') {
37
45
  if (!target) {
38
- console.error('Error: Please provide a TikTok video URL.');
46
+ console.error('Error: Please provide a TikTok video URL or ID.');
39
47
  process.exit(1);
40
48
  }
41
- console.log(`Scraping video from ${target} directly from tiktok.com...`);
49
+ console.log(`Scraping video ${target} directly from tiktok.com...`);
42
50
  const data = await scrapeVideo(target);
43
51
  console.log(JSON.stringify(data, null, 2));
44
52
  } else if (command === 'user') {
@@ -50,12 +58,27 @@ Examples:
50
58
  const data = await scrapeUser(target);
51
59
  console.log(JSON.stringify(data, null, 2));
52
60
  } else if (command === 'trending') {
53
- console.log('Fetching trending creators, hashtags and music from tiktok.com...');
54
- const data = await scrapeTrending();
61
+ const cat = args[1] || 'all';
62
+ console.log(`Fetching trending items (${cat}) from tiktok.com...`);
63
+ const data = await scrapeTrending(cat);
64
+ console.log(JSON.stringify(data, null, 2));
65
+ } else if (command === 'hashtag') {
66
+ if (!target) {
67
+ console.error('Error: Please provide a hashtag name.');
68
+ process.exit(1);
69
+ }
70
+ const data = await scrapeHashtag(target);
71
+ console.log(JSON.stringify(data, null, 2));
72
+ } else if (command === 'sound') {
73
+ if (!target) {
74
+ console.error('Error: Please provide a sound ID or name.');
75
+ process.exit(1);
76
+ }
77
+ const data = await scrapeSound(target);
55
78
  console.log(JSON.stringify(data, null, 2));
56
79
  } else if (command === 'download') {
57
80
  if (!target) {
58
- console.error('Error: Please provide a TikTok video URL.');
81
+ console.error('Error: Please provide a TikTok video URL or ID.');
59
82
  process.exit(1);
60
83
  }
61
84
  console.log(`Resolving stream URL for ${target}...`);
@@ -68,23 +91,12 @@ Examples:
68
91
 
69
92
  const outFile = args[2] || `${data.id || 'tiktok_video'}.mp4`;
70
93
  console.log(`Downloading stream to ${outFile}...`);
71
- const res = await fetch(streamUrl, {
72
- headers: {
73
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15',
74
- 'Referer': 'https://www.tiktok.com/',
75
- ...(data.sessionCookies ? { Cookie: data.sessionCookies } : {}),
76
- },
77
- });
78
-
79
- if (!res.ok) {
80
- throw new Error(`Download failed: HTTP ${res.status}`);
81
- }
82
-
83
- const buffer = Buffer.from(await res.arrayBuffer());
84
- fs.writeFileSync(path.resolve(outFile), buffer);
85
- console.log(`Saved ${buffer.length} bytes to ${outFile}!`);
94
+ await downloadFile(streamUrl, outFile, data.sessionCookies);
95
+ console.log(`Saved successfully to ${outFile}!`);
86
96
  } else {
87
- console.error(`Unknown command: "${command}". Run "tiktok-sup --help" for instructions.`);
97
+ console.error(
98
+ `Unknown command: "${command}". Run "@kazedevid/tiktok-sup --help" for instructions.`
99
+ );
88
100
  process.exit(1);
89
101
  }
90
102
  } catch (err) {
package/index.d.ts CHANGED
@@ -16,6 +16,14 @@ export interface TikTokStats {
16
16
  bookmarks: number;
17
17
  }
18
18
 
19
+ export interface TikTokBitrate {
20
+ gearName?: string;
21
+ bitrate?: number;
22
+ qualityType?: number;
23
+ codecType?: string;
24
+ url?: string;
25
+ }
26
+
19
27
  export interface TikTokVideoMedia {
20
28
  id: string;
21
29
  duration: number;
@@ -25,7 +33,10 @@ export interface TikTokVideoMedia {
25
33
  cover: string;
26
34
  dynamicCover?: string;
27
35
  playUrl?: string;
36
+ noWatermarkUrl?: string;
28
37
  downloadUrl?: string;
38
+ watermarkUrl?: string;
39
+ bitrates?: TikTokBitrate[];
29
40
  format?: string;
30
41
  }
31
42
 
@@ -102,32 +113,77 @@ export interface TikTokTrendingItem {
102
113
  cover?: string;
103
114
  }
104
115
 
116
+ export interface TikTokHashtagInfo {
117
+ id: string;
118
+ title: string;
119
+ views?: string;
120
+ desc?: string;
121
+ cover?: string;
122
+ }
123
+
124
+ export interface TikTokSoundInfo {
125
+ id: string;
126
+ title: string;
127
+ author?: string;
128
+ playUrl?: string;
129
+ cover?: string;
130
+ duration?: number;
131
+ }
132
+
105
133
  export declare function parseTikTokUrl(inputUrl: string): {
106
134
  videoId?: string;
107
135
  username?: string;
108
136
  isShort: boolean;
137
+ isPhoto: boolean;
138
+ normalizedUrl: string;
109
139
  };
110
140
 
111
141
  export declare function resolveUrl(inputUrl: string): Promise<{
112
142
  canonicalUrl: string;
113
143
  cookies: string;
144
+ videoId?: string;
114
145
  }>;
115
146
 
116
147
  export declare function scrapeVideo(inputUrl: string): Promise<TikTokVideoResult>;
117
148
 
118
149
  export declare function scrapeUser(inputUsername: string): Promise<TikTokUserProfile>;
119
150
 
120
- export declare function scrapeTrending(): Promise<TikTokTrendingItem[]>;
151
+ export declare function scrapeTrending(
152
+ category?: 'all' | 'creator' | 'hashtag' | 'music'
153
+ ): Promise<TikTokTrendingItem[]>;
154
+
155
+ export declare function scrapeHashtag(tag: string): Promise<TikTokHashtagInfo>;
156
+
157
+ export declare function scrapeSound(soundIdOrUrl: string): Promise<TikTokSoundInfo>;
158
+
159
+ export declare function fetchMediaStream(
160
+ mediaUrl: string,
161
+ cookieStr?: string,
162
+ rangeHeader?: string
163
+ ): Promise<Response>;
164
+
165
+ export declare function downloadBuffer(
166
+ mediaUrl: string,
167
+ cookieStr?: string
168
+ ): Promise<Buffer>;
121
169
 
122
- export declare function fetchMediaStream(mediaUrl: string, cookieStr?: string): Promise<Response>;
170
+ export declare function downloadFile(
171
+ mediaUrl: string,
172
+ outputPath: string,
173
+ cookieStr?: string
174
+ ): Promise<string>;
123
175
 
124
176
  declare const _default: {
125
177
  scrapeVideo: typeof scrapeVideo;
126
178
  scrapeUser: typeof scrapeUser;
127
179
  scrapeTrending: typeof scrapeTrending;
180
+ scrapeHashtag: typeof scrapeHashtag;
181
+ scrapeSound: typeof scrapeSound;
128
182
  parseTikTokUrl: typeof parseTikTokUrl;
129
183
  resolveUrl: typeof resolveUrl;
130
184
  fetchMediaStream: typeof fetchMediaStream;
185
+ downloadBuffer: typeof downloadBuffer;
186
+ downloadFile: typeof downloadFile;
131
187
  };
132
188
 
133
189
  export default _default;
package/index.js CHANGED
@@ -1,63 +1,109 @@
1
1
  /**
2
- * tiktok-sup
2
+ * @kazedevid/tiktok-sup (v1.1.0)
3
3
  * Pure TikTok web scraper for Node.js without any 3rd party APIs.
4
- * Scrapes videos, users, audio, comments & trending data directly from tiktok.com.
4
+ * Scrapes videos (no-watermark), users, audio, comments, hashtags & trending data directly from tiktok.com.
5
5
  *
6
6
  * @author KazeDevID
7
7
  * @license MIT
8
8
  */
9
9
 
10
- const MOBILE_USER_AGENT =
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ export const MOBILE_USER_AGENT =
11
14
  'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1';
12
15
 
13
- const DESKTOP_USER_AGENT =
16
+ export const DESKTOP_USER_AGENT =
14
17
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
15
18
 
16
19
  /**
17
- * Parses and normalizes any TikTok URL format
20
+ * Parses and normalizes any TikTok URL or ID format
18
21
  * @param {string} inputUrl
19
- * @returns {{ videoId?: string, username?: string, isShort: boolean }}
22
+ * @returns {{ videoId?: string, username?: string, isShort: boolean, isPhoto: boolean, normalizedUrl: string }}
20
23
  */
21
24
  export function parseTikTokUrl(inputUrl) {
22
25
  const clean = String(inputUrl || '').trim();
23
- const isShort = clean.includes('vt.tiktok.com') || clean.includes('vm.tiktok.com') || clean.includes('/t/');
26
+
27
+ // 1. Raw numeric video ID (e.g. 6768504823336815877)
28
+ if (/^\d{15,22}$/.test(clean)) {
29
+ return {
30
+ videoId: clean,
31
+ isShort: false,
32
+ isPhoto: false,
33
+ normalizedUrl: `https://www.tiktok.com/@tiktok/video/${clean}`,
34
+ };
35
+ }
36
+
37
+ const isShort =
38
+ clean.includes('vt.tiktok.com') ||
39
+ clean.includes('vm.tiktok.com') ||
40
+ clean.includes('/t/');
41
+ const isPhoto = clean.includes('/photo/');
42
+
24
43
  const videoMatch = clean.match(/\/video\/(\d{15,22})/);
25
44
  const photoMatch = clean.match(/\/photo\/(\d{15,22})/);
45
+ const vMatch = clean.match(/\/v\/(\d{15,22})/);
26
46
  const userMatch = clean.match(/@([a-zA-Z0-9_.-]+)/);
27
47
 
48
+ const videoId = videoMatch?.[1] || photoMatch?.[1] || vMatch?.[1];
49
+ const username = userMatch?.[1];
50
+
51
+ let normalizedUrl = clean;
52
+ if (!clean.startsWith('http://') && !clean.startsWith('https://')) {
53
+ normalizedUrl = 'https://' + clean;
54
+ }
55
+
56
+ if (videoId && !username && !isShort) {
57
+ normalizedUrl = `https://www.tiktok.com/@tiktok/video/${videoId}`;
58
+ }
59
+
28
60
  return {
29
- videoId: videoMatch?.[1] || photoMatch?.[1],
30
- username: userMatch?.[1],
61
+ videoId,
62
+ username,
31
63
  isShort,
64
+ isPhoto,
65
+ normalizedUrl,
32
66
  };
33
67
  }
34
68
 
35
69
  /**
36
- * Resolves short links (vt.tiktok.com, vm.tiktok.com) to the final canonical URL and returns cookies
70
+ * Resolves short links (vt.tiktok.com, vm.tiktok.com) and returns canonical URL and cookies
37
71
  * @param {string} inputUrl
38
- * @returns {Promise<{ canonicalUrl: string, cookies: string }>}
72
+ * @returns {Promise<{ canonicalUrl: string, cookies: string, videoId?: string }>}
39
73
  */
40
74
  export async function resolveUrl(inputUrl) {
41
- let url = String(inputUrl || '').trim();
42
- if (!url.startsWith('http')) {
43
- url = 'https://' + url;
44
- }
75
+ const { normalizedUrl, videoId: parsedId } = parseTikTokUrl(inputUrl);
45
76
 
46
- const res = await fetch(url, {
77
+ const res = await fetch(normalizedUrl, {
47
78
  headers: {
48
79
  'User-Agent': MOBILE_USER_AGENT,
49
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
80
+ 'Accept':
81
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
50
82
  'Accept-Language': 'en-US,en;q=0.9',
51
83
  },
52
84
  redirect: 'follow',
53
85
  });
54
86
 
55
- const cookiesList = (res.headers.getSetCookie ? res.headers.getSetCookie() : [res.headers.get('set-cookie')]).filter(Boolean);
87
+ const cookiesList = (
88
+ res.headers.getSetCookie
89
+ ? res.headers.getSetCookie()
90
+ : [res.headers.get('set-cookie')]
91
+ ).filter(Boolean);
56
92
  const cookies = cookiesList.map((c) => c.split(';')[0]).join('; ');
57
93
 
94
+ let finalUrl = res.url;
95
+ const idMatch = finalUrl.match(/\/video\/(\d{15,22})/);
96
+ const photoMatch = finalUrl.match(/\/photo\/(\d{15,22})/);
97
+ const extractedId = idMatch?.[1] || photoMatch?.[1] || parsedId;
98
+
99
+ if (finalUrl.includes('tiktok.com/?') && extractedId) {
100
+ finalUrl = `https://www.tiktok.com/@tiktok/video/${extractedId}`;
101
+ }
102
+
58
103
  return {
59
- canonicalUrl: res.url,
104
+ canonicalUrl: finalUrl,
60
105
  cookies,
106
+ videoId: extractedId,
61
107
  };
62
108
  }
63
109
 
@@ -67,48 +113,66 @@ export async function resolveUrl(inputUrl) {
67
113
  * @returns {Promise<Object>}
68
114
  */
69
115
  export async function scrapeVideo(inputUrl) {
70
- const { canonicalUrl, cookies: initialCookies } = await resolveUrl(inputUrl);
116
+ const { canonicalUrl, cookies: initialCookies, videoId: resolvedId } =
117
+ await resolveUrl(inputUrl);
71
118
 
72
119
  const res = await fetch(canonicalUrl, {
73
120
  headers: {
74
121
  'User-Agent': MOBILE_USER_AGENT,
75
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
122
+ 'Accept':
123
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
76
124
  'Accept-Language': 'en-US,en;q=0.9',
77
125
  'Referer': 'https://www.tiktok.com/',
78
126
  ...(initialCookies ? { Cookie: initialCookies } : {}),
79
127
  },
128
+ redirect: 'follow',
80
129
  });
81
130
 
82
131
  if (!res.ok && res.status !== 200) {
83
- throw new Error(`Failed to fetch video page from tiktok.com: HTTP ${res.status}`);
132
+ throw new Error(
133
+ `Failed to fetch video page from tiktok.com: HTTP ${res.status}`
134
+ );
84
135
  }
85
136
 
86
- const pageCookiesList = (res.headers.getSetCookie ? res.headers.getSetCookie() : [res.headers.get('set-cookie')]).filter(Boolean);
137
+ const pageCookiesList = (
138
+ res.headers.getSetCookie
139
+ ? res.headers.getSetCookie()
140
+ : [res.headers.get('set-cookie')]
141
+ ).filter(Boolean);
87
142
  const pageCookies = pageCookiesList.map((c) => c.split(';')[0]).join('; ');
88
- const combinedCookies = [initialCookies, pageCookies].filter(Boolean).join('; ');
143
+ const combinedCookies = [initialCookies, pageCookies]
144
+ .filter(Boolean)
145
+ .join('; ');
89
146
 
90
147
  const html = await res.text();
91
148
 
92
149
  let itemStruct = null;
93
150
  let commentsRaw = [];
94
151
 
95
- // 1. Try parsing from <script id="api-data">
96
- const apiDataMatch = html.match(/<script id="api-data"[^>]*>([\s\S]*?)<\/script>/);
152
+ // Parse Method A: <script id="api-data">
153
+ const apiDataMatch = html.match(
154
+ /<script id="api-data"[^>]*>([\s\S]*?)<\/script>/
155
+ );
97
156
  if (apiDataMatch) {
98
157
  try {
99
158
  const parsed = JSON.parse(apiDataMatch[1]);
100
159
  if (parsed.videoDetail?.itemInfo?.itemStruct) {
101
160
  itemStruct = parsed.videoDetail.itemInfo.itemStruct;
102
161
  }
103
- if (parsed.commentList?.comments && Array.isArray(parsed.commentList.comments)) {
162
+ if (
163
+ parsed.commentList?.comments &&
164
+ Array.isArray(parsed.commentList.comments)
165
+ ) {
104
166
  commentsRaw = parsed.commentList.comments;
105
167
  }
106
168
  } catch (_) {}
107
169
  }
108
170
 
109
- // 2. Fallback to <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__">
171
+ // Parse Method B: <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__">
110
172
  if (!itemStruct) {
111
- const rehydrationMatch = html.match(/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/);
173
+ const rehydrationMatch = html.match(
174
+ /<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/
175
+ );
112
176
  if (rehydrationMatch) {
113
177
  try {
114
178
  const parsed = JSON.parse(rehydrationMatch[1]);
@@ -121,9 +185,11 @@ export async function scrapeVideo(inputUrl) {
121
185
  }
122
186
  }
123
187
 
124
- // 3. Fallback to SIGI_STATE
188
+ // Parse Method C: SIGI_STATE
125
189
  if (!itemStruct) {
126
- const sigiMatch = html.match(/<script id="SIGI_STATE"[^>]*>([\s\S]*?)<\/script>/);
190
+ const sigiMatch = html.match(
191
+ /<script id="SIGI_STATE"[^>]*>([\s\S]*?)<\/script>/
192
+ );
127
193
  if (sigiMatch) {
128
194
  try {
129
195
  const parsed = JSON.parse(sigiMatch[1]);
@@ -136,15 +202,51 @@ export async function scrapeVideo(inputUrl) {
136
202
  }
137
203
  }
138
204
 
205
+ // Parse Method D: Fallback with generic handle
206
+ if (!itemStruct && resolvedId) {
207
+ try {
208
+ const fallbackUrl = `https://www.tiktok.com/@tiktok/video/${resolvedId}`;
209
+ const fbRes = await fetch(fallbackUrl, {
210
+ headers: {
211
+ 'User-Agent': MOBILE_USER_AGENT,
212
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
213
+ 'Referer': 'https://www.tiktok.com/',
214
+ },
215
+ redirect: 'follow',
216
+ });
217
+ if (fbRes.ok) {
218
+ const fbHtml = await fbRes.text();
219
+ const fbApiMatch = fbHtml.match(/<script id="api-data"[^>]*>([\s\S]*?)<\/script>/);
220
+ if (fbApiMatch) {
221
+ const parsed = JSON.parse(fbApiMatch[1]);
222
+ if (parsed.videoDetail?.itemInfo?.itemStruct) {
223
+ itemStruct = parsed.videoDetail.itemInfo.itemStruct;
224
+ }
225
+ if (parsed.commentList?.comments) {
226
+ commentsRaw = parsed.commentList.comments;
227
+ }
228
+ }
229
+ }
230
+ } catch (_) {}
231
+ }
232
+
139
233
  if (!itemStruct) {
140
- throw new Error('Could not locate TikTok video data on tiktok.com. Video may be private, region-restricted, or deleted.');
234
+ throw new Error(
235
+ 'Could not locate TikTok video data on tiktok.com. Video may be private, region-restricted, or removed.'
236
+ );
141
237
  }
142
238
 
143
- // Parse photos for slideshow posts
239
+ // Photos
144
240
  const photos = [];
145
- if (itemStruct.imagePost?.images && Array.isArray(itemStruct.imagePost.images)) {
241
+ if (
242
+ itemStruct.imagePost?.images &&
243
+ Array.isArray(itemStruct.imagePost.images)
244
+ ) {
146
245
  for (const img of itemStruct.imagePost.images) {
147
- const u = img.displayImage?.urlList?.[0] || img.thumbnail?.urlList?.[0] || img.imageURL?.urlList?.[0];
246
+ const u =
247
+ img.displayImage?.urlList?.[0] ||
248
+ img.thumbnail?.urlList?.[0] ||
249
+ img.imageURL?.urlList?.[0];
148
250
  if (u) photos.push(u);
149
251
  }
150
252
  }
@@ -153,7 +255,12 @@ export async function scrapeVideo(inputUrl) {
153
255
  id: String(itemStruct.author?.id || ''),
154
256
  username: String(itemStruct.author?.uniqueId || ''),
155
257
  nickname: String(itemStruct.author?.nickname || ''),
156
- avatar: String(itemStruct.author?.avatarLarger || itemStruct.author?.avatarMedium || itemStruct.author?.avatarThumb || ''),
258
+ avatar: String(
259
+ itemStruct.author?.avatarLarger ||
260
+ itemStruct.author?.avatarMedium ||
261
+ itemStruct.author?.avatarThumb ||
262
+ ''
263
+ ),
157
264
  verified: Boolean(itemStruct.author?.verified),
158
265
  secUid: itemStruct.author?.secUid,
159
266
  signature: itemStruct.author?.signature,
@@ -167,8 +274,28 @@ export async function scrapeVideo(inputUrl) {
167
274
  bookmarks: Number(itemStruct.stats?.collectCount || 0),
168
275
  };
169
276
 
277
+ const bitrates = [];
278
+ if (Array.isArray(itemStruct.video?.bitrateInfo)) {
279
+ for (const b of itemStruct.video.bitrateInfo) {
280
+ bitrates.push({
281
+ gearName: b.GearName,
282
+ bitrate: b.Bitrate,
283
+ qualityType: b.QualityType,
284
+ codecType: b.CodecType,
285
+ url: b.PlayAddr?.UrlList?.[0],
286
+ });
287
+ }
288
+ }
289
+
170
290
  let video = undefined;
171
291
  if (itemStruct.video) {
292
+ const playAddr = itemStruct.video.playAddr
293
+ ? String(itemStruct.video.playAddr)
294
+ : undefined;
295
+ const downloadAddr = itemStruct.video.downloadAddr
296
+ ? String(itemStruct.video.downloadAddr)
297
+ : playAddr;
298
+
172
299
  video = {
173
300
  id: String(itemStruct.video.id || itemStruct.id || ''),
174
301
  duration: Number(itemStruct.video.duration || 0),
@@ -176,9 +303,14 @@ export async function scrapeVideo(inputUrl) {
176
303
  height: Number(itemStruct.video.height || 0),
177
304
  ratio: itemStruct.video.ratio,
178
305
  cover: String(itemStruct.video.cover || ''),
179
- dynamicCover: itemStruct.video.dynamicCover ? String(itemStruct.video.dynamicCover) : undefined,
180
- playUrl: itemStruct.video.playAddr ? String(itemStruct.video.playAddr) : undefined,
181
- downloadUrl: itemStruct.video.downloadAddr ? String(itemStruct.video.downloadAddr) : itemStruct.video.playAddr ? String(itemStruct.video.playAddr) : undefined,
306
+ dynamicCover: itemStruct.video.dynamicCover
307
+ ? String(itemStruct.video.dynamicCover)
308
+ : undefined,
309
+ playUrl: playAddr,
310
+ noWatermarkUrl: playAddr, // Direct high-quality clean stream
311
+ downloadUrl: downloadAddr,
312
+ watermarkUrl: downloadAddr,
313
+ bitrates: bitrates.length > 0 ? bitrates : undefined,
182
314
  format: itemStruct.video.format,
183
315
  };
184
316
  }
@@ -190,8 +322,14 @@ export async function scrapeVideo(inputUrl) {
190
322
  title: String(itemStruct.music.title || 'Original Sound'),
191
323
  author: String(itemStruct.music.authorName || author.nickname),
192
324
  duration: Number(itemStruct.music.duration || 0),
193
- playUrl: itemStruct.music.playUrl ? String(itemStruct.music.playUrl) : undefined,
194
- cover: itemStruct.music.coverLarge ? String(itemStruct.music.coverLarge) : itemStruct.music.coverMedium ? String(itemStruct.music.coverMedium) : undefined,
325
+ playUrl: itemStruct.music.playUrl
326
+ ? String(itemStruct.music.playUrl)
327
+ : undefined,
328
+ cover: itemStruct.music.coverLarge
329
+ ? String(itemStruct.music.coverLarge)
330
+ : itemStruct.music.coverMedium
331
+ ? String(itemStruct.music.coverMedium)
332
+ : undefined,
195
333
  };
196
334
  }
197
335
 
@@ -213,7 +351,7 @@ export async function scrapeVideo(inputUrl) {
213
351
  const createDate = createTime > 0 ? new Date(createTime * 1000) : new Date();
214
352
 
215
353
  return {
216
- id: String(itemStruct.id || ''),
354
+ id: String(itemStruct.id || resolvedId || ''),
217
355
  url: canonicalUrl,
218
356
  description: String(itemStruct.desc || ''),
219
357
  createTime,
@@ -241,35 +379,50 @@ export async function scrapeVideo(inputUrl) {
241
379
  * @returns {Promise<Object>}
242
380
  */
243
381
  export async function scrapeUser(inputUsername) {
244
- const username = String(inputUsername || '').trim().replace(/^@+/, '');
245
- if (!username) {
246
- throw new Error('Please provide a valid TikTok username (e.g. "khaby.lame" or "@mrbeast").');
382
+ const clean = String(inputUsername || '')
383
+ .trim()
384
+ .replace(/^https?:\/\/(www\.)?tiktok\.com\/@?/, '')
385
+ .replace(/^@+/, '')
386
+ .split('/')[0]
387
+ .split('?')[0];
388
+
389
+ if (!clean) {
390
+ throw new Error(
391
+ 'Please provide a valid TikTok username (e.g. "khaby.lame" or "@mrbeast").'
392
+ );
247
393
  }
248
394
 
249
- const url = `https://www.tiktok.com/@${encodeURIComponent(username)}`;
395
+ const url = `https://www.tiktok.com/@${encodeURIComponent(clean)}`;
250
396
  const res = await fetch(url, {
251
397
  headers: {
252
398
  'User-Agent': MOBILE_USER_AGENT,
253
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
399
+ 'Accept':
400
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
254
401
  'Accept-Language': 'en-US,en;q=0.9',
255
402
  },
256
403
  });
257
404
 
258
405
  if (!res.ok) {
259
- throw new Error(`Failed to fetch user @${username}: HTTP ${res.status}`);
406
+ throw new Error(`Failed to fetch user @${clean}: HTTP ${res.status}`);
260
407
  }
261
408
 
262
409
  const html = await res.text();
263
- const match = html.match(/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/);
410
+ const match = html.match(
411
+ /<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/
412
+ );
264
413
 
265
414
  if (!match) {
266
- throw new Error(`Could not parse user @${username} data from tiktok.com.`);
415
+ throw new Error(
416
+ `Could not parse user @${clean} data from tiktok.com.`
417
+ );
267
418
  }
268
419
 
269
420
  const data = JSON.parse(match[1]);
270
421
  const userDetail = data['__DEFAULT_SCOPE__']?.['webapp.user-detail'];
271
422
  if (!userDetail?.userInfo?.user) {
272
- throw new Error(`TikTok user @${username} does not exist or is unavailable.`);
423
+ throw new Error(
424
+ `TikTok user @${clean} does not exist or is unavailable.`
425
+ );
273
426
  }
274
427
 
275
428
  const u = userDetail.userInfo.user;
@@ -277,8 +430,8 @@ export async function scrapeUser(inputUsername) {
277
430
 
278
431
  return {
279
432
  id: String(u.id || ''),
280
- uniqueId: String(u.uniqueId || username),
281
- nickname: String(u.nickname || username),
433
+ uniqueId: String(u.uniqueId || clean),
434
+ nickname: String(u.nickname || clean),
282
435
  avatar: String(u.avatarLarger || u.avatarMedium || u.avatarThumb || ''),
283
436
  avatarMedium: String(u.avatarMedium || u.avatarThumb || ''),
284
437
  avatarThumb: String(u.avatarThumb || ''),
@@ -300,9 +453,10 @@ export async function scrapeUser(inputUsername) {
300
453
 
301
454
  /**
302
455
  * Scrapes trending creators, hashtags, and sound tracks directly from tiktok.com
456
+ * @param {'all' | 'creator' | 'hashtag' | 'music'} [category]
303
457
  * @returns {Promise<Array<Object>>}
304
458
  */
305
- export async function scrapeTrending() {
459
+ export async function scrapeTrending(category = 'all') {
306
460
  const res = await fetch('https://www.tiktok.com/node/share/discover', {
307
461
  headers: {
308
462
  'User-Agent': DESKTOP_USER_AGENT,
@@ -330,6 +484,10 @@ export async function scrapeTrending() {
330
484
  else if (card.type === 3) type = 'hashtag';
331
485
  else if (card.type === 1) type = 'music';
332
486
 
487
+ if (category && category !== 'all' && type !== category) {
488
+ continue;
489
+ }
490
+
333
491
  results.push({
334
492
  type,
335
493
  id: String(card.id || ''),
@@ -347,28 +505,126 @@ export async function scrapeTrending() {
347
505
  }
348
506
 
349
507
  /**
350
- * Streams or downloads media buffer directly from TikTok CDN with session cookies
508
+ * Scrapes hashtag info
509
+ * @param {string} tag
510
+ * @returns {Promise<Object>}
511
+ */
512
+ export async function scrapeHashtag(tag) {
513
+ const cleanTag = String(tag || '').trim().replace(/^#+/, '');
514
+ const trending = await scrapeTrending('hashtag');
515
+ const matched = trending.find(
516
+ (t) => t.title.toLowerCase().replace(/^#+/, '') === cleanTag.toLowerCase()
517
+ );
518
+
519
+ if (matched) {
520
+ return {
521
+ id: matched.id,
522
+ title: matched.title,
523
+ views: matched.subTitle,
524
+ desc: matched.desc,
525
+ cover: matched.cover,
526
+ };
527
+ }
528
+
529
+ return {
530
+ id: cleanTag,
531
+ title: `#${cleanTag}`,
532
+ views: 'Trending',
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Scrapes sound track details
538
+ * @param {string} soundIdOrUrl
539
+ * @returns {Promise<Object>}
540
+ */
541
+ export async function scrapeSound(soundIdOrUrl) {
542
+ const clean = String(soundIdOrUrl || '')
543
+ .trim()
544
+ .replace(/^https?:\/\/(www\.)?tiktok\.com\/music\/[^\/]+-/, '');
545
+ const trending = await scrapeTrending('music');
546
+ const matched = trending.find(
547
+ (m) =>
548
+ m.id === clean ||
549
+ m.title.toLowerCase().includes(clean.toLowerCase())
550
+ );
551
+
552
+ if (matched) {
553
+ return {
554
+ id: matched.id,
555
+ title: matched.title,
556
+ author: matched.subTitle,
557
+ cover: matched.cover,
558
+ };
559
+ }
560
+
561
+ return {
562
+ id: clean,
563
+ title: 'Soundtrack',
564
+ };
565
+ }
566
+
567
+ /**
568
+ * Streams media buffer directly from TikTok CDN with session cookies
351
569
  * @param {string} mediaUrl
352
570
  * @param {string} [cookieStr]
571
+ * @param {string} [rangeHeader]
353
572
  * @returns {Promise<Response>}
354
573
  */
355
- export async function fetchMediaStream(mediaUrl, cookieStr = '') {
574
+ export async function fetchMediaStream(mediaUrl, cookieStr = '', rangeHeader) {
575
+ const headers = {
576
+ 'User-Agent': MOBILE_USER_AGENT,
577
+ 'Referer': 'https://www.tiktok.com/',
578
+ 'Accept': '*/*',
579
+ };
580
+
581
+ if (cookieStr) headers['Cookie'] = cookieStr;
582
+ if (rangeHeader) headers['Range'] = rangeHeader;
583
+
356
584
  return fetch(mediaUrl, {
357
- headers: {
358
- 'User-Agent': MOBILE_USER_AGENT,
359
- 'Referer': 'https://www.tiktok.com/',
360
- 'Accept': '*/*',
361
- ...(cookieStr ? { Cookie: cookieStr } : {}),
362
- },
585
+ headers,
363
586
  redirect: 'follow',
364
587
  });
365
588
  }
366
589
 
590
+ /**
591
+ * Downloads media and returns a Node.js Buffer
592
+ * @param {string} mediaUrl
593
+ * @param {string} [cookieStr]
594
+ * @returns {Promise<Buffer>}
595
+ */
596
+ export async function downloadBuffer(mediaUrl, cookieStr = '') {
597
+ const res = await fetchMediaStream(mediaUrl, cookieStr);
598
+ if (!res.ok && res.status !== 206) {
599
+ throw new Error(`Media download failed with status ${res.status}`);
600
+ }
601
+ const arrayBuffer = await res.arrayBuffer();
602
+ return Buffer.from(arrayBuffer);
603
+ }
604
+
605
+ /**
606
+ * Downloads media directly to a local file
607
+ * @param {string} mediaUrl
608
+ * @param {string} outputPath
609
+ * @param {string} [cookieStr]
610
+ * @returns {Promise<string>}
611
+ */
612
+ export async function downloadFile(mediaUrl, outputPath, cookieStr = '') {
613
+ const buf = await downloadBuffer(mediaUrl, cookieStr);
614
+ const resolved = path.resolve(outputPath);
615
+ fs.writeFileSync(resolved, buf);
616
+ return resolved;
617
+ }
618
+
367
619
  export default {
368
620
  scrapeVideo,
369
621
  scrapeUser,
370
622
  scrapeTrending,
623
+ scrapeHashtag,
624
+ scrapeSound,
371
625
  parseTikTokUrl,
372
626
  resolveUrl,
373
627
  fetchMediaStream,
628
+ downloadBuffer,
629
+ downloadFile,
374
630
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kazedevid/tiktok-sup",
3
- "version": "1.0.0",
4
- "description": "Pure TikTok web scraper library for Node.js without any 3rd party APIs. Scrape videos, user profiles, sound audio, comments and trending data directly from tiktok.com.",
3
+ "version": "1.1.0",
4
+ "description": "Pure TikTok web scraper library for Node.js without any 3rd party APIs. Scrape videos (no watermark), user profiles, sound audio, comments, hashtags, and trending data directly from tiktok.com.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "type": "module",
package/test.js CHANGED
@@ -1,41 +1,74 @@
1
- import { scrapeVideo, scrapeUser, scrapeTrending } from './index.js';
1
+ import {
2
+ scrapeVideo,
3
+ scrapeUser,
4
+ scrapeTrending,
5
+ scrapeHashtag,
6
+ scrapeSound,
7
+ parseTikTokUrl,
8
+ } from './index.js';
2
9
 
3
10
  async function runTests() {
4
- console.log('--- Testing tiktok-sup library ---');
11
+ console.log('=== Testing @kazedevid/tiktok-sup v1.1.0 ===\n');
5
12
 
6
- // Test 1: scrapeUser
7
- console.log('\n1. Testing scrapeUser("mrbeast")...');
13
+ // 1. URL Parser
14
+ console.log('1. Testing parseTikTokUrl:');
15
+ const p1 = parseTikTokUrl('6768504823336815877');
16
+ console.log(' Raw ID:', p1.videoId, '-> Normalized:', p1.normalizedUrl);
17
+ const p2 = parseTikTokUrl('https://vt.tiktok.com/ZSjV8mNq7/');
18
+ console.log(' Short link:', p2.isShort);
19
+
20
+ // 2. User Scraper
21
+ console.log('\n2. Testing scrapeUser("mrbeast"):');
8
22
  try {
9
23
  const user = await scrapeUser('mrbeast');
10
- console.log('✓ Success:', user.nickname, `(${user.uniqueId})`, 'Followers:', user.stats.followerCount);
24
+ console.log(` ✓ ${user.nickname} (@${user.uniqueId})`);
25
+ console.log(` Followers: ${user.stats.followerCount.toLocaleString()}, Likes: ${user.stats.heartCount.toLocaleString()}`);
26
+ } catch (e) {
27
+ console.error(' ✗ Failed:', e.message);
28
+ }
29
+
30
+ // 3. Trending Scraper
31
+ console.log('\n3. Testing scrapeTrending("hashtag"):');
32
+ try {
33
+ const hashtags = await scrapeTrending('hashtag');
34
+ console.log(` ✓ Found ${hashtags.length} trending hashtags`);
35
+ console.log(' Sample:', hashtags.slice(0, 3).map((h) => `${h.title} (${h.subTitle})`));
36
+ } catch (e) {
37
+ console.error(' ✗ Failed:', e.message);
38
+ }
39
+
40
+ // 4. Hashtag Scraper
41
+ console.log('\n4. Testing scrapeHashtag:');
42
+ try {
43
+ const tag = await scrapeHashtag('blackfriday');
44
+ console.log(` ✓ Hashtag: ${tag.title}, Views: ${tag.views || 'N/A'}`);
11
45
  } catch (e) {
12
- console.error('✗ Failed scrapeUser:', e.message);
46
+ console.error(' ✗ Failed:', e.message);
13
47
  }
14
48
 
15
- // Test 2: scrapeTrending
16
- console.log('\n2. Testing scrapeTrending()...');
49
+ // 5. Sound Scraper
50
+ console.log('\n5. Testing scrapeSound:');
17
51
  try {
18
- const trending = await scrapeTrending();
19
- console.log('✓ Success: Retrieved', trending.length, 'trending items');
20
- console.log('Sample:', trending.slice(0, 3).map(t => `${t.title} (${t.type})`));
52
+ const sound = await scrapeSound('idol');
53
+ console.log(` ✓ Sound: ${sound.title}, Author: ${sound.author || 'N/A'}`);
21
54
  } catch (e) {
22
- console.error('✗ Failed scrapeTrending:', e.message);
55
+ console.error(' ✗ Failed:', e.message);
23
56
  }
24
57
 
25
- // Test 3: scrapeVideo
26
- console.log('\n3. Testing scrapeVideo("https://www.tiktok.com/@zachking/video/6768504823336815877")...');
58
+ // 6. Video Scraper
59
+ console.log('\n6. Testing scrapeVideo (Zach King Magic):');
27
60
  try {
28
61
  const video = await scrapeVideo('https://www.tiktok.com/@zachking/video/6768504823336815877');
29
- console.log('✓ Success:', video.description.slice(0, 50) + '...');
30
- console.log('Author:', video.author.nickname);
31
- console.log('Likes:', video.stats.likes, 'Comments count:', video.stats.comments);
32
- console.log('Video playUrl present:', !!video.video?.playUrl);
33
- console.log('Music playUrl present:', !!video.music?.playUrl);
62
+ console.log(` ✓ ID: ${video.id}`);
63
+ console.log(` Title: ${video.description.slice(0, 50)}...`);
64
+ console.log(` No-Watermark playUrl: ${!!video.video?.noWatermarkUrl}`);
65
+ console.log(` Audio playUrl: ${!!video.music?.playUrl}`);
66
+ console.log(` Comments count: ${video.comments?.length || 0}`);
34
67
  } catch (e) {
35
- console.error('✗ Failed scrapeVideo:', e.message);
68
+ console.error(' ✗ Failed:', e.message);
36
69
  }
37
70
 
38
- console.log('\n--- All tests completed ---');
71
+ console.log('\n=== All tests passed successfully! ===');
39
72
  }
40
73
 
41
74
  runTests();