feat: add page & totalResults object

This commit is contained in:
Tobi Saputra 2024-12-07 23:54:36 +07:00
parent 687a3daf1a
commit 1bae63d2e8
8 changed files with 321 additions and 153 deletions

View File

@ -2,6 +2,8 @@ export type TiktokLiveSearchResponse = {
status: "success" | "error"
message?: string
result?: Result[]
page?: number
totalResults?: number
}
export type Result = {
@ -34,7 +36,7 @@ export type Stats = {
}
export type Owner = {
id: string
uid: string
nickname: string
username: string
signature: string

View File

@ -1,135 +0,0 @@
export type StalkResult = {
status: "success" | "error"
message?: string
result?: {
users: Users
stats: Stats
posts: Posts[]
}
}
export type Users = {
id: string
username: string
nickname: string
avatarLarger: string
avatarThumb: string
avatarMedium: string
signature: string
verified: boolean
privateAccount: boolean
region: string
commerceUser: boolean
usernameModifyTime: number
nicknameModifyTime: number
}
export type Stats = {
followerCount: number
followingCount: number
heartCount: number
videoCount: number
likeCount: number
friendCount: number
postCount: number
}
export type Statistics = {
likeCount: number
shareCount: number
commentCount: number
playCount: number
favoriteCount: number
}
export type Video = {
id: string
duration: string
ratio: string
cover: string
originCover: string
dynamicCover: string
playAddr: string
downloadAddr: string
format: string
bitrate: number
}
export type Music = {
id: string
title: string
album: string
playUrl: string
coverLarge: string
coverMedium: string
coverThumb: string
authorName: string
duration: string
}
export type Posts = {
id: string
desc: string
createTime: number
digged: number
duetEnabled: number
forFriend: number
officalItem: number
originalItem: number
privateItem: number
shareEnabled: number
stitchEnabled: number
stats: StatsPost
author: AuthorPost
video?: VideoPost
music: MusicPost
images?: string[]
}
export type StatsPost = {
collectCount: number
commentCount: number
diggCount: number
playCount: number
shareCount: number
}
export type AuthorPost = {
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 VideoPost = {
id: string
duration: number
ratio: string
cover: string
originCover: string
dynamicCover: string
playAddr: string
downloadAddr: string
format: string
bitrate: number
}
export type MusicPost = {
authorName: string
coverLarge: string
coverMedium: string
coverThumb: string
duration: number
id: string
title: string
playUrl: string
original: boolean
}

View File

@ -2,6 +2,8 @@ export type TiktokUserSearchResponse = {
status: "success" | "error"
message?: string
result?: Result[]
page?: number
totalResults?: number
}
export type Result = {

View File

@ -21,8 +21,8 @@ import { SocksProxyAgent } from "socks-proxy-agent"
const TiktokURLregex =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
const getRequest = (url: string, proxy?: string) =>
new Promise<getRequest>((resolve) => {
const getRequest = (url: string, proxy?: string): Promise<getRequest> =>
new Promise((resolve) => {
if (!TiktokURLregex.test(url)) {
return resolve({
status: "error",
@ -98,8 +98,11 @@ const getRequest = (url: string, proxy?: string) =>
* @returns {Promise<MusicalDownResponse>}
*/
export const MusicalDown = (url: string, proxy?: string) =>
new Promise<MusicalDownResponse>(async (resolve) => {
export const MusicalDown = (
url: string,
proxy?: string
): Promise<MusicalDownResponse> =>
new Promise(async (resolve) => {
const request: getRequest = await getRequest(url)
if (request.status !== "success")
return resolve({ status: "error", message: request.message })

View File

@ -19,8 +19,8 @@ import { SocksProxyAgent } from "socks-proxy-agent"
const TiktokURLregex =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
const fetchTT = (proxy?: string) =>
new Promise<SSSTikFetchTT>(async (resolve) => {
const fetchTT = (proxy?: string): Promise<SSSTikFetchTT> =>
new Promise(async (resolve) => {
Axios(_ssstikurl, {
method: "GET",
headers: {
@ -59,8 +59,8 @@ const fetchTT = (proxy?: string) =>
* @returns {Promise<SSSTikResponse>}
*/
export const SSSTik = (url: string, proxy?: string) =>
new Promise<SSSTikResponse>(async (resolve) => {
export const SSSTik = (url: string, proxy?: string): Promise<SSSTikResponse> =>
new Promise(async (resolve) => {
try {
if (!TiktokURLregex.test(url)) {
return resolve({

269
src/utils/get/getProfile.ts Normal file
View File

@ -0,0 +1,269 @@
import Axios from "axios"
import { load } from "cheerio"
import { _tiktokGetPosts, _tiktokurl } from "../../constants/api"
import {
AuthorPost,
Posts,
StalkResult,
Stats,
Users
} from "../../types/get/getProfile"
import { _getUserPostsParams, _xttParams } from "../../constants/params"
import { createCipheriv } from "crypto"
import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent"
/**
* Tiktok Stalk User
* @param {string} username - The username you want to stalk
* @param {object|string} cookie - Your Tiktok Cookie (optional)
* @param {number} postLimit - The limit of post you want to get (optional)
* @param {string} proxy - Your Proxy (optional)
* @returns {Promise<StalkResult>}
*/
export const StalkUser = (
username: string,
cookie?: any,
postLimit?: number,
proxy?: string
): Promise<StalkResult> =>
new Promise(async (resolve) => {
username = username.replace("@", "")
Axios(`${_tiktokurl}/@${username}`, {
method: "GET",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
cookie:
typeof cookie === "object"
? cookie.map((v: any) => `${v.name}=${v.value}`).join("; ")
: cookie
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
.then(async ({ data }) => {
const $ = load(data)
const result = JSON.parse(
$("script#__UNIVERSAL_DATA_FOR_REHYDRATION__").text()
)
if (
!result["__DEFAULT_SCOPE__"] &&
!result["__DEFAULT_SCOPE__"]["webapp.user-detail"]
) {
return resolve({
status: "error",
message: "User not found!"
})
}
const dataUser =
result["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"]
const posts: Posts[] = await parsePosts(dataUser, postLimit, proxy)
const { users, stats } = parseDataUser(dataUser, posts)
let response: StalkResult = {
status: "success",
result: {
users,
stats,
posts
},
totalPosts: posts.length
}
resolve(response)
})
.catch((e) => resolve({ status: "error", message: e.message }))
})
/**
* Thanks to:
* https://github.com/atharahmed/tiktok-private-api/blob/020ede2eaa6021bcd363282d8cef1aacaff2f88c/src/repositories/user.repository.ts#L148
*/
const getUserPosts = async (
secUid: string,
cursor = 0,
count = 30,
proxy?: string
) => {
const { data } = await Axios.get(
`${_tiktokGetPosts(_getUserPostsParams())}`,
{
headers: {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35",
"X-tt-params": xttparams(_xttParams(secUid, cursor, count))
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
}
)
return data
}
const parseDataUser = (dataUser: any, posts: Posts[]) => {
// User Info Result
const users: Users = {
uid: dataUser.user.id,
username: dataUser.user.uniqueId,
nickname: dataUser.user.nickname,
avatarLarger: dataUser.user.avatarLarger,
avatarThumb: dataUser.user.avatarThumb,
avatarMedium: dataUser.user.avatarMedium,
signature: dataUser.user.signature,
verified: dataUser.user.verified,
privateAccount: dataUser.user.privateAccount,
region: dataUser.user.region,
commerceUser: dataUser.user.commerceUserInfo.commerceUser,
usernameModifyTime: dataUser.user.uniqueIdModifyTime,
nicknameModifyTime: dataUser.user.nickNameModifyTime
}
// Statistics Result
const stats: Stats = {
followerCount: dataUser.stats.followerCount,
followingCount: dataUser.stats.followingCount,
heartCount: dataUser.stats.heartCount,
videoCount: dataUser.stats.videoCount,
likeCount: dataUser.stats.diggCount,
friendCount: dataUser.stats.friendCount,
postCount: posts.length
}
return { users, stats }
}
const parsePosts = async (
dataUser: any,
postLimit?: number,
proxy?: string
): Promise<Posts[]> => {
// Posts Result
let hasMore = true
let cursor: number | null = null
const posts: Posts[] = []
while (hasMore) {
let result: any | null = null
let counter = 0
// Prevent missing response posts
for (let i = 0; i < 30; i++) {
result = await getUserPosts(dataUser.user.secUid, cursor, 30, proxy)
if (result !== "") break
}
// Validate
if (result === "") hasMore = false // No More Post
result?.itemList?.forEach((v: any) => {
const author: AuthorPost = {
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 images: string[] = v.imagePost.images.map(
(img: any) => img.imageURL.urlList[0]
)
posts.push({
id: v.id,
desc: v.desc,
createTime: v.createTime,
digged: v.digged,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend,
officalItem: v.officalItem,
originalItem: v.originalItem,
privateItem: v.privateItem,
shareEnabled: v.shareEnabled,
stitchEnabled: v.stitchEnabled,
stats: v.stats,
music: v.music,
author,
images
})
} 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,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend,
officalItem: v.officalItem,
originalItem: v.originalItem,
privateItem: v.privateItem,
shareEnabled: v.shareEnabled,
stitchEnabled: v.stitchEnabled,
stats: v.stats,
music: v.music,
author,
video
})
}
})
// Restrict too many data requests
if (postLimit !== 0) {
let loopCount = Math.floor(postLimit / 30)
if (counter >= loopCount) hasMore = false
}
hasMore = result.hasMore
cursor = hasMore ? result.cursor : null
counter++
}
return postLimit ? posts.slice(0, postLimit) : posts
}
const xttparams = (params: any) => {
const cipher = createCipheriv(
"aes-128-cbc",
"webapp1.0+202106",
"webapp1.0+202106"
)
return Buffer.concat([cipher.update(params), cipher.final()]).toString(
"base64"
)
}

View File

@ -1,14 +1,19 @@
import Axios from "axios"
import { _tiktokSearchLiveFull } from "../../constants/api"
import { _liveSearchParams } from "../../constants/params"
import { LiveInfo, Owner, OwnerStats } from "../../types/search/liveSearch"
import {
LiveInfo,
Owner,
OwnerStats,
TiktokLiveSearchResponse
} from "../../types/search/liveSearch"
import { SocksProxyAgent } from "socks-proxy-agent"
import { HttpsProxyAgent } from "https-proxy-agent"
/**
* Tiktok Search Live
* @param {string} keyword - The keyword you want to search
* @param {object|string} cookie - Your Tiktok cookie (optional)
* @param {string | any[]} cookie - Your Tiktok cookie (optional)
* @param {number} page - The page you want to search (optional)
* @param {string} proxy - Your Proxy (optional)
* @returns {Promise<TiktokLiveSearchResponse>}
@ -16,11 +21,17 @@ import { HttpsProxyAgent } from "https-proxy-agent"
export const SearchLive = async (
keyword: string,
cookie?: any,
cookie: string | any[],
page: number = 1,
proxy?: string
) =>
): Promise<TiktokLiveSearchResponse> =>
new Promise(async (resolve) => {
if (!cookie) {
return resolve({
status: "error",
message: "Cookie is required!"
})
}
Axios(_tiktokSearchLiveFull(_liveSearchParams(keyword, page)), {
method: "GET",
headers: {
@ -75,7 +86,7 @@ export const SearchLive = async (
likeCount: content.like_count
},
owner: {
id: content.owner.id,
uid: content.owner.id,
nickname: content.owner.nickname,
username: content.owner.display_id,
signature: content.owner.bio_description,
@ -101,7 +112,12 @@ export const SearchLive = async (
result.push({ roomInfo, liveInfo })
})
resolve({ status: "success", result })
resolve({
status: "success",
result,
page,
totalResults: data.result.length
})
})
.catch((e) => {
resolve({ status: "error", message: e.message })

View File

@ -31,7 +31,7 @@ export const generateURLXbogus = (username: string, page: number) => {
/**
* Tiktok Search User
* @param {string} username - The username you want to search
* @param {object|string} cookie - Your Tiktok cookie (optional)
* @param {string | any[]} cookie - Your Tiktok cookie (optional)
* @param {number} page - The page you want to search (optional)
* @param {string} proxy - Your Proxy (optional)
* @returns {Promise<TiktokUserSearchResponse>}
@ -39,11 +39,17 @@ export const generateURLXbogus = (username: string, page: number) => {
export const SearchUser = (
username: string,
cookie?: any,
cookie: string | any[],
page: number = 1,
proxy?: string
): Promise<TiktokUserSearchResponse> =>
new Promise(async (resolve) => {
if (!cookie) {
return resolve({
status: "error",
message: "Cookie is required!"
})
}
Axios(generateURLXbogus(username, page), {
method: "GET",
headers: {
@ -93,7 +99,12 @@ export const SearchUser = (
})
}
resolve({ status: "success", result })
resolve({
status: "success",
result,
page,
totalResults: data.result.length
})
})
.catch((e) => {
resolve({ status: "error", message: e.message })