Merge pull request #42 from tinolaomahei/get-collection

Get collection
This commit is contained in:
Tobi Saputra 2025-05-25 16:35:58 +07:00 committed by GitHub
commit b5955b75b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 662 additions and 4 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ test.js
bun.lockb
tsconfig.tsbuildinfo
cookies.json
dist

150
README.md
View File

@ -56,6 +56,7 @@
- [Tiktok Video Comments](#tiktok-video-comments-1)
- [Tiktok User Posts](#tiktok-user-posts)
- [Tiktok User Liked Videos](#tiktok-user-liked-videos)
- [Tiktok Collection](#tiktok-collection)
- [Contributing](#contributing)
- [License](#license)
@ -260,6 +261,99 @@ Tiktok.GetUserLiked(username, {
- [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
## 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
- All changes will be documented in the [CHANGELOG.md](https://github.com/TobyG74/tiktok-api-dl/blob/master/CHANGELOG.md) file.

View 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()

View File

@ -12,6 +12,8 @@ export const _tiktokGetComments = (params: any): string =>
`${_tiktokurl}/api/comment/list/?${params}`
export const _tiktokGetUserLiked = (params: any): string =>
`${_tiktokurl}/api/favorite/item_list/?${params}`
export const _tiktokGetCollection = (params: any): string =>
`${_tiktokurl}/api/collection/item_list/?${params}`
/** Tiktokv */
export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us`

View File

@ -353,4 +353,49 @@ const generateOdinId = () => {
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 }

View File

@ -18,6 +18,7 @@ import { TiktokStalkUserResponse } from "./types/get/getProfile"
import { TiktokVideoCommentsResponse } from "./types/get/getComments"
import { TiktokUserPostsResponse } from "./types/get/getUserPosts"
import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked"
import { TiktokCollectionResponse } from "./types/get/getCollection"
/** Services */
import { TiktokAPI } from "./utils/downloader/tiktokApi"
@ -30,6 +31,8 @@ import { getComments } from "./utils/get/getComments"
import { getUserPosts } from "./utils/get/getUserPosts"
import { getUserLiked } from "./utils/get/getUserLiked"
import { SearchVideo } from "./utils/search/videoSearch"
import { getCollection } from "./utils/get/getCollection"
import { extractCollectionId } from "./utils/downloader/tiktokApi"
/** 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
* @param {string} keyword - The query you want to search

View File

@ -12,8 +12,10 @@ export type TiktokAPIResponse = BaseContentResponse
export type AuthorTiktokAPI = Author & {
uid: string
username: string
uniqueId: string
avatarThumb: string
avatarMedium: string
url: string
}
export type StatisticsTiktokAPI = Statistics
@ -30,3 +32,22 @@ export type ResponseParserTiktokAPI = {
author?: AuthorTiktokAPI
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
}
}

View 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
}
}
}

View File

@ -1,14 +1,15 @@
import Axios from "axios"
import asyncRetry from "async-retry"
import { _tiktokvFeed, _tiktokurl } from "../../constants/api"
import { _tiktokApiParams } from "../../constants/params"
import { _tiktokvFeed, _tiktokurl, _tiktokGetCollection } from "../../constants/api"
import { _tiktokApiParams, _getCollectionParams } from "../../constants/params"
import {
AuthorTiktokAPI,
TiktokAPIResponse,
StatisticsTiktokAPI,
MusicTiktokAPI,
ResponseParserTiktokAPI,
VideoTiktokAPI
VideoTiktokAPI,
TiktokCollectionResponse
} from "../../types/downloader/tiktokApi"
import { HttpsProxyAgent } from "https-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+)/
const USER_AGENT =
"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 */
interface ProxyConfig {
@ -61,6 +63,7 @@ const parseStatistics = (content: any): StatisticsTiktokAPI => ({
const parseAuthor = (content: any): AuthorTiktokAPI => ({
uid: content.author.uid,
username: content.author.unique_id,
uniqueId: content.author.unique_id,
nickname: content.author.nickname,
signature: content.author.signature,
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
* @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
}
}
}

View 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
View 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()