commit
b5955b75b4
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ test.js
|
|||||||
bun.lockb
|
bun.lockb
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
cookies.json
|
cookies.json
|
||||||
|
dist
|
||||||
150
README.md
150
README.md
@ -56,6 +56,7 @@
|
|||||||
- [Tiktok Video Comments](#tiktok-video-comments-1)
|
- [Tiktok Video Comments](#tiktok-video-comments-1)
|
||||||
- [Tiktok User Posts](#tiktok-user-posts)
|
- [Tiktok User Posts](#tiktok-user-posts)
|
||||||
- [Tiktok User Liked Videos](#tiktok-user-liked-videos)
|
- [Tiktok User Liked Videos](#tiktok-user-liked-videos)
|
||||||
|
- [Tiktok Collection](#tiktok-collection)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
@ -260,6 +261,99 @@ Tiktok.GetUserLiked(username, {
|
|||||||
|
|
||||||
- [Tiktok User Liked Videos Response](#tiktok-user-liked-videos)
|
- [Tiktok User Liked Videos Response](#tiktok-user-liked-videos)
|
||||||
|
|
||||||
|
## Tiktok Collection
|
||||||
|
|
||||||
|
Get videos from a TikTok collection (supports collection ID or URL)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const Tiktok = require("@tobyg74/tiktok-api-dl")
|
||||||
|
|
||||||
|
// Using collection ID
|
||||||
|
const collectionId = "7507916135931218695"
|
||||||
|
Tiktok.Collection(collectionId, {
|
||||||
|
page: 1, // optional, default is 1
|
||||||
|
count: 5, // optional, default is 5
|
||||||
|
proxy: "YOUR_PROXY" // optional
|
||||||
|
}).then((result) => console.log(result))
|
||||||
|
|
||||||
|
// Using collection URL
|
||||||
|
const collectionUrl = "https://www.tiktok.com/@username/collection/name-id"
|
||||||
|
Tiktok.Collection(collectionUrl, {
|
||||||
|
page: 1,
|
||||||
|
count: 5,
|
||||||
|
proxy: "YOUR_PROXY"
|
||||||
|
}).then((result) => console.log(result))
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using collection ID
|
||||||
|
tiktokdl collection 7507916135931218695 -n 5
|
||||||
|
|
||||||
|
# Using collection URL
|
||||||
|
tiktokdl collection "https://www.tiktok.com/@username/collection/name-id" -n 5
|
||||||
|
|
||||||
|
# With page for pagination
|
||||||
|
tiktokdl collection 7507916135931218695 -p 1 -n 5
|
||||||
|
|
||||||
|
# With proxy
|
||||||
|
tiktokdl collection 7507916135931218695 -n 5 -proxy "http://your-proxy-url"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Type
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TiktokCollectionResponse {
|
||||||
|
status: "success" | "error"
|
||||||
|
message?: string
|
||||||
|
result?: {
|
||||||
|
itemList: Array<{
|
||||||
|
id: string
|
||||||
|
desc: string
|
||||||
|
createTime: number
|
||||||
|
author?: {
|
||||||
|
id: string
|
||||||
|
uniqueId: string
|
||||||
|
nickname: string
|
||||||
|
avatarThumb: string
|
||||||
|
avatarMedium: string
|
||||||
|
avatarLarger: string
|
||||||
|
signature: string
|
||||||
|
verified: boolean
|
||||||
|
}
|
||||||
|
statistics?: {
|
||||||
|
playCount: number
|
||||||
|
diggCount: number
|
||||||
|
shareCount: number
|
||||||
|
commentCount: number
|
||||||
|
collectCount: number
|
||||||
|
}
|
||||||
|
video?: {
|
||||||
|
id: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
duration: number
|
||||||
|
ratio: string
|
||||||
|
cover: string
|
||||||
|
originCover: string
|
||||||
|
dynamicCover: string
|
||||||
|
playAddr: string
|
||||||
|
downloadAddr: string
|
||||||
|
format: string
|
||||||
|
bitrate: number
|
||||||
|
}
|
||||||
|
textExtra?: Array<{
|
||||||
|
hashtagName: string
|
||||||
|
hashtagId: string
|
||||||
|
type: number
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# API Response Types
|
# API Response Types
|
||||||
|
|
||||||
## Tiktok Downloader
|
## Tiktok Downloader
|
||||||
@ -712,6 +806,62 @@ interface TiktokUserFavoriteVideosResponse {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Tiktok Collection
|
||||||
|
|
||||||
|
### Collection Response
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TiktokCollectionResponse {
|
||||||
|
status: "success" | "error"
|
||||||
|
message?: string
|
||||||
|
result?: {
|
||||||
|
itemList: Array<{
|
||||||
|
id: string
|
||||||
|
desc: string
|
||||||
|
createTime: number
|
||||||
|
author?: {
|
||||||
|
id: string
|
||||||
|
uniqueId: string
|
||||||
|
nickname: string
|
||||||
|
avatarThumb: string
|
||||||
|
avatarMedium: string
|
||||||
|
avatarLarger: string
|
||||||
|
signature: string
|
||||||
|
verified: boolean
|
||||||
|
}
|
||||||
|
statistics?: {
|
||||||
|
playCount: number
|
||||||
|
diggCount: number
|
||||||
|
shareCount: number
|
||||||
|
commentCount: number
|
||||||
|
collectCount: number
|
||||||
|
}
|
||||||
|
video?: {
|
||||||
|
id: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
duration: number
|
||||||
|
ratio: string
|
||||||
|
cover: string
|
||||||
|
originCover: string
|
||||||
|
dynamicCover: string
|
||||||
|
playAddr: string
|
||||||
|
downloadAddr: string
|
||||||
|
format: string
|
||||||
|
bitrate: number
|
||||||
|
}
|
||||||
|
textExtra?: Array<{
|
||||||
|
hashtagName: string
|
||||||
|
hashtagId: string
|
||||||
|
type: number
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
hasMore: boolean
|
||||||
|
cursor: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- All changes will be documented in the [CHANGELOG.md](https://github.com/TobyG74/tiktok-api-dl/blob/master/CHANGELOG.md) file.
|
- All changes will be documented in the [CHANGELOG.md](https://github.com/TobyG74/tiktok-api-dl/blob/master/CHANGELOG.md) file.
|
||||||
|
|||||||
@ -392,4 +392,99 @@ program
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Collection Command
|
||||||
|
program
|
||||||
|
.command("collection")
|
||||||
|
.description(
|
||||||
|
"Get videos from a TikTok collection (supports collection ID or URL)"
|
||||||
|
)
|
||||||
|
.argument(
|
||||||
|
"<collectionIdOrUrl>",
|
||||||
|
"Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id)"
|
||||||
|
)
|
||||||
|
.option("-p, --page <number>", "Page number", "1")
|
||||||
|
.option("--proxy <proxy>", "Proxy URL (http/https/socks)")
|
||||||
|
.option(
|
||||||
|
"-n, --count <number>",
|
||||||
|
"Number of items to fetch",
|
||||||
|
(val) => parseInt(val),
|
||||||
|
5
|
||||||
|
)
|
||||||
|
.action(async (collectionIdOrUrl, options) => {
|
||||||
|
try {
|
||||||
|
Logger.info(`Fetching page ${options.page} with ${options.count} items per page from collection...`)
|
||||||
|
const results = await Tiktok.Collection(collectionIdOrUrl, {
|
||||||
|
page: options.page,
|
||||||
|
proxy: options.proxy,
|
||||||
|
count: options.count
|
||||||
|
})
|
||||||
|
|
||||||
|
if (results.status === "success" && results.result) {
|
||||||
|
const { itemList, hasMore } = results.result
|
||||||
|
|
||||||
|
Logger.info(`Found ${itemList.length} videos in collection`)
|
||||||
|
Logger.info(`Has more videos: ${hasMore}`)
|
||||||
|
|
||||||
|
for (const [index, video] of itemList.entries()) {
|
||||||
|
Logger.info(`---- VIDEO ${index + 1} ----`)
|
||||||
|
Logger.result(`Video ID: ${video.id}`, chalk.green)
|
||||||
|
Logger.result(`Description: ${video.desc}`, chalk.yellow)
|
||||||
|
Logger.result(
|
||||||
|
`Author: ${video.author?.nickname || "Unknown"}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
Logger.result(
|
||||||
|
`Created: ${new Date(video.createTime * 1000).toLocaleString()}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
|
||||||
|
if (video.statistics) {
|
||||||
|
Logger.info(`---- STATISTICS ----`)
|
||||||
|
Logger.result(
|
||||||
|
`Likes: ${video.statistics.likeCount || 0}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
Logger.result(
|
||||||
|
`Comments: ${video.statistics.commentCount || 0}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
Logger.result(
|
||||||
|
`Shares: ${video.statistics.shareCount || 0}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
Logger.result(
|
||||||
|
`Plays: ${video.statistics.playCount || 0}`,
|
||||||
|
chalk.yellow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.video) {
|
||||||
|
Logger.info(`---- VIDEO URLs ----`)
|
||||||
|
const videoUrl = `${_tiktokurl}/@${video.author?.uniqueId || "unknown"
|
||||||
|
}/video/${video.id}`
|
||||||
|
Logger.result(`Video URL: ${videoUrl}`, chalk.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.textExtra?.length > 0) {
|
||||||
|
Logger.info(`---- HASHTAGS ----`)
|
||||||
|
video.textExtra.forEach((tag) => {
|
||||||
|
if (tag.hashtagName) {
|
||||||
|
Logger.result(`#${tag.hashtagName}`, chalk.cyan)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
Logger.info("\nTo fetch more videos, use:")
|
||||||
|
Logger.info(`tiktokdl collection ${collectionIdOrUrl} -p ${parseInt(options.page) + 1}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.error(`Error: ${results.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Error: ${error.message}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
program.parse()
|
program.parse()
|
||||||
|
|||||||
@ -12,6 +12,8 @@ export const _tiktokGetComments = (params: any): string =>
|
|||||||
`${_tiktokurl}/api/comment/list/?${params}`
|
`${_tiktokurl}/api/comment/list/?${params}`
|
||||||
export const _tiktokGetUserLiked = (params: any): string =>
|
export const _tiktokGetUserLiked = (params: any): string =>
|
||||||
`${_tiktokurl}/api/favorite/item_list/?${params}`
|
`${_tiktokurl}/api/favorite/item_list/?${params}`
|
||||||
|
export const _tiktokGetCollection = (params: any): string =>
|
||||||
|
`${_tiktokurl}/api/collection/item_list/?${params}`
|
||||||
|
|
||||||
/** Tiktokv */
|
/** Tiktokv */
|
||||||
export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us`
|
export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us`
|
||||||
|
|||||||
@ -353,4 +353,49 @@ const generateOdinId = () => {
|
|||||||
return `${prefix}${random}`
|
return `${prefix}${random}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const _getCollectionParams = (collectionId: string, page: number = 1, count: number = 5) => {
|
||||||
|
let cursor = 0
|
||||||
|
if (page > 0) {
|
||||||
|
cursor = (page - 1) * count
|
||||||
|
}
|
||||||
|
|
||||||
|
return qs.stringify({
|
||||||
|
WebIdLastTime: Date.now(),
|
||||||
|
aid: 1988,
|
||||||
|
app_language: "en",
|
||||||
|
app_name: "tiktok_web",
|
||||||
|
browser_language: "en-US",
|
||||||
|
browser_name: "Mozilla",
|
||||||
|
browser_online: true,
|
||||||
|
browser_platform: "Win32",
|
||||||
|
browser_version: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||||
|
channel: "tiktok_web",
|
||||||
|
collectionId,
|
||||||
|
cookie_enabled: true,
|
||||||
|
count,
|
||||||
|
cursor: cursor.toString(),
|
||||||
|
data_collection_enabled: true,
|
||||||
|
device_id: "7002566096994190854",
|
||||||
|
device_platform: "web_pc",
|
||||||
|
focus_state: true,
|
||||||
|
from_page: "user",
|
||||||
|
history_len: 3,
|
||||||
|
is_fullscreen: false,
|
||||||
|
is_page_visible: true,
|
||||||
|
language: "en",
|
||||||
|
odinId: "7458943931621032978",
|
||||||
|
os: "windows",
|
||||||
|
priority_region: "NZ",
|
||||||
|
referer: "",
|
||||||
|
region: "NZ",
|
||||||
|
screen_height: 1440,
|
||||||
|
screen_width: 2560,
|
||||||
|
sourceType: 113,
|
||||||
|
tz_name: "Pacific/Auckland",
|
||||||
|
user_is_login: true,
|
||||||
|
verifyFp: "verify_lacphy8d_z2ux9idt_xdmu_4gKb_9nng_NNTTTvsFS8ao",
|
||||||
|
webcast_language: "en"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export { randomChar, generateSearchId, generateDeviceId, generateOdinId }
|
export { randomChar, generateSearchId, generateDeviceId, generateOdinId }
|
||||||
|
|||||||
30
src/index.ts
30
src/index.ts
@ -18,6 +18,7 @@ import { TiktokStalkUserResponse } from "./types/get/getProfile"
|
|||||||
import { TiktokVideoCommentsResponse } from "./types/get/getComments"
|
import { TiktokVideoCommentsResponse } from "./types/get/getComments"
|
||||||
import { TiktokUserPostsResponse } from "./types/get/getUserPosts"
|
import { TiktokUserPostsResponse } from "./types/get/getUserPosts"
|
||||||
import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked"
|
import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked"
|
||||||
|
import { TiktokCollectionResponse } from "./types/get/getCollection"
|
||||||
|
|
||||||
/** Services */
|
/** Services */
|
||||||
import { TiktokAPI } from "./utils/downloader/tiktokApi"
|
import { TiktokAPI } from "./utils/downloader/tiktokApi"
|
||||||
@ -30,6 +31,8 @@ import { getComments } from "./utils/get/getComments"
|
|||||||
import { getUserPosts } from "./utils/get/getUserPosts"
|
import { getUserPosts } from "./utils/get/getUserPosts"
|
||||||
import { getUserLiked } from "./utils/get/getUserLiked"
|
import { getUserLiked } from "./utils/get/getUserLiked"
|
||||||
import { SearchVideo } from "./utils/search/videoSearch"
|
import { SearchVideo } from "./utils/search/videoSearch"
|
||||||
|
import { getCollection } from "./utils/get/getCollection"
|
||||||
|
import { extractCollectionId } from "./utils/downloader/tiktokApi"
|
||||||
|
|
||||||
/** Constants */
|
/** Constants */
|
||||||
import { DOWNLOADER_VERSIONS, SEARCH_TYPES } from "./constants"
|
import { DOWNLOADER_VERSIONS, SEARCH_TYPES } from "./constants"
|
||||||
@ -122,6 +125,33 @@ export = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TikTok Collection
|
||||||
|
* @param {string} collectionIdOrUrl - Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id)
|
||||||
|
* @param {Object} options - The options for collection
|
||||||
|
* @param {string} [options.proxy] - Optional proxy URL
|
||||||
|
* @param {string} [options.page] - Optional page for pagination
|
||||||
|
* @param {number} [options.count] - Optional number of items to fetch
|
||||||
|
* @returns {Promise<TiktokCollectionResponse>}
|
||||||
|
*/
|
||||||
|
Collection: async (
|
||||||
|
collectionIdOrUrl: string,
|
||||||
|
options?: {
|
||||||
|
proxy?: string
|
||||||
|
page?: number
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
): Promise<TiktokCollectionResponse> => {
|
||||||
|
const collectionId = extractCollectionId(collectionIdOrUrl)
|
||||||
|
if (!collectionId) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid collection ID or URL format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await getCollection(collectionId, options?.proxy, options?.page, options?.count)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tiktok Search
|
* Tiktok Search
|
||||||
* @param {string} keyword - The query you want to search
|
* @param {string} keyword - The query you want to search
|
||||||
|
|||||||
@ -12,8 +12,10 @@ export type TiktokAPIResponse = BaseContentResponse
|
|||||||
export type AuthorTiktokAPI = Author & {
|
export type AuthorTiktokAPI = Author & {
|
||||||
uid: string
|
uid: string
|
||||||
username: string
|
username: string
|
||||||
|
uniqueId: string
|
||||||
avatarThumb: string
|
avatarThumb: string
|
||||||
avatarMedium: string
|
avatarMedium: string
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StatisticsTiktokAPI = Statistics
|
export type StatisticsTiktokAPI = Statistics
|
||||||
@ -30,3 +32,22 @@ export type ResponseParserTiktokAPI = {
|
|||||||
author?: AuthorTiktokAPI
|
author?: AuthorTiktokAPI
|
||||||
music?: MusicTiktokAPI
|
music?: MusicTiktokAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TiktokCollectionResponse = {
|
||||||
|
status: "success" | "error"
|
||||||
|
message?: string
|
||||||
|
result?: {
|
||||||
|
itemList: Array<{
|
||||||
|
id: string
|
||||||
|
desc: string
|
||||||
|
createTime: number
|
||||||
|
author: AuthorTiktokAPI
|
||||||
|
statistics: StatisticsTiktokAPI
|
||||||
|
video: VideoTiktokAPI
|
||||||
|
textExtra: Array<{
|
||||||
|
hashtagName?: string
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
54
src/types/get/getCollection.ts
Normal file
54
src/types/get/getCollection.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { AuthorTiktokAPI, StatisticsTiktokAPI, MusicTiktokAPI, VideoTiktokAPI } from "../downloader/tiktokApi"
|
||||||
|
|
||||||
|
export interface CollectionItem {
|
||||||
|
id: string
|
||||||
|
desc: string
|
||||||
|
createTime: number
|
||||||
|
author: AuthorTiktokAPI
|
||||||
|
statistics: StatisticsTiktokAPI
|
||||||
|
video: VideoTiktokAPI
|
||||||
|
music: MusicTiktokAPI
|
||||||
|
challenges: Array<{
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
coverLarger: string
|
||||||
|
coverMedium: string
|
||||||
|
coverThumb: string
|
||||||
|
profileLarger: string
|
||||||
|
profileMedium: string
|
||||||
|
profileThumb: string
|
||||||
|
}>
|
||||||
|
collected: boolean
|
||||||
|
digged: boolean
|
||||||
|
duetDisplay: number
|
||||||
|
forFriend: boolean
|
||||||
|
officalItem: boolean
|
||||||
|
originalItem: boolean
|
||||||
|
privateItem: boolean
|
||||||
|
shareEnabled: boolean
|
||||||
|
stitchDisplay: number
|
||||||
|
textExtra: Array<{
|
||||||
|
awemeId: string
|
||||||
|
end: number
|
||||||
|
hashtagName: string
|
||||||
|
isCommerce: boolean
|
||||||
|
start: number
|
||||||
|
subType: number
|
||||||
|
type: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TiktokCollectionResponse {
|
||||||
|
status: "success" | "error"
|
||||||
|
message?: string
|
||||||
|
result?: {
|
||||||
|
hasMore: boolean
|
||||||
|
itemList: CollectionItem[]
|
||||||
|
extra?: {
|
||||||
|
fatal_item_ids: string[]
|
||||||
|
logid: string
|
||||||
|
now: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import Axios from "axios"
|
import Axios from "axios"
|
||||||
import asyncRetry from "async-retry"
|
import asyncRetry from "async-retry"
|
||||||
import { _tiktokvFeed, _tiktokurl } from "../../constants/api"
|
import { _tiktokvFeed, _tiktokurl, _tiktokGetCollection } from "../../constants/api"
|
||||||
import { _tiktokApiParams } from "../../constants/params"
|
import { _tiktokApiParams, _getCollectionParams } from "../../constants/params"
|
||||||
import {
|
import {
|
||||||
AuthorTiktokAPI,
|
AuthorTiktokAPI,
|
||||||
TiktokAPIResponse,
|
TiktokAPIResponse,
|
||||||
StatisticsTiktokAPI,
|
StatisticsTiktokAPI,
|
||||||
MusicTiktokAPI,
|
MusicTiktokAPI,
|
||||||
ResponseParserTiktokAPI,
|
ResponseParserTiktokAPI,
|
||||||
VideoTiktokAPI
|
VideoTiktokAPI,
|
||||||
|
TiktokCollectionResponse
|
||||||
} from "../../types/downloader/tiktokApi"
|
} from "../../types/downloader/tiktokApi"
|
||||||
import { HttpsProxyAgent } from "https-proxy-agent"
|
import { HttpsProxyAgent } from "https-proxy-agent"
|
||||||
import { SocksProxyAgent } from "socks-proxy-agent"
|
import { SocksProxyAgent } from "socks-proxy-agent"
|
||||||
@ -19,6 +20,7 @@ const TIKTOK_URL_REGEX =
|
|||||||
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
|
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
|
||||||
const USER_AGENT =
|
const USER_AGENT =
|
||||||
"com.zhiliaoapp.musically/300904 (2018111632; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)"
|
"com.zhiliaoapp.musically/300904 (2018111632; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)"
|
||||||
|
const COLLECTION_URL_REGEX = /collection\/[^/]+-(\d+)/
|
||||||
|
|
||||||
/** Types */
|
/** Types */
|
||||||
interface ProxyConfig {
|
interface ProxyConfig {
|
||||||
@ -61,6 +63,7 @@ const parseStatistics = (content: any): StatisticsTiktokAPI => ({
|
|||||||
const parseAuthor = (content: any): AuthorTiktokAPI => ({
|
const parseAuthor = (content: any): AuthorTiktokAPI => ({
|
||||||
uid: content.author.uid,
|
uid: content.author.uid,
|
||||||
username: content.author.unique_id,
|
username: content.author.unique_id,
|
||||||
|
uniqueId: content.author.unique_id,
|
||||||
nickname: content.author.nickname,
|
nickname: content.author.nickname,
|
||||||
signature: content.author.signature,
|
signature: content.author.signature,
|
||||||
region: content.author.region,
|
region: content.author.region,
|
||||||
@ -194,6 +197,37 @@ const createVideoResponse = (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleRedirect = async (url: string, proxy?: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response = await Axios(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
maxRedirects: 5,
|
||||||
|
validateStatus: (status) => status >= 200 && status < 400,
|
||||||
|
...createProxyAgent(proxy)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the final URL after all redirects
|
||||||
|
const finalUrl = response.request.res.responseUrl
|
||||||
|
|
||||||
|
// Remove query parameters
|
||||||
|
return finalUrl.split('?')[0]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling redirect:', error)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractCollectionId = (input: string): string | null => {
|
||||||
|
// If it's already just a number, return it
|
||||||
|
if (/^\d+$/.test(input)) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract from URL
|
||||||
|
const match = input.match(COLLECTION_URL_REGEX)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tiktok API Downloader
|
* Tiktok API Downloader
|
||||||
* @param {string} url - Tiktok URL
|
* @param {string} url - Tiktok URL
|
||||||
@ -264,3 +298,67 @@ export const TiktokAPI = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Collection = async (
|
||||||
|
collectionIdOrUrl: string,
|
||||||
|
options?: {
|
||||||
|
page?: number,
|
||||||
|
proxy?: string,
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
): Promise<TiktokCollectionResponse> => {
|
||||||
|
try {
|
||||||
|
// Only handle redirects if the input is a URL
|
||||||
|
const processedUrl = collectionIdOrUrl.startsWith('http')
|
||||||
|
? await handleRedirect(collectionIdOrUrl, options?.proxy)
|
||||||
|
: collectionIdOrUrl
|
||||||
|
|
||||||
|
const collectionId = extractCollectionId(processedUrl)
|
||||||
|
if (!collectionId) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid collection ID or URL format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await Axios(
|
||||||
|
_tiktokGetCollection(
|
||||||
|
_getCollectionParams(collectionId, options.page, options.count)
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.7",
|
||||||
|
Referer: "https://www.tiktok.com/",
|
||||||
|
Origin: "https://www.tiktok.com"
|
||||||
|
},
|
||||||
|
...createProxyAgent(options?.proxy)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.data && response.data.status_code === 0) {
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
result: {
|
||||||
|
itemList: data.itemList || [],
|
||||||
|
hasMore: data.hasMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: ERROR_MESSAGES.NETWORK_ERROR
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
92
src/utils/get/getCollection.ts
Normal file
92
src/utils/get/getCollection.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import Axios from "axios"
|
||||||
|
import { _tiktokGetCollection } from "../../constants/api"
|
||||||
|
import { _getCollectionParams } from "../../constants/params"
|
||||||
|
import { HttpsProxyAgent } from "https-proxy-agent"
|
||||||
|
import { SocksProxyAgent } from "socks-proxy-agent"
|
||||||
|
import { TiktokCollectionResponse } from "../../types/get/getCollection"
|
||||||
|
import { ERROR_MESSAGES } from "../../constants"
|
||||||
|
import retry from "async-retry"
|
||||||
|
|
||||||
|
/** Types */
|
||||||
|
interface ProxyConfig {
|
||||||
|
httpsAgent?: HttpsProxyAgent<string> | SocksProxyAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
const createProxyAgent = (proxy?: string): ProxyConfig => {
|
||||||
|
if (!proxy) return {}
|
||||||
|
|
||||||
|
if (proxy.startsWith("socks")) {
|
||||||
|
return {
|
||||||
|
httpsAgent: new SocksProxyAgent(proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpsAgent: new HttpsProxyAgent(proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TikTok Collection
|
||||||
|
* @param {string} collectionId - Collection ID
|
||||||
|
* @param {string} proxy - Your Proxy (optional)
|
||||||
|
* @param {string} page - Page for pagination (optional)
|
||||||
|
* @param {number} count - Number of items to fetch (optional)
|
||||||
|
* @returns {Promise<TiktokCollectionResponse>}
|
||||||
|
*/
|
||||||
|
export const getCollection = async (
|
||||||
|
collectionId: string,
|
||||||
|
proxy?: string,
|
||||||
|
page: number = 1,
|
||||||
|
count: number = 5
|
||||||
|
): Promise<TiktokCollectionResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await retry(
|
||||||
|
async () => {
|
||||||
|
const res = await Axios(
|
||||||
|
_tiktokGetCollection(
|
||||||
|
_getCollectionParams(collectionId, page, count)
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.7",
|
||||||
|
Referer: "https://www.tiktok.com/",
|
||||||
|
Origin: "https://www.tiktok.com"
|
||||||
|
},
|
||||||
|
...createProxyAgent(proxy)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.data && res.data.statusCode === 0) {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(ERROR_MESSAGES.NETWORK_ERROR)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retries: 20,
|
||||||
|
minTimeout: 200,
|
||||||
|
maxTimeout: 1000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
result: {
|
||||||
|
hasMore: response.hasMore,
|
||||||
|
itemList: response.itemList || [],
|
||||||
|
extra: response.extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
test/collection-test.ts
Normal file
70
test/collection-test.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Collection } from "../src/utils/downloader/tiktokApi"
|
||||||
|
|
||||||
|
async function testCollection() {
|
||||||
|
try {
|
||||||
|
// You can use either a collection ID or URL
|
||||||
|
const collectionId = "7507916135931218695"
|
||||||
|
const collectionUrl = "https://www.tiktok.com/@getrex.co.nz/collection/big%20back-7507916135931218695"
|
||||||
|
const collectionShareableLink = "https://vt.tiktok.com/ZShvmqNjQ/"
|
||||||
|
|
||||||
|
console.log("Testing Collection method...")
|
||||||
|
const result = await Collection(collectionId, {
|
||||||
|
page: 1,
|
||||||
|
count: 5, // Optional: Number of items to fetch
|
||||||
|
proxy: undefined // Optional: Add your proxy if needed
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.status === "success" && result.result) {
|
||||||
|
console.log("\nCollection fetched successfully!")
|
||||||
|
console.log("========================")
|
||||||
|
console.log("Collection Overview:")
|
||||||
|
console.log("========================")
|
||||||
|
console.log(`Total items fetched: ${result.result.itemList.length}`)
|
||||||
|
console.log(`Has more items: ${result.result.hasMore}`)
|
||||||
|
|
||||||
|
// Log all items
|
||||||
|
result.result.itemList.forEach((item, index) => {
|
||||||
|
console.log(`\nItem ${index + 1}:`)
|
||||||
|
console.log("-------------------")
|
||||||
|
console.log(`ID: ${item.id}`)
|
||||||
|
console.log(`Description: ${item.desc}`)
|
||||||
|
console.log(`Author: ${item.author.nickname}`)
|
||||||
|
console.log(`Created: ${new Date(item.createTime * 1000).toLocaleString()}`)
|
||||||
|
|
||||||
|
// Log video URL
|
||||||
|
if (item.video?.playAddr?.[0]) {
|
||||||
|
console.log(`Video URL: ${item.video.playAddr[0]}`)
|
||||||
|
} else {
|
||||||
|
console.log("No video URL available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log item statistics
|
||||||
|
if (item.statistics) {
|
||||||
|
console.log("\nStatistics:")
|
||||||
|
console.log(`- Likes: ${item.statistics.likeCount || 0}`)
|
||||||
|
console.log(`- Comments: ${item.statistics.commentCount || 0}`)
|
||||||
|
console.log(`- Shares: ${item.statistics.shareCount || 0}`)
|
||||||
|
console.log(`- Plays: ${item.statistics.playCount || 0}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log hashtags if available
|
||||||
|
if (item.textExtra?.length > 0) {
|
||||||
|
console.log("\nHashtags:")
|
||||||
|
item.textExtra.forEach(tag => {
|
||||||
|
if (tag.hashtagName) {
|
||||||
|
console.log(`- #${tag.hashtagName}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log("========================")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error("Error:", result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Test failed:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testCollection()
|
||||||
Loading…
x
Reference in New Issue
Block a user