feat: add postLimit & fix missing posts response
This commit is contained in:
parent
9f2e424ca4
commit
c1be62ea4a
@ -1,14 +1,15 @@
|
||||
/** Tiktok */
|
||||
export const _tiktokurl: string = "https://www.tiktok.com"
|
||||
export const _tiktokapi = (params: any): string => `https://api.tiktokv.com/aweme/v1/feed/?${params}`
|
||||
export const _tiktokSearchUserFull = (params: any): string => _tiktokurl + `/api/search/user/full/?${params}`
|
||||
export const _tiktokSearchVideoFull = (params: any): string => _tiktokurl + `/api/search/item/full/?${params}`
|
||||
export const _tiktokSearchUserFull = (params: any): string => `${_tiktokurl}/api/search/user/full/?${params}`
|
||||
export const _tiktokSearchVideoFull = (params: any): string => `${_tiktokurl}/api/search/item/full/?${params}`
|
||||
export const _tiktokGetPosts = (params: any) => `${_tiktokurl}/api/post/item_list/?${params}`
|
||||
|
||||
/** SSSTik */
|
||||
export const _ssstikapi: string = "https://ssstik.io/abc?url=dl"
|
||||
export const _ssstikurl: string = "https://ssstik.io"
|
||||
export const _ssstikapi: string = `${_ssstikurl}/abc?url=dl`
|
||||
|
||||
/** Musicaldown */
|
||||
export const _musicaldownapi: string = "https://musicaldown.com/download"
|
||||
export const _musicaldownurl: string = "https://musicaldown.com"
|
||||
export const _musicaldownmusicapi: string = "https://musicaldown.com/mp3/download"
|
||||
export const _musicaldownapi: string = `${_musicaldownurl}/download`
|
||||
export const _musicaldownmusicapi: string = `${_musicaldownurl}/mp3/download`
|
||||
|
||||
@ -111,6 +111,25 @@ export const _tiktokApiParams = (args: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const _xttParams = (secUid: string, cursor: number, count: number) => {
|
||||
return qs.stringify({
|
||||
aid: "1988",
|
||||
cookie_enabled: true,
|
||||
screen_width: 0,
|
||||
screen_height: 0,
|
||||
browser_language: "",
|
||||
browser_platform: "",
|
||||
browser_name: "",
|
||||
browser_version: "",
|
||||
browser_online: "",
|
||||
timezone_name: "Europe/London",
|
||||
secUid,
|
||||
cursor,
|
||||
count,
|
||||
is_encryption: 1
|
||||
})
|
||||
}
|
||||
|
||||
const randomChar = (char: string, range: number) => {
|
||||
let chars = ""
|
||||
|
||||
|
||||
38
src/index.ts
38
src/index.ts
@ -1,11 +1,11 @@
|
||||
/** Downloader */
|
||||
import { MusicalDown } from "./utils/downloader/downloader_musicaldown"
|
||||
import { SSSTik } from "./utils/downloader/downloader_ssstik"
|
||||
import { TiktokAPI } from "./utils/downloader/downloader_tiktokApi"
|
||||
import { MusicalDown } from "./utils/downloader/musicalDown"
|
||||
import { SSSTik } from "./utils/downloader/ssstik"
|
||||
import { TiktokAPI } from "./utils/downloader/tiktokApi"
|
||||
|
||||
/** Search */
|
||||
import { StalkUser } from "./utils/search/tiktok_stalker"
|
||||
import { SearchUser } from "./utils/search/tiktok_user_search"
|
||||
import { StalkUser } from "./utils/search/stalker"
|
||||
import { SearchUser } from "./utils/search/userSearch"
|
||||
|
||||
/** Types */
|
||||
import { MusicalDownResponse } from "./types/downloader/musicaldown"
|
||||
@ -18,6 +18,14 @@ type TiktokDownloaderResponse<T extends "v1" | "v2" | "v3"> = T extends "v1" ? T
|
||||
type TiktokSearchResponse<T extends "user" | "video"> = T extends "user" ? TiktokUserSearchResponse : T extends "video" ? any : TiktokUserSearchResponse
|
||||
|
||||
export = {
|
||||
/**
|
||||
* Tiktok Downloader
|
||||
* @param {string} url - The Tiktok URL you want to download
|
||||
* @param {object} options - The options for downloader
|
||||
* @param {string} options.version - The version of downloader
|
||||
* @returns {Promise<TiktokDownloaderResponse>}
|
||||
*/
|
||||
|
||||
Downloader: async <T extends "v1" | "v2" | "v3">(url: string, options?: { version: T }): Promise<TiktokDownloaderResponse<T>> => {
|
||||
switch (options?.version) {
|
||||
case "v1": {
|
||||
@ -38,6 +46,15 @@ export = {
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Tiktok Search
|
||||
* @param {string} query - The query you want to search
|
||||
* @param {object} options - The options for search
|
||||
* @param {string} options.type - The type of search
|
||||
* @param {string} options.cookie - Your Tiktok Cookie (optional)
|
||||
* @param {number} options.page - The page of search (optional)
|
||||
* @returns {Promise<TiktokSearchResponse>}
|
||||
*/
|
||||
Search: async <T extends "user" | "video">(query: string, options: { type: T; cookie?: string; page?: number }): Promise<TiktokSearchResponse<T>> => {
|
||||
switch (options?.type) {
|
||||
case "user": {
|
||||
@ -54,8 +71,15 @@ export = {
|
||||
}
|
||||
}
|
||||
},
|
||||
StalkUser: async (username: string, options?: { cookie?: string }): Promise<StalkResult> => {
|
||||
const response = await StalkUser(username, options?.cookie)
|
||||
/**
|
||||
* Tiktok Stalk User
|
||||
* @param {string} username - The username you want to stalk
|
||||
* @param {object} options - The options for stalk
|
||||
* @param {string} options.cookie - Your Tiktok Cookie (optional)
|
||||
* @returns {Promise<StalkResult>}
|
||||
*/
|
||||
StalkUser: async (username: string, options?: { cookie?: string; postLimit?: number }): Promise<StalkResult> => {
|
||||
const response = await StalkUser(username, options?.cookie, options?.postLimit)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
196
src/utils/search/stalker.ts
Normal file
196
src/utils/search/stalker.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import Axios from "axios"
|
||||
import qs from "qs"
|
||||
import { load } from "cheerio"
|
||||
import { _tiktokGetPosts, _tiktokurl } from "../../constants/api"
|
||||
import { AuthorPost, Posts, StalkResult, Stats, Users } from "../../types/search/stalker"
|
||||
import { _userPostsParams, _xttParams } from "../../constants/params"
|
||||
import { createCipheriv } from "crypto"
|
||||
|
||||
/**
|
||||
* Tiktok Stalk User
|
||||
* @param {string} username - The username you want to stalk
|
||||
* @param {object|string} cookie - Your Tiktok Cookie (optional)
|
||||
* @returns {Promise<StalkResult>}
|
||||
*/
|
||||
|
||||
export const StalkUser = (username: string, cookie?: any, postLimit?: number): Promise<StalkResult> =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
username = username.replace("@", "")
|
||||
Axios.get(`${_tiktokurl}/@${username}`, {
|
||||
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) => `${v.name}=${v.value}`).join("; ") : cookie
|
||||
}
|
||||
})
|
||||
.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)
|
||||
const { users, stats } = parseDataUser(dataUser, posts)
|
||||
|
||||
resolve({
|
||||
status: "success",
|
||||
result: {
|
||||
users,
|
||||
stats,
|
||||
posts
|
||||
}
|
||||
})
|
||||
})
|
||||
.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 request = async (secUid: string, cursor = 0, count = 30) => {
|
||||
const { data } = await Axios.get(`${_tiktokGetPosts(_userPostsParams())}`, {
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const parseDataUser = (dataUser: any, posts: Posts[]) => {
|
||||
// User Info Result
|
||||
const users: Users = {
|
||||
id: 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): Promise<Posts[]> => {
|
||||
// Posts Result
|
||||
let hasMore = true
|
||||
let cursor: number | null = null
|
||||
const posts: Posts[] = []
|
||||
while (hasMore) {
|
||||
let result2: any | null = null
|
||||
|
||||
// Prevent missing response posts
|
||||
for (let i = 0; i < 30; i++) {
|
||||
result2 = await request(dataUser.user.secUid, cursor, 30)
|
||||
if (result2 !== "") break
|
||||
}
|
||||
|
||||
// Validate
|
||||
if (result2 === "") hasMore = false // No More Post
|
||||
|
||||
result2?.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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
hasMore = result2.hasMore
|
||||
cursor = hasMore ? result2.cursor : null
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@ -1,196 +0,0 @@
|
||||
import Axios from "axios"
|
||||
import qs from "qs"
|
||||
import { load } from "cheerio"
|
||||
import { _tiktokurl } from "../../constants/api"
|
||||
import { AuthorPost, Posts, StalkResult, Stats, Users } from "../../types/search/stalker"
|
||||
import { _userPostsParams } from "../../constants/params"
|
||||
import { createCipheriv } from "crypto"
|
||||
|
||||
/**
|
||||
* Tiktok Stalk User
|
||||
* @param {string} username - The username you want to stalk
|
||||
* @param {object|string} cookie - Your Tiktok Cookie (optional)
|
||||
* @returns {Promise<StalkResult>}
|
||||
*/
|
||||
|
||||
export const StalkUser = (username: string, cookie?: any): Promise<StalkResult> =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
username = username.replace("@", "")
|
||||
Axios.get(`${_tiktokurl}/@${username}`, {
|
||||
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) => `${v.name}=${v.value}`).join("; ") : cookie
|
||||
}
|
||||
})
|
||||
.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"]
|
||||
|
||||
// Posts Result
|
||||
let hasMore = true
|
||||
let cursor
|
||||
const posts: Posts[] = []
|
||||
|
||||
while (hasMore) {
|
||||
const result2 = await request(dataUser.user.secUid, cursor, 30)
|
||||
|
||||
if (result2 === "") hasMore = false
|
||||
|
||||
result2?.itemList?.forEach((v) => {
|
||||
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) => 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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
hasMore = result2.hasMore
|
||||
cursor = hasMore ? result2.cursor : null
|
||||
}
|
||||
|
||||
// User Info Result
|
||||
const users: Users = {
|
||||
id: 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
|
||||
}
|
||||
|
||||
resolve({
|
||||
status: "success",
|
||||
result: {
|
||||
users,
|
||||
stats,
|
||||
posts
|
||||
}
|
||||
})
|
||||
})
|
||||
.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 request = async (secUid: string, cursor = 0, count = 30) => {
|
||||
const { data } = await Axios.get(`https://www.tiktok.com/api/post/item_list/?${_userPostsParams()}`, {
|
||||
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(
|
||||
qs.stringify({
|
||||
aid: "1988",
|
||||
cookie_enabled: true,
|
||||
screen_width: 0,
|
||||
screen_height: 0,
|
||||
browser_language: "",
|
||||
browser_platform: "",
|
||||
browser_name: "",
|
||||
browser_version: "",
|
||||
browser_online: "",
|
||||
timezone_name: "Europe/London",
|
||||
secUid,
|
||||
cursor,
|
||||
count,
|
||||
is_encryption: 1
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const xttparams = (params) => {
|
||||
const cipher = createCipheriv("aes-128-cbc", "webapp1.0+202106", "webapp1.0+202106")
|
||||
return Buffer.concat([cipher.update(params), cipher.final()]).toString("base64")
|
||||
}
|
||||
@ -17,7 +17,7 @@ export const SearchUser = (username: string, cookie?: any, page?: number): Promi
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0",
|
||||
cookie: typeof cookie === "object" ? cookie.map((v) => `${v.name}=${v.value}`).join("; ") : cookie
|
||||
cookie: typeof cookie === "object" ? cookie.map((v: any) => `${v.name}=${v.value}`).join("; ") : cookie
|
||||
}
|
||||
})
|
||||
.then(({ data }) => {
|
||||
Loading…
x
Reference in New Issue
Block a user