├── server ├── .babelrc ├── .eslintrc.js ├── package.json └── app.js ├── .gitignore ├── client ├── script.js ├── style.css └── index.html └── README.md /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-3"] 3 | } -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base" 3 | }; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon --exec babel-node app.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "express": "^4.17.0", 14 | "ytdl-core": "^2.0.1" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "^6.26.0", 18 | "babel-preset-env": "^1.7.0", 19 | "babel-preset-stage-3": "^6.24.1", 20 | "eslint": "^5.16.0", 21 | "eslint-config-airbnb-base": "^13.1.0", 22 | "eslint-plugin-import": "^2.17.2", 23 | "nodemon": "^1.19.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # webstorm 64 | /.idea 65 | -------------------------------------------------------------------------------- /client/script.js: -------------------------------------------------------------------------------- 1 | const convertButton = document.querySelector('.button-convert'); 2 | const inputURL = document.querySelector('.input-url'); 3 | const format = document.getElementById('format'); 4 | const quality = document.getElementById('quality'); 5 | 6 | const serverURL = 'http://localhost:8000'; 7 | 8 | convertButton.addEventListener('click', () => { 9 | const selectedFormat = format[format.selectedIndex].value; 10 | const selectedQuality = quality[quality.selectedIndex].value; 11 | if (inputURL.value.trim().length > 0) { 12 | sendButton(inputURL.value, selectedFormat, selectedQuality); 13 | } 14 | }); 15 | 16 | const sendButton = (videoURL, format, quality) => { 17 | fetch(`${serverURL}/check-download?URL=${videoURL}`) 18 | .then(response => response.json()) 19 | .then(resData => { 20 | const data = JSON.parse(JSON.stringify(resData)); 21 | if (data.status === true) { 22 | document.getElementById( 23 | 'downloading', 24 | ).innerHTML = `Starting the download of ${data.title} by ${ 25 | data.author 26 | }...`; 27 | window.location.href = `${serverURL}/download?URL=${videoURL}&downloadFormat=${format}&quality=${quality}&title=${ 28 | data.title 29 | }`; 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | html { 3 | font-family: 'Raleway', sans-serif; 4 | box-sizing: border-box; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | height: 100vh; 13 | } 14 | 15 | .options > select { 16 | text-align: center; 17 | } 18 | 19 | .input-button { 20 | flex-direction: row; 21 | } 22 | 23 | .header { 24 | text-align: center; 25 | } 26 | 27 | #downloading { 28 | color: #0485ff; 29 | } 30 | 31 | select { 32 | margin: 50px; 33 | width: 268px; 34 | padding: 5px; 35 | font-size: 16px; 36 | line-height: 1; 37 | border: 0; 38 | border-radius: 5px; 39 | height: 34px; 40 | background: url(http://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png) 41 | no-repeat right #ddd; 42 | -webkit-appearance: none; 43 | background-position-x: 244px; 44 | text-align-last: center; 45 | } 46 | 47 | .input-url { 48 | border-radius: 4px 0 0 4px; 49 | width: 30em; 50 | border: 2px solid #eeeeee; 51 | background: #eeeeee; 52 | outline: none; 53 | } 54 | 55 | .status { 56 | padding: 2em; 57 | } 58 | 59 | .input-url:focus { 60 | border: 2px solid #0485ff; 61 | } 62 | 63 | .button-convert { 64 | border-radius: 0 4px 4px 0; 65 | border: 2px solid #0485ff; 66 | background: #0485ff; 67 | color: #fff; 68 | } 69 | 70 | .input-url, 71 | .button-convert { 72 | font-size: 1.3em; 73 | padding: 5px 10px; 74 | } 75 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | import ytdl from 'ytdl-core'; 3 | 4 | const express = require('express'); 5 | 6 | const app = express(); 7 | 8 | app.use(cors('*')); 9 | 10 | app.get('/check-download', async (req, res, next) => { 11 | try { 12 | const { URL } = req.query; 13 | const { 14 | player_response: { 15 | videoDetails: { title, author }, 16 | }, 17 | } = await ytdl.getBasicInfo(URL); 18 | res.json({ 19 | status: true, 20 | title, 21 | author, 22 | }); 23 | next(); 24 | } catch (e) { 25 | console.log(e); 26 | } 27 | }); 28 | 29 | app.get('/download', async (req, res) => { 30 | try { 31 | const { 32 | URL, downloadFormat, quality, title, 33 | } = req.query; 34 | if (downloadFormat === 'audio-only') { 35 | res.setHeader( 36 | 'Content-Disposition', 37 | `attachment; filename=${title.substring(0, 40)}.mp3`, 38 | ); 39 | ytdl(URL, { 40 | filter: format => format.container === 'm4a' && !format.encoding, 41 | quality: quality === 'high' ? 'highest' : 'lowest', 42 | }).pipe(res); 43 | } else { 44 | res.header( 45 | 'Content-Disposition', 46 | `attachment; filename="${title.substring(0, 25)}.mp4"`, 47 | ); 48 | ytdl(URL, { 49 | filter: downloadFormat === 'video-only' ? 'videoonly' : 'audioandvideo', 50 | quality: quality === 'high' ? 'highestvideo' : 'lowestvideo', 51 | }).pipe(res); 52 | } 53 | } catch (e) { 54 | console.log(e); 55 | } 56 | }); 57 | 58 | // eslint-disable-next-line no-console 59 | app.listen({ port: 8000 }, () => console.log('🚀 Server ready at http://localhost:8000')); 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube Downloader & Converter 2 | 3 | Download and convert any YouTube video to MP3 or MP4. Built using Vanilla JS and Node.js 4 | 5 | Deployed **[live](https://convert-and-download-yt.herokuapp.com/)** 6 | 7 | ***N.b. Only to be used in accordance with YouTube's [terms of service](https://www.youtube.com/static?gl=GB&template=terms)*** 8 | 9 | ### Code style 10 | [](https://github.com/feross/standard) 11 | [](https://stormy-reaches-60483.herokuapp.com/) 12 | [](https://stormy-reaches-60483.herokuapp.com/) 13 | 14 | ### Demo 15 | 16 |
17 |
18 |
19 |
20 |
21 |