fix: applying new types

This commit is contained in:
Tobi Saputra 2025-05-13 18:26:10 +07:00
parent a19dd9c062
commit cb64f0585a
3 changed files with 607 additions and 503 deletions

View File

@ -1,5 +1,6 @@
import Axios from "axios" import Axios from "axios"
import { load } from "cheerio" import { load } from "cheerio"
type CheerioAPI = ReturnType<typeof load>
import { import {
MusicalDownResponse, MusicalDownResponse,
GetMusicalDownMusic, GetMusicalDownMusic,
@ -12,87 +13,178 @@ import {
} from "../../constants/api" } from "../../constants/api"
import { HttpsProxyAgent } from "https-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent"
import { ERROR_MESSAGES } from "../../constants"
/** /** Constants */
* Using API from Website: const TIKTOK_URL_REGEX =
* BASE URL : https://ssstik.io
*/
const TiktokURLregex =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ /https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
const USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
const getRequest = ( /** Types */
interface ProxyConfig {
httpsAgent?: HttpsProxyAgent<string> | SocksProxyAgent
}
interface RequestForm {
[key: string]: string
}
/** Helper Functions */
const createProxyAgent = (proxy?: string): ProxyConfig => {
if (!proxy) return {}
const isHttpProxy = proxy.startsWith("http") || proxy.startsWith("https")
const isSocksProxy = proxy.startsWith("socks")
if (!isHttpProxy && !isSocksProxy) return {}
return {
httpsAgent: isHttpProxy
? new HttpsProxyAgent(proxy)
: new SocksProxyAgent(proxy)
}
}
const validateTikTokUrl = (url: string): boolean => {
return TIKTOK_URL_REGEX.test(url)
}
const isValidUrl = (url: string): boolean => {
try {
new URL(url)
return true
} catch {
return false
}
}
const extractRequestForm = ($: CheerioAPI): RequestForm => {
const input = $("div > input").map((_, el) => $(el))
return {
[input.get(0).attr("name") || ""]: input.get(0).attr("value") || "",
[input.get(1).attr("name") || ""]: input.get(1).attr("value") || "",
[input.get(2).attr("name") || ""]: input.get(2).attr("value") || ""
}
}
const parseImages = ($: CheerioAPI): string[] => {
const images: string[] = []
$("div.row > div[class='col s12 m3']").each((_, v) => {
const src = $(v).find("img").attr("src")
if (src) images.push(src)
})
return images
}
const parseVideos = ($: CheerioAPI): Record<string, string> => {
const videos: Record<string, string> = {}
const videoContainer = $("div.row > div")
.map((_, el) => $(el))
.get(1)
if (!videoContainer) return videos
$(videoContainer)
.find("a")
.each((_, v) => {
const href = $(v).attr("href")
if (!href || href === "#modal2") return
if (!isValidUrl(href)) return
const dataEvent = $(v).attr("data-event") || ""
const onclick = $(v).attr("onclick") || ""
const downloadUrl =
href !== undefined ? href : /downloadX\('([^']+)'\)/.exec(onclick)?.[1]
if (!downloadUrl) return
if (dataEvent.includes("hd")) {
videos.videoHD = downloadUrl
} else if (dataEvent.includes("mp4")) {
videos.videoSD = downloadUrl
} else if (dataEvent.includes("watermark")) {
videos.videoWatermark = downloadUrl
} else if (href.includes("type=mp3")) {
videos.music = downloadUrl
}
})
return videos
}
const createImageResponse = (images: string[]): MusicalDownResponse => ({
status: "success",
result: {
type: "image",
images
}
})
const createVideoResponse = (
$: CheerioAPI,
videos: Record<string, string>
): MusicalDownResponse => ({
status: "success",
result: {
type: "video",
author: {
avatar: $("div.img-area > img").attr("src") || "",
nickname: $("h2.video-author > b").text()
},
desc: $("p.video-desc").text(),
...videos
}
})
const getRequest = async (
url: string, url: string,
proxy?: string proxy?: string
): Promise<GetMusicalDownReuqest> => ): Promise<GetMusicalDownReuqest> => {
new Promise((resolve) => { try {
if (!TiktokURLregex.test(url)) { if (!validateTikTokUrl(url)) {
return resolve({ return {
status: "error", status: "error",
message: "Invalid Tiktok URL. Make sure your url is correct!" message: ERROR_MESSAGES.INVALID_URL
}) }
} }
Axios(_musicaldownurl, {
const { data, headers } = await Axios(_musicaldownurl, {
method: "GET", method: "GET",
headers: { headers: {
Accept: Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Update-Insecure-Requests": "1", "Update-Insecure-Requests": "1",
"User-Agent": "User-Agent": USER_AGENT
"Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
}, },
httpsAgent: ...createProxyAgent(proxy)
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
}) })
.then((data) => {
const cookie = data.headers["set-cookie"][0].split(";")[0]
const $ = load(data.data)
const input = $("div > input").map((_, el) => $(el))
const request = {
[input.get(0).attr("name")]: url,
[input.get(1).attr("name")]: input.get(1).attr("value"),
[input.get(2).attr("name")]: input.get(2).attr("value")
}
resolve({ status: "success", request, cookie })
})
.catch((e) =>
resolve({ status: "error", message: "Failed to get the request form!" })
)
})
// const getMusic = (cookie: string, proxy?: string) => const cookie = headers["set-cookie"]?.[0]?.split(";")[0]
// new Promise<getMusic>((resolve) => { if (!cookie) {
// Axios(_musicaldownmusicapi, { return {
// method: "GET", status: "error",
// headers: { message: ERROR_MESSAGES.NETWORK_ERROR
// cookie: cookie, }
// "Upgrade-Insecure-Requests": "1", }
// "User-Agent":
// "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0" const $ = load(data)
// }, const request = extractRequestForm($)
// httpsAgent:
// (proxy && return {
// (proxy.startsWith("http") || proxy.startsWith("https") status: "success",
// ? new HttpsProxyAgent(proxy) request,
// : proxy.startsWith("socks") cookie
// ? new SocksProxyAgent(proxy) }
// : undefined)) || } catch (error) {
// undefined return {
// }) status: "error",
// .then(({ data }) => { message:
// const $ = load(data) error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
// const music = $("audio > source").attr("src") }
// resolve({ status: "success", result: music }) }
// }) }
// .catch((e) => resolve({ status: "error" }))
// })
/** /**
* Tiktok MusicalDown Downloader * Tiktok MusicalDown Downloader
@ -100,16 +192,20 @@ const getRequest = (
* @param {string} proxy - Proxy * @param {string} proxy - Proxy
* @returns {Promise<MusicalDownResponse>} * @returns {Promise<MusicalDownResponse>}
*/ */
export const MusicalDown = async (
export const MusicalDown = (
url: string, url: string,
proxy?: string proxy?: string
): Promise<MusicalDownResponse> => ): Promise<MusicalDownResponse> => {
new Promise(async (resolve) => { try {
const request: GetMusicalDownReuqest = await getRequest(url) const request = await getRequest(url, proxy)
if (request.status !== "success") if (request.status !== "success") {
return resolve({ status: "error", message: request.message }) return {
Axios(_musicaldownapi, { status: "error",
message: request.message
}
}
const { data } = await Axios(_musicaldownapi, {
method: "POST", method: "POST",
headers: { headers: {
cookie: request.cookie, cookie: request.cookie,
@ -117,98 +213,33 @@ export const MusicalDown = (
Origin: "https://musicaldown.com", Origin: "https://musicaldown.com",
Referer: "https://musicaldown.com/en", Referer: "https://musicaldown.com/en",
"Upgrade-Insecure-Requests": "1", "Upgrade-Insecure-Requests": "1",
"User-Agent": "User-Agent": USER_AGENT
"Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
}, },
data: new URLSearchParams(Object.entries(request.request)), data: new URLSearchParams(Object.entries(request.request)),
httpsAgent: ...createProxyAgent(proxy)
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
}) })
.then(async ({ data }) => {
const $ = load(data)
// Get Image Video const $ = load(data)
const images = [] const images = parseImages($)
$("div.row > div[class='col s12 m3']")
.get()
.map((v) => {
images.push($(v).find("img").attr("src"))
})
// Result if (images.length > 0) {
if (images.length !== 0) { return createImageResponse(images)
// Images or Slide Result }
resolve({
status: "success",
result: {
type: "image",
images
}
})
} else {
// Video Result
// Get Result Video
let i = 1
let videos = {}
$("div.row > div")
.map((_, el) => $(el))
.get(1)
.find("a")
.get()
.map((v: any) => {
if ($(v).attr("href") !== "#modal2") {
if (!isURL($(v).attr("href"))) return
videos[
$(v).attr("data-event").includes("hd")
? "videoHD"
: $(v).attr("data-event").includes("mp4")
? "videoSD"
: $(v).attr("data-event").includes("watermark")
? "videoWatermark"
: $(v).attr("href").includes("type=mp3") && "music"
] =
$(v).attr("href") != undefined
? $(v).attr("href")
: /downloadX\('([^']+)'\)/.exec($(v).attr("onclick"))[1]
i++
}
})
if (Object.keys(videos).length === 0) const videos = parseVideos($)
return resolve({ if (Object.keys(videos).length === 0) {
status: "success", return {
message: "There is an error. Can't find download link" status: "error",
}) message: "There is an error. Can't find download link"
resolve({ }
status: "success", }
result: {
type: "video",
author: {
avatar: $("div.img-area > img").attr("src"),
nickname: $("h2.video-author > b").text()
},
desc: $("p.video-desc").text(),
...videos
}
})
}
})
.catch((e) => resolve({ status: "error", message: e.message }))
})
const isURL = (url: string) => { return createVideoResponse($, videos)
let status = false } catch (error) {
try { return {
new URL(url) status: "error",
status = true message:
} catch { error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
status = false }
} }
return status
} }

View File

@ -1,6 +1,7 @@
import Axios from "axios" import Axios from "axios"
import asyncRetry from "async-retry" import asyncRetry from "async-retry"
import { load } from "cheerio" import { load } from "cheerio"
type CheerioAPI = ReturnType<typeof load>
import { import {
AuthorSSSTik, AuthorSSSTik,
StatisticsSSSTik, StatisticsSSSTik,
@ -10,47 +11,137 @@ import {
import { _ssstikapi, _ssstikurl } from "../../constants/api" import { _ssstikapi, _ssstikurl } from "../../constants/api"
import { HttpsProxyAgent } from "https-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent"
import { ERROR_MESSAGES } from "../../constants"
/** /**
* Using API from Website: * Using API from Website:
* BASE URL : https://ssstik.io * BASE URL : https://ssstik.io
*/ */
const TiktokURLregex = /** Constants */
const TIKTOK_URL_REGEX =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ /https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
const USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0"
const fetchTT = (proxy?: string): Promise<SSSTikFetchTT> => /** Types */
new Promise(async (resolve) => { interface ProxyConfig {
Axios(_ssstikurl, { httpsAgent?: HttpsProxyAgent<string> | SocksProxyAgent
method: "GET", }
headers: {
"User-Agent": /** Helper Functions */
"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" const createProxyAgent = (proxy?: string): ProxyConfig => {
}, if (!proxy) return {}
httpsAgent:
(proxy && const isHttpProxy = proxy.startsWith("http") || proxy.startsWith("https")
(proxy.startsWith("http") || proxy.startsWith("https") const isSocksProxy = proxy.startsWith("socks")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks") if (!isHttpProxy && !isSocksProxy) return {}
? new SocksProxyAgent(proxy)
: undefined)) || return {
undefined httpsAgent: isHttpProxy
}) ? new HttpsProxyAgent(proxy)
.then(({ data }) => { : new SocksProxyAgent(proxy)
const regex = /s_tt\s*=\s*["']([^"']+)["']/ }
const match = data.match(regex) }
if (match) {
const value = match[1] const validateTikTokUrl = (url: string): boolean => {
return resolve({ status: "success", result: value }) return TIKTOK_URL_REGEX.test(url)
} else { }
return resolve({
status: "error", const extractTTValue = (html: string): string | null => {
message: "Failed to get the request form!" const regex = /s_tt\s*=\s*["']([^"']+)["']/
}) const match = html.match(regex)
} return match ? match[1] : null
}) }
.catch((e) => resolve({ status: "error", message: e.message }))
const parseAuthor = ($: CheerioAPI): AuthorSSSTik => ({
avatar: $("img.result_author").attr("src") || "",
nickname: $("h2").text().trim()
})
const parseStatistics = ($: CheerioAPI): StatisticsSSSTik => ({
likeCount: $("#trending-actions > .justify-content-start").text().trim(),
commentCount: $("#trending-actions > .justify-content-center").text().trim(),
shareCount: $("#trending-actions > .justify-content-end").text().trim()
})
const parseImages = ($: CheerioAPI): string[] => {
const images: string[] = []
$("ul.splide__list > li").each((_, img) => {
const href = $(img).find("a").attr("href")
if (href) images.push(href)
}) })
return images
}
const createImageResponse = (
$: CheerioAPI,
author: AuthorSSSTik,
statistics: StatisticsSSSTik,
images: string[],
music?: string
): SSSTikResponse["result"] => ({
type: "image",
desc: $("p.maintext").text().trim(),
author,
statistics,
images,
...(music && { music: { playUrl: [music] } })
})
const createVideoResponse = (
$: CheerioAPI,
author: AuthorSSSTik,
statistics: StatisticsSSSTik,
video: string,
music?: string
): SSSTikResponse["result"] => ({
type: "video",
desc: $("p.maintext").text().trim(),
author,
statistics,
video: { playAddr: [video] },
...(music && { music: { playUrl: [music] } })
})
const createMusicResponse = (
music: string,
direct?: string
): SSSTikResponse["result"] => ({
type: "music",
music: { playUrl: [music] },
direct: direct || ""
})
const fetchTT = async (proxy?: string): Promise<SSSTikFetchTT> => {
try {
const { data } = await Axios(_ssstikurl, {
method: "GET",
headers: { "User-Agent": USER_AGENT },
...createProxyAgent(proxy)
})
const ttValue = extractTTValue(data)
if (!ttValue) {
return {
status: "error",
message: ERROR_MESSAGES.NETWORK_ERROR
}
}
return {
status: "success",
result: ttValue
}
} catch (error) {
return {
status: "error",
message:
error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
}
}
}
/** /**
* Tiktok SSSTik Downloader * Tiktok SSSTik Downloader
@ -58,128 +149,89 @@ const fetchTT = (proxy?: string): Promise<SSSTikFetchTT> =>
* @param {string} proxy - Your Proxy (optional) * @param {string} proxy - Your Proxy (optional)
* @returns {Promise<SSSTikResponse>} * @returns {Promise<SSSTikResponse>}
*/ */
export const SSSTik = async (
export const SSSTik = (url: string, proxy?: string): Promise<SSSTikResponse> => url: string,
new Promise(async (resolve) => { proxy?: string
try { ): Promise<SSSTikResponse> => {
if (!TiktokURLregex.test(url)) { try {
return resolve({ if (!validateTikTokUrl(url)) {
status: "error", return {
message: "Invalid Tiktok URL. Make sure your url is correct!" status: "error",
}) message: ERROR_MESSAGES.INVALID_URL
} }
const tt: SSSTikFetchTT = await fetchTT(proxy)
if (tt.status !== "success")
return resolve({ status: "error", message: tt.message })
const response = asyncRetry(
async () => {
const res = await Axios(_ssstikapi, {
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded; charset=UTF-8",
Origin: _ssstikurl,
Referer: _ssstikurl + "/en",
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0"
},
data: new URLSearchParams(
Object.entries({
id: url,
locale: "en",
tt: tt.result
})
),
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
if (res.status === 200 && res.data !== "") return res.data
throw new Error("Failed to fetch data from SSSTik!")
},
{
retries: 20,
minTimeout: 200,
maxTimeout: 1000
}
)
const $ = load(await response)
// Result
const author: AuthorSSSTik = {
avatar: $("img.result_author").attr("src"),
nickname: $("h2").text().trim()
}
const statistics: StatisticsSSSTik = {
likeCount: $("#trending-actions > .justify-content-start")
.text()
.trim(),
commentCount: $("#trending-actions > .justify-content-center")
.text()
.trim(),
shareCount: $("#trending-actions > .justify-content-end").text().trim()
}
// Video & Music Result
const video = $("a.without_watermark").attr("href")
const music = $("a.music").attr("href")
const direct = $("a.music_direct").attr("href")
// Images / Slide Result
const images: string[] = []
$("ul.splide__list > li")
.get()
.map((img) => {
images.push($(img).find("a").attr("href"))
})
let result: SSSTikResponse["result"]
if (images.length !== 0) {
// Images / Slide Result
result = {
type: "image",
desc: $("p.maintext").text().trim(),
author,
statistics,
images
}
if (music) {
result.music = music
}
} else if (video) {
// Video Result
result = {
type: "video",
desc: $("p.maintext").text().trim(),
author,
statistics,
video
}
if (music) {
result.music = music
}
} else if (music) {
// Music Result
result = {
type: "music",
music,
direct: direct || ""
}
}
resolve({ status: "success", result })
} catch (err) {
resolve({ status: "error", message: err.message })
} }
})
const tt = await fetchTT(proxy)
if (tt.status !== "success") {
return {
status: "error",
message: tt.message
}
}
const response = await asyncRetry(
async () => {
const res = await Axios(_ssstikapi, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
Origin: _ssstikurl,
Referer: `${_ssstikurl}/en`,
"User-Agent": USER_AGENT
},
data: new URLSearchParams({
id: url,
locale: "en",
tt: tt.result
}),
...createProxyAgent(proxy)
})
if (res.status === 200 && res.data) {
return res.data
}
throw new Error(ERROR_MESSAGES.NETWORK_ERROR)
},
{
retries: 20,
minTimeout: 200,
maxTimeout: 1000
}
)
const $ = load(response)
const author = parseAuthor($)
const statistics = parseStatistics($)
const video = $("a.without_watermark").attr("href")
const music = $("a.music").attr("href")
const direct = $("a.music_direct").attr("href")
const images = parseImages($)
let result: SSSTikResponse["result"]
if (images.length > 0) {
result = createImageResponse($, author, statistics, images, music)
} else if (video) {
result = createVideoResponse($, author, statistics, video, music)
} else if (music) {
result = createMusicResponse(music, direct)
} else {
return {
status: "error",
message: ERROR_MESSAGES.NETWORK_ERROR
}
}
return {
status: "success",
result
}
} catch (error) {
return {
status: "error",
message:
error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
}
}
}

View File

@ -12,168 +12,122 @@ import {
} from "../../types/downloader/tiktokApi" } from "../../types/downloader/tiktokApi"
import { HttpsProxyAgent } from "https-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent"
import { ERROR_MESSAGES } from "../../constants"
const TiktokURLregex = /** Constants */
const TIKTOK_URL_REGEX =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ /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)"
/** /** Types */
* Tiktok API Downloader interface ProxyConfig {
* @param {string} url - Tiktok URL httpsAgent?: HttpsProxyAgent<string> | SocksProxyAgent
* @param {string} proxy - Your Proxy (optional) }
* @param {boolean} showOriginalResponse - Show Original Response (optional)
* @returns {Promise<TiktokAPIResponse>}
*/
export const TiktokAPI = ( /** Helper Functions */
url: string, const createProxyAgent = (proxy?: string): ProxyConfig => {
proxy?: string, if (!proxy) return {}
showOriginalResponse?: boolean
): Promise<TiktokAPIResponse> =>
new Promise((resolve) => {
if (!TiktokURLregex.test(url)) {
return resolve({
status: "error",
message: "Invalid Tiktok URL. Make sure your url is correct!"
})
}
url = url.replace("https://vm", "https://vt")
Axios(url, {
method: "HEAD",
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
.then(async ({ request }) => {
const { responseUrl } = request.res
let ID = responseUrl.match(/\d{17,21}/g)
if (ID === null)
return resolve({
status: "error",
message:
"Failed to fetch tiktok url. Make sure your tiktok url is correct!"
})
ID = ID[0]
let data2 = await fetchTiktokData(ID, proxy) const isHttpProxy = proxy.startsWith("http") || proxy.startsWith("https")
const isSocksProxy = proxy.startsWith("socks")
if (!data2?.content) { if (!isHttpProxy && !isSocksProxy) return {}
return resolve({
status: "error",
message:
"Failed to fetch tiktok data. Make sure your tiktok url is correct!"
})
}
const { content, author, statistics, music } = data2 return {
httpsAgent: isHttpProxy
? new HttpsProxyAgent(proxy)
: new SocksProxyAgent(proxy)
}
}
let response: TiktokAPIResponse const validateTikTokUrl = (url: string): boolean => {
// Download Result return TIKTOK_URL_REGEX.test(url)
if (content.image_post_info) { }
// Images or Slide Result
response = {
status: "success",
result: {
type: "image",
id: content.aweme_id,
createTime: content.create_time,
description: content.desc,
isTurnOffComment: content.item_comment_settings === 3,
hashtag: content.text_extra
.filter((x: any) => x.hashtag_name !== undefined)
.map((v: any) => v.hashtag_name),
isADS: content.is_ads,
author,
statistics,
images:
content.image_post_info.images?.map(
(v: any) => v?.display_image?.url_list[0]
) || [],
music
}
}
} else {
// Video Result
const video: VideoTiktokAPI = {
ratio: content.video.ratio,
duration: content.video.duration,
playAddr: content.video?.play_addr?.url_list || [], // No Watermark Video
downloadAddr: content.video?.download_addr?.url_list || [], // Watermark Video
cover: content.video?.cover?.url_list || [],
dynamicCover: content.video?.dynamic_cover?.url_list || [],
originCover: content.video?.origin_cover?.url_list || []
}
response = { const extractVideoId = (responseUrl: string): string | null => {
status: "success", const matches = responseUrl.match(/\d{17,21}/g)
result: { return matches ? matches[0] : null
type: "video", }
id: content.aweme_id,
createTime: content.create_time,
description: content.desc,
isTurnOffComment: content.item_comment_settings === 3,
hashtag: content.text_extra
.filter((x: any) => x.hashtag_name !== undefined)
.map((v: any) => v.hashtag_name),
isADS: content.is_ads,
author,
statistics,
video,
music
}
}
}
// Show Original Response const parseStatistics = (content: any): StatisticsTiktokAPI => ({
if (showOriginalResponse) { commentCount: content.statistics.comment_count,
response = { likeCount: content.statistics.digg_count,
status: "success", shareCount: content.statistics.share_count,
resultNotParsed: data2 playCount: content.statistics.play_count,
} downloadCount: content.statistics.download_count
} })
resolve(response)
}) const parseAuthor = (content: any): AuthorTiktokAPI => ({
.catch((e) => resolve({ status: "error", message: e.message })) uid: content.author.uid,
}) username: content.author.unique_id,
nickname: content.author.nickname,
signature: content.author.signature,
region: content.author.region,
avatarThumb: content.author?.avatar_thumb?.url_list || [],
avatarMedium: content.author?.avatar_medium?.url_list || [],
url: `${_tiktokurl}/@${content.author.unique_id}`
})
const parseMusic = (content: any): MusicTiktokAPI => ({
id: content.music.id,
title: content.music.title,
author: content.music.author,
album: content.music.album,
playUrl: content.music?.play_url?.url_list || [],
coverLarge: content.music?.cover_large?.url_list || [],
coverMedium: content.music?.cover_medium?.url_list || [],
coverThumb: content.music?.cover_thumb?.url_list || [],
duration: content.music.duration,
isCommerceMusic: content.music.is_commerce_music,
isOriginalSound: content.music.is_original_sound,
isAuthorArtist: content.music.is_author_artist
})
const parseVideo = (content: any): VideoTiktokAPI => ({
ratio: content.video.ratio,
duration: content.video.duration,
playAddr: content.video?.play_addr?.url_list || [],
downloadAddr: content.video?.download_addr?.url_list || [],
cover: content.video?.cover?.url_list || [],
dynamicCover: content.video?.dynamic_cover?.url_list || [],
originCover: content.video?.origin_cover?.url_list || []
})
const parseTiktokData = (ID: string, data: any): ResponseParserTiktokAPI => {
const content = data?.aweme_list?.find((v: any) => v.aweme_id === ID)
if (!content) return { content: null }
return {
content,
statistics: parseStatistics(content),
author: parseAuthor(content),
music: parseMusic(content)
}
}
const fetchTiktokData = async ( const fetchTiktokData = async (
ID: string, ID: string,
proxy?: string proxy?: string
): Promise<ResponseParserTiktokAPI> | null => { ): Promise<ResponseParserTiktokAPI | null> => {
try { try {
const response = asyncRetry( const response = await asyncRetry(
async () => { async () => {
const res = await Axios( const res = await Axios(
_tiktokvFeed( _tiktokvFeed(_tiktokApiParams({ aweme_id: ID })),
_tiktokApiParams({
aweme_id: ID
})
),
{ {
method: "OPTIONS", method: "OPTIONS",
headers: { headers: { "User-Agent": USER_AGENT },
"User-Agent": ...createProxyAgent(proxy)
"com.zhiliaoapp.musically/300904 (2018111632; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)"
},
httpsAgent:
proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)
} }
) )
if (res.data !== "" && res.data.status_code === 0) { if (res.data && res.data.status_code === 0) {
return res.data return res.data
} }
throw new Error("Failed to fetch tiktok data") throw new Error(ERROR_MESSAGES.NETWORK_ERROR)
}, },
{ {
retries: 20, retries: 20,
@ -182,64 +136,131 @@ const fetchTiktokData = async (
} }
) )
const data = await response return parseTiktokData(ID, response)
if (data) { } catch (error) {
return parseTiktokData(ID, data) console.error("Error fetching TikTok data:", error)
}
} catch {
return null return null
} }
} }
const parseTiktokData = (ID: string, data: any): ResponseParserTiktokAPI => { const createImageResponse = (
let content = data?.aweme_list content: any,
author: AuthorTiktokAPI,
if (!content) return { content: null } statistics: StatisticsTiktokAPI,
music: MusicTiktokAPI
content = content.find((v: any) => v.aweme_id === ID) ): TiktokAPIResponse => ({
status: "success",
// Statistics Result result: {
const statistics: StatisticsTiktokAPI = { type: "image",
commentCount: content.statistics.comment_count, id: content.aweme_id,
diggCount: content.statistics.digg_count, createTime: content.create_time,
downloadCount: content.statistics.download_count, desc: content.desc,
playCount: content.statistics.play_count, isTurnOffComment: content.item_comment_settings === 3,
shareCount: content.statistics.share_count, hashtag: content.text_extra
forwardCount: content.statistics.forward_count, .filter((x: any) => x.hashtag_name !== undefined)
loseCount: content.statistics.lose_count, .map((v: any) => v.hashtag_name),
loseCommentCount: content.statistics.lose_comment_count, isADS: content.is_ads,
whatsappShareCount: content.statistics.whatsapp_share_count, author,
collectCount: content.statistics.collect_count, statistics,
repostCount: content.statistics.repost_count images:
content.image_post_info.images?.map(
(v: any) => v?.display_image?.url_list[0]
) || [],
music
} }
})
// Author Result const createVideoResponse = (
const author: AuthorTiktokAPI = { content: any,
uid: content.author.uid, author: AuthorTiktokAPI,
username: content.author.unique_id, statistics: StatisticsTiktokAPI,
nickname: content.author.nickname, music: MusicTiktokAPI
signature: content.author.signature, ): TiktokAPIResponse => ({
region: content.author.region, status: "success",
avatarThumb: content.author?.avatar_thumb?.url_list || [], result: {
avatarMedium: content.author?.avatar_medium?.url_list || [], type: "video",
url: `${_tiktokurl}/@${content.author.unique_id}` id: content.aweme_id,
createTime: content.create_time,
desc: content.desc,
isTurnOffComment: content.item_comment_settings === 3,
hashtag: content.text_extra
.filter((x: any) => x.hashtag_name !== undefined)
.map((v: any) => v.hashtag_name),
isADS: content.is_ads,
author,
statistics,
video: parseVideo(content),
music
} }
})
// Music Result /**
const music: MusicTiktokAPI = { * Tiktok API Downloader
id: content.music.id, * @param {string} url - Tiktok URL
title: content.music.title, * @param {string} proxy - Your Proxy (optional)
author: content.music.author, * @param {boolean} showOriginalResponse - Show Original Response (optional)
album: content.music.album, * @returns {Promise<TiktokAPIResponse>}
playUrl: content.music?.play_url?.url_list || [], */
coverLarge: content.music?.cover_large?.url_list || [], export const TiktokAPI = async (
coverMedium: content.music?.cover_medium?.url_list || [], url: string,
coverThumb: content.music?.cover_thumb?.url_list || [], proxy?: string,
duration: content.music.duration, showOriginalResponse?: boolean
isCommerceMusic: content.music.is_commerce_music, ): Promise<TiktokAPIResponse> => {
isOriginalSound: content.music.is_original_sound, try {
isAuthorArtist: content.music.is_author_artist if (!validateTikTokUrl(url)) {
return {
status: "error",
message: ERROR_MESSAGES.INVALID_URL
}
}
// Normalize URL
url = url.replace("https://vm", "https://vt")
// Get video ID
const { request } = await Axios(url, {
method: "HEAD",
...createProxyAgent(proxy)
})
const videoId = extractVideoId(request.res.responseUrl)
if (!videoId) {
return {
status: "error",
message: ERROR_MESSAGES.INVALID_URL
}
}
// Fetch TikTok data
const data = await fetchTiktokData(videoId, proxy)
if (!data?.content) {
return {
status: "error",
message: ERROR_MESSAGES.NETWORK_ERROR
}
}
const { content, author, statistics, music } = data
// Create response based on content type
const response = content.image_post_info
? createImageResponse(content, author, statistics, music)
: createVideoResponse(content, author, statistics, music)
// Return original response if requested
if (showOriginalResponse) {
return {
status: "success",
resultNotParsed: data
}
}
return response
} catch (error) {
return {
status: "error",
message:
error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR
}
} }
return { content, statistics, author, music }
} }