feat: add get user repost videos

This commit is contained in:
Tobi Saputra 2025-07-12 18:05:26 +07:00
parent d71a970977
commit 9bab4ad652
7 changed files with 519 additions and 25 deletions

View File

@ -400,42 +400,49 @@ program
}
})
// Get User Favorites Command
// Get User Reposts Command
program
.command("getuserliked")
.description("Get user liked videos from a TikTok user")
.command("getuserreposts")
.description("Get reposts from a TikTok user")
.argument("<username>", "TikTok username")
.option("-l, --limit <number>", "Limit of posts", "5")
.option("-l, --limit <number>", "Limit of reposts", "5")
.option("--proxy <proxy>", "Proxy URL (http/https/socks)")
.action(async (username, options) => {
try {
const postLimit = parseInt(options.limit)
const results = await Tiktok.GetUserLiked(username, {
cookie: cookieManager.getCookie(),
const results = await Tiktok.GetUserReposts(username, {
postLimit: postLimit,
proxy: options.proxy
})
if (results.status === "success") {
const data = results.result
for (const [index, liked] of data.entries()) {
Logger.info(`---- FAVORITE ${index + 1} ----`)
Logger.result(`Video ID: ${liked.id}`, chalk.green)
Logger.result(`Description: ${liked.desc}`, chalk.yellow)
Logger.result(`Author: ${liked.author.nickname}`, chalk.yellow)
Logger.result(
`Video URL: ${_tiktokurl}/@${liked.author.username}/video/${liked.video.id}`,
chalk.yellow
)
for (const [index, repost] of data.entries()) {
Logger.info(`---- REPOST ${index + 1} ----`)
Logger.result(`Video ID: ${repost.id}`, chalk.green)
Logger.result(`Description: ${repost.desc}`, chalk.yellow)
Logger.info(`---- STATISTICS ----`)
Logger.result(`Likes: ${liked.stats.diggCount}`, chalk.yellow)
Logger.result(`Favorites: ${liked.stats.collectCount}`, chalk.yellow)
Logger.result(`Views: ${liked.stats.playCount}`, chalk.yellow)
Logger.result(`Shares: ${liked.stats.shareCount}`, chalk.yellow)
Logger.result(`Comments: ${liked.stats.commentCount}`, chalk.yellow)
Logger.result(`Reposts: ${liked.stats.repostCount}`, chalk.yellow)
Logger.result(`Shares: ${repost.stats.shareCount}`, chalk.yellow)
if (repost.stats.likeCount) {
Logger.result(`Likes: ${repost.stats.likeCount}`, chalk.yellow)
}
if (repost.stats.collectCount) {
Logger.result(
`Favorites: ${repost.stats.collectCount}`,
chalk.yellow
)
}
if (repost.stats.playCount) {
Logger.result(`Views: ${repost.stats.playCount}`, chalk.yellow)
}
if (repost.stats.commentCount) {
Logger.result(
`Comments: ${repost.stats.commentCount}`,
chalk.yellow
)
}
}
Logger.info(`Total Liked Videos: ${data.length}`)
Logger.info(`Total reposts: ${data.length}`)
} else {
Logger.error(`Error: ${results.message}`)
}

View File

@ -8,6 +8,8 @@ export const _tiktokSearchLiveFull = (params: any): string =>
`${_tiktokurl}/api/search/live/full/?${params}`
export const _tiktokGetPosts = (params: any): string =>
`${_tiktokurl}/api/post/item_list/?${params}`
export const _tiktokGetReposts = (params: any): string =>
`${_tiktokurl}/api/repost/item_list/?${params}`
export const _tiktokGetComments = (params: any): string =>
`${_tiktokurl}/api/comment/list/?${params}`
export const _tiktokGetUserLiked = (params: any): string =>

View File

@ -8,6 +8,7 @@ import { VideoSearchResult } from "./types/search/videoSearch"
import { TiktokStalkUserResponse } from "./types/get/getProfile"
import { TiktokVideoCommentsResponse } from "./types/get/getComments"
import { TiktokUserPostsResponse } from "./types/get/getUserPosts"
import { TiktokUserRepostsResponse } from "./types/get/getUserReposts"
import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked"
import { TiktokCollectionResponse } from "./types/get/getCollection"
@ -20,6 +21,7 @@ import { SearchUser } from "./utils/search/userSearch"
import { SearchLive } from "./utils/search/liveSearch"
import { getComments } from "./utils/get/getComments"
import { getUserPosts } from "./utils/get/getUserPosts"
import { getUserReposts } from "./utils/get/getUserRepost"
import { getUserLiked } from "./utils/get/getUserLiked"
import { SearchVideo } from "./utils/search/videoSearch"
import { getCollection } from "./utils/get/getCollection"
@ -236,7 +238,7 @@ export = {
/**
* Tiktok Get User Posts
* @param {string} username - The username you want to get posts from
* @param {Object} options - The options for getting posts
* @param {Object} [options] - The options for getting posts
* @param {number} [options.postLimit] - Limit number of posts to fetch
* @param {string} [options.proxy] - Optional proxy URL
* @returns {Promise<TiktokUserPostsResponse>}
@ -282,6 +284,31 @@ export = {
)
},
/**
* Tiktok Get User Reposts
* @param {string} username - The username you want to get reposts from
* @param {Object} [options] - The options for getting reposts
* @param {number} [options.postLimit] - Limit number of reposts to fetch
* @param {string} [options.proxy] - Optional proxy URL
* @param {boolean} [options.filterDeletedPost] - Whether to filter deleted posts ()
* @returns {Promise<TiktokUserRepostsResponse>}
*/
GetUserReposts: async (
username: string,
options?: {
postLimit?: number
proxy?: string
filterDeletedPost?: boolean
}
): Promise<TiktokUserRepostsResponse> => {
return await getUserReposts(
username,
options?.proxy,
options?.postLimit,
options?.filterDeletedPost
)
},
/**
* Get TikTok Collection
* @param {string} collectionIdOrUrl - Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id)

View File

@ -0,0 +1,112 @@
export type TiktokUserRepostsResponse = {
status: "success" | "error"
message?: string
result?: Reposts[]
totalReposts?: number
}
export type Reposts = {
id: string
desc: string
createTime: number
digged: boolean
duetEnabled?: boolean
forFriend: boolean
officalItem: boolean
originalItem: boolean
privateItem: boolean
secret: boolean
shareEnabled: boolean
stitchEnabled?: boolean
stats: StatsRepost
author: AuthorRepost
video?: VideoRepost
music: MusicRepost
imagePost?: ImageRepost
AIGCDescription?: string
CategoryType?: number
collected?: boolean
contents?: any[]
challenges?: any[]
textExtra?: any[]
textLanguage?: string
textTranslatable?: boolean
titleLanguage?: string
titleTranslatable?: boolean
isAd?: boolean
isReviewing?: boolean
itemCommentStatus?: number
item_control?: ItemControl
duetDisplay?: number
stitchDisplay?: number
diversificationId?: number
backendSourceEventTracking?: string
stickersOnItem?: any[]
videoSuggestWordsList?: any
}
export type StatsRepost = {
shareCount: number
collectCount?: number
commentCount?: number
likeCount?: number
playCount?: number
repostCount?: number
}
export type AuthorRepost = {
id: string
username: string
nickname: string
avatarLarger: string
avatarThumb: string
avatarMedium: string
signature: string
verified: boolean
openFavorite?: boolean
privateAccount?: boolean
isADVirtual?: boolean
isEmbedBanned?: boolean
}
export type VideoRepost = {
id: string
duration: number
ratio: string
cover: string
originCover: string
dynamicCover: string
playAddr: string
downloadAddr: string
format: string
bitrate: number
}
export type MusicRepost = {
authorName?: string
coverLarge?: string
coverMedium?: string
coverThumb?: string
duration?: number
id?: string
title?: string
playUrl?: string
original?: boolean
tt2dsp?: any
}
export type ImageRepost = {
title: string
images?: ImageRepostItem[]
}
export type ImageRepostItem = {
imageURL: {
urlList: string[]
}
}
export type ItemControl = {
can_repost?: boolean
can_share?: boolean
}

View File

@ -66,7 +66,6 @@ const parseUserLiked = async (
): Promise<LikedResponse[]> => {
// Liked Result
let hasMore = true
let cursor = 0
const favorites: LikedResponse[] = []
let counter = 0
while (hasMore) {
@ -186,7 +185,6 @@ const parseUserLiked = async (
// Update hasMore and cursor for next iteration
hasMore = result.hasMore
cursor = hasMore ? result.cursor : null
counter++
// Check post limit if specified

View File

@ -0,0 +1,291 @@
import Axios from "axios"
import { _tiktokGetReposts } from "../../constants/api"
import {
AuthorRepost,
Reposts,
TiktokUserRepostsResponse
} from "../../types/get/getUserReposts"
import { _getUserRepostsParams } from "../../constants/params"
import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent"
import { StalkUser } from "../get/getProfile"
import retry from "async-retry"
export const getUserReposts = (
username: string,
proxy?: string,
postLimit?: number,
filterDeletedPost: boolean = true
): Promise<TiktokUserRepostsResponse> =>
new Promise((resolve) => {
try {
StalkUser(username).then(async (res) => {
if (res.status === "error") {
return resolve({
status: "error",
message: res.message
})
}
const secUid = res.result.user.secUid
const data = await parseUserReposts(
secUid,
postLimit,
proxy,
filterDeletedPost
)
if (!data.length)
return resolve({
status: "error",
message: "User not found!"
})
resolve({
status: "success",
result: data,
totalReposts: data.length
})
})
} catch (err) {
if (
err.status == 400 ||
(err.response.data && err.response.data.statusCode == 10201)
) {
return resolve({
status: "error",
message: "Video not found!"
})
}
}
})
const parseUserReposts = async (
secUid: string,
postLimit?: number,
proxy?: string,
filterDeletedPost?: boolean
): Promise<Reposts[]> => {
// Posts Result
let page = 1
let hasMore = true
let responseCursor = 0
const posts: Reposts[] = []
let counter = 0
while (hasMore) {
let result: any | null = null
let urlCursor = 0
let urlCount = 0
if (page === 1) {
urlCount = 16
urlCursor = 0
} else {
urlCount = 16
urlCursor = responseCursor
}
// Prevent missing response posts
result = await requestUserReposts(proxy, secUid, urlCursor, urlCount)
if (!result || !result.itemList || result.itemList.length === 0) {
hasMore = false
break
}
// Filter out deleted posts if specified
if (filterDeletedPost) {
result.itemList = result.itemList.filter(
(item: any) => item.createTime !== 0
)
}
result?.itemList?.forEach((v: any) => {
const author: AuthorRepost = {
id: v.author?.id,
username: v.author?.uniqueId,
nickname: v.author?.nickname,
avatarLarger: v.author?.avatarLarger,
avatarThumb: v.author?.avatarThumb,
avatarMedium: v.author?.avatarMedium,
signature: v.author?.signature,
verified: v.author?.verified,
openFavorite: v.author?.openFavorite,
privateAccount: v.author?.privateAccount,
isADVirtual: v.author?.isADVirtual,
isEmbedBanned: v.author?.isEmbedBanned
}
if (v.imagePost) {
const imagePost = {
title: v.imagePost.title || "",
images:
v.imagePost.images?.map((img: any) => ({
imageURL: {
urlList: img.imageURL?.urlList || []
}
})) || []
}
posts.push({
id: v.id,
desc: v.desc,
createTime: v.createTime,
digged: v.digged || false,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend || false,
officalItem: v.officalItem || false,
originalItem: v.originalItem || false,
privateItem: v.privateItem || false,
secret: v.secret || false,
shareEnabled: v.shareEnabled || false,
stitchEnabled: v.stitchEnabled,
stats: v.stats || { shareCount: 0 },
music: v.music || {},
author,
imagePost,
AIGCDescription: v.AIGCDescription,
CategoryType: v.CategoryType,
collected: v.collected,
contents: v.contents || [],
challenges: v.challenges || [],
textExtra: v.textExtra || [],
textLanguage: v.textLanguage,
textTranslatable: v.textTranslatable,
titleLanguage: v.titleLanguage,
titleTranslatable: v.titleTranslatable,
isAd: v.isAd,
isReviewing: v.isReviewing,
itemCommentStatus: v.itemCommentStatus,
item_control: v.item_control,
duetDisplay: v.duetDisplay,
stitchDisplay: v.stitchDisplay,
diversificationId: v.diversificationId,
backendSourceEventTracking: v.backendSourceEventTracking,
stickersOnItem: v.stickersOnItem || [],
videoSuggestWordsList: v.videoSuggestWordsList
})
} else {
const video = {
id: v.video?.id,
duration: v.video?.duration,
format: v.video?.format,
bitrate: v.video?.bitrate,
ratio: v.video?.ratio,
playAddr: v.video?.playAddr,
cover: v.video?.cover,
originCover: v.video?.originCover,
dynamicCover: v.video?.dynamicCover,
downloadAddr: v.video?.downloadAddr
}
posts.push({
id: v.id,
desc: v.desc,
createTime: v.createTime,
digged: v.digged || false,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend || false,
officalItem: v.officalItem || false,
originalItem: v.originalItem || false,
privateItem: v.privateItem || false,
secret: v.secret || false,
shareEnabled: v.shareEnabled || false,
stitchEnabled: v.stitchEnabled,
stats: v.stats || { shareCount: 0 },
music: v.music || {},
author,
video,
AIGCDescription: v.AIGCDescription,
CategoryType: v.CategoryType,
collected: v.collected,
contents: v.contents || [],
challenges: v.challenges || [],
textExtra: v.textExtra || [],
textLanguage: v.textLanguage,
textTranslatable: v.textTranslatable,
titleLanguage: v.titleLanguage,
titleTranslatable: v.titleTranslatable,
isAd: v.isAd,
isReviewing: v.isReviewing,
itemCommentStatus: v.itemCommentStatus,
item_control: v.item_control,
duetDisplay: v.duetDisplay,
stitchDisplay: v.stitchDisplay,
diversificationId: v.diversificationId,
backendSourceEventTracking: v.backendSourceEventTracking,
stickersOnItem: v.stickersOnItem || [],
videoSuggestWordsList: v.videoSuggestWordsList
})
}
})
hasMore = result.hasMore
responseCursor = hasMore ? result.cursor : 0
page++
counter++
// Check post limit if specified
if (postLimit && posts.length >= postLimit) {
hasMore = false
break
}
}
return postLimit ? posts.slice(0, postLimit) : posts
}
const requestUserReposts = async (
proxy?: string,
secUid: string = "",
cursor: number = 0,
count: number = 0
): Promise<any> => {
return retry(
async (bail, attempt) => {
try {
let urlParams = _getUserRepostsParams(secUid, cursor, count)
const { data } = await Axios.get(`${_tiktokGetReposts(urlParams)}`, {
headers: {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0"
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
if (data === "") {
throw new Error("Empty response")
}
return data
} catch (error) {
if (
error.response?.status === 400 ||
error.response?.data?.statusCode === 10201
) {
bail(new Error("Video not found!"))
return
}
throw error
}
},
{
retries: 10,
minTimeout: 1000,
maxTimeout: 5000,
factor: 2,
onRetry: (error, attempt) => {
console.log(`Retry attempt ${attempt} due to: ${error}`)
}
}
)
}

57
test/userreposts-test.ts Normal file
View File

@ -0,0 +1,57 @@
// Test for Tiktok Get User Reposts
import Tiktok from "../src/index"
async function testUserReposts() {
try {
const username = "Tobz2k19" // Change to a valid TikTok username
const result = await Tiktok.GetUserReposts(username, {
postLimit: 30,
proxy: undefined
})
if (result.status === "success" && result.result) {
console.log("\nUser Reposts fetched successfully!")
console.log("========================")
console.log("Reposts Overview:")
console.log("========================")
console.log(`Total reposts fetched: ${result.result.length}`)
result.result.forEach((post, index) => {
console.log(`\nRepost ${index + 1}:`)
console.log("-------------------")
console.log(`ID: ${post.id}`)
console.log(`Description: ${post.desc}`)
if (post.author) {
console.log(
`Author: ${post.author.nickname} (@${post.author.username})`
)
}
console.log(
`Create Time: ${new Date(post.createTime * 1000).toLocaleString()}`
)
if (post.stats) {
console.log(`Share Count: ${post.stats.shareCount}`)
}
if (post.video) {
console.log(`Video Duration: ${post.video.duration}s`)
console.log(`Video Format: ${post.video.format}`)
}
if (
post.imagePost &&
post.imagePost.images &&
post.imagePost.images.length > 0
) {
console.log(`Image Post: ${post.imagePost.images.length} images`)
}
if (post.music && post.music.title) {
console.log(`Music: ${post.music.title}`)
}
})
} else {
console.error("Failed to fetch reposts:", result.message)
}
} catch (error) {
console.error("Error:", error)
}
}
testUserReposts()