├── .prettierrc ├── utils.js ├── history.js ├── package.json ├── .github └── workflows │ └── npm-publish.yml ├── downloadVideo.js ├── videoInfo.js ├── downloadSlideshow.js ├── .gitignore ├── README.md ├── downloadSounds.js └── index.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import slugify from 'slugify'; 2 | import chalk from 'chalk'; 3 | 4 | export function cleanFileName(filename) { 5 | let fixed = slugify(filename, { 6 | remove: /[/\\?%*:|"<>]/g, 7 | locale: 'en', 8 | }); 9 | return fixed; 10 | } 11 | -------------------------------------------------------------------------------- /history.js: -------------------------------------------------------------------------------- 1 | import { open } from 'node:fs/promises' 2 | import chalk from 'chalk'; 3 | 4 | export async function openHistory() { 5 | const historyFile = await open('./history.txt', 'a+'); 6 | var history = []; 7 | for await (const line of historyFile.readLines()) { 8 | history.push(line); 9 | } 10 | if (history.length > 0) { 11 | console.log(chalk.cyan('Read ' + history.length + ' lines from history file.')); 12 | 13 | } 14 | 15 | return history; 16 | } 17 | 18 | //module.exports = { openHistory }; 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tikfav", 3 | "version": "1.5.0", 4 | "type": "module", 5 | "description": "Download your tiktok favorites using the user data download files from tiktok", 6 | "main": "index.js", 7 | "bin": { 8 | "tikfav": "./index.js" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "", 14 | "license": "AGPL-3.0-or-later", 15 | "dependencies": { 16 | "chalk": "^5.2.0", 17 | "commander": "^10.0.0", 18 | "dotenv": "^16.0.3", 19 | "node-fetch": "^3.3.1", 20 | "slugify": "^1.6.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | 20 | publish-npm: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm ci 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | -------------------------------------------------------------------------------- /downloadVideo.js: -------------------------------------------------------------------------------- 1 | import { cleanFileName } from './utils.js'; 2 | import fetch from 'node-fetch'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import { pipeline } from 'stream/promises'; 6 | 7 | export async function downloadVideo(dlFolder, responseData, date) { 8 | 9 | let vidURL = responseData.data.hdplay; 10 | let author = cleanFileName(responseData.data.author.unique_id); 11 | let createTime = responseData.data.create_time; 12 | let videoID = responseData.data.id; 13 | 14 | //START video download 15 | // parameters: dlFolder, responseData, 16 | let videoFile; 17 | try { 18 | //fetch the video .MP4 from CDN 19 | videoFile = await fetch(vidURL); 20 | } catch (error) { 21 | console.log(chalk.redBright('Error downloading video:')); 22 | console.log(chalk.red(error)); 23 | return -1; 24 | } 25 | 26 | try { 27 | //set filename and create a WriteStream 28 | // ${vidDate} 29 | let filename = `${dlFolder}/${date}_${author}_${videoID}.mp4`; 30 | let file = fs.createWriteStream(filename); 31 | //write the response body to a file 32 | file.on('finish', () => { 33 | file.close(); 34 | }); 35 | await pipeline(videoFile.body, file); 36 | return 0; 37 | } catch (error) { 38 | console.log(chalk.redBright('Error writing file to disk.')); 39 | console.log(error); 40 | } 41 | } -------------------------------------------------------------------------------- /videoInfo.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import fetch from 'node-fetch'; 3 | 4 | //fetch video info from API 5 | export async function getVideoData(url, apiKey) { 6 | const options = { 7 | method: 'POST', 8 | headers: { 9 | 'content-type': 'application/x-www-form-urlencoded', 10 | 'X-RapidAPI-Key': apiKey, 11 | 'X-RapidAPI-Host': 'tiktok-video-no-watermark2.p.rapidapi.com', 12 | }, 13 | }; 14 | 15 | // add the url to the query parameters 16 | const encodedParams = new URLSearchParams(); 17 | encodedParams.append('url', url); 18 | encodedParams.append('hd', '1'); 19 | // copy the options object with our API key and add the parameters as the body 20 | let fetchOptions = options; 21 | fetchOptions.body = encodedParams; 22 | 23 | // Make POST request using fetch, get JSON from response, and return the data 24 | const response = await fetch( 25 | 'https://tiktok-video-no-watermark2.p.rapidapi.com/', 26 | fetchOptions 27 | ); 28 | try { 29 | var responseData = await response.json(); 30 | } catch (error) { 31 | console.error('fetch error') 32 | } 33 | 34 | // Log response status, calling function will handle errors 35 | if (process.env.NODE_ENV === 'development') { 36 | console.log(responseData); 37 | console.log( 38 | chalk.white('Got metadata with HTTP response ' + response.status) 39 | ); 40 | } 41 | return responseData; 42 | } 43 | 44 | //fetch sound info from API 45 | export async function getSoundData(url, apiKey) { 46 | const options = { 47 | method: 'GET', 48 | headers: { 49 | 'X-RapidAPI-Key': apiKey, 50 | 'X-RapidAPI-Host': 'tiktok-video-no-watermark2.p.rapidapi.com', 51 | }, 52 | }; 53 | 54 | let searchURL = 'https://tiktok-video-no-watermark2.p.rapidapi.com/music/info?url=' + url; 55 | const response = await fetch(searchURL, options); 56 | try { 57 | var responseData = await response.json(); 58 | } catch (error) { 59 | console.error("Couldn't parse response data") 60 | } 61 | 62 | // Log response status 63 | if (process.env.NODE_ENV === 'development') { 64 | console.log(responseData); 65 | console.log( 66 | chalk.white('Got metadata with HTTP response ' + response.status) 67 | ); 68 | } 69 | 70 | return responseData; 71 | 72 | } -------------------------------------------------------------------------------- /downloadSlideshow.js: -------------------------------------------------------------------------------- 1 | import { cleanFileName } from './utils.js'; 2 | import chalk from 'chalk'; 3 | import { pipeline } from 'stream/promises'; 4 | import fs from 'fs'; 5 | 6 | export async function downloadSlideshow(dlFolder, responseData, date) { 7 | let soundURL = responseData.data.music; 8 | let photoURLs = responseData.data.images; //array of photo urls 9 | let author = cleanFileName(responseData.data.author.unique_id); 10 | let path = `${dlFolder}/${date}_${author}_${responseData.data.id}/`; 11 | 12 | //download photos 13 | let photos = []; 14 | try { 15 | for (let url of photoURLs) { 16 | let p = await fetch(url); 17 | photos.push(p); 18 | } 19 | } catch (error) { 20 | console.log(chalk.redBright('Error downloading photos:')); 21 | console.log(chalk.red(error)); 22 | return -1; 23 | } 24 | //create subfolder 25 | try { 26 | if (!fs.existsSync(path)) { 27 | fs.mkdirSync(path); 28 | } 29 | } catch (error) { 30 | console.log(chalk.red('Error creating slideshow download folder')); 31 | console.log(error); 32 | } 33 | 34 | //save downloaded photos 35 | try { 36 | for (let i = 0; i < photos.length; i++) { 37 | let filename = path + `${i + 1}.jpg`; 38 | let file = fs.createWriteStream(filename); 39 | //close file when finished writing 40 | file.on('finish', () => { 41 | console.log( 42 | chalk.greenBright(`Saved Photo!`) 43 | ); 44 | file.close(); 45 | }); 46 | //write data to file 47 | console.log(chalk.blue(`Downloading photo ${i}/${photos.length}`)); 48 | await pipeline(photos[i].body, file); 49 | } 50 | } catch (error) { 51 | console.log(chalk.red('Error writing photos to disk:')); 52 | console.log(error); 53 | return -1; 54 | } 55 | 56 | //Download and save slideshow audio 57 | let sound; 58 | try { 59 | sound = await fetch(soundURL); 60 | } catch (e) { 61 | console.log(chalk.red('Error downloading slideshow music:')); 62 | console.log(e); 63 | } 64 | try { 65 | let filename = path + 'music.mp3'; 66 | let file = fs.createWriteStream(filename); 67 | file.on('finish', () => { 68 | console.log(chalk.greenBright('Saved Slideshow Music!')); 69 | file.close(); 70 | }); 71 | await pipeline(sound.body, file); 72 | return 0; 73 | } catch (e) { 74 | console.log(chalk.red('Error saving music to disk: ')); 75 | console.log(e); 76 | return -1; 77 | } 78 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # TikTok user data files 133 | user_data.json 134 | test.json 135 | 136 | *.mp4 137 | .vscode 138 | history.txt 139 | 140 | tiktok-downloads 141 | .DS_Store 142 | data2.json 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TikFav 2 | 3 | ![npm](https://img.shields.io/npm/v/tikfav) 4 | 5 | ### Scared of losing your favorited TikToks? Switching accounts and want to archive videos you saved on your old one? Use this tool to save all your favorited Tiktoks using the data download file you can get from TikTok. Videos are downloaded in 1080p with no watermarks! 6 | 7 | This tool uses a third-party API to get direct MP4 links to TikTok videos. It currently 8 | supports [this RapidAPI service](https://rapidapi.com/yi005/api/tiktok-video-no-watermark2) which returns metadata with 9 | watermark-free, 1080p MP4 links. The downside is that this API only allows 150 requests per month and 1 request per 10 | second unless you pay $12/month for a premium plan with 600k requests/month. 11 | If there's another API that you'd like me to integrate pls create an issue. 12 | 13 | # Prerequisites 14 | 15 | ### Tiktok Data Download 16 | 17 | To use this tool you'll need to download your user data from Tiktok by going to Settings -> Account -> Download your 18 | data and requesting a JSON download. This request will take a few days to process before you can download the data. 19 | 20 | ### Video Download API Key 21 | 22 | Sign up for either a free or paid plan here: https://rapidapi.com/yi005/api/tiktok-video-no-watermark2 23 | You'll need to pass this key to the app with the -k option at runtime. 24 | 25 | # Installation 26 | 27 | `npm install -g tikfav` 28 | 29 | # Usage 30 | 31 | `tikfav -k xxxxxxxxxxxxx -u user_data.json favorites` 32 | 33 | Run `tikfav` followed by the command you want to run. It will look for a data file called `user_data.json` by default. 34 | If yours has a different name, or resides in a different directroy, specify the path with the `-u` option. You also need 35 | to give it your RapidAPI key with the `-k` option. See above for instructions about the API. 36 | 37 | ### Commands 38 | 39 | `favorites` download the videos in your Favorites list 40 | `liked` download the videos in your Liked list 41 | `sounds` download the sounds in your Favorites list 42 | `shared` download videos you shared 43 | `history` download videos from your browsing history 44 | 45 | ### Options 46 | 47 | `-k` your RapidAPI key, REQUIRED 48 | `-u` path to user data file, default is ./user_data.json 49 | 50 | TikFav saves the url's of videos you download to a file called `history.txt`. This is useful if you want to periodically 51 | request a data download from tiktok and only download videos you don't already have. 52 | 53 | Videos are downloaded to a subfolder called `tiktok-downloads` in whichever directory you run the app. 54 | 55 | # Upcoming Features 56 | 57 | - [x] download Tiktok sounds and videos from your share history 58 | - [x] download shared videos from direct message history 59 | - [ ] migrate to typescript 60 | - [ ] release single file executable for easier installation 61 | 62 | # Known Issues 63 | 64 | - slow downloads and high RAM usage when running on WSL 65 | -------------------------------------------------------------------------------- /downloadSounds.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import chalk from 'chalk'; 3 | import { openHistory } from './history.js'; 4 | import fs from 'fs'; 5 | import { program } from 'commander'; 6 | import { getSoundData } from './videoInfo.js'; 7 | import { pipeline } from 'stream/promises'; 8 | import { setTimeout } from 'timers/promises'; 9 | import { cleanFileName } from './utils.js'; 10 | 11 | export async function downloadSounds(list, apiKey) { 12 | let history = await openHistory(); 13 | 14 | let dlFolder = './tiktok-downloads/sounds'; 15 | //create the download foler if it doesn't exist 16 | try { 17 | if (!fs.existsSync(dlFolder)) { 18 | fs.mkdirSync(dlFolder, { recursive: true }, (err) => { 19 | if (err) throw err; 20 | }); 21 | } 22 | } catch (error) { 23 | console.log(error); 24 | program.error( 25 | "Couldn't create download directories. Please make sure you have permission to write to this folder." 26 | ); 27 | } 28 | 29 | var writeHistory = fs.createWriteStream('history.txt', { flags: 'a' }); 30 | 31 | let DLCount = 0; 32 | 33 | for (let i = 0; i < list.length; i++) { 34 | let qLength = list.length; 35 | let sound = list[i]; 36 | 37 | let soundURL = sound.Link; 38 | let og_Date = sound.Date; 39 | let soundDate = og_Date.replace(/:/g, ''); 40 | if (history.indexOf(soundURL) != -1) { 41 | console.log(chalk.magenta('Sound was found in history file, skipping.')); 42 | continue; 43 | } 44 | 45 | console.log(chalk.green('Getting sound metadata for: ' + soundURL)); 46 | 47 | var responseData = await getSoundData(soundURL, apiKey); 48 | 49 | await setTimeout(250); 50 | 51 | if (responseData.code != 0) { 52 | if ((responseData.code = -1)) { 53 | console.log( 54 | chalk.red("Couldn't get data for this URL, sound may be deleted") 55 | ); 56 | } else { 57 | console.log( 58 | chalk.red('Error getting sound metadata for URL ' + favoriteURL) 59 | ); 60 | } 61 | continue; 62 | } 63 | 64 | //extract info from the API response data 65 | let soundMP3 = responseData.data.play; 66 | let author = cleanFileName(responseData.data.author); 67 | let title = cleanFileName(responseData.data.title); 68 | let soundID = responseData.data.id; 69 | 70 | //check for unavailable sound 71 | if (!soundMP3) { 72 | console.log(chalk.red('This sound is unavailable, skipping.')); 73 | continue; 74 | } 75 | 76 | // fetch MP3 file 77 | let soundFile = await fetch(soundMP3); 78 | 79 | try { 80 | // FILENAMING SCHEME 81 | let filename = `${dlFolder}/${soundDate}_${title}_${author}_${soundID}.mp3`; 82 | let file = fs.createWriteStream(filename); 83 | 84 | file.on('finish', () => { 85 | console.log( 86 | chalk.greenBright(`Finished downloading sound ` + soundURL) 87 | ); 88 | file.close(); 89 | }); 90 | console.log(chalk.blue(`Downloading sound ${i}/${qLength}...`)); 91 | await pipeline(soundFile.body, file); 92 | } catch (error) { 93 | console.error(chalk.red('Error saving file to disk\n' + error)); 94 | } 95 | 96 | writeHistory.write('\n' + soundURL); 97 | DLCount++; 98 | } 99 | 100 | console.log(chalk.greenBright('Saved ' + DLCount + ' sounds. Goodbye.')); 101 | } 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { Command, Option } from 'commander'; 4 | import chalk from 'chalk'; 5 | import fetch from 'node-fetch'; 6 | import { readFileSync } from 'fs'; 7 | import fs from 'fs'; 8 | import { pipeline } from 'stream/promises'; 9 | import { openHistory } from './history.js'; 10 | import { setTimeout } from 'timers/promises'; 11 | import { getVideoData, getSoundData } from './videoInfo.js'; 12 | import packageInfo from './package.json' with { type: 'json' }; 13 | import { downloadSounds } from './downloadSounds.js'; 14 | import { cleanFileName } from './utils.js'; 15 | import 'dotenv/config'; 16 | import { downloadSlideshow } from './downloadSlideshow.js'; 17 | import { downloadVideo } from './downloadVideo.js'; 18 | //get version info from package.json 19 | const version = packageInfo.version; 20 | //commander setup 21 | const program = new Command(); 22 | program 23 | .version(version) 24 | .name('tikfav') 25 | .description( 26 | 'Downloader utility that downloads your favorite videos from your TikTok user data file.' 27 | ) 28 | .option('-u ', 'choose user data file', 'user_data.json') 29 | .requiredOption('-k ', 'your RapidAPI key'); 30 | 31 | program 32 | .command('favorites') 33 | .description('download your favorite videos') 34 | .action(async () => { 35 | try { 36 | const task = await readData('favorites'); 37 | let list = task[0]; 38 | let apiKey = task[2]; 39 | await downloader(list, 'favorites', apiKey); 40 | } catch (error) { 41 | console.error('getVideo failed', error); 42 | } 43 | }); 44 | 45 | program 46 | .command('liked') 47 | .description('download your liked videos') 48 | .action(async () => { 49 | try { 50 | const task = await readData('liked'); 51 | let list = task[0]; 52 | let apiKey = task[2]; 53 | await downloader(list, 'liked', apiKey); 54 | } catch (error) { 55 | console.error('getVideo failed', error); 56 | } 57 | }); 58 | 59 | program 60 | .command('sounds') 61 | .description('download your favorite sounds') 62 | .action(async () => { 63 | const task = await readData('sounds'); 64 | let list = task[0]; 65 | let apiKey = task[2]; 66 | console.log(chalk.blue('Success: Read favorite sounds list.')); 67 | await downloadSounds(list, apiKey); 68 | }); 69 | 70 | program 71 | .command('shared') 72 | .description('download videos you shared') 73 | .action(async () => { 74 | const task = await readData('shared'); 75 | console.log(chalk.blue('Success: Read shared videos list.')); 76 | await downloader(task[0], 'shared', task[2]); 77 | }); 78 | 79 | program 80 | .command('history') 81 | .description('download video browsing history') 82 | .action(async () => { 83 | const task = await readData('history'); 84 | console.log(chalk.blue('Success: Read browsing history list.')); 85 | await downloader(task[0], 'history', task[2]); 86 | }); 87 | 88 | program.command('messages') 89 | .description('download shared videos from dms') 90 | .action(async () => { 91 | const task = await readData('messages'); 92 | console.log(chalk.blue('Success: Read DM history list.')); 93 | for (const chat of task[0]) { 94 | let user = chat[0]['user']; 95 | console.log('history for user ' + user); 96 | await downloader(chat, 'messages', task[2], user); 97 | } 98 | 99 | }); 100 | 101 | program.parse(process.argv); 102 | 103 | async function readData(category) { 104 | // Initialize variables from CLI args 105 | const opts = program.opts(); 106 | const userDataFile = opts.u; 107 | var apiKey = opts.k; 108 | if (apiKey != undefined) { 109 | console.log(chalk.green.bold('Using RapidAPI key ' + apiKey)); 110 | } 111 | console.log(chalk.green('Reading from user data file ' + opts.u)); 112 | 113 | //read and parse user data file JSON and gets the list of Favorite Videos 114 | try { 115 | var data = readFileSync(`./${userDataFile}`); 116 | } catch (error) { 117 | //console.log("Error reading userdata file:", error); 118 | program.error(chalk.red('Couldn\'t read user data file, does it exist?')); 119 | } 120 | try { 121 | const info = JSON.parse(data); 122 | var list = []; 123 | if (category === 'favorites') { 124 | list = info['Activity']['Favorite Videos']['FavoriteVideoList']; 125 | } else if (category === 'liked') { 126 | list = info['Activity']['Like List']['ItemFavoriteList']; 127 | } else if (category === 'sounds') { 128 | list = info['Activity']['Favorite Sounds']['FavoriteSoundList']; 129 | } else if (category === 'shared') { 130 | let rawList = info['Activity']['Share History']['ShareHistoryList']; 131 | list = rawList.filter((value, index, array) => { 132 | return value.SharedContent == 'video'; 133 | }); 134 | } else if (category === 'history') { 135 | list = info['Activity']['Video Browsing History']['VideoList']; 136 | } else if (category === 'messages') { 137 | //get list of chat history, append each item from each chat to LIST and make sure it specifies the folder to save to 138 | let messages = info['Direct Messages']['Chat History']['ChatHistory']; 139 | Object.keys(messages).forEach(value => { 140 | let chat = messages[value]; 141 | let name = value.split(/\s+/).pop(); 142 | chat[0].user = cleanFileName(name); 143 | list.push(chat); 144 | }); 145 | } 146 | 147 | } catch (error) { 148 | console.log(error); 149 | program.error( 150 | chalk.red( 151 | 'Couldn\'t parse JSON data. Make sure you have chosen an unmodified TikTok data JSON file.' 152 | ) 153 | ); 154 | } 155 | 156 | return [list, category, apiKey]; 157 | } 158 | 159 | async function downloader(list, category, apiKey, subFolder = '') { 160 | // openHistory returns an array of strings containing all the URL's in the history file 161 | let history = await openHistory(); 162 | 163 | // Create download folder if it doesn't exist 164 | let dlFolder = './tiktok-downloads/' + category; 165 | if (subFolder.length > 0) { 166 | dlFolder += '/' + subFolder; 167 | } 168 | try { 169 | if (!fs.existsSync(dlFolder)) { 170 | fs.mkdirSync(dlFolder, { recursive: true }, (err) => { 171 | if (err) throw err; 172 | }); 173 | } 174 | } catch (error) { 175 | console.log(error); 176 | program.error( 177 | 'Couldn\'t create download directories. Please make sure you have permission to write to this folder.' 178 | ); 179 | } 180 | 181 | // open writeStream for history file 182 | var writeHistory = fs.createWriteStream('history.txt', { flags: 'a' }); 183 | //count successfully downloaded videos 184 | let DLCount = 0; 185 | 186 | 187 | for (let i = 0; i < list.length; i++) { 188 | //set the property names for the video items in the list bc dm lists have a different naming scheme 189 | let link = 'Link'; 190 | let date = 'Date'; 191 | if (category === 'messages') { 192 | link = 'Content'; 193 | } else if (category === 'liked') { 194 | link = 'link'; 195 | date = 'date'; 196 | } 197 | let qLength = list.length; 198 | let video = list[i]; 199 | //get data from an entry in the Favorites list 200 | let favoriteURL = video[link]; 201 | // replace colons in date field for Windows filename compatability 202 | let og_Date = video[date]; 203 | let vidDate = og_Date.replace(/:/g, ''); 204 | //check if url contains valid tiktok url 205 | if (!favoriteURL.startsWith('https://')) { 206 | continue; 207 | } 208 | if (history.indexOf(favoriteURL) !== -1) { 209 | console.log(chalk.magenta('Video was found in history file, skipping.')); 210 | continue; 211 | } 212 | //___METADATA FETCHING 213 | console.log(chalk.green('Getting video metadata for: ' + favoriteURL)); 214 | 215 | // get the video information from API and check for errors. 216 | // if the tiktok has been deleted, or there's another issue with the URL, it's logged and skipped 217 | var responseData = await getVideoData(favoriteURL, apiKey); 218 | // very mid way to avoid API rate limits by setting a 1 sec timeout after every metadata API call 219 | await setTimeout(250); 220 | 221 | if (responseData.code != 0) { 222 | if (responseData.code == -1) { 223 | console.log( 224 | chalk.red('Couldn\'t get data for this URL, video may be deleted') 225 | ); 226 | } else { 227 | console.log( 228 | chalk.red('Error getting video metadata for URL ' + favoriteURL) 229 | ); 230 | } 231 | continue; 232 | } 233 | //debug logging 234 | if (process.env.NODE_ENV === 'dev') { 235 | console.log(chalk.blueBright(JSON.stringify(responseData.data))); 236 | } 237 | let success = -1; 238 | //call slideshow downloader for slideshows 239 | if (responseData.data.duration === 0) { 240 | success = await downloadSlideshow(dlFolder, responseData, vidDate); 241 | continue; 242 | } else { 243 | console.log(chalk.blue(`Downloading video ${i}/${qLength}...`)); 244 | success = await downloadVideo(dlFolder, responseData, vidDate); 245 | } 246 | if (success === 0) { 247 | console.log( 248 | chalk.greenBright(`Finished downloading video ` + favoriteURL) 249 | ); 250 | // write URL to history file after download is finished 251 | writeHistory.write('\n' + favoriteURL); 252 | DLCount++; 253 | } 254 | } 255 | 256 | console.log(chalk.greenBright('Saved ' + DLCount + ' videos. Goodbye.')); 257 | } 258 | --------------------------------------------------------------------------------