get collection

This commit is contained in:
Tino Laomahei 2025-05-24 21:50:12 +12:00
parent 79913008bf
commit 5bd743a888
10 changed files with 507 additions and 3 deletions

1
.gitignore vendored
View File

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

View File

@ -392,4 +392,94 @@ 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("-c, --cursor <cursor>", "Cursor for pagination", "0")
.option("-p, --proxy <proxy>", "Proxy URL (http/https/socks)")
.action(async (collectionIdOrUrl, options) => {
try {
Logger.info(`Fetching collection...`)
const results = await Tiktok.Collection(collectionIdOrUrl, {
cursor: options.cursor,
proxy: options.proxy
})
if (results.status === "success" && results.result) {
const { itemList, hasMore, cursor } = results.result
Logger.info(`Found ${itemList.length} videos in collection`)
Logger.info(`Has more videos: ${hasMore}`)
Logger.info(`Next cursor: ${cursor}\n`)
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} -c ${cursor}`)
}
} 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,45 @@ const generateOdinId = () => {
return `${prefix}${random}`
}
export const _getCollectionParams = (collectionId: string, cursor: string = "0") => {
return qs.stringify({
WebIdLastTime: 1741246176,
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",
clientABVersions: "70508271,72437276,73356773,73406215,73464037,73558921,73561312,73563784,73607175,73713381,73720541,73737112,73759867,73768252,73787023,73810364,73810951,73811265,73812969,73815488,73815490,73817289,73821742,73849114,73855857,73858886,73858985,73867894,73880997,73902810,70138197,70156809,70405643,71057832,71200802,71381811,71516509,71803300,71962127,72360691,72408100,72854054,72892778,73004916,73171280,73208420,73574728,73628214",
collectionId,
cookie_enabled: true,
count: 30,
cursor,
data_collection_enabled: true,
device_id: "7478595310673266194",
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_mb1zbd2f_sMPZ5W5a_A3yc_4dmk_8NT3_kp4HJQOdrhp5",
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,31 @@ 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.cursor] - Optional cursor for pagination
* @returns {Promise<TiktokCollectionResponse>}
*/
Collection: async (
collectionIdOrUrl: string,
options?: {
proxy?: string
cursor?: string
}
): 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?.cursor)
},
/**
* 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,23 @@ 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
cursor: string
}
}

View File

@ -0,0 +1,55 @@
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?: {
cursor: string
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 { _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,17 @@ const createVideoResponse = (
}
})
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 +278,85 @@ export const TiktokAPI = async (
}
}
}
export const Collection = async (
collectionIdOrUrl: string,
options?: {
cursor?: string
proxy?: string
}
): Promise<TiktokCollectionResponse> => {
try {
const collectionId = extractCollectionId(collectionIdOrUrl)
if (!collectionId) {
return {
status: "error",
message: "Invalid collection ID or URL format"
}
}
const response = await Axios(
_tiktokvFeed(_getCollectionParams(collectionId, options?.cursor)),
{
method: "OPTIONS",
headers: { "User-Agent": USER_AGENT },
...createProxyAgent(options?.proxy)
}
)
if (response.data && response.data.status_code === 0) {
const data = response.data
const itemList = data.aweme_list.map((item: any) => ({
id: item.aweme_id,
desc: item.desc,
createTime: item.create_time,
author: {
uid: item.author.uid,
username: item.author.unique_id,
uniqueId: item.author.unique_id,
nickname: item.author.nickname,
signature: item.author.signature,
region: item.author.region,
avatarThumb: item.author?.avatar_thumb?.url_list || [],
avatarMedium: item.author?.avatar_medium?.url_list || [],
url: `${_tiktokurl}/@${item.author.unique_id}`
},
statistics: {
likeCount: item.statistics.digg_count,
commentCount: item.statistics.comment_count,
shareCount: item.statistics.share_count,
playCount: item.statistics.play_count
},
video: {
ratio: item.video.ratio,
duration: item.video.duration,
playAddr: item.video?.play_addr?.url_list || [],
downloadAddr: item.video?.download_addr?.url_list || [],
cover: item.video?.cover?.url_list || [],
dynamicCover: item.video?.dynamic_cover?.url_list || [],
originCover: item.video?.origin_cover?.url_list || []
},
textExtra: item.text_extra || []
}))
return {
status: "success",
result: {
itemList,
hasMore: data.has_more,
cursor: data.cursor
}
}
}
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,84 @@
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} cursor - Cursor for pagination (optional)
* @returns {Promise<TiktokCollectionResponse>}
*/
export const getCollection = async (
collectionId: string,
proxy?: string,
cursor: string = "0"
): Promise<TiktokCollectionResponse> => {
try {
const response = await retry(
async () => {
const res = await Axios(_tiktokGetCollection(_getCollectionParams(collectionId, cursor)), {
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: {
cursor: response.cursor,
hasMore: response.hasMore,
itemList: response.itemList,
extra: response.extra
}
}
} catch (error) {
return {
status: "error",
message: error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
}
}
}

85
test/collection.ts Normal file
View File

@ -0,0 +1,85 @@
import Tiktok from "../src"
async function testCollection() {
try {
// Test collection ID from your example
const collectionId = "7507916135931218695"
console.log("Fetching collection...")
const collection = await Tiktok.Collection(collectionId, {
cursor: "0" // Optional: For pagination
})
console.log(collection)
if (collection.status === "success" && collection.result) {
const { itemList, hasMore, cursor } = collection.result
console.log(`\nFound ${itemList.length} videos in collection`)
console.log(`Has more videos: ${hasMore}`)
console.log(`Next cursor: ${cursor}\n`)
// Print details of first video
if (itemList.length > 0) {
const firstVideo = itemList[0]
console.log("First video details:")
console.log("-------------------")
console.log(`Description: ${firstVideo.desc}`)
console.log(`Author: ${firstVideo.author?.nickname || 'Unknown'}`)
console.log(
`Created: ${new Date(firstVideo.createTime * 1000).toLocaleString()}`
)
// Print statistics if available
if (firstVideo.statistics) {
console.log("\nStatistics:")
console.log(`- Likes: ${firstVideo.statistics.likeCount || 0}`)
console.log(`- Comments: ${firstVideo.statistics.commentCount || 0}`)
console.log(`- Shares: ${firstVideo.statistics.shareCount || 0}`)
console.log(`- Plays: ${firstVideo.statistics.playCount || 0}`)
}
// Print video URLs if available
if (firstVideo.video) {
console.log("\nVideo URLs:")
if (firstVideo.video.playAddr?.[0]) {
console.log(`- Play URL: ${firstVideo.video.playAddr[0]}`)
}
if (firstVideo.video.downloadAddr?.[0]) {
console.log(`- Download URL: ${firstVideo.video.downloadAddr[0]}`)
}
}
// Print hashtags if available
if (firstVideo.textExtra?.length > 0) {
console.log("\nHashtags:")
firstVideo.textExtra.forEach((tag) => {
if (tag.hashtagName) {
console.log(`- #${tag.hashtagName}`)
}
})
}
}
// If there are more videos, you can fetch the next page
if (hasMore) {
console.log("\nFetching next page...")
const nextPage = await Tiktok.Collection(collectionId, {
proxy: "http://your-proxy-url", // Optional: Add your proxy if needed
cursor: cursor
})
if (nextPage.status === "success" && nextPage.result) {
console.log(`Found ${nextPage.result.itemList.length} more videos`)
}
}
} else {
console.error("Error:", collection.message)
}
} catch (error) {
console.error("Test failed:", error)
}
}
// Run the test
testCollection()