@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 +71 -74
- package/bin/cli.js +45 -33
- package/index.d.ts +58 -2
- package/index.js +318 -62
- package/package.json +2 -2
- package/test.js +54 -21
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
|
|
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
|
-
[](https://www.npmjs.com/package/tiktok-sup)
|
|
5
|
+
[](https://www.npmjs.com/package/@kazedevid/tiktok-sup)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/KazeDevID/tiktok-sup)
|
|
8
8
|
|
|
@@ -10,134 +10,131 @@
|
|
|
10
10
|
|
|
11
11
|
## ✨ Features
|
|
12
12
|
|
|
13
|
-
- 🎯 **Pure tiktok.com
|
|
14
|
-
- ⚡ **
|
|
15
|
-
- 🎬 **
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
## 🚀
|
|
43
|
+
## 🚀 Usage Guide
|
|
43
44
|
|
|
44
|
-
### 1. Scrape Video
|
|
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
|
|
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
|
|
53
|
+
console.log('Title:', video.description);
|
|
53
54
|
console.log('Author:', video.author.nickname, `(@${video.author.username})`);
|
|
54
|
-
console.log('
|
|
55
|
-
console.log('
|
|
56
|
-
console.log('
|
|
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
|
-
|
|
71
|
+
// Pass username with or without '@', or full profile URL
|
|
72
|
+
const user = await scrapeUser('mrbeast');
|
|
65
73
|
|
|
66
|
-
console.log('Name:',
|
|
67
|
-
console.log('Followers:',
|
|
68
|
-
console.log('Total Likes:',
|
|
69
|
-
console.log('Bio:',
|
|
70
|
-
console.log('
|
|
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.
|
|
81
|
+
### 3. Get Media Buffer (for Bots / Telegram / Discord)
|
|
74
82
|
|
|
75
83
|
```javascript
|
|
76
|
-
import {
|
|
84
|
+
import { scrapeVideo, downloadBuffer } from '@kazedevid/tiktok-sup';
|
|
77
85
|
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
-
|
|
89
|
+
// Send buffer to Telegram, Discord, or S3
|
|
90
|
+
// bot.sendVideo(chatId, buffer);
|
|
91
|
+
```
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
### 4. Trending & Hashtags
|
|
87
94
|
|
|
88
95
|
```javascript
|
|
89
|
-
import {
|
|
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
|
-
//
|
|
96
|
-
const
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
117
|
-
npx tiktok-sup trending
|
|
117
|
+
# List trending creators & sounds
|
|
118
|
+
npx @kazedevid/tiktok-sup trending
|
|
118
119
|
|
|
119
|
-
# Download video
|
|
120
|
-
npx tiktok-sup download "https://vt.tiktok.com/..."
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
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>
|
|
23
|
-
npx tiktok-sup user <username>
|
|
24
|
-
npx tiktok-sup trending
|
|
25
|
-
npx tiktok-sup
|
|
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
|
|
29
|
-
npx tiktok-sup user
|
|
30
|
-
npx tiktok-sup
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
username
|
|
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)
|
|
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
|
-
|
|
42
|
-
if (!url.startsWith('http')) {
|
|
43
|
-
url = 'https://' + url;
|
|
44
|
-
}
|
|
75
|
+
const { normalizedUrl, videoId: parsedId } = parseTikTokUrl(inputUrl);
|
|
45
76
|
|
|
46
|
-
const res = await fetch(
|
|
77
|
+
const res = await fetch(normalizedUrl, {
|
|
47
78
|
headers: {
|
|
48
79
|
'User-Agent': MOBILE_USER_AGENT,
|
|
49
|
-
'Accept':
|
|
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 = (
|
|
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:
|
|
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 } =
|
|
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':
|
|
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(
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Failed to fetch video page from tiktok.com: HTTP ${res.status}`
|
|
134
|
+
);
|
|
84
135
|
}
|
|
85
136
|
|
|
86
|
-
const pageCookiesList = (
|
|
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]
|
|
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
|
-
//
|
|
96
|
-
const apiDataMatch = html.match(
|
|
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 (
|
|
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
|
-
//
|
|
171
|
+
// Parse Method B: <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__">
|
|
110
172
|
if (!itemStruct) {
|
|
111
|
-
const rehydrationMatch = html.match(
|
|
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
|
-
//
|
|
188
|
+
// Parse Method C: SIGI_STATE
|
|
125
189
|
if (!itemStruct) {
|
|
126
|
-
const sigiMatch = html.match(
|
|
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(
|
|
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
|
-
//
|
|
239
|
+
// Photos
|
|
144
240
|
const photos = [];
|
|
145
|
-
if (
|
|
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 =
|
|
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(
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
194
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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(
|
|
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':
|
|
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 @${
|
|
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(
|
|
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(
|
|
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(
|
|
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 ||
|
|
281
|
-
nickname: String(u.nickname ||
|
|
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
|
-
*
|
|
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.
|
|
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 {
|
|
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('
|
|
11
|
+
console.log('=== Testing @kazedevid/tiktok-sup v1.1.0 ===\n');
|
|
5
12
|
|
|
6
|
-
//
|
|
7
|
-
console.log('
|
|
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(
|
|
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
|
|
46
|
+
console.error(' ✗ Failed:', e.message);
|
|
13
47
|
}
|
|
14
48
|
|
|
15
|
-
//
|
|
16
|
-
console.log('\
|
|
49
|
+
// 5. Sound Scraper
|
|
50
|
+
console.log('\n5. Testing scrapeSound:');
|
|
17
51
|
try {
|
|
18
|
-
const
|
|
19
|
-
console.log(
|
|
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
|
|
55
|
+
console.error(' ✗ Failed:', e.message);
|
|
23
56
|
}
|
|
24
57
|
|
|
25
|
-
//
|
|
26
|
-
console.log('\
|
|
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(
|
|
30
|
-
console.log(
|
|
31
|
-
console.log(
|
|
32
|
-
console.log(
|
|
33
|
-
console.log(
|
|
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
|
|
68
|
+
console.error(' ✗ Failed:', e.message);
|
|
36
69
|
}
|
|
37
70
|
|
|
38
|
-
console.log('\n
|
|
71
|
+
console.log('\n=== All tests passed successfully! ===');
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
runTests();
|