From 6ef5db2aac6722375c0b0d4ff7c13881f707d15d Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 16:26:26 +0700 Subject: [PATCH 01/10] chore: bump to version 1.3.2 --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index 85fb7b7..27ada11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tobyg74/tiktok-api-dl", - "version": "1.3.1-fix", + "version": "1.3.2", "description": "Scraper for downloading media in the form of videos, images and audio from Tiktok. Also for stalking Tiktok Users", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -27,10 +27,6 @@ "tiktok-stalk" ], "author": "Tobz", - "contributors": [ - "aqulzz", - "nugraizy" - ], "license": "ISC", "bugs": { "url": "https://github.com/TobyG74/tiktok-api-dl/issues" From db1686fc9b0480402db7bd6bd0c41f899ff56668 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 16:45:45 +0700 Subject: [PATCH 02/10] docs: add new docs for tiktok playlist --- README.md | 247 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 193 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 98b98e1..28f24f6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ - [Tiktok Video User Comments](#tiktok-video-comments) - [Tiktok Get User Posts](#tiktok-get-user-posts) - [Tiktok Get User Favorite Videos](#tiktok-get-user-favorite-videos) + - [Tiktok Collection](#tiktok-collection) + - [Tiktok Playlist](#tiktok-playlist) - [API Response Types](#api-response-types) - [Tiktok Downloader](#tiktok-downloader-1) - [Version 1 Response](#version-1-response) @@ -182,6 +184,22 @@ Tiktok.Downloader(url, { }).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Download Tiktok Video +tiktokdl download "https://vt.tiktok.com/xxxxxxxx" + +# Download Tiktok Video with version +tiktokdl download "https://vt.tiktok.com/xxxxxxxx" -v v1 + +# Download Tiktok Video with Custom Output Directory Path +tiktokdl download "https://vt.tiktok.com/xxxxxxxx" -v v1 -o "/path/to/save/video.mp4" + +# Download Tiktok Video with Proxy +tiktokdl download "https://vt.tiktok.com/xxxxxxxx" -v v1 -proxy "http://your-proxy-url" +``` + - [Version 1 Response](#version-1-response) - [Version 2 Response](#version-2-response) - [Version 3 Response](#version-3-response) @@ -192,15 +210,47 @@ Tiktok.Downloader(url, { const Tiktok = require("@tobyg74/tiktok-api-dl") Tiktok.Search("username", { - type: "user", // "user" | "live" + type: "user", // "user" | "live" | "video" page: 1, cookie: "YOUR_COOKIE", // needed proxy: "YOUR_PROXY" // optional }).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Search Tiktok Users +tiktokdl search user + +# Search Tiktok Users with pagination +tiktokdl search user -p 1 + +# Search Tiktok Users with proxy +tiktokdl search user -p 1 -proxy "http://your-proxy-url" + +# Search Tiktok Live Streams +tiktokdl search live + +# Search Tiktok Live Streams with pagination +tiktokdl search live -p 1 + +# Search Tiktok Live Streams with proxy +tiktokdl search live -p 1 -proxy "http://your-proxy-url" + +# Search Tiktok Videos +tiktokdl search video + +# Search Tiktok Videos with pagination +tiktokdl search video -p 1 + +# Search Tiktok Videos with proxy +tiktokdl search video -p 1 -proxy "http://your-proxy-url" +``` + - [User Search Response](#user-search-response) - [Live Search Response](live-search-response) +- [Video Search Response](#video-search-response) ## Tiktok Stalk User Profile @@ -209,11 +259,20 @@ const Tiktok = require("@tobyg74/tiktok-api-dl") const username = "Tobz2k19" Tiktok.Stalker(username, { - cookie: "YOUR_COOKIE", // optional, if response null proxy: "YOUR_PROXY" // optional }).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Stalk User Profile +tiktokdl stalk + +# Stalk User Profile with proxy +tiktokdl stalk -proxy "http://your-proxy-url" +``` + - [Tiktok Stalk User Response](#tiktok-stalk-user-profile-1) ## Tiktok Video Comments @@ -228,6 +287,19 @@ Tiktok.GetVideoComments(url, { }).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Get Video Comments +tiktokdl getvideocomments "https://vt.tiktok.com/xxxxxxxx" + +# Get Video Comments with limit of comments +tiktokdl getvideocomments "https://vt.tiktok.com/xxxxxxxx" -l 10 + +# Get Video Comments with proxy +tiktokdl getvideocomments "https://vt.tiktok.com/xxxxxxxx" -l 10 -proxy "http://your-proxy-url" +``` + - [Tiktok Video Comments Response](#tiktok-video-comments-1) ## Tiktok Get User Posts @@ -242,6 +314,19 @@ Tiktok.GetUserPosts(username, { }).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Get User Posts +tiktokdl getuserposts + +# Get User Posts with limit of posts +tiktokdl getuserposts -l 10 + +# Get User Posts with proxy +tiktokdl getuserposts -l 10 -proxy "http://your-proxy-url" +``` + - [Tiktok User Posts Response](#tiktok-user-posts) ## Tiktok Get User Liked Videos @@ -259,6 +344,19 @@ Tiktok.GetUserLiked(username, { }) ``` +### CLI Usage + +```bash +# Get User Liked Videos +tiktokdl getuserliked + +# Get User Liked Videos with limit of posts +tiktokdl getuserliked -l 10 + +# Get User Liked Videos with proxy +tiktokdl getuserliked -l 10 -proxy "http://your-proxy-url" +``` + - [Tiktok User Liked Videos Response](#tiktok-user-liked-videos) ## Tiktok Collection @@ -301,59 +399,41 @@ tiktokdl collection 7507916135931218695 -p 1 -n 5 tiktokdl collection 7507916135931218695 -n 5 -proxy "http://your-proxy-url" ``` -### Response Type +- [Tiktok Collection Response](#tiktok-collection-1) -```typescript -interface TiktokCollectionResponse { - status: "success" | "error" - message?: string - result?: { - itemList: Array<{ - id: string - desc: string - createTime: number - author?: { - id: string - uniqueId: string - nickname: string - avatarThumb: string - avatarMedium: string - avatarLarger: string - signature: string - verified: boolean - } - statistics?: { - playCount: number - diggCount: number - shareCount: number - commentCount: number - collectCount: number - } - video?: { - id: string - height: number - width: number - duration: number - ratio: string - cover: string - originCover: string - dynamicCover: string - playAddr: string - downloadAddr: string - format: string - bitrate: number - } - textExtra?: Array<{ - hashtagName: string - hashtagId: string - type: number - }> - }> - hasMore: boolean - } -} +## Tiktok Playlist + +Get videos from a TikTok playlist (supports playlist ID or URL) + +```javascript +const Tiktok = require("@tobyg74/tiktok-api-dl") + +const playlistIdOrUrl = "https://www.tiktok.com/@username/playlist/name-id" +Tiktok.Playlist(playlistIdOrUrl, { + page: 1, + count: 5, + proxy: "YOUR_PROXY" +}).then((result) => console.log(result)) ``` +### CLI Usage + +```bash +# Using playlist ID +tiktokdl playlist 7507916135931218695 -n 5 + +# Using playlist URL +tiktokdl playlist "https://www.tiktok.com/@username/playlist/name-id" -n 5 + +# With page for pagination +tiktokdl playlist 7507916135931218695 -p 1 -n 5 + +# With proxy +tiktokdl playlist 7507916135931218695 -n 5 -proxy "http://your-proxy-url" +``` + +- [Tiktok Playlist Response](#tiktok-playlist-1) + # API Response Types ## Tiktok Downloader @@ -446,8 +526,12 @@ interface SSSTikResponse { shareCount: string } images?: string[] - video?: string - music?: string + video?: { + playAddr: string + } + music?: { + playUrl: string + } direct?: string } } @@ -862,6 +946,61 @@ interface TiktokCollectionResponse { } ``` +## Tiktok Playlist + +### Playlist Response + +```typescript +status: "success" | "error" +message?: string +result?: { + hasMore: boolean + itemList: Array<{ + id: string + desc: string + createTime: number + author: PlaylistAuthor + stats: Statistics + video: VideoTiktokAPI + music: MusicTiktokAPI + challenges: Array<{ + id: string + title: string + desc: string + coverLarger: string + coverMedium: string + coverThumb: string + profileLarger: string + profileMedium: string + profileThumb: string + }> + collected: boolean + digged: boolean + duetDisplay: number + forFriend: boolean + officalItem: boolean + originalItem: boolean + privateItem: boolean + shareEnabled: boolean + stitchDisplay: number + textExtra: Array<{ + awemeId: string + end: number + hashtagName: string + isCommerce: boolean + start: number + subType: number + type: number + }> + }> + extra?: { + fatal_item_ids: string[] + logid: string + now: number + } +} +``` + # Changelog - All changes will be documented in the [CHANGELOG.md](https://github.com/TobyG74/tiktok-api-dl/blob/master/CHANGELOG.md) file. From 1ad6d8baea7c2ac45ffc6cccd089ce193d17f491 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:09:32 +0700 Subject: [PATCH 03/10] fix: downloader types --- src/index.ts | 92 ++++++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/index.ts b/src/index.ts index aa8c1c1..a28250c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /** Types */ -import { TiktokAPIResponse } from "./types/downloader/tiktokApi" -import { SSSTikResponse } from "./types/downloader/ssstik" -import { MusicalDownResponse } from "./types/downloader/musicaldown" +import { TiktokAPIResponse } from "./types/downloader/tiktokApiDownloader" +import { SSSTikResponse } from "./types/downloader/ssstikDownloader" +import { MusicalDownResponse } from "./types/downloader/musicaldownDownloader" import { UserSearchResult } from "./types/search/userSearch" import { LiveSearchResult } from "./types/search/liveSearch" import { VideoSearchResult } from "./types/search/videoSearch" @@ -12,9 +12,9 @@ import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked" import { TiktokCollectionResponse } from "./types/get/getCollection" /** Services */ -import { extractPlaylistId, TiktokAPI } from "./utils/downloader/tiktokApi" -import { SSSTik } from "./utils/downloader/ssstik" -import { MusicalDown } from "./utils/downloader/musicalDown" +import { TiktokAPI } from "./utils/downloader/tiktokAPIDownloader" +import { SSSTik } from "./utils/downloader/ssstikDownloader" +import { MusicalDown } from "./utils/downloader/musicaldownDownloader" import { StalkUser } from "./utils/get/getProfile" import { SearchUser } from "./utils/search/userSearch" import { SearchLive } from "./utils/search/liveSearch" @@ -23,7 +23,6 @@ import { getUserPosts } from "./utils/get/getUserPosts" import { getUserLiked } from "./utils/get/getUserLiked" import { SearchVideo } from "./utils/search/videoSearch" import { getCollection } from "./utils/get/getCollection" -import { extractCollectionId } from "./utils/downloader/tiktokApi" /** Constants */ import { DOWNLOADER_VERSIONS, SEARCH_TYPES } from "./constants" @@ -31,6 +30,8 @@ import { ERROR_MESSAGES } from "./constants" import { validateCookie } from "./utils/validator" import { TiktokPlaylistResponse } from "./types/get/getPlaylist" import { getPlaylist } from "./utils/get/getPlaylist" +import { extractPlaylistId } from "./utils/get/getPlaylist" +import { extractCollectionId } from "./utils/get/getCollection" /** Types */ type DownloaderVersion = "v1" | "v2" | "v3" @@ -82,7 +83,7 @@ export = { Downloader: async ( url: string, options?: { - version: DownloaderVersion + version: T proxy?: string showOriginalResponse?: boolean } @@ -118,38 +119,6 @@ export = { } }, - /** - * Get TikTok Collection - * @param {string} collectionIdOrUrl - Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id) - * @param {Object} options - The options for collection - * @param {string} [options.proxy] - Optional proxy URL - * @param {string} [options.page] - Optional page for pagination - * @param {number} [options.count] - Optional number of items to fetch - * @returns {Promise} - */ - Collection: async ( - collectionIdOrUrl: string, - options?: { - proxy?: string - page?: number - count?: number - } - ): Promise => { - const collectionId = extractCollectionId(collectionIdOrUrl) - if (!collectionId) { - return { - status: "error", - message: "Invalid collection ID or URL format" - } - } - return await getCollection( - collectionId, - options?.proxy, - options?.page, - options?.count - ) - }, - /** * Tiktok Search * @param {string} keyword - The query you want to search @@ -164,7 +133,7 @@ export = { keyword: string, options?: { type?: T - cookie?: string + cookie: string | any[] page?: number proxy?: string } @@ -240,11 +209,10 @@ export = { StalkUser: async ( username: string, options?: { - cookie?: string | any[] proxy?: string } ): Promise => { - return await StalkUser(username, options?.cookie, options?.proxy) + return await StalkUser(username, options?.proxy) }, /** @@ -314,9 +282,41 @@ export = { ) }, + /** + * Get TikTok Collection + * @param {string} collectionIdOrUrl - Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id) + * @param {Object} options - The options for collection + * @param {string} [options.proxy] - Optional proxy URL + * @param {string} [options.page] - Optional page for pagination + * @param {number} [options.count] - Optional number of items to fetch + * @returns {Promise} + */ + Collection: async ( + collectionIdOrUrl: string, + options?: { + proxy?: string + page?: number + count?: number + } + ): Promise => { + const collectionId = extractCollectionId(collectionIdOrUrl) + if (!collectionId) { + return { + status: "error", + message: "Invalid collection ID or URL format" + } + } + return await getCollection( + collectionId, + options?.proxy, + options?.page, + options?.count + ) + }, + /** * Get TikTok Playlist - * @param {string} url - URL (e.g. https://www.tiktok.com/@username/playlist/name-id) + * @param {string} playlistIdOrUrl - Playlist ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/playlist/name-id) * @param {Object} options - The options for playlist * @param {string} [options.proxy] - Optional proxy URL * @param {string} [options.page] - Optional page for pagination @@ -324,14 +324,14 @@ export = { * @returns {Promise} */ Playlist: async ( - url: string, + playlistIdOrUrl: string, options?: { proxy?: string page?: number count?: number } ): Promise => { - const playlistId = extractPlaylistId(url) + const playlistId = extractPlaylistId(playlistIdOrUrl) if (!playlistId) { return { status: "error", From ae537757d8fa274e52060f445d4822c617f9ac93 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:10:11 +0700 Subject: [PATCH 04/10] feat: adding a new downloader for playlists and collections --- src/cli/index.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index 7528155..84bfbee 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -10,9 +10,6 @@ import { handleMediaDownload } from "../services/downloadManager" import { _tiktokurl } from "../constants/api" -import path from "path" -import * as fs from "fs" -import axios from "axios" const cookieManager = new CookieManager() @@ -86,7 +83,7 @@ cookieCommand const searchCommand = program .command("search") - .description("Search TikTok users or live streams") + .description("Search TikTok users or live streams or videos") searchCommand .command("user") @@ -503,7 +500,7 @@ program .command("playlist") .description("Get videos from a TikTok playlist") .argument( - "", + "", "Collection URL (e.g. https://www.tiktok.com/@username/playlist/name-id)" ) .option("-p, --page ", "Page number", "1") @@ -590,4 +587,122 @@ program } }) +// Download all items in a TikTok playlist +program + .command("download-playlist") + .description("Download all videos from a TikTok playlist") + .argument( + "", + "Playlist URL (e.g. https://www.tiktok.com/@username/playlist/name-id)" + ) + .option("-o, --output ", "Output directory path") + .option("-v, --version ", "Downloader version (v1/v2/v3)", "v1") + .option("-p, --proxy ", "Proxy URL (http/https/socks)") + .option( + "-c, --count ", + "Number of items to fetch (max: 20)", + (val) => parseInt(val), + 20 + ) + .action(async (url, options) => { + try { + const outputPath = options.output || getDefaultDownloadPath() + const version = options.version.toLowerCase() + Logger.info(`Fetching playlist items...`) + const results = await Tiktok.Playlist(url, { + page: 1, + proxy: options.proxy, + count: options.count + }) + if (results.status === "success" && results.result) { + const { itemList } = results.result + Logger.info( + `Found ${itemList.length} items in playlist. Starting download...` + ) + for (const [index, item] of itemList.entries()) { + Logger.info( + `Downloading [${index + 1}/${itemList.length}]: ${item.id}` + ) + const videoUrl = `https://www.tiktok.com/@${ + item.author?.uniqueId || "unknown" + }/video/${item.id}` + try { + const data = await Tiktok.Downloader(videoUrl, { + version: version, + proxy: options.proxy + }) + await handleMediaDownload(data, outputPath, version) + Logger.success(`Downloaded: ${videoUrl}`) + } catch (err) { + Logger.error(`Failed to download ${videoUrl}: ${err.message}`) + } + } + Logger.info("All downloads finished.") + } else { + Logger.error(`Error: ${results.message}`) + } + } catch (error) { + Logger.error(`Error: ${error.message}`) + } + }) + +// Download all items in a TikTok collection +program + .command("download-collection") + .description("Download all videos from a TikTok collection") + .argument( + "", + "Collection ID or URL (e.g. 7507916135931218695 or https://www.tiktok.com/@username/collection/name-id)" + ) + .option("-o, --output ", "Output directory path") + .option("-v, --version ", "Downloader version (v1/v2/v3)", "v1") + .option("-p, --proxy ", "Proxy URL (http/https/socks)") + .option( + "-n, --count ", + "Number of items to fetch", + (val) => parseInt(val), + 20 + ) + .action(async (collectionIdOrUrl, options) => { + try { + const outputPath = options.output || getDefaultDownloadPath() + const version = options.version.toLowerCase() + Logger.info(`Fetching collection items...`) + const results = await Tiktok.Collection(collectionIdOrUrl, { + page: 1, + proxy: options.proxy, + count: options.count + }) + if (results.status === "success" && results.result) { + const { itemList } = results.result + Logger.info( + `Found ${itemList.length} items in collection. Starting download...` + ) + for (const [index, item] of itemList.entries()) { + Logger.info( + `Downloading [${index + 1}/${itemList.length}]: ${item.id}` + ) + const videoUrl = `https://www.tiktok.com/@${ + item.author?.uniqueId || "unknown" + }/video/${item.id}` + try { + const data = await Tiktok.Downloader(videoUrl, { + version: version, + proxy: options.proxy + }) + await handleMediaDownload(data, outputPath, version) + Logger.success(`Downloaded: ${videoUrl}`) + } catch (err) { + Logger.error(`Failed to download ${videoUrl}: ${err.message}`) + } + } + Logger.info("All downloads finished.") + } else { + Logger.error(`Error: ${results.message}`) + } + } catch (error) { + Logger.error(`Error: ${error.message}`) + } + }) + program.parse() From a56bc8c9d9bedd42305c92a7bb15edb3ecad390a Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:11:46 +0700 Subject: [PATCH 05/10] fix handle downloader function --- .gitignore | 1 + src/lib/logger.ts | 8 ++++---- src/services/downloadManager.ts | 2 +- src/services/tiktokService.ts | 13 +++++++++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 65872b4..89e8678 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ pnpm-lock.yaml package-lock.json yarn.lock lib +!src/lib test.js bun.lockb tsconfig.tsbuildinfo diff --git a/src/lib/logger.ts b/src/lib/logger.ts index eeb392c..cf720be 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -2,19 +2,19 @@ import chalk from "chalk" export class Logger { static success(message: string): void { - console.log(chalk.green("✓ " + message)) + console.log(chalk.green("✓ " + message)) } static error(message: string): void { - console.error(chalk.red("✗ " + message)) + console.error(chalk.red("✗ " + message)) } static info(message: string): void { - console.log(chalk.blue("ℹ " + message)) + console.log(chalk.blue("ℹ " + message)) } static warning(message: string): void { - console.log(chalk.yellow("⚠ " + message)) + console.log(chalk.yellow("⚠ " + message)) } static result(message: string, color = chalk.cyan): void { diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 6393c7a..eef4bfd 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -61,7 +61,7 @@ async function handleMediaDownload( case "video": { const videoUrl = version === "v1" - ? result.video.downloadAddr[0] + ? result.video.playAddr[0] : version === "v2" ? result.video.playAddr[0] : result.videoHD diff --git a/src/services/tiktokService.ts b/src/services/tiktokService.ts index 8943a16..cd50a4b 100644 --- a/src/services/tiktokService.ts +++ b/src/services/tiktokService.ts @@ -5,6 +5,7 @@ import { userAgent, webUserAgent } from "../constants/headers" import qs from "qs" import fs from "fs" import { createCipheriv } from "crypto" +import path from "path" export class TiktokService { /** @@ -82,6 +83,7 @@ export class TiktokService { const baseUrl = `${TiktokService.BASE_URL}api/search/user/full/?` const queryParams = _userSearchParams(username, page) const xbogusParams = xbogus(`${baseUrl}${queryParams}`, userAgent) + console.log(`${baseUrl}${_userSearchParams(username, page, xbogusParams)}`) return `${baseUrl}${_userSearchParams(username, page, xbogusParams)}` } @@ -102,11 +104,18 @@ export class TiktokService { } } + private static readonly FILE_PATH = path.join(__dirname, "../../helper") private static readonly BASE_URL = "https://www.tiktok.com/" private static readonly AES_KEY = "webapp1.0+202106" private static readonly AES_IV = "webapp1.0+202106" - private signaturejs = fs.readFileSync("./helper/signature.js", "utf-8") - private webmssdk = fs.readFileSync("./helper/webmssdk.js", "utf-8") + private signaturejs = fs.readFileSync( + path.join(TiktokService.FILE_PATH, "signature.js"), + "utf-8" + ) + private webmssdk = fs.readFileSync( + path.join(TiktokService.FILE_PATH, "webmssdk.js"), + "utf-8" + ) private resourceLoader = new ResourceLoader({ userAgent: "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" From 9d53637ac267568fc0f2d3c4556dd2367b4cbf19 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:13:13 +0700 Subject: [PATCH 06/10] fix: duplicate types --- src/types/downloader/musicaldown.ts | 16 - src/types/downloader/musicaldownDownloader.ts | 32 ++ src/types/downloader/ssstik.ts | 13 - src/types/downloader/ssstikDownloader.ts | 35 ++ .../{tiktokApi.ts => tiktokApiDownloader.ts} | 0 src/types/get/getCollection.ts | 2 +- src/types/get/getPlaylist.ts | 2 +- ...usicalDown.ts => musicaldownDownloader.ts} | 2 +- .../{ssstik.ts => ssstikDownloader.ts} | 2 +- src/utils/downloader/tiktokApi.ts | 446 ------------------ 10 files changed, 71 insertions(+), 479 deletions(-) delete mode 100644 src/types/downloader/musicaldown.ts create mode 100644 src/types/downloader/musicaldownDownloader.ts delete mode 100644 src/types/downloader/ssstik.ts create mode 100644 src/types/downloader/ssstikDownloader.ts rename src/types/downloader/{tiktokApi.ts => tiktokApiDownloader.ts} (100%) rename src/utils/downloader/{musicalDown.ts => musicaldownDownloader.ts} (99%) rename src/utils/downloader/{ssstik.ts => ssstikDownloader.ts} (99%) delete mode 100644 src/utils/downloader/tiktokApi.ts diff --git a/src/types/downloader/musicaldown.ts b/src/types/downloader/musicaldown.ts deleted file mode 100644 index a7ab306..0000000 --- a/src/types/downloader/musicaldown.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseResponse, Content } from "../common" - -export type GetMusicalDownReuqest = BaseResponse & { - request?: { - [key: string]: string - } - cookie?: string -} - -export type MusicalDownResponse = BaseResponse & { - result?: Content -} - -export type GetMusicalDownMusic = BaseResponse & { - result?: string -} diff --git a/src/types/downloader/musicaldownDownloader.ts b/src/types/downloader/musicaldownDownloader.ts new file mode 100644 index 0000000..32b0e56 --- /dev/null +++ b/src/types/downloader/musicaldownDownloader.ts @@ -0,0 +1,32 @@ +import { BaseResponse } from "../common" + +export type GetMusicalDownReuqest = BaseResponse & { + request?: { + [key: string]: string + } + cookie?: string +} + +export type MusicalDownResponse = BaseResponse & { + result?: ContentMusicalDown +} + +export type ContentMusicalDown = { + type: "video" | "image" + author?: AuthorMusicalDown + desc?: string + images?: string[] + videoHD?: string + videoSD?: string + videoWatermark?: string + music?: string +} + +export type AuthorMusicalDown = { + avatar: string + nickname: string +} + +export type GetMusicalDownMusic = BaseResponse & { + result?: string +} diff --git a/src/types/downloader/ssstik.ts b/src/types/downloader/ssstik.ts deleted file mode 100644 index 7ed4413..0000000 --- a/src/types/downloader/ssstik.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BaseResponse, Content, Author, Statistics } from "../common" - -export type SSSTikFetchTT = BaseResponse & { - result?: string -} - -export type SSSTikResponse = BaseResponse & { - result?: Content -} - -export type AuthorSSSTik = Author - -export type StatisticsSSSTik = Statistics diff --git a/src/types/downloader/ssstikDownloader.ts b/src/types/downloader/ssstikDownloader.ts new file mode 100644 index 0000000..9b6015a --- /dev/null +++ b/src/types/downloader/ssstikDownloader.ts @@ -0,0 +1,35 @@ +import { BaseResponse } from "../common" + +export type SSSTikFetchTT = BaseResponse & { + result?: string +} + +export type SSSTikResponse = BaseResponse & { + result?: ResultContentSSSTik +} + +export type ResultContentSSSTik = { + type: "video" | "music" | "image" + desc?: string + author?: AuthorSSSTik + statistics?: StatisticsSSSTik + video?: { + playAddr: string[] + } + music?: { + playUrl: string[] + } + images?: string[] + direct?: string +} + +export type AuthorSSSTik = { + avatar: string + nickname: string +} + +export type StatisticsSSSTik = { + likeCount: string + commentCount: string + shareCount: string +} diff --git a/src/types/downloader/tiktokApi.ts b/src/types/downloader/tiktokApiDownloader.ts similarity index 100% rename from src/types/downloader/tiktokApi.ts rename to src/types/downloader/tiktokApiDownloader.ts diff --git a/src/types/get/getCollection.ts b/src/types/get/getCollection.ts index 56fa278..602abe0 100644 --- a/src/types/get/getCollection.ts +++ b/src/types/get/getCollection.ts @@ -2,7 +2,7 @@ import { StatisticsTiktokAPI, MusicTiktokAPI, VideoTiktokAPI -} from "../downloader/tiktokApi" +} from "../downloader/tiktokApiDownloader" import { PlaylistAuthor } from "./getPlaylist" export interface CollectionItem { diff --git a/src/types/get/getPlaylist.ts b/src/types/get/getPlaylist.ts index dae94ab..352e462 100644 --- a/src/types/get/getPlaylist.ts +++ b/src/types/get/getPlaylist.ts @@ -2,7 +2,7 @@ import { AuthorTiktokAPI, MusicTiktokAPI, VideoTiktokAPI -} from "../downloader/tiktokApi" +} from "../downloader/tiktokApiDownloader" export interface PlaylistAuthor extends Omit { diff --git a/src/utils/downloader/musicalDown.ts b/src/utils/downloader/musicaldownDownloader.ts similarity index 99% rename from src/utils/downloader/musicalDown.ts rename to src/utils/downloader/musicaldownDownloader.ts index 6f6fb42..3a1fb69 100644 --- a/src/utils/downloader/musicalDown.ts +++ b/src/utils/downloader/musicaldownDownloader.ts @@ -3,7 +3,7 @@ import { load } from "cheerio" import { MusicalDownResponse, GetMusicalDownReuqest -} from "../../types/downloader/musicaldown" +} from "../../types/downloader/musicaldownDownloader" import { _musicaldownapi, _musicaldownurl } from "../../constants/api" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" diff --git a/src/utils/downloader/ssstik.ts b/src/utils/downloader/ssstikDownloader.ts similarity index 99% rename from src/utils/downloader/ssstik.ts rename to src/utils/downloader/ssstikDownloader.ts index a765405..09f399b 100644 --- a/src/utils/downloader/ssstik.ts +++ b/src/utils/downloader/ssstikDownloader.ts @@ -7,7 +7,7 @@ import { StatisticsSSSTik, SSSTikFetchTT, SSSTikResponse -} from "../../types/downloader/ssstik" +} from "../../types/downloader/ssstikDownloader" import { _ssstikapi, _ssstikurl } from "../../constants/api" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" diff --git a/src/utils/downloader/tiktokApi.ts b/src/utils/downloader/tiktokApi.ts deleted file mode 100644 index 4a9f90b..0000000 --- a/src/utils/downloader/tiktokApi.ts +++ /dev/null @@ -1,446 +0,0 @@ -import Axios from "axios" -import asyncRetry from "async-retry" -import { - _tiktokvFeed, - _tiktokurl, - _tiktokGetCollection, - _tiktokGetPlaylist -} from "../../constants/api" -import { - _tiktokApiParams, - _getCollectionParams, - _getPlaylistParams -} from "../../constants/params" -import { - AuthorTiktokAPI, - TiktokAPIResponse, - StatisticsTiktokAPI, - MusicTiktokAPI, - ResponseParserTiktokAPI, - VideoTiktokAPI, - TiktokCollectionResponse -} from "../../types/downloader/tiktokApi" -import { HttpsProxyAgent } from "https-proxy-agent" -import { SocksProxyAgent } from "socks-proxy-agent" -import { ERROR_MESSAGES } from "../../constants" -import { TiktokPlaylistResponse } from "../../types/get/getPlaylist" - -/** 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)" -const COLLECTION_URL_REGEX = /collection\/[^/]+-(\d+)/ -const PLAYLIST_URL_REGEX = /playlist\/[^/]+-(\d+)/ - -/** 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 extractVideoId = (responseUrl: string): string | null => { - const matches = responseUrl.match(/\d{17,21}/g) - return matches ? matches[0] : null -} - -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, - uniqueId: 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 => { - try { - const response = await asyncRetry( - async () => { - const res = await Axios( - _tiktokvFeed(_tiktokApiParams({ aweme_id: ID })), - { - method: "OPTIONS", - headers: { "User-Agent": USER_AGENT }, - ...createProxyAgent(proxy) - } - ) - - if (res.data && res.data.status_code === 0) { - return res.data - } - - throw new Error(ERROR_MESSAGES.NETWORK_ERROR) - }, - { - retries: 20, - minTimeout: 200, - maxTimeout: 1000 - } - ) - - return parseTiktokData(ID, response) - } catch (error) { - console.error("Error fetching TikTok data:", error) - return null - } -} - -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 - } -}) - -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 - } -}) - -const handleRedirect = async (url: string, proxy?: string): Promise => { - try { - const response = await Axios(url, { - method: "HEAD", - maxRedirects: 5, - validateStatus: (status) => status >= 200 && status < 400, - ...createProxyAgent(proxy) - }) - - // Get the final URL after all redirects - const finalUrl = response.request.res.responseUrl - - // Remove query parameters - return finalUrl.split("?")[0] - } catch (error) { - console.error("Error handling redirect:", error) - return url - } -} - -export const extractCollectionId = (input: string): string | null => { - // If it's already just a number, return it - if (/^\d+$/.test(input)) { - return input - } - - // Try to extract from URL - const match = input.match(COLLECTION_URL_REGEX) - return match ? match[1] : null -} - -export const extractPlaylistId = (url: string): string | null => { - const match = url.match(PLAYLIST_URL_REGEX) - return match ? match[1] : null -} - -/** - * 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 - } - } -} - -export const Collection = async ( - collectionIdOrUrl: string, - options?: { - page?: number - proxy?: string - count?: number - } -): Promise => { - try { - // Only handle redirects if the input is a URL - const processedUrl = collectionIdOrUrl.startsWith("http") - ? await handleRedirect(collectionIdOrUrl, options?.proxy) - : collectionIdOrUrl - - const collectionId = extractCollectionId(processedUrl) - if (!collectionId) { - return { - status: "error", - message: "Invalid collection ID or URL format" - } - } - - const response = await Axios( - _tiktokGetCollection( - _getCollectionParams(collectionId, options.page, options.count) - ), - { - method: "GET", - headers: { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", - Accept: "*/*", - "Accept-Language": "en-US,en;q=0.7", - Referer: "https://www.tiktok.com/", - Origin: "https://www.tiktok.com" - }, - ...createProxyAgent(options?.proxy) - } - ) - - if (response.data && response.data.status_code === 0) { - const data = response.data - - return { - status: "success", - result: { - itemList: data.itemList || [], - hasMore: data.hasMore - } - } - } - - return { - status: "error", - message: ERROR_MESSAGES.NETWORK_ERROR - } - } catch (error) { - return { - status: "error", - message: - error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR - } - } -} - -export const Playlist = async ( - url: string, - options?: { - page?: number - proxy?: string - count?: number - } -): Promise => { - try { - const processedUrl = url.startsWith("http") - ? await handleRedirect(url, options?.proxy) - : url - - const playlistId = extractPlaylistId(processedUrl) - if (!playlistId) { - return { - status: "error", - message: "Invalid playlist ID or URL format" - } - } - - const response = await Axios( - _tiktokGetPlaylist( - _getPlaylistParams(playlistId, options.page, options.count) - ), - { - method: "GET", - headers: { - "User-Agent": - "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0", - Accept: "*/*", - "Accept-Language": "en-US,en;q=0.7", - Referer: "https://www.tiktok.com/", - Origin: "https://www.tiktok.com" - }, - ...createProxyAgent(options?.proxy) - } - ) - - if (response.data && response.data.status_code === 0) { - const data = response.data - - return { - status: "success", - result: { - itemList: data.itemList || [], - hasMore: data.hasMore, - extra: data.extra - } - } - } - - return { - status: "error", - message: ERROR_MESSAGES.NETWORK_ERROR - } - } catch (error) { - return { - status: "error", - message: - error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR - } - } -} From c83e329f27bd744dd2274604e15e10ec2264e083 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:14:33 +0700 Subject: [PATCH 07/10] feat: moving the function to the function owner's file --- src/utils/downloader/tiktokAPIDownloader.ts | 299 ++++++++++++++++++++ src/utils/get/getCollection.ts | 84 +++++- src/utils/get/getPlaylist.ts | 84 +++++- src/utils/search/videoSearch.ts | 1 - 4 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 src/utils/downloader/tiktokAPIDownloader.ts diff --git a/src/utils/downloader/tiktokAPIDownloader.ts b/src/utils/downloader/tiktokAPIDownloader.ts new file mode 100644 index 0000000..0102b43 --- /dev/null +++ b/src/utils/downloader/tiktokAPIDownloader.ts @@ -0,0 +1,299 @@ +import Axios from "axios" +import asyncRetry from "async-retry" +import { + _tiktokvFeed, + _tiktokurl, + _tiktokGetCollection, + _tiktokGetPlaylist +} from "../../constants/api" +import { + _tiktokApiParams, + _getCollectionParams, + _getPlaylistParams +} from "../../constants/params" +import { + AuthorTiktokAPI, + TiktokAPIResponse, + StatisticsTiktokAPI, + MusicTiktokAPI, + ResponseParserTiktokAPI, + VideoTiktokAPI +} from "../../types/downloader/tiktokApiDownloader" +import { HttpsProxyAgent } from "https-proxy-agent" +import { SocksProxyAgent } from "socks-proxy-agent" +import { ERROR_MESSAGES } from "../../constants" + +/** 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)" + +/** 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 extractVideoId = (responseUrl: string): string | null => { + const matches = responseUrl.match(/\d{17,21}/g) + return matches ? matches[0] : null +} + +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, + uniqueId: 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 => { + try { + const response = await asyncRetry( + async () => { + const res = await Axios( + _tiktokvFeed(_tiktokApiParams({ aweme_id: ID })), + { + method: "OPTIONS", + headers: { "User-Agent": USER_AGENT }, + ...createProxyAgent(proxy) + } + ) + + if (res.data && res.data.status_code === 0) { + return res.data + } + + throw new Error(ERROR_MESSAGES.NETWORK_ERROR) + }, + { + retries: 20, + minTimeout: 200, + maxTimeout: 1000 + } + ) + + return parseTiktokData(ID, response) + } catch (error) { + console.error("Error fetching TikTok data:", error) + return null + } +} + +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 + } +}) + +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 + } +}) + +export const handleRedirect = async ( + url: string, + proxy?: string +): Promise => { + try { + const response = await Axios(url, { + method: "HEAD", + maxRedirects: 5, + validateStatus: (status) => status >= 200 && status < 400, + ...createProxyAgent(proxy) + }) + + // Get the final URL after all redirects + const finalUrl = response.request.res.responseUrl + + // Remove query parameters + return finalUrl.split("?")[0] + } catch (error) { + console.error("Error handling redirect:", error) + return url + } +} + +/** + * 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 + } + } +} diff --git a/src/utils/get/getCollection.ts b/src/utils/get/getCollection.ts index 6fe7858..8479372 100644 --- a/src/utils/get/getCollection.ts +++ b/src/utils/get/getCollection.ts @@ -6,6 +6,10 @@ import { SocksProxyAgent } from "socks-proxy-agent" import { TiktokCollectionResponse } from "../../types/get/getCollection" import { ERROR_MESSAGES } from "../../constants" import retry from "async-retry" +import { handleRedirect } from "../downloader/tiktokAPIDownloader" + +/** Constants */ +const COLLECTION_URL_REGEX = /collection\/[^/]+-(\d+)/ /** Types */ interface ProxyConfig { @@ -44,9 +48,7 @@ export const getCollection = async ( const response = await retry( async () => { const res = await Axios( - _tiktokGetCollection( - _getCollectionParams(collectionId, page, count) - ), + _tiktokGetCollection(_getCollectionParams(collectionId, page, count)), { method: "GET", headers: { @@ -90,3 +92,79 @@ export const getCollection = async ( } } } + +export const Collection = async ( + collectionIdOrUrl: string, + options?: { + page?: number + proxy?: string + count?: number + } +): Promise => { + try { + // Only handle redirects if the input is a URL + const processedUrl = collectionIdOrUrl.startsWith("http") + ? await handleRedirect(collectionIdOrUrl, options?.proxy) + : collectionIdOrUrl + + const collectionId = extractCollectionId(processedUrl) + if (!collectionId) { + return { + status: "error", + message: "Invalid collection ID or URL format" + } + } + + const response = await Axios( + _tiktokGetCollection( + _getCollectionParams(collectionId, options.page, options.count) + ), + { + method: "GET", + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + Accept: "*/*", + "Accept-Language": "en-US,en;q=0.7", + Referer: "https://www.tiktok.com/", + Origin: "https://www.tiktok.com" + }, + ...createProxyAgent(options?.proxy) + } + ) + + if (response.data && response.data.status_code === 0) { + const data = response.data + + return { + status: "success", + result: { + itemList: data.itemList || [], + hasMore: data.hasMore + } + } + } + + return { + status: "error", + message: ERROR_MESSAGES.NETWORK_ERROR + } + } catch (error) { + return { + status: "error", + message: + error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR + } + } +} + +export const extractCollectionId = (input: string): string | null => { + // If it's already just a number, return it + if (/^\d+$/.test(input)) { + return input + } + + // Try to extract from URL + const match = input.match(COLLECTION_URL_REGEX) + return match ? match[1] : null +} diff --git a/src/utils/get/getPlaylist.ts b/src/utils/get/getPlaylist.ts index ee0354e..c91853a 100644 --- a/src/utils/get/getPlaylist.ts +++ b/src/utils/get/getPlaylist.ts @@ -6,6 +6,10 @@ import { SocksProxyAgent } from "socks-proxy-agent" import { ERROR_MESSAGES } from "../../constants" import retry from "async-retry" import { TiktokPlaylistResponse } from "../../types/get/getPlaylist" +import { handleRedirect } from "../downloader/tiktokAPIDownloader" + +/** Constants */ +const PLAYLIST_URL_REGEX = /playlist\/[^/]+-(\d+)/ /** Types */ interface ProxyConfig { @@ -54,7 +58,7 @@ export const getPlaylist = async ( "Accept-Language": "en-US,en;q=0.7", Referer: "https://www.tiktok.com/", Origin: "https://www.tiktok.com", - "Content-Type": "application/json", + "Content-Type": "application/json" }, ...createProxyAgent(proxy) } @@ -78,7 +82,7 @@ export const getPlaylist = async ( result: { hasMore: response.hasMore, itemList: response.itemList || [], - extra: response.extra, + extra: response.extra } } } catch (error) { @@ -89,3 +93,79 @@ export const getPlaylist = async ( } } } + +export const Playlist = async ( + url: string, + options?: { + page?: number + proxy?: string + count?: number + } +): Promise => { + try { + const processedUrl = url.startsWith("http") + ? await handleRedirect(url, options?.proxy) + : url + + const playlistId = extractPlaylistId(processedUrl) + if (!playlistId) { + return { + status: "error", + message: "Invalid playlist ID or URL format" + } + } + + const response = await Axios( + _tiktokGetPlaylist( + _getPlaylistParams(playlistId, options.page, options.count) + ), + { + method: "GET", + headers: { + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0", + Accept: "*/*", + "Accept-Language": "en-US,en;q=0.7", + Referer: "https://www.tiktok.com/", + Origin: "https://www.tiktok.com" + }, + ...createProxyAgent(options?.proxy) + } + ) + + if (response.data && response.data.status_code === 0) { + const data = response.data + + return { + status: "success", + result: { + itemList: data.itemList || [], + hasMore: data.hasMore, + extra: data.extra + } + } + } + + return { + status: "error", + message: ERROR_MESSAGES.NETWORK_ERROR + } + } catch (error) { + return { + status: "error", + message: + error instanceof Error ? error.message : ERROR_MESSAGES.NETWORK_ERROR + } + } +} + +export const extractPlaylistId = (input: string): string | null => { + // If it's already just a number, return it + if (/^\d+$/.test(input)) { + return input + } + + // Try to extract from URL + const match = input.match(PLAYLIST_URL_REGEX) + return match ? match[1] : null +} diff --git a/src/utils/search/videoSearch.ts b/src/utils/search/videoSearch.ts index 14e6211..06c31b3 100644 --- a/src/utils/search/videoSearch.ts +++ b/src/utils/search/videoSearch.ts @@ -3,7 +3,6 @@ import { _tiktokSearchVideoFull } from "../../constants/api" import { _liveSearchParams, _videoSearchParams } from "../../constants/params" import { SocksProxyAgent } from "socks-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent" -import { TiktokService } from "../../services/tiktokService" import retry from "async-retry" import { TiktokVideoSearchResponse, From c7af8d53a99fbbf1bda453a9ee1293bfb7ac6cf4 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:15:50 +0700 Subject: [PATCH 08/10] fix: removing cookie options on tiktok stalk user --- README.md | 2 +- src/utils/get/getProfile.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 28f24f6..047cded 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ tiktokdl search video -p 1 -proxy "http://your-proxy-url" const Tiktok = require("@tobyg74/tiktok-api-dl") const username = "Tobz2k19" -Tiktok.Stalker(username, { +Tiktok.StalkUser(username, { proxy: "YOUR_PROXY" // optional }).then((result) => console.log(result)) ``` diff --git a/src/utils/get/getProfile.ts b/src/utils/get/getProfile.ts index b8f04e9..b5e58da 100644 --- a/src/utils/get/getProfile.ts +++ b/src/utils/get/getProfile.ts @@ -23,7 +23,6 @@ import { SocksProxyAgent } from "socks-proxy-agent" export const StalkUser = ( username: string, - cookie?: string | any[], proxy?: string ): Promise => new Promise(async (resolve) => { @@ -31,10 +30,6 @@ export const StalkUser = ( Axios(`${_tiktokurl}/@${username}`, { method: "GET", headers: { - cookie: - typeof cookie === "object" - ? cookie.map((v: any) => `${v.name}=${v.value}`).join("; ") - : cookie, "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" }, From 4f7cd9f083d1798c18a0cad823b2e9b224893f14 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:16:21 +0700 Subject: [PATCH 09/10] feat: adding some test files --- test/collection-test.ts | 15 +++++---- test/comments-test.ts | 54 +++++++++++++++++++++++++++++++ test/downloader-v1-test.ts | 49 ++++++++++++++++++++++++++++ test/downloader-v2-test.ts | 47 +++++++++++++++++++++++++++ test/downloader-v3-test.ts | 35 ++++++++++++++++++++ test/playlist-test.ts | 48 ++++++++++++++++++++++++++++ test/profile-test.ts | 41 ++++++++++++++++++++++++ test/search-live-test.ts | 42 ++++++++++++++++++++++++ test/search-user-test.ts | 46 +++++++++++++++++++++++++++ test/search-video-test.ts | 53 +++++++++++++++++++++++++++++++ test/userliked-test.ts | 65 ++++++++++++++++++++++++++++++++++++++ test/userposts-test.ts | 55 ++++++++++++++++++++++++++++++++ 12 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 test/comments-test.ts create mode 100644 test/downloader-v1-test.ts create mode 100644 test/downloader-v2-test.ts create mode 100644 test/downloader-v3-test.ts create mode 100644 test/playlist-test.ts create mode 100644 test/profile-test.ts create mode 100644 test/search-live-test.ts create mode 100644 test/search-user-test.ts create mode 100644 test/search-video-test.ts create mode 100644 test/userliked-test.ts create mode 100644 test/userposts-test.ts diff --git a/test/collection-test.ts b/test/collection-test.ts index bc8f832..2c5238a 100644 --- a/test/collection-test.ts +++ b/test/collection-test.ts @@ -1,14 +1,15 @@ -import { Collection } from "../src/utils/downloader/tiktokApi" +import Tiktok from "../src/index" async function testCollection() { try { // You can use either a collection ID or URL const collectionId = "7507916135931218695" - const collectionUrl = "https://www.tiktok.com/@getrex.co.nz/collection/big%20back-7507916135931218695" + const collectionUrl = + "https://www.tiktok.com/@getrex.co.nz/collection/big%20back-7507916135931218695" const collectionShareableLink = "https://vt.tiktok.com/ZShvmqNjQ/" console.log("Testing Collection method...") - const result = await Collection(collectionId, { + const result = await Tiktok.Collection(collectionId, { page: 1, count: 5, // Optional: Number of items to fetch proxy: undefined // Optional: Add your proxy if needed @@ -29,7 +30,9 @@ async function testCollection() { console.log(`ID: ${item.id}`) console.log(`Description: ${item.desc}`) console.log(`Author: ${item.author.nickname}`) - console.log(`Created: ${new Date(item.createTime * 1000).toLocaleString()}`) + console.log( + `Created: ${new Date(item.createTime * 1000).toLocaleString()}` + ) // Log video URL if (item.video?.playAddr?.[0]) { @@ -50,7 +53,7 @@ async function testCollection() { // Log hashtags if available if (item.textExtra?.length > 0) { console.log("\nHashtags:") - item.textExtra.forEach(tag => { + item.textExtra.forEach((tag) => { if (tag.hashtagName) { console.log(`- #${tag.hashtagName}`) } @@ -67,4 +70,4 @@ async function testCollection() { } // Run the test -testCollection() \ No newline at end of file +testCollection() diff --git a/test/comments-test.ts b/test/comments-test.ts new file mode 100644 index 0000000..63c242d --- /dev/null +++ b/test/comments-test.ts @@ -0,0 +1,54 @@ +// Test for Tiktok Video Comments +import Tiktok from "../src/index" + +async function testComments() { + try { + const url = "https://www.tiktok.com/@tobz2k19/video/7451777267107187986" // Change to a valid TikTok video URL + const result = await Tiktok.GetVideoComments(url, { + commentLimit: 10, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("\nComments fetched successfully!") + console.log("========================") + console.log("Comments Overview:") + console.log("========================") + console.log(`Total comments fetched: ${result.result.length}`) + // Log all comments + result.result.forEach((comment, index) => { + console.log(`\nComment ${index + 1}:`) + console.log("-------------------") + console.log(`ID: ${comment.cid}`) + if (comment.user) { + console.log( + `Author: ${comment.user.nickname} (@${comment.user.username})` + ) + console.log(`Verified: ${comment.user.isVerified ? "Yes" : "No"}`) + } + console.log(`Text: ${comment.text}`) + if (comment.createTime) { + console.log( + `Created: ${new Date(comment.createTime * 1000).toLocaleString()}` + ) + } + // Log comment statistics + if (typeof comment.likeCount !== "undefined") { + console.log("\nStatistics:") + console.log(`- Likes: ${comment.likeCount}`) + } + if (typeof comment.replyCommentTotal !== "undefined") { + console.log(`- Replies: ${comment.replyCommentTotal}`) + } + if (comment.isAuthorLiked) console.log("👍 Liked by author") + if (comment.isCommentTranslatable) console.log("🌐 Translatable") + console.log("========================") + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testComments() diff --git a/test/downloader-v1-test.ts b/test/downloader-v1-test.ts new file mode 100644 index 0000000..54e2feb --- /dev/null +++ b/test/downloader-v1-test.ts @@ -0,0 +1,49 @@ +// Test for Tiktok Downloader v1 +import Tiktok from "../src/index" + +async function testDownloaderV1() { + try { + const url = "https://www.tiktok.com/@tobz2k19/video/7451777267107187986" // Change to a valid TikTok video URL + console.log(`\nTesting Downloader version: v1`) + const result = await Tiktok.Downloader(url, { + version: "v1", + proxy: undefined + }) + if (result.status === "success" && result.result) { + const r = result.result + console.log(`Type: ${r.type}`) + console.log(`ID: ${r.id}`) + console.log(`Description: ${r.desc}`) + if (r.author) { + console.log(`Author: ${r.author.nickname}`) + } + if (r.statistics) { + console.log("Statistics:") + console.log(`- Likes: ${r.statistics.likeCount}`) + console.log(`- Comments: ${r.statistics.commentCount}`) + console.log(`- Shares: ${r.statistics.shareCount}`) + console.log(`- Plays: ${r.statistics.playCount}`) + } + if (r.video?.playAddr?.length) { + console.log(`Video URL: ${r.video.playAddr[0]}`) + } + if (r.images?.length) { + console.log(`Images: ${r.images.join(", ")}`) + } + if (r.music) { + console.log(`Music:`) + console.log(`- Title: ${r.music.title}`) + if (r.music.playUrl?.length) { + console.log(`- Music URL: ${r.music.playUrl[0]}`) + } + } + console.log("========================") + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testDownloaderV1() diff --git a/test/downloader-v2-test.ts b/test/downloader-v2-test.ts new file mode 100644 index 0000000..2250103 --- /dev/null +++ b/test/downloader-v2-test.ts @@ -0,0 +1,47 @@ +// Test for Tiktok Downloader v2 +import Tiktok from "../src/index" + +async function testDownloaderV2() { + try { + const url = "https://www.tiktok.com/@tobz2k19/video/7451777267107187986" // Change to a valid TikTok video URL + console.log(`\nTesting Downloader version: v2`) + const result = await Tiktok.Downloader(url, { + version: "v2", + proxy: undefined + }) + if (result.status === "success" && result.result) { + const r = result.result + console.log(`Type: ${r.type}`) + if (r.desc) console.log(`Description: ${r.desc}`) + if (r.author && r.author.nickname) { + console.log(`Author: ${r.author.nickname}`) + } else if (r.author && r.author.avatar) { + // fallback for v2 author structure + console.log(`Author Avatar: ${r.author.avatar}`) + } + if (r.statistics) { + console.log("Statistics:") + if (r.statistics.likeCount !== undefined) + console.log(`- Likes: ${r.statistics.likeCount}`) + if (r.statistics.commentCount !== undefined) + console.log(`- Comments: ${r.statistics.commentCount}`) + if (r.statistics.shareCount !== undefined) + console.log(`- Shares: ${r.statistics.shareCount}`) + } + if (r.video?.playAddr?.length) { + console.log(`Video URL: ${r.video.playAddr[0]}`) + } + if (r.music?.playUrl?.length) { + console.log(`Music URL: ${r.music.playUrl[0]}`) + } + if (r.images?.length) console.log(`Images: ${r.images.join(", ")}`) + console.log("========================") + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testDownloaderV2() diff --git a/test/downloader-v3-test.ts b/test/downloader-v3-test.ts new file mode 100644 index 0000000..0ced1f5 --- /dev/null +++ b/test/downloader-v3-test.ts @@ -0,0 +1,35 @@ +// Test for Tiktok Downloader v3 +import Tiktok from "../src/index" + +async function testDownloaderV3() { + try { + const url = "https://www.tiktok.com/@tobz2k19/video/7451777267107187986" // Change to a valid TikTok video URL + console.log(`\nTesting Downloader version: v3`) + const result = await Tiktok.Downloader(url, { + version: "v3", + proxy: undefined + }) + if (result.status === "success" && result.result) { + const r = result.result + console.log(`Type: ${r.type}`) + if (r.desc) console.log(`Description: ${r.desc}`) + if (r.author && r.author.nickname) { + console.log(`Author: ${r.author.nickname}`) + } else if (r.author && r.author.avatar) { + // fallback for v3 author structure + console.log(`Author Avatar: ${r.author.avatar}`) + } + if (r.videoHD) console.log(`Video HD: ${r.videoHD}`) + if (r.videoWatermark) console.log(`Video Watermark: ${r.videoWatermark}`) + if (r.images?.length) console.log(`Images: ${r.images.join(", ")}`) + if (r.music) console.log(`Music: ${r.music}`) + console.log("========================") + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testDownloaderV3() diff --git a/test/playlist-test.ts b/test/playlist-test.ts new file mode 100644 index 0000000..e4c108b --- /dev/null +++ b/test/playlist-test.ts @@ -0,0 +1,48 @@ +// Test for Tiktok Playlist +import Tiktok from "../src/index" + +async function testPlaylist() { + try { + const playlistUrl = + "https://www.tiktok.com/@tobz2k19/playlist/tset-7511644672511626004" // Ganti dengan URL playlist yang valid jika perlu + console.log(`\nTesting Playlist: ${playlistUrl}`) + const result = await Tiktok.Playlist(playlistUrl, { + proxy: undefined, + page: 1, + count: 5 + }) + if (result.status === "success" && result.result) { + const { itemList, hasMore, extra } = result.result + console.log(`Total Videos: ${itemList.length}`) + itemList.forEach((item, idx) => { + console.log(`\n[${idx + 1}] ID: ${item.id}`) + console.log(`Description: ${item.desc}`) + if (item.author) { + console.log(`Author: ${item.author.nickname}`) + } + if (item.stats) { + console.log("Statistics:") + console.log(`- Likes: ${item.stats.diggCount}`) + console.log(`- Comments: ${item.stats.commentCount}`) + console.log(`- Shares: ${item.stats.shareCount}`) + console.log(`- Plays: ${item.stats.playCount}`) + } + if (item.video?.playAddr?.length) { + console.log(`Video URL: ${item.video.playAddr}`) + } + }) + console.log("========================") + if (hasMore) { + console.log( + "There are more videos. Use the 'page' option to fetch next page." + ) + } + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testPlaylist() diff --git a/test/profile-test.ts b/test/profile-test.ts new file mode 100644 index 0000000..08d24cf --- /dev/null +++ b/test/profile-test.ts @@ -0,0 +1,41 @@ +// Test for Tiktok Stalk User Profile +import Tiktok from "../src/index" + +async function testProfile() { + try { + const username = "tobz2k19" // Change to a valid TikTok username + const result = await Tiktok.StalkUser(username, { + proxy: undefined + }) + if (result.status === "success" && result.result) { + const user = result.result.user + const stats = result.result.stats + console.log("\nProfile fetched successfully!") + console.log("========================") + console.log("User Profile:") + console.log("========================") + console.log(`Username: @${user.username}`) + console.log(`Nickname: ${user.nickname}`) + console.log(`Signature: ${user.signature}`) + console.log(`Verified: ${user.verified ? "Yes" : "No"}`) + console.log(`Region: ${user.region}`) + console.log(`Private Account: ${user.privateAccount ? "Yes" : "No"}`) + console.log(`Commerce User: ${user.commerceUser ? "Yes" : "No"}`) + console.log(`Avatar: ${user.avatarLarger}`) + console.log("\nStats:") + console.log(`- Followers: ${stats.followerCount}`) + console.log(`- Following: ${stats.followingCount}`) + console.log(`- Hearts: ${stats.heartCount}`) + console.log(`- Videos: ${stats.videoCount}`) + console.log(`- Likes: ${stats.likeCount}`) + console.log(`- Friends: ${stats.friendCount}`) + console.log("========================") + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testProfile() diff --git a/test/search-live-test.ts b/test/search-live-test.ts new file mode 100644 index 0000000..18fcfe2 --- /dev/null +++ b/test/search-live-test.ts @@ -0,0 +1,42 @@ +// Test for Tiktok Search Live +import Tiktok from "../src/index" + +async function testSearchLive() { + try { + const keyword = "call of duty" // Change to a valid search keyword + const cookie = "" // Optional: provide a valid TikTok cookie if needed + console.log(`\nTesting Search type: live`) + const result = await Tiktok.Search(keyword, { + type: "live", + cookie, + page: 1, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("Success! Parsed Result:") + result.result.forEach((item, index) => { + if (item.type === "live") { + const live = item as typeof item & { liveInfo: any } + if (live.liveInfo) { + console.log(`\nResult ${index + 1}:`) + console.log("-------------------") + console.log(`ID: ${live.liveInfo.id}`) + console.log(`Title: ${live.liveInfo.title}`) + console.log(`Hashtag: ${live.liveInfo.hashtag}`) + if (live.liveInfo.owner) + console.log(`Owner: ${live.liveInfo.owner.nickname}`) + if (live.liveInfo.stats) + console.log(`Viewers: ${live.liveInfo.stats.viewerCount}`) + console.log("========================") + } + } + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testSearchLive() diff --git a/test/search-user-test.ts b/test/search-user-test.ts new file mode 100644 index 0000000..27e184a --- /dev/null +++ b/test/search-user-test.ts @@ -0,0 +1,46 @@ +// Test for Tiktok Search User +import Tiktok from "../src/index" + +async function testSearchUser() { + try { + const keyword = "call of duty" // Change to a valid search keyword + const cookie = "" // Optional: provide a valid TikTok cookie if needed + console.log(`\nTesting Search type: user`) + const result = await Tiktok.Search(keyword, { + type: "user", + cookie, + page: 1, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("Success! Parsed Result:") + result.result.forEach((item, index) => { + if (item.type === "user") { + const user = item as typeof item & { + uid: string + username: string + nickname: string + followerCount: number + isVerified: boolean + url: string + } + console.log(`\nResult ${index + 1}:`) + console.log("-------------------") + console.log(`UID: ${user.uid}`) + console.log(`Username: ${user.username}`) + console.log(`Nickname: ${user.nickname}`) + console.log(`Followers: ${user.followerCount}`) + console.log(`Verified: ${user.isVerified ? "Yes" : "No"}`) + console.log(`Profile URL: ${user.url}`) + console.log("========================") + } + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testSearchUser() diff --git a/test/search-video-test.ts b/test/search-video-test.ts new file mode 100644 index 0000000..1940e17 --- /dev/null +++ b/test/search-video-test.ts @@ -0,0 +1,53 @@ +// Test for Tiktok Search Video +import Tiktok from "../src/index" + +async function testSearchVideo() { + try { + const keyword = "call of duty" // Change to a valid search keyword + const cookie = "" // Optional: provide a valid TikTok cookie if needed + console.log(`\nTesting Search type: video`) + const result = await Tiktok.Search(keyword, { + type: "video", + cookie, + page: 1, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("Success! Parsed Result:") + result.result.forEach((item, index) => { + if (item.type === "video") { + const video = item as typeof item & { + id: string + desc: string + author: any + createTime: number + stats: any + } + console.log(`\nResult ${index + 1}:`) + console.log("-------------------") + console.log(`ID: ${video.id}`) + console.log(`Description: ${video.desc}`) + if (video.author) console.log(`Author: ${video.author.nickname}`) + if (video.createTime) + console.log( + `Created: ${new Date(video.createTime * 1000).toLocaleString()}` + ) + if (video.stats) { + console.log("Statistics:") + console.log(`- Likes: ${video.stats.likeCount}`) + console.log(`- Comments: ${video.stats.commentCount}`) + console.log(`- Shares: ${video.stats.shareCount}`) + console.log(`- Plays: ${video.stats.playCount}`) + } + console.log("========================") + } + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testSearchVideo() diff --git a/test/userliked-test.ts b/test/userliked-test.ts new file mode 100644 index 0000000..c027f4c --- /dev/null +++ b/test/userliked-test.ts @@ -0,0 +1,65 @@ +// Test for Tiktok Get User Liked Videos +import Tiktok from "../src/index" + +async function testUserLiked() { + try { + const username = "Tobz2k19" // Change to a valid TikTok username + const cookie = "" // Optional: provide a valid TikTok cookie if needed + const result = await Tiktok.GetUserLiked(username, { + cookie, + postLimit: 5, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("\nUser Liked Videos fetched successfully!") + console.log("========================") + console.log("Liked Videos Overview:") + console.log("========================") + console.log(`Total liked videos fetched: ${result.result.length}`) + result.result.forEach((liked, index) => { + console.log(`\nLiked Video ${index + 1}:`) + console.log("-------------------") + console.log(`ID: ${liked.id}`) + console.log(`Description: ${liked.desc}`) + if (liked.author) { + console.log( + `Author: ${liked.author.nickname} (@${liked.author.username})` + ) + } + if (liked.createTime) { + console.log( + `Created: ${new Date( + Number(liked.createTime) * 1000 + ).toLocaleString()}` + ) + } + if (liked.stats) { + console.log("Statistics:") + console.log(`- Likes: ${liked.stats.diggCount}`) + console.log(`- Favorites: ${liked.stats.collectCount}`) + console.log(`- Comments: ${liked.stats.commentCount}`) + console.log(`- Shares: ${liked.stats.shareCount}`) + console.log(`- Plays: ${liked.stats.playCount}`) + console.log(`- Reposts: ${liked.stats.repostCount}`) + } + if (liked.video?.playAddr) { + console.log(`Video URL: ${liked.video.playAddr}`) + } + if (liked.imagePost?.length) { + console.log( + `Images: \n${liked.imagePost + .map((img) => img.images) + .join("\n - ")}` + ) + } + console.log("========================") + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testUserLiked() diff --git a/test/userposts-test.ts b/test/userposts-test.ts new file mode 100644 index 0000000..0083f2b --- /dev/null +++ b/test/userposts-test.ts @@ -0,0 +1,55 @@ +// Test for Tiktok Get User Posts +import Tiktok from "../src/index" + +async function testUserPosts() { + try { + const username = "Tobz2k19" // Change to a valid TikTok username + const result = await Tiktok.GetUserPosts(username, { + postLimit: 5, + proxy: undefined + }) + if (result.status === "success" && result.result) { + console.log("\nUser Posts fetched successfully!") + console.log("========================") + console.log("Posts Overview:") + console.log("========================") + console.log(`Total posts fetched: ${result.result.length}`) + result.result.forEach((post, index) => { + console.log(`\nPost ${index + 1}:`) + console.log("-------------------") + console.log(`ID: ${post.id}`) + console.log(`Description: ${post.desc}`) + if (post.author) { + console.log( + `Author: ${post.author.nickname} (@${post.author.username})` + ) + } + if (post.createTime) { + console.log( + `Created: ${new Date(post.createTime * 1000).toLocaleString()}` + ) + } + if (post.stats) { + console.log("Statistics:") + console.log(`- Likes: ${post.stats.likeCount}`) + console.log(`- Comments: ${post.stats.commentCount}`) + console.log(`- Shares: ${post.stats.shareCount}`) + console.log(`- Plays: ${post.stats.playCount}`) + } + if (post.video?.playAddr) { + console.log(`Video URL: ${post.video.playAddr}`) + } + if (post.imagePost?.length) { + console.log(`Images: ${post.imagePost.join(", ")}`) + } + console.log("========================") + }) + } else { + console.error("Error:", result.message) + } + } catch (error) { + console.error("Test failed:", error) + } +} + +testUserPosts() From 4d7b32ceef254cea125970a2cd6c53c0e30052eb Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Tue, 3 Jun 2025 17:25:25 +0700 Subject: [PATCH 10/10] docs: update changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd8e2c..7e9b466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -371,3 +371,27 @@ ### Changed - Update Documentation [4260ea6](https://github.com/TobyG74/tiktok-api-dl/commit/4260ea653ee50569f898cc0653cb35e4557992a9) + +### Version 1.3.2 - 03-06-2025 + +### Added + +- Add Tiktok Collection [5bd743a](https://github.com/TobyG74/tiktok-api-dl/pull/42/commits/5bd743a888cfafe932f083a2f887dbdd98e99d0c) +- Add Tiktok Collection Documentation [a37640e](https://github.com/TobyG74/tiktok-api-dl/pull/42/commits/a37640e332a43827bca8599881c931097d07256e) +- Add Tiktok Collection Types [baa8fa2](https://github.com/TobyG74/tiktok-api-dl/pull/42/commits/baa8fa2cc8d1bcc7aabc7fdef5a93677fed10be5) +- Add Tiktok Playlist [2fa9e6f](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/2fa9e6fef414c2825cf3072655998e1322210062) +- Add Tiktok Playlist Documentation [d0b1f2c](https://github.com/TobyG74/tiktok-api-dl/commit/db1686fc9b0480402db7bd6bd0c41f899ff56668) +- Add a New CLI Downloader for Tiktok Collection & Playlist [ae53775](https://github.com/TobyG74/tiktok-api-dl/commit/ae537757d8fa274e52060f445d4822c617f9ac93) +- Add Some Test Files [4f7cd9f](https://github.com/TobyG74/tiktok-api-dl/commit/4f7cd9f083d1798c18a0cad823b2e9b224893f14) + +### Fixed + +- Repair Musicaldown Broken Logics [732c0d4](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/732c0d4c146d7ed743b5902fbd392717e2df5692) +- Fix Downloader Types [1ad6d8b](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/1ad6d8baea7c2ac45ffc6cccd089ce193d17f491) +- Fix Handle Downloader Function [a56bc8c](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/a56bc8c9d9bedd42305c92a7bb15edb3ecad390a) +- Fix Duplicate Types [9d53637](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/9d53637ac267568fc0f2d3c4556dd2367b4cbf19) + +### Changed + +- Moving the Function to the Function Owner's File [c83e329](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/c83e329f27bd744dd2274604e15e10ec2264e083) +- Removing Cookie Options on Tiktok Stalk User [c7af8d5](https://github.com/TobyG74/tiktok-api-dl/pull/45/commits/c7af8d53a99fbbf1bda453a9ee1293bfb7ac6cf4)