├── .gitignore ├── .env.example ├── package.json ├── README.md ├── LICENSE ├── lib ├── utils.js ├── signer.js └── tt-signature.js └── dump-account.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | node_modules/ 4 | downloads/* 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # TikTok Cookies 2 | COOKIES="" 3 | 4 | # Browser User-Agent 5 | USERAGENT="" 6 | 7 | # Also download thumbnails (normal and animated) 8 | THUMBNAILS="false" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiktok-scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "tik", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.5.1", 13 | "canvas": "^2.11.2", 14 | "dotenv": "^16.3.1", 15 | "httpreq": "^1.1.1", 16 | "jsdom": "^22.1.0" 17 | }, 18 | "volta": { 19 | "node": "18.18.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TikTok Scripts 2 | 3 | ## Environment Setup 4 | - Scripts require `Node.js` (Tested with `LTS v18.18.0`) 5 | - Install from [nodejs.org](https://nodejs.org/en/download) 6 | - OR Install with [Volta (A modern NVM alternative)](https://volta.sh/) 7 | - Install dependencies with `npm` or `yarn` 8 | - `npm install` 9 | - Proceed to run scripts 10 | --- 11 | ## Download Private Accounts 12 | 13 | - Move `.env.example` to `.env` 14 | - Edit the values in `.env` 15 | - `COOKIES`: The cookies from your account which follows the private user 16 | - `USERAGENT`: The 'User-Agent' of the browser thats logged into the account 17 | - `THUMBNAILS`: Download the default and animated thumbnails for videos 18 | - Always set to `true` if submitting to Tik.fail 19 | - Run `dump-account.js` 20 | - Usage: `node dump-account.js ` 21 | - Example: `node dump-account.js poki` 22 | - The following contents will be saved to `./downloads/username/` using the video ID as the filenames 23 | - **Videos**: Unwatermarked video (+thumbnails if enabled) 24 | - **Slideshows**: Unwatermarked images 25 | - **All**: Metadata JSON 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 tik.fail 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 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const axios = require('axios') 4 | 5 | exports.downloadMedia = async (url, savePath, cookies = '') => { 6 | const response = await axios.get(url, { 7 | headers: { 8 | 'Accept': '*/*', 9 | 'Accept-Language': 'en-US,en;q=0.9', 10 | 'Cache-Control': 'no-cache', 11 | 'Pragma': 'no-cache', 12 | 'Range': 'bytes=0-', 13 | 'Sec-Ch-Ua': '"Not.A/Brand";v="8", "Chromium";v="114"', 14 | 'Sec-Ch-Ua-Mobile': '?0', 15 | 'Sec-Ch-Ua-Platform': '"Windows"', 16 | 'Sec-Fetch-Dest': 'video', 17 | 'Sec-Fetch-Mode': 'cors', 18 | 'Sec-Fetch-Site': 'same-site', 19 | 'Cookie': cookies, 20 | 'Referer': 'https://www.tiktok.com/', 21 | 'Referrer-Policy': 'strict-origin-when-cross-origin' 22 | }, 23 | responseType: 'stream', 24 | }) 25 | 26 | const filePath = path.join(__dirname, `./../${savePath}`) 27 | 28 | const writer = fs.createWriteStream(filePath) 29 | response.data.pipe(writer) 30 | 31 | return new Promise((resolve, reject) => { 32 | writer.on('finish', resolve) 33 | writer.on('error', reject) 34 | }) 35 | } 36 | 37 | exports.sleep = (sec = 1) => { 38 | return new Promise(resolve => { 39 | return setTimeout(resolve, sec * 1000) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /lib/signer.js: -------------------------------------------------------------------------------- 1 | // Modified from https://github.com/pablouser1/SignTok 2 | // License: MIT 3 | require('dotenv').config({ path: '.env' }) 4 | const fs = require('fs') 5 | const { JSDOM, ResourceLoader } = require('jsdom') 6 | const { createCipheriv } = require('crypto') 7 | 8 | const UA = process.env.USERAGENT 9 | 10 | class Signer { 11 | static DEFAULT_USERAGENT = UA 12 | static PASSWORD = 'webapp1.0+202106' 13 | /** 14 | * @type Window 15 | */ 16 | window = null 17 | 18 | constructor(userAgent = Signer.DEFAULT_USERAGENT) { 19 | const signature_js = fs.readFileSync('./lib/tt-signature.js', 'utf-8') 20 | const webmssdk = fs.readFileSync('./lib/tt-webmssdk.js', 'utf-8') 21 | const resourceLoader = new ResourceLoader({ userAgent }) 22 | 23 | const { window } = new JSDOM('', { 24 | url: 'https://www.tiktok.com', 25 | referrer: 'https://www.tiktok.com', 26 | contentType: 'text/html', 27 | includeNodeLocations: false, 28 | runScripts: 'outside-only', 29 | pretendToBeVisual: true, 30 | resources: resourceLoader 31 | }) 32 | this.window = window 33 | this.window.eval(signature_js.toString()) 34 | this.window.byted_acrawler.init({ 35 | aid: 24, 36 | dfp: true 37 | }) 38 | this.window.eval(webmssdk) 39 | } 40 | 41 | navigator() { 42 | return { 43 | deviceScaleFactor: this.window.devicePixelRatio, 44 | user_agent: this.window.navigator.userAgent, 45 | browser_language: this.window.navigator.language, 46 | browser_platform: this.window.navigator.platform, 47 | browser_name: this.window.navigator.appCodeName, 48 | browser_version: this.window.navigator.appVersion 49 | } 50 | } 51 | 52 | signature(url) { 53 | return this.window.byted_acrawler.sign({ url }) 54 | } 55 | 56 | bogus(params) { 57 | return this.window._0x32d649(params) 58 | } 59 | 60 | xttparams(params) { 61 | params += '&verifyFp=undefined' 62 | params += '&is_encryption=1' 63 | const cipher = createCipheriv('aes-128-cbc', Signer.PASSWORD, Signer.PASSWORD) 64 | return Buffer.concat([ cipher.update(params), cipher.final() ]).toString('base64') 65 | } 66 | 67 | sign(url_str) { 68 | const url = new URL(url_str) 69 | const signature = this.signature(url.toString()) 70 | url.searchParams.append('_signature', signature) 71 | const bogus = this.bogus(url.searchParams.toString()) 72 | url.searchParams.append('X-Bogus', bogus) 73 | const xttparams = this.xttparams(url.searchParams.toString()) 74 | return { 75 | 'signature': signature, 76 | 'signed_url': url.toString(), 77 | 'x-tt-params': xttparams, 78 | 'X-Bogus': bogus 79 | } 80 | } 81 | } 82 | 83 | module.exports = Signer 84 | -------------------------------------------------------------------------------- /dump-account.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | require('dotenv').config({ path: '.env' }) 3 | const fs = require('fs') 4 | const path = require('path') 5 | const axios = require('axios') 6 | 7 | // const Signer = require('./lib/signer') 8 | 9 | const { downloadMedia, sleep } = require('./lib/utils') 10 | 11 | const UA = process.env.USERAGENT 12 | const COOKIES = process.env.COOKIES 13 | 14 | const precheck = async (username) => { 15 | if (!COOKIES) { 16 | console.log('[warn] No cookies have been supplied') 17 | console.log('[warn] Press Ctrl+C and edit .env if you need cookies') 18 | await sleep(3) 19 | } 20 | 21 | if (!fs.existsSync(path.join(__dirname, 'downloads/'))) fs.mkdirSync(path.join(__dirname, 'downloads/')) 22 | if (!fs.existsSync(path.join(__dirname, `downloads/${username}/`))) fs.mkdirSync(path.join(__dirname, `downloads/${username}/`)) 23 | } 24 | 25 | const dumpUserPosts = async (username, cursor = 0) => { 26 | // const signer = new Signer(UA) 27 | let secuid = username 28 | 29 | if (cursor === 0) { 30 | cursor = new Date().getTime() 31 | precheck(username) 32 | } 33 | 34 | // eslint-disable-next-line no-negated-condition 35 | if (!username.startsWith('MS4wLjABAAAA')) { 36 | const userAxiosConfig = { 37 | method: 'GET', 38 | url: `https://api.tik.fail/v2/search/user?q=${username}`, 39 | headers: { 40 | 'User-Agent': 'TikTokScripts/dump-user (https://github.com/tikfail/tiktok-scripts)' 41 | }, 42 | responseType: 'json', 43 | responseEncoding: 'utf8' 44 | } 45 | const { data } = await axios(userAxiosConfig) 46 | const meta = data.data[0] 47 | secuid = meta.sec_uid 48 | console.log(meta) 49 | 50 | if (!data.success || !secuid.startsWith('MS4wLjABAAAA')) { 51 | console.log('[error] Couldn\'t get user sec_uid') 52 | console.log(meta) 53 | process.exit(1) 54 | } 55 | } 56 | 57 | const tiktokAxiosConfig = { 58 | method: 'GET', 59 | url: 'https://www.tiktok.com/api/creator/item_list/', 60 | headers: { 61 | 'Origin': 'https://www.tiktok.com', 62 | 'User-Agent': UA, 63 | 'Cookie': COOKIES 64 | }, 65 | params: { 66 | aid: 1988, 67 | count: 15, 68 | cursor: cursor, 69 | secUid: secuid, 70 | type: 1, 71 | verifyFp: 'verify_' 72 | }, 73 | responseType: 'json', 74 | responseEncoding: 'utf8' 75 | } 76 | 77 | const { data } = await axios(tiktokAxiosConfig) 78 | 79 | if (!data.itemList) { 80 | console.log(`[warn] Got hasMore but itemList is empty | ${cursor}`) 81 | console.log(data) 82 | return process.exit(1) 83 | } 84 | 85 | const total = data.itemList.length 86 | const nextCursor = Math.floor(Number(data.itemList.at(-1).createTime) * 1000) 87 | 88 | console.log(`[----] Got ${total} Total Items | hasMore: ${data.hasMorePrevious}`) 89 | 90 | for (let i = 0; i < total; ++i) { 91 | const v = data.itemList[i] 92 | const vid = v.id 93 | 94 | // Write Metadata 95 | fs.writeFileSync(path.join(__dirname, `downloads/${v.author.uniqueId}/${vid}.json`), JSON.stringify(v, null, 2)) 96 | 97 | // Slideshows 98 | if (v.imagePost) { 99 | const totalImages = v.imagePost.images.length 100 | for (let img = 0; img < totalImages; img++) { 101 | const image = v.imagePost.images[img].imageURL.urlList[0] 102 | await downloadMedia(image, `downloads/${v.author.uniqueId}/${vid}-${String(img + 1).padStart(2, '0')}.jpg`, COOKIES) 103 | } 104 | console.log(`[image] ${vid} - ${totalImages} Images`) 105 | continue 106 | } 107 | 108 | // Video 109 | try { 110 | await downloadMedia(v.video.playAddr, `downloads/${v.author.uniqueId}/${vid}.mp4`, COOKIES) 111 | console.log(`[video] ${vid}`) 112 | } catch (error) { 113 | console.log(`[error] Failed to download video: ${vid}`) 114 | console.log(error) 115 | } 116 | 117 | // Video Thumbnails 118 | if (process.env.THUMBNAILS) { 119 | await downloadMedia(v.video.dynamicCover, `downloads/${v.author.uniqueId}/${vid}.webp`, COOKIES) 120 | await downloadMedia(v.video.cover, `downloads/${v.author.uniqueId}/${vid}.jpg`, COOKIES) 121 | } 122 | } 123 | 124 | 125 | if (data.hasMorePrevious) { 126 | console.log(`=========================[ Cursor: ${cursor} --> ${nextCursor} | hasMore: ${data.hasMorePrevious} ]=========================`) 127 | return dumpUserPosts(secuid, nextCursor) 128 | } 129 | 130 | console.log('[info] Done') 131 | return process.exit(0) 132 | } 133 | 134 | if (require.main === module) { 135 | if (process.argv[2]) { 136 | dumpUserPosts(process.argv[2]) 137 | } else { 138 | console.log('[error] Usage: node dump-account.sh ') 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/tt-signature.js: -------------------------------------------------------------------------------- 1 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(f){return typeof f}:function(f){return f&&"function"==typeof Symbol&&f.constructor===Symbol&&f!==Symbol.prototype?"symbol":typeof f};TAC=function(){function f(f,a,b,d,c,r){null==r&&(r=this);var n,i,o={},l=o.d=c?c.d+1:0;for(o["$"+l]=o,i=0;i>1)&255}return d}null==v&&(v=this);var g,C,x,I,S=[],A=0;y&&(g=y);for(var w=o+2*l;o>=2))){if(0==(z>>=2))return[1,S[A--]];if(2==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],S[++A]=+I;else if(4==z)g=S[A--],S[A]=S[A]*g;else if(6==z)g=S[A--],S[A]=S[A]!=g;else if(13==z)C=S[A--],x=S[A--],(I=S[A--]).x===e?S[++A]=f(r,I.pc,I.len,C,I.z,x):S[++A]=I.apply(x,C);else{if(15!=z)break;oprand=n(r,o),I=oprand[1],S[A]=function(a,b){var d=function e(){return f(r,e.pc,e.len,arguments,e.z,this)};return d.pc=o+6,d.len=b,d.x=e,d.z=t,d}(0,I-4),o+=2*I-2}}else if(1==(3&z))if(3==(z>>=2))g=S[--A],S[A]=g(S[A+1]);else if(5==z)S[A-=1]=S[A][S[A+1]];else if(7==z)S[A]=--S[A];else{if(9!=z)break;g=S[A--],S[A]=typeof g}else if(2==(3&z))if(6==(z>>=2))S[A]=u(S[A]);else if(8==z)g=S[A--],oprand=n(r,o),o+=2*oprand[0],S[A--][m(a[oprand[1]],oprand[1])]=g;else{if(10!=z){if(12==z)throw S[A--];break}S[A]=~S[A]}else if(0==(z>>=2))S[++A]=null;else if(2==z)g=S[A--],S[A]=S[A]>=g;else if(9==z)g=k(),C=k(),t[0]=65599*t[0]+t[g].charCodeAt(C)>>>0;else if(11==z)S[++A]=void 0;else{if(13!=z)break;g=S[A--],S[A]=S[A]&&g}else if(1==(3&z))if(0==(3&(z>>=2))){if(4==(z>>=2)){oprand=n(r,o),I=oprand[1];try{if(d[c][2]=1,1==(g=e(r,o+6,I-4,t,v))[0])return g}catch(y){if(d[c]&&d[c][1]&&1==(g=e(r,d[c][1][0],d[c][1][1],t,v,y))[0])return g}finally{if(d[c]&&d[c][0]&&1==(g=e(r,d[c][0][0],d[c][0][1],t,v))[0])return g;d[c]=0,c--}o+=2*I-2}else if(6==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],S[A-=I]=p("x,y","return new x[y]("+Array(I+1).join(",x[++y]").substr(1)+")")(S,A);else if(8==z)g=S[A--],S[A]=S[A]&g;else if(10!=z)break}else if(1==(3&z))if(0==(z>>=2))S[A]=!S[A];else if(7==z)C=S[A--],g=delete S[A--][C];else if(9==z)oprand=n(r,o),o+=2*oprand[0],S[A]=S[A][m(a[oprand[1]],oprand[1])];else{if(11!=z)break;g=S[A--],S[A]=S[A]<>=2))S[++A]=g;else if(3==z)g=S[A--],S[A]=S[A]<=g;else if(10==z)g=S[A-=2][S[A+1]]=S[A+2],A--;else if(12==z)g=S[A],S[++A]=g;else{if(14!=z)break;g=S[A--],S[A]=S[A]||g}else if(0==(z>>=2))S[A]=!S[A];else if(2==z)oprand=n(r,o),o+=2*(I=oprand[1])-2;else if(4==z)g=S[A--],S[A]=S[A]/g;else if(6==z)g=S[A--],S[A]=S[A]!==g;else{if(13!=z)break;S[++A]=v}else if(2==(3&z))if(0==(3&(z>>=2)))if(1==(z>>=2))g=S[A--],S[A]=S[A]>g;else if(8==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],C=A+1,S[A-=I-1]=I?S.slice(A,C):[];else if(10==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],g=S[A--],t[I]=g;else{if(12!=z)break;g=S[A--],S[A]=S[A]>>g}else if(1==(3&z))if(0==(z>>=2))S[++A]=s;else if(2==z)g=S[A--],S[A]=S[A]+g;else if(4==z)g=S[A--],S[A]=S[A]==g;else if(11==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],S[--A]=p("x,y","return x "+m(a[I],I)+" y")(S[A],S[A+1]);else{if(13!=z)break;g=S[A-1],C=S[A],S[++A]=g,S[++A]=C}else if(2==(3&z))if(1==(z>>=2))oprand=n(r,o),o+=2*oprand[0],S[++A]=m(a[oprand[1]],oprand[1]);else if(3==z)S[A--]?o+=6:(oprand=n(r,o),o+=2*(I=oprand[1])-2);else if(5==z)g=S[A--],S[A]=S[A]%g;else if(7==z)g=S[A--],S[A]=S[A]instanceof g;else{if(14!=z)break;S[++A]=!1}else if(4==(z>>=2))oprand=n(r,o),I=oprand[1],d[c][0]&&!d[c][2]?d[c][1]=[o+6,I-4]:d[c++]=[0,[o+6,I-4],0],o+=2*I-2;else if(6==z)oprand=n(r,o),o+=2*oprand[0],I=oprand[1],S[++A]=t["$"+I];else{if(8!=z)break;g=S[A--],S[A]=S[A]|g}else if(0==(3&(z>>=2)))if(1==(z>>=2))oprand=n(r,o),o+=2*oprand[0],I=oprand[1],S[++A]=+m(a[I],I);else if(3==z)g=S[A--],S[A]=S[A]-g;else if(5==z)g=S[A--],S[A]=S[A]===g;else if(12==z)C=S[A--],x=S[A--],(I=S[A--]).x===e?S[++A]=f(r,I.pc,I.len,C,I.z,x):S[++A]=I.apply(x,C);else{if(14!=z)break;g=S[A],S[A]=S[A-1],S[A-1]=g}else if(1==(3&z))if(2==(z>>=2))h(function(f){var e=0,a=f.length;return function(){var b=e>=2));else if(7==z)g=S[A--];else if(9==z)g=S[A--],S[A]=S[A]^g;else{if(11!=z)break;oprand=n(r,o),I=oprand[1],d[++c]=[[o+6,I-4],0,0],o+=2*I-2}else if(1==(z>>=2))g=S[A--],S[A]=S[A]>>g}}return[0,null]}var a=[],b=0,d=[],c=0,r=function(f,e){var a=""+f[e++]+f[e];return parseInt(a,16)},n=function(f,e){var a=f[e++],b=f[e],d=parseInt(""+a+b,16);if(d>>7==0)return d>>6!=0&&(d=-64|63&d),[1,d];if(d>>6==2){var c=parseInt(""+f[++e]+f[++e],16);return 0!=(32&d)?d=-32|31&d:d&=31,[2,c=(d<<=8)+c]}if(d>>6==3){var r=parseInt(""+f[++e]+f[++e],16),n=parseInt(""+f[++e]+f[++e],16);return 0!=(32&d)?d=-32|31&d:d&=31,[3,n=(d<<=16)+(r<<=8)+n]}},i=function(f,e){var a=f[e++],b=f[e];return parseInt(""+a+b,16)},o=function(f,e){var a=""+f[e++]+f[e];return a=parseInt(a,16),String.fromCharCode(a)},l=function(f,e,a){for(var b="",d=0;d