├── LICENSE ├── README.md ├── esm └── index.mjs ├── examples ├── discord.js └── download.js ├── index.js ├── package.json ├── src ├── Structures │ └── Util.js └── TikTok.js └── test ├── TikTok.mp4 └── index.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Snowflake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TikTok Search 2 | Simple module to fetch data from TikTok. 3 | 4 | # Installing 5 | 6 | ```sh 7 | npm install tiktok-search 8 | ``` 9 | 10 | # Getting Started 11 | - Fetch song data 12 | 13 | ```js 14 | const TikTok = require("tiktok-search"); 15 | 16 | TikTok.getInfo("https://www.tiktok.com/@scout2015/video/6718335390845095173") 17 | .then(console.log); 18 | 19 | ``` 20 | 21 | # API 22 | ## getInfo(video_url) 23 | Returns video info. 24 | 25 | ## getUser(username) 26 | Returns User info. 27 | 28 | ## getEmbed(embedURL) 29 | Returns embed data. 30 | 31 | ## validateURL(url) 32 | Validates TikTok url. 33 | 34 | # Example 35 | ## Downloading TikTok Video 36 | 37 | ```js 38 | const TikTok = require("tiktok-search"); 39 | const fs = require("fs"); 40 | 41 | TikTok.download("https://vm.tiktok.com/ZMJrea3bs/") 42 | .then(res => { 43 | res.pipe(fs.createWriteStream(`./song.mp4`)); 44 | }); 45 | 46 | ``` 47 | 48 | # Responses 49 | ## Song Info 50 | 51 | ```js 52 | { 53 | id: '6718335390845095173', 54 | title: 'Scout and Suki on TikTok', 55 | description: 'Scramble up ur name & I’ll try to guess it�❤️ #foryoupage #petsoftiktok #aesthetic', 56 | url: 'https://www.tiktok.com/@scout2015/video/6718335390845095173', 57 | embedURL: 'https://www.tiktok.com/oembed?url=https://www.tiktok.com/@scout2015/video/6718335390845095173', 58 | thumbnail: { 59 | url: 'https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/06kv6rfcesljdjr45ukb0000d844090v0200000a05?x-expires=1597762800&x-signature=7tCHAFoBKVNGGQ6nUtVIar8LVcM%3D', 60 | size: { width: 720, height: 1280 } 61 | }, 62 | country: 'NP', 63 | streamURL: 'https://v16m.tiktokcdn.com/6faf787cbfcd5c681717b08adf975e5e/5f3bfa67/video/tos/useast2a/tos-useast2a-ve-0068/15fbafb086324317bf77a649580b1f95/?a=1233&br=4778&bt=2389&cr=0&cs=0&dr=0&ds=3&er=&l=2020081615572701011511924012B98AD5&lr=tiktok_m&mime_type=video_mp4&qs=0&rc=M245aWhvZ3U4bjMzZzczM0ApOTtmOzdoaDtnNzM5aTo1ZGczc29gcGdnMXJfLS01MTZzczI2L2FiLWFeLzI0MmJhYV86Yw%3D%3D&vl=&vr=', 64 | streams: [ 65 | 'https://v16m.tiktokcdn.com/6faf787cbfcd5c681717b08adf975e5e/5f3bfa67/video/tos/useast2a/tos-useast2a-ve-0068/15fbafb086324317bf77a649580b1f95/?a=1233&br=4778&bt=2389&cr=0&cs=0&dr=0&ds=3&er=&l=2020081615572701011511924012B98AD5&lr=tiktok_m&mime_type=video_mp4&qs=0&rc=M245aWhvZ3U4bjMzZzczM0ApOTtmOzdoaDtnNzM5aTo1ZGczc29gcGdnMXJfLS01MTZzczI2L2FiLWFeLzI0MmJhYV86Yw%3D%3D&vl=&vr=' 66 | ], 67 | videoDetails: { width: 720, height: 1280, ratio: 10, duration: 10 }, 68 | duration: 10000, 69 | covers: [ 70 | 'https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/06kv6rfcesljdjr45ukb0000d844090v0200000a05?x-expires=1597762800&x-signature=7tCHAFoBKVNGGQ6nUtVIar8LVcM%3D' 71 | ], 72 | comments: 8996, 73 | views: 144200, 74 | createTime: '1564234358', 75 | digg: 33700, 76 | shares: 796, 77 | author: { 78 | verified: true, 79 | secUid: 'MS4wLjABAAAAPZWNdkF_cmVSPCvV0Y6MCsH29mlAOlMOX3ikzGvlfMm3K6OMZv-JrRImpEHxdIMI', 80 | username: 'scout2015', 81 | id: '53279706535428096', 82 | displayName: 'Scout and Suki', 83 | covers: [ 84 | 'https://p16-amd-va.tiktokcdn.com/img/musically-maliva-obj/3c3deb8e39fe38fd856147bc5c598ba9~c5_100x100.jpeg', 85 | 'https://p16-tiktok-va-h2.ibyteimg.com/img/musically-maliva-obj/3c3deb8e39fe38fd856147bc5c598ba9~c5_100x100.jpeg' 86 | ], 87 | profile: 'https://www.tiktok.com/@scout2015', 88 | followers: 3700000, 89 | hearts: '54800000' 90 | }, 91 | challenge: { id: null, name: null }, 92 | music: { 93 | id: '6689804660171082501', 94 | name: 'original sound', 95 | author: '�������', 96 | covers: [ 97 | 'https://p16-amd-va.tiktokcdn.com/img/musically-maliva-obj/adf0c5e7b9ee237c29c4350fa892167a~c5_100x100.jpeg', 98 | 'https://p16-tiktok-va-h2.ibyteimg.com/img/musically-maliva-obj/adf0c5e7b9ee237c29c4350fa892167a~c5_100x100.jpeg' 99 | ] 100 | }, 101 | keywords: [ 102 | 'Scout and Suki', 'scout2015', 103 | 'foryoupage', 'PetsOfTikTok', 104 | 'aesthetic', 'bonevoyage', 105 | 'TikTok', 'ティックトック', 106 | 'tik tok', 'tick tock', 107 | 'tic tok', 'tic toc', 108 | 'tictok', 'тик ток', 109 | 'ticktock' 110 | ] 111 | } 112 | 113 | ``` 114 | 115 | ## Embed 116 | 117 | ```js 118 | { 119 | version: '1.0', 120 | type: 'video', 121 | title: 'Scramble up ur name & I’ll try to guess it�❤️ #foryoupage #petsoftiktok #aesthetic', 122 | author: { 123 | username: 'Scout and Suki', 124 | profile: 'https://www.tiktok.com/@scout2015' 125 | }, 126 | scale: { width: '100%', height: '100%' }, 127 | html: '
@scout2015

Scramble up ur name & I’ll try to guess it�❤️ #foryoupage #petsoftiktok #aesthetic

♬ original sound - �������
', 128 | thumbnail: { 129 | url: 'https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/06kv6rfcesljdjr45ukb0000d844090v0200000a05?x-expires=1597762800&x-signature=7tCHAFoBKVNGGQ6nUtVIar8LVcM%3D', 130 | height: 1280, 131 | width: 720 132 | }, 133 | provider: { name: 'TikTok', url: 'https://www.tiktok.com' } 134 | } 135 | 136 | ``` 137 | 138 | # User Profile 139 | 140 | ```js 141 | { 142 | id: '53279706535428096', 143 | username: 'scout2015', 144 | displayName: 'Scout and Suki', 145 | title: 'Scout and Suki on TikTok', 146 | description: '@scout2015 3.0m Followers, 1168 Following, 54.0m Likes - Watch awesome short videos created by Scout and Suki', 147 | profile: 'https://www.tiktok.com/@scout2015?lang=en', 148 | avatars: { 149 | thumbnail: 'https://p16-amd-va.tiktokcdn.com/img/musically-maliva-obj/3c3deb8e39fe38fd856147bc5c598ba9~c5_100x100.jpeg', 150 | medium: 'https://p16-amd-va.tiktokcdn.com/img/musically-maliva-obj/3c3deb8e39fe38fd856147bc5c598ba9~c5_720x720.jpeg' 151 | }, 152 | signature: '�3.0M Pawsitive Pack Memebers�\n' + 153 | '� @julyjackplayz �\n' + 154 | '��50% off pet plate��', 155 | verified: true, 156 | private: false, 157 | secUid: 'MS4wLjABAAAAPZWNdkF_cmVSPCvV0Y6MCsH29mlAOlMOX3ikzGvlfMm3K6OMZv-JrRImpEHxdIMI', 158 | following: 1168, 159 | followers: 3700000, 160 | hearts: '54800000', 161 | videos: 1644, 162 | digg: 0 163 | } 164 | 165 | ``` 166 | 167 | # Join my discord 168 | **[https://discord.gg/2SUybzb](https://discord.gg/2SUybzb)** -------------------------------------------------------------------------------- /esm/index.mjs: -------------------------------------------------------------------------------- 1 | import TikTok from "../index.js"; 2 | 3 | export default TikTok; 4 | export const { 5 | download, 6 | getEmbed, 7 | getInfo, 8 | getUser, 9 | validateURL 10 | } = TikTok; -------------------------------------------------------------------------------- /examples/discord.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a basic example on playing tiktok videos over discord. 3 | For this, you need to install FFmpeg, discord.js & @discordjs/opus. 4 | */ 5 | 6 | const Discord = require("discord.js"); 7 | const client = new Discord.Client(); 8 | const TikTok = require("tiktok-search"); 9 | 10 | client.on("ready", () => { 11 | console.log("Bot is online!"); 12 | }); 13 | 14 | client.on("message", async message => { 15 | if (message.author.bot || !message.guild) return; 16 | const prefix = "!"; 17 | if (!message.content.startsWith(prefix)) return; 18 | const args = message.content.slice(prefix.length).trim().split(" "); 19 | const command = args.shift().toLowerCase(); 20 | 21 | if (command === "ping") { 22 | if (!message.member.voice.channel) return message.reply("Join a voice channel!"); 23 | const song = args[0]; 24 | if (!TikTok.validateURL(song)) return message.channel.send("Provide a valid tiktok url."); 25 | 26 | const video = await TikTok.getInfo(song); 27 | if (!video) return message.channel.send("Invalid url."); 28 | 29 | const stream = await TikTok.download(song); 30 | 31 | message.member.voice.channel.join() 32 | .then(connection => { 33 | connection.play(stream) 34 | .on("start", () => { 35 | message.channel.send(`Started playing **${video.title}**.`); 36 | }) 37 | .on("finish", () => { 38 | message.guild.me.voice.channel.leave(); 39 | message.channel.send("Song ended!"); 40 | }) 41 | }); 42 | } 43 | }); 44 | 45 | client.login("SOME_REAL_TOKEN"); -------------------------------------------------------------------------------- /examples/download.js: -------------------------------------------------------------------------------- 1 | const TikTok = require("../index.js"); 2 | const fs = require("fs"); 3 | 4 | TikTok.download("TIKTOK_VIDEO_URL") 5 | .then(stream => stream.pipe(fs.createWriteStream("./TikTok.mp4"))); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src/TikTok.js"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiktok-search", 3 | "version": "2.0.3", 4 | "description": "Simple module to fetch data from TikTok.", 5 | "main": "index.js", 6 | "exports": { 7 | ".": [ 8 | { 9 | "require": "./index.js", 10 | "import": "./esm/index.mjs" 11 | }, 12 | "./index.js" 13 | ], 14 | "./esm": "./esm/index.mjs" 15 | }, 16 | "scripts": { 17 | "test": "cd test && node index.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Snowflake107/TikTok-Search.git" 22 | }, 23 | "keywords": [ 24 | "tiktok", 25 | "tiktok-search", 26 | "tiktok-scraper", 27 | "tiktok-api", 28 | "tiktok-downloader", 29 | "tiktok-video-downloader", 30 | "tiktok-signature", 31 | "tiktok-info" 32 | ], 33 | "author": "Snowflake107", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/Snowflake107/TikTok-Search/issues" 37 | }, 38 | "homepage": "https://github.com/Snowflake107/TikTok-Search#readme", 39 | "dependencies": { 40 | "cheerio": "^1.0.0-rc.3", 41 | "node-fetch": "^2.6.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Structures/Util.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const cheerio = require("cheerio"); 3 | const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3738.0 Safari/537.36"; 4 | const REGEX = /^https?:\/\/(www\.|vm\.)?(tiktok\.com)\/?(.*)$/; 5 | 6 | class Util { 7 | 8 | /** 9 | * Parses plain html of a given url 10 | * @param {string} url Input url 11 | * @returns {Promise} 12 | */ 13 | static async html(url) { 14 | try { 15 | let res = await fetch(url, { 16 | headers: { 17 | "User-Agent": USER_AGENT 18 | } 19 | }); 20 | let plainText = await res.text(); 21 | return plainText; 22 | } catch(e) { 23 | return ""; 24 | } 25 | } 26 | 27 | /** 28 | * JQuery 29 | * @param {string} html Input html 30 | */ 31 | static getDocument(html) { 32 | return cheerio.load(html); 33 | } 34 | 35 | /** 36 | * Parses tiktok video details 37 | * @param {string} raw raw data 38 | */ 39 | static parseVideoData(raw) { 40 | if (!raw) return null; 41 | try { 42 | const data = JSON.parse(raw); 43 | const BASE = data.props.initialProps; 44 | const author = data.props.pageProps.videoData.authorInfos; 45 | const authorStats = data.props.pageProps.videoData.authorStats; 46 | const music = data.props.pageProps.videoData.musicInfos; 47 | 48 | const obj = { 49 | id: data.props.pageProps.videoData.itemInfos.id, 50 | title: data.props.pageProps.shareMeta.title, 51 | description: data.props.pageProps.shareMeta.desc, 52 | url: `https://${BASE["$host"]}${BASE["$pageUrl"]}`, 53 | embedURL: `https://www.tiktok.com/oembed?url=https://${BASE["$host"]}${BASE["$pageUrl"]}`, 54 | thumbnail: { 55 | url: data.props.pageProps.shareMeta.image.url, 56 | size: { 57 | width: data.props.pageProps.shareMeta.image.width, 58 | height: data.props.pageProps.shareMeta.image.height 59 | } 60 | }, 61 | country: BASE["$region"], 62 | streamURL: data.props.pageProps.videoData.itemInfos.video.urls[0] || null, 63 | streams: data.props.pageProps.videoData.itemInfos.video.urls, 64 | videoDetails: data.props.pageProps.videoData.itemInfos.video.videoMeta, 65 | duration: data.props.pageProps.videoData.itemInfos.video.videoMeta.duration * 1000, 66 | covers: data.props.pageProps.videoData.itemInfos.covers, 67 | comments: data.props.pageProps.videoData.itemInfos.commentCount, 68 | views: data.props.pageProps.videoData.itemInfos.playCount, 69 | createTime: data.props.pageProps.videoData.itemInfos.createTime, 70 | digg: data.props.pageProps.videoData.itemInfos.diggCount, 71 | shares: data.props.pageProps.videoData.itemInfos.shareCount, 72 | author: { 73 | verified: author.verified, 74 | secUid: author.secUid, 75 | username: author.uniqueId, 76 | id: author.userId, 77 | displayName: author.nickName, 78 | covers: author.covers, 79 | profile: `https://${BASE["$host"]}/@${author.uniqueId}`, 80 | followers: authorStats.followerCount, 81 | hearts: authorStats.heartCount 82 | }, 83 | challenge: { 84 | id: data.props.pageProps.videoData.challengeInfoList.challengeId || null, 85 | name: data.props.pageProps.videoData.challengeInfoList.challengeName || null 86 | }, 87 | music: { 88 | id: music.musicId, 89 | name: music.musicName, 90 | author: music.authorName, 91 | covers: music.covers 92 | }, 93 | keywords: data.props.pageProps.metaParams.keywords.split(",").map(m => m.trim()) 94 | }; 95 | 96 | return obj; 97 | 98 | } catch(e) { 99 | return null; 100 | } 101 | } 102 | 103 | static parseUserData(raw) { 104 | if (!raw) return null; 105 | try { 106 | const data = JSON.parse(raw); 107 | const BASE = data.props.pageProps; 108 | 109 | const obj = { 110 | id: BASE.userInfo.user.id, 111 | username: BASE.uniqueId, 112 | displayName: BASE.userInfo.user.nickname, 113 | title: BASE.shareMeta.title, 114 | description: BASE.shareMeta.desc, 115 | profile: BASE.metaParams.canonicalHref, 116 | avatars: { 117 | thumbnail: BASE.userInfo.user.avatarThumb, 118 | medium: BASE.userInfo.user.avatarMedium 119 | }, 120 | signature: BASE.userInfo.user.signature, 121 | verified: BASE.userInfo.user.verified, 122 | private: BASE.userInfo.user.secret, 123 | secUid: BASE.userInfo.user.secUid, 124 | following: BASE.userInfo.stats.followingCount, 125 | followers: BASE.userInfo.stats.followerCount, 126 | hearts: BASE.userInfo.stats.heartCount, 127 | videos: BASE.userInfo.stats.videoCount, 128 | digg: BASE.userInfo.stats.diggCount, 129 | }; 130 | 131 | return obj; 132 | 133 | } catch (e) { 134 | return null; 135 | } 136 | } 137 | 138 | /** 139 | * Parses embed 140 | * @param {string} url Embed url to parse 141 | */ 142 | static async parseEmbed(url) { 143 | try { 144 | let data = await fetch(url); 145 | let res = await data.json(); 146 | 147 | let obj = { 148 | version: res.version, 149 | type: res.type, 150 | title: res.title, 151 | author: { 152 | username: res.author_name, 153 | profile: res.author_url 154 | }, 155 | scale: { 156 | width: res.width, 157 | height: res.height 158 | }, 159 | html: res.html, 160 | thumbnail: { 161 | url: res.thumbnail_url, 162 | height: res.thumbnail_height, 163 | width: res.thumbnail_width 164 | }, 165 | provider: { 166 | name: res.provider_name, 167 | url: res.provider_url 168 | } 169 | }; 170 | 171 | return obj; 172 | } catch(e) { 173 | return null; 174 | } 175 | } 176 | 177 | /** 178 | * Validates TikTok URL 179 | * @param {string} url url to validate 180 | */ 181 | static validate(url) { 182 | if (!url || typeof url !== "string") return false; 183 | return REGEX.test(url); 184 | } 185 | 186 | } 187 | 188 | module.exports = Util; -------------------------------------------------------------------------------- /src/TikTok.js: -------------------------------------------------------------------------------- 1 | const Util = require("./Structures/Util.js"); 2 | const https = require("https"); 3 | 4 | /** 5 | * Validates tiktok url 6 | * @param {string} url Url to validate 7 | */ 8 | function validateURL(url) { 9 | return Util.validate(url); 10 | } 11 | 12 | /** 13 | * Parses TikTok video info 14 | * @param {string} url TikTok video url 15 | */ 16 | async function getInfo(url) { 17 | if (!validateURL(url)) throw new Error("Invalid url"); 18 | const html = await Util.html(url); 19 | if (!html) return null; 20 | const $ = Util.getDocument(html); 21 | const rawJSON = $("#__NEXT_DATA__")[0].children[0].data; 22 | return Util.parseVideoData(rawJSON); 23 | } 24 | 25 | /** 26 | * Parses TikTok user data 27 | * @param {string} username Username of a TikToker 28 | */ 29 | async function getUser(username) { 30 | const html = await Util.html(`https://www.tiktok.com/@${username}`); 31 | if (!html) return null; 32 | const $ = Util.getDocument(html); 33 | const rawJSON = $("#__NEXT_DATA__")[0].children[0].data; 34 | return Util.parseUserData(rawJSON); 35 | } 36 | 37 | /** 38 | * Fetches embed 39 | * @param {string} url Embed url to fetch 40 | */ 41 | async function getEmbed(url) { 42 | if (!url) throw new Error("Invalid url"); 43 | return await Util.parseEmbed(url); 44 | } 45 | 46 | /** 47 | * Downloads tiktok video 48 | * @param {string} url TikTok video url 49 | */ 50 | function download(url) { 51 | return new Promise(async (resolve, reject) => { 52 | try { 53 | const data = await getInfo(url); 54 | if (!data || !data.streamURL) return reject(new Error("Couldn't resolve stream.")); 55 | https.get(data.streamURL, res => resolve(res)); 56 | } catch(e) { 57 | reject("Couldn't resolve stream."); 58 | } 59 | }); 60 | 61 | } 62 | 63 | module.exports = { 64 | download, 65 | validateURL, 66 | getInfo, 67 | getUser, 68 | getEmbed 69 | }; -------------------------------------------------------------------------------- /test/TikTok.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twlite/TikTok-Search/4b01d735895ecf1c500d5b8456bec120cda0ed7e/test/TikTok.mp4 -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const TikTok = require("../index.js"); 2 | const fs = require("fs"); 3 | 4 | TikTok.download("https://www.tiktok.com/@twinny__girls/video/6861609989497277697") 5 | .then(stream => stream.pipe(fs.createWriteStream("./TikTok.mp4"))); --------------------------------------------------------------------------------