diff --git a/src/utils/downloader/musicalDown.ts b/src/utils/downloader/musicalDown.ts index d9963fc..f3c0d48 100644 --- a/src/utils/downloader/musicalDown.ts +++ b/src/utils/downloader/musicalDown.ts @@ -1,5 +1,6 @@ import Axios from "axios" import { load } from "cheerio" +type CheerioAPI = ReturnType import { MusicalDownResponse, GetMusicalDownMusic, @@ -12,87 +13,178 @@ import { } from "../../constants/api" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" +import { ERROR_MESSAGES } from "../../constants" -/** - * Using API from Website: - * 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+)/ +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 | 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 => { + const videos: Record = {} + 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 +): 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, proxy?: string -): Promise => - new Promise((resolve) => { - if (!TiktokURLregex.test(url)) { - return resolve({ +): Promise => { + try { + if (!validateTikTokUrl(url)) { + return { 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", headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Update-Insecure-Requests": "1", - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0" + "User-Agent": USER_AGENT }, - httpsAgent: - (proxy && - (proxy.startsWith("http") || proxy.startsWith("https") - ? new HttpsProxyAgent(proxy) - : proxy.startsWith("socks") - ? new SocksProxyAgent(proxy) - : undefined)) || - undefined + ...createProxyAgent(proxy) }) - .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) => -// new Promise((resolve) => { -// Axios(_musicaldownmusicapi, { -// method: "GET", -// headers: { -// cookie: cookie, -// "Upgrade-Insecure-Requests": "1", -// "User-Agent": -// "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0" -// }, -// httpsAgent: -// (proxy && -// (proxy.startsWith("http") || proxy.startsWith("https") -// ? new HttpsProxyAgent(proxy) -// : proxy.startsWith("socks") -// ? new SocksProxyAgent(proxy) -// : undefined)) || -// undefined -// }) -// .then(({ data }) => { -// const $ = load(data) -// const music = $("audio > source").attr("src") -// resolve({ status: "success", result: music }) -// }) -// .catch((e) => resolve({ status: "error" })) -// }) + const cookie = headers["set-cookie"]?.[0]?.split(";")[0] + if (!cookie) { + return { + status: "error", + message: ERROR_MESSAGES.NETWORK_ERROR + } + } + + const $ = load(data) + const request = extractRequestForm($) + + return { + status: "success", + request, + cookie + } + } catch (error) { + return { + status: "error", + message: + error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR + } + } +} /** * Tiktok MusicalDown Downloader @@ -100,16 +192,20 @@ const getRequest = ( * @param {string} proxy - Proxy * @returns {Promise} */ - -export const MusicalDown = ( +export const MusicalDown = async ( url: string, proxy?: string -): Promise => - new Promise(async (resolve) => { - const request: GetMusicalDownReuqest = await getRequest(url) - if (request.status !== "success") - return resolve({ status: "error", message: request.message }) - Axios(_musicaldownapi, { +): Promise => { + try { + const request = await getRequest(url, proxy) + if (request.status !== "success") { + return { + status: "error", + message: request.message + } + } + + const { data } = await Axios(_musicaldownapi, { method: "POST", headers: { cookie: request.cookie, @@ -117,98 +213,33 @@ export const MusicalDown = ( Origin: "https://musicaldown.com", Referer: "https://musicaldown.com/en", "Upgrade-Insecure-Requests": "1", - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0" + "User-Agent": USER_AGENT }, data: new URLSearchParams(Object.entries(request.request)), - httpsAgent: - (proxy && - (proxy.startsWith("http") || proxy.startsWith("https") - ? new HttpsProxyAgent(proxy) - : proxy.startsWith("socks") - ? new SocksProxyAgent(proxy) - : undefined)) || - undefined + ...createProxyAgent(proxy) }) - .then(async ({ data }) => { - const $ = load(data) - // Get Image Video - const images = [] - $("div.row > div[class='col s12 m3']") - .get() - .map((v) => { - images.push($(v).find("img").attr("src")) - }) + const $ = load(data) + const images = parseImages($) - // Result - if (images.length !== 0) { - // 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 (images.length > 0) { + return createImageResponse(images) + } - if (Object.keys(videos).length === 0) - return resolve({ - status: "success", - 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 videos = parseVideos($) + if (Object.keys(videos).length === 0) { + return { + status: "error", + message: "There is an error. Can't find download link" + } + } -const isURL = (url: string) => { - let status = false - try { - new URL(url) - status = true - } catch { - status = false + return createVideoResponse($, videos) + } catch (error) { + return { + status: "error", + message: + error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR + } } - return status } diff --git a/src/utils/downloader/ssstik.ts b/src/utils/downloader/ssstik.ts index ba1eb78..a765405 100644 --- a/src/utils/downloader/ssstik.ts +++ b/src/utils/downloader/ssstik.ts @@ -1,6 +1,7 @@ import Axios from "axios" import asyncRetry from "async-retry" import { load } from "cheerio" +type CheerioAPI = ReturnType import { AuthorSSSTik, StatisticsSSSTik, @@ -10,47 +11,137 @@ import { import { _ssstikapi, _ssstikurl } from "../../constants/api" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" +import { ERROR_MESSAGES } from "../../constants" /** * Using API from Website: * 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+)/ +const USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" -const fetchTT = (proxy?: string): Promise => - new Promise(async (resolve) => { - Axios(_ssstikurl, { - method: "GET", - headers: { - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" - }, - httpsAgent: - (proxy && - (proxy.startsWith("http") || proxy.startsWith("https") - ? new HttpsProxyAgent(proxy) - : proxy.startsWith("socks") - ? new SocksProxyAgent(proxy) - : undefined)) || - undefined - }) - .then(({ data }) => { - const regex = /s_tt\s*=\s*["']([^"']+)["']/ - const match = data.match(regex) - if (match) { - const value = match[1] - return resolve({ status: "success", result: value }) - } else { - return resolve({ - status: "error", - message: "Failed to get the request form!" - }) - } - }) - .catch((e) => resolve({ status: "error", message: e.message })) +/** Types */ +interface ProxyConfig { + httpsAgent?: HttpsProxyAgent | SocksProxyAgent +} + +/** 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 extractTTValue = (html: string): string | null => { + const regex = /s_tt\s*=\s*["']([^"']+)["']/ + const match = html.match(regex) + return match ? match[1] : null +} + +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 => { + 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 @@ -58,128 +149,89 @@ const fetchTT = (proxy?: string): Promise => * @param {string} proxy - Your Proxy (optional) * @returns {Promise} */ - -export const SSSTik = (url: string, proxy?: string): Promise => - new Promise(async (resolve) => { - try { - if (!TiktokURLregex.test(url)) { - return resolve({ - status: "error", - message: "Invalid Tiktok URL. Make sure your url is correct!" - }) +export const SSSTik = async ( + url: string, + proxy?: string +): Promise => { + try { + if (!validateTikTokUrl(url)) { + return { + 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 + } + } +} diff --git a/src/utils/downloader/tiktokApi.ts b/src/utils/downloader/tiktokApi.ts index d720435..c4d6c1e 100644 --- a/src/utils/downloader/tiktokApi.ts +++ b/src/utils/downloader/tiktokApi.ts @@ -12,168 +12,122 @@ import { } from "../../types/downloader/tiktokApi" import { HttpsProxyAgent } from "https-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+)/ +const USER_AGENT = + "com.zhiliaoapp.musically/300904 (2018111632; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)" -/** - * Tiktok API Downloader - * @param {string} url - Tiktok URL - * @param {string} proxy - Your Proxy (optional) - * @param {boolean} showOriginalResponse - Show Original Response (optional) - * @returns {Promise} - */ +/** Types */ +interface ProxyConfig { + httpsAgent?: HttpsProxyAgent | SocksProxyAgent +} -export const TiktokAPI = ( - url: string, - proxy?: string, - showOriginalResponse?: boolean -): Promise => - 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] +/** Helper Functions */ +const createProxyAgent = (proxy?: string): ProxyConfig => { + if (!proxy) return {} - let data2 = await fetchTiktokData(ID, proxy) + const isHttpProxy = proxy.startsWith("http") || proxy.startsWith("https") + const isSocksProxy = proxy.startsWith("socks") - if (!data2?.content) { - return resolve({ - status: "error", - message: - "Failed to fetch tiktok data. Make sure your tiktok url is correct!" - }) - } + if (!isHttpProxy && !isSocksProxy) return {} - const { content, author, statistics, music } = data2 + return { + httpsAgent: isHttpProxy + ? new HttpsProxyAgent(proxy) + : new SocksProxyAgent(proxy) + } +} - let response: TiktokAPIResponse - // Download Result - 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 || [] - } +const validateTikTokUrl = (url: string): boolean => { + return TIKTOK_URL_REGEX.test(url) +} - response = { - status: "success", - result: { - 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 - } - } - } +const extractVideoId = (responseUrl: string): string | null => { + const matches = responseUrl.match(/\d{17,21}/g) + return matches ? matches[0] : null +} - // Show Original Response - if (showOriginalResponse) { - response = { - status: "success", - resultNotParsed: data2 - } - } - resolve(response) - }) - .catch((e) => resolve({ status: "error", message: e.message })) - }) +const parseStatistics = (content: any): StatisticsTiktokAPI => ({ + commentCount: content.statistics.comment_count, + likeCount: content.statistics.digg_count, + shareCount: content.statistics.share_count, + playCount: content.statistics.play_count, + downloadCount: content.statistics.download_count +}) + +const parseAuthor = (content: any): AuthorTiktokAPI => ({ + 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 ( ID: string, proxy?: string -): Promise | null => { +): Promise => { try { - const response = asyncRetry( + const response = await asyncRetry( async () => { const res = await Axios( - _tiktokvFeed( - _tiktokApiParams({ - aweme_id: ID - }) - ), + _tiktokvFeed(_tiktokApiParams({ aweme_id: ID })), { method: "OPTIONS", - headers: { - "User-Agent": - "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) + headers: { "User-Agent": USER_AGENT }, + ...createProxyAgent(proxy) } ) - if (res.data !== "" && res.data.status_code === 0) { + if (res.data && res.data.status_code === 0) { return res.data } - throw new Error("Failed to fetch tiktok data") + throw new Error(ERROR_MESSAGES.NETWORK_ERROR) }, { retries: 20, @@ -182,64 +136,131 @@ const fetchTiktokData = async ( } ) - const data = await response - if (data) { - return parseTiktokData(ID, data) - } - } catch { + return parseTiktokData(ID, response) + } catch (error) { + console.error("Error fetching TikTok data:", error) return null } } -const parseTiktokData = (ID: string, data: any): ResponseParserTiktokAPI => { - let content = data?.aweme_list - - if (!content) return { content: null } - - content = content.find((v: any) => v.aweme_id === ID) - - // Statistics Result - const statistics: StatisticsTiktokAPI = { - commentCount: content.statistics.comment_count, - diggCount: content.statistics.digg_count, - downloadCount: content.statistics.download_count, - playCount: content.statistics.play_count, - shareCount: content.statistics.share_count, - forwardCount: content.statistics.forward_count, - loseCount: content.statistics.lose_count, - loseCommentCount: content.statistics.lose_comment_count, - whatsappShareCount: content.statistics.whatsapp_share_count, - collectCount: content.statistics.collect_count, - repostCount: content.statistics.repost_count +const createImageResponse = ( + content: any, + author: AuthorTiktokAPI, + statistics: StatisticsTiktokAPI, + music: MusicTiktokAPI +): TiktokAPIResponse => ({ + status: "success", + result: { + type: "image", + 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, + images: + content.image_post_info.images?.map( + (v: any) => v?.display_image?.url_list[0] + ) || [], + music } +}) - // Author Result - const author: AuthorTiktokAPI = { - 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 createVideoResponse = ( + content: any, + author: AuthorTiktokAPI, + statistics: StatisticsTiktokAPI, + music: MusicTiktokAPI +): TiktokAPIResponse => ({ + status: "success", + result: { + type: "video", + 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 = { - 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 +/** + * Tiktok API Downloader + * @param {string} url - Tiktok URL + * @param {string} proxy - Your Proxy (optional) + * @param {boolean} showOriginalResponse - Show Original Response (optional) + * @returns {Promise} + */ +export const TiktokAPI = async ( + url: string, + proxy?: string, + showOriginalResponse?: boolean +): Promise => { + try { + 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 } }