├── .dockerignore ├── .env ├── .github └── FUNDING.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── app.json ├── bin ├── pre_www └── www ├── package.json ├── routes ├── index.js └── video.js └── views └── index.hjs /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgatti/How-to-Stream-Torrents-using-NodeJS/4bf801e105fee7464e17d3b599922b54da5cafb2/.env -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://paypal.me/gattidavid 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | *.DS_Store 39 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at github@gatti.pl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # First off 2 | 3 | Thank you for considering contributing to this project 😀. All my project have one goal, to teach how to write simpler, easier to understand code to the point that a non technical person will understand it. For example: 4 | 5 | - The way I write my comments is on purpose, because I found that this way is the easiest way for a brain to filter out and understand 6 | - In JavaScript code I just use `let` instead of `const` so no one will get confused, I want people to focus on the article or example, instead of why I used `let` here, and `const` there. 7 | - On purpose I use as little variables as possible to yet again, not confuse anybody. 8 | 9 | # What I believe in 10 | 11 | - Making everything simpler for everyone to understand. 12 | - Frameworks force you to be organized, instead of teaching you how to be organized. 13 | 14 | # Childish Code 15 | 16 | A way to say that code should be understood even by a child. Write simple to understand code. Don't make it obscure, and be the only one that can understand it. Be mindful of other people, and their time. 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Base Image 3 | # 4 | FROM debian:8.6 5 | 6 | # 7 | # Basic Setup 8 | # 9 | MAINTAINER David Gatti 10 | 11 | # 12 | # preparing the environment 13 | # 14 | RUN apt-get update 15 | RUN apt-get upgrade -y 16 | RUN apt-get install -y curl 17 | RUN apt-get install -y sudo 18 | RUN curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash - 19 | RUN apt-get install -y nodejs 20 | 21 | # 22 | # Create app directory 23 | # 24 | RUN mkdir -p /home/app 25 | 26 | # 27 | # Copy the content of the app 28 | # 29 | COPY . /home/app 30 | 31 | # 32 | # Switch working directory 33 | # 34 | WORKDIR /home/app 35 | 36 | # 37 | # Prepare the app by installing all the dependencies 38 | # 39 | RUN npm install 40 | 41 | # 42 | # Run the app 43 | # 44 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David Gatti 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/pre_www 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍿 How to stream Torrent movies using NodeJS and the HTML5 video tag 2 | 3 | After working on the [Understanding Streams in NodeJS](https://github.com/davidgatti/Understanding-Streams-in-NodeJS) article, I moved on to [How to Stream Movies using NodeJS](https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS). Then I thought... *Can I stream a Torrent in real time*? Turns out that yes, I can! And it's surprisingly easy, thanks to the [WebTorrent](https://webtorrent.io) npm module. 4 | 5 | And, to be honest, I'm completely blown away by this technology. I mean, the potential here is endless. I still can't believe that I didn't get it sooner. But better now than never. ;) 6 | 7 | # What is this repository 8 | 9 | This repo is a tech demo/proof of concept/article showcasing what can you do using NodeJS and the WebTorrent module. This is also a good example of how powerful the idea of streams is in general. Because you'll see that even before the file is completely downloaded to the disk, we can read the chunks of data that we have. Proving that we don't need the whole thing to stream data to the client. 10 | 11 | # Deployment 12 | 13 | 14 | 15 | 16 | Follow the instructions on the Heroku main app page, and then once the app is deployed, visit the main page. Once you do that, you should see the site, with some examples of free Magnet Hashes so you can test out the live streaming. 17 | 18 | # Where to start 19 | 20 | 1. Visit the home page of your deployment 21 | 1. Click on one of the demo Hashes 22 | 1. You should see the Client Stats start to show some stats 23 | 1. In a moment, you should see the content of the Magnet Hash 24 | 1. Select a movie file from the Magnet Contents section 25 | 1. After few seconds, you should see the video player buffering the movie 26 | 1. It should start playing shortly 27 | 28 | And there you have it! Proof that you are streaming a Torrent live. 29 | 30 | # High level explanation 31 | 32 | We know how we can stream binary files, thanks to the `.createReadStream()` method, which gives us the possibility of specifying how much data should be read by setting the starting and finishing position of the chunk that we are interested in. 33 | 34 | With `.createReadStream()`, we don't care if the whole file is actually on the local drive, until we have the right section, we can read it and send it, while in the background the file is being constantly downloaded. 35 | 36 | This also proves that we're actually dealing with only chunks, and are not loading the whole file into memory. If you want to learn more about streams in NodeJS, check out my previous article titled [How to Stream Movies using NodeJS](https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS). 37 | 38 | 39 | # How to understand the code 40 | 41 | The code is composed of two parts: We have the front-end and we have the back-end. Shocking, I know. The good part is that the front has no UI design and only two lines of CSS. You can focus on the code itself. Aside from having very detailed comments, the code is also numbered to help you follow what is happening within each step. 42 | 43 | The back-end is similar. If you read the [How to Stream Movies using NodeJS](https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS) article first, you see only new code related to the [WebTorrent](https://webtorrent.io) module. Meaning that if you feel confused, you should check [How to Stream Movies using NodeJS](https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS) first to understand the basics of streaming videos with NodeJS. 44 | 45 | # The End 46 | 47 | If you enjoyed this project, please consider giving it a 🌟. And check out my [GitHub account](https://github.com/davidgatti), where you'll find additional resources you might find useful or interesting. 48 | 49 | ## Sponsor 🎊 50 | 51 | This project is brought to you by 0x4447 LLC, a software company specializing in building custom solutions on top of AWS. Follow this link to learn more: https://0x4447.com. Alternatively, send an email to [hello@0x4447.email](mailto:hello@0x4447.email?Subject=Hello%20From%20Repo&Body=Hi%2C%0A%0AMy%20name%20is%20NAME%2C%20and%20I%27d%20like%20to%20get%20in%20touch%20with%20someone%20at%200x4447.%0A%0AI%27d%20like%20to%20discuss%20the%20following%20topics%3A%0A%0A-%20LIST_OF_TOPICS_TO_DISCUSS%0A%0ASome%20useful%20information%3A%0A%0A-%20My%20full%20name%20is%3A%20FIRST_NAME%20LAST_NAME%0A-%20My%20time%20zone%20is%3A%20TIME_ZONE%0A-%20My%20working%20hours%20are%20from%3A%20TIME%20till%20TIME%0A-%20My%20company%20name%20is%3A%20COMPANY%20NAME%0A-%20My%20company%20website%20is%3A%20https%3A%2F%2F%0A%0ABest%20regards.). 52 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | let cors = require('cors'); 3 | let logger = require('morgan'); 4 | let express = require('express'); 5 | let bodyParser = require('body-parser'); 6 | 7 | let app = express(); 8 | 9 | // 10 | // Set public paths 11 | // 12 | app.set('views', path.join(__dirname, 'views')); 13 | 14 | // 15 | // Set the front end rendering engine 16 | // 17 | app.set('view engine', 'hjs'); 18 | 19 | // 20 | // Add cors to make jQuery API requests 21 | // 22 | app.use(cors()); 23 | 24 | // 25 | // Check for HTTPS 26 | // 27 | app.use(force_https); 28 | 29 | // 30 | // Expose the public folder to the world 31 | // 32 | app.use(express.static(path.join(__dirname, 'public'))); 33 | 34 | // 35 | // Remove the information about what type of framework is the site running on 36 | // 37 | app.disable('x-powered-by'); 38 | 39 | // 40 | // HTTP request logger middleware for node.js 41 | // 42 | app.use(logger('dev')); 43 | 44 | // 45 | // Parse all request as regular text, and not JSON objects 46 | // 47 | app.use(bodyParser.json()); 48 | 49 | // 50 | // Parse application/x-www-form-urlencoded 51 | // 52 | app.use(bodyParser.urlencoded({ extended: false })); 53 | 54 | ////////////////////////////////////////////////////////////////////////////// 55 | 56 | app.use('/', require('./routes/index')); 57 | app.use('/video', require('./routes/video')); 58 | 59 | ////////////////////////////////////////////////////////////////////////////// 60 | 61 | // 62 | // 63 | // If nonce of the above routes matches, we create an error to let the 64 | // user know that the URL accessed doesn't match anything. 65 | // 66 | app.use(function(req, res, next) { 67 | 68 | let err = new Error('Not Found'); 69 | err.status = 404; 70 | 71 | next(err); 72 | 73 | }); 74 | 75 | // 76 | // Display any error that occurred during the request. 77 | // 78 | app.use(function(err, req, res, next) { 79 | 80 | // 81 | // 1. Set the basic information about the error, that is going to be 82 | // displayed to user and developers regardless. 83 | // 84 | let obj_message = { 85 | message: err.message 86 | }; 87 | 88 | // 89 | // 2. Check if the environment is development, and if it is we 90 | // will display the stack-trace 91 | // 92 | if(process.env.NODE_ENV == 'development') 93 | { 94 | // 95 | // 1. Set the variable to show the stack-trace to the developer 96 | // 97 | obj_message.error = err; 98 | 99 | // 100 | // -> Show the error in the console 101 | // 102 | console.error(err); 103 | } 104 | 105 | // 106 | // 3. Display a default status error, or pass the one from 107 | // the error message 108 | // 109 | res.status(err.status || 500); 110 | 111 | // 112 | // -> Show the error 113 | // 114 | res.json(obj_message); 115 | 116 | }); 117 | 118 | // _ _ ______ _ _____ ______ _____ _____ 119 | // | | | | ____| | | __ \| ____| __ \ / ____| 120 | // | |__| | |__ | | | |__) | |__ | |__) | (___ 121 | // | __ | __| | | | ___/| __| | _ / \___ \ 122 | // | | | | |____| |____| | | |____| | \ \ ____) | 123 | // |_| |_|______|______|_| |______|_| \_\_____/ 124 | // 125 | 126 | // 127 | // Check if the connection is secure, if not, redirect to a secure one. 128 | // 129 | function force_https(req, res, next) 130 | { 131 | // 132 | // 1. Redirect only in the production environment 133 | // 134 | if(process.env.NODE_ENV == 'production') 135 | { 136 | // 137 | // 1. Check what protocol are we using 138 | // 139 | if(req.headers['x-forwarded-proto'] !== 'https') 140 | { 141 | // 142 | // -> Redirect the user to the same URL that he requested, but 143 | // with HTTPS instead of HTTP 144 | // 145 | return res.redirect('https://' + req.get('host') + req.url); 146 | } 147 | } 148 | 149 | // 150 | // 2. If the protocol is already HTTPS the, we just keep going. 151 | // 152 | next(); 153 | } 154 | 155 | module.exports = app; 156 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-torrents-in-nodejs", 3 | "description": "How to Stream Torrent Movies using NodeJS and the HTML5 Video tag", 4 | "repository": "https://github.com/davidgatti/How-to-Stream-Torrents-in-NodeJS", 5 | "success_url": "/", 6 | "env": { 7 | "NODE_ENV": { 8 | "description": "The environment can be: production, staging, development or local (defaults to production)", 9 | "value": "production" 10 | }, 11 | "NPM_CONFIG_PRODUCTION": { 12 | "description": "By default Heroku sets this to true, if you are deploying a dev version of the code, set this false.", 13 | "value": "true" 14 | }, 15 | "BASE_URL": { 16 | "description": "The URL of the project", 17 | "value": "https://NAME_OF_THE_SERVER.herokuapp.com" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bin/pre_www: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ "$NODE_ENV" = "development" ]; then 3 | nodemon ./bin/www 4 | else 5 | node ./bin/www 6 | fi 7 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let app = require('../app'); 4 | let http = require('http'); 5 | let cluster = require('cluster'); 6 | 7 | let server; 8 | 9 | // 10 | // 1. Get port from environment variables, if it is not set, we can use the 11 | // default port. 12 | // 13 | let port = process.env.PORT || 3000; 14 | 15 | // 16 | // 2. Assign the port to the app. 17 | // 18 | app.set('port', port); 19 | 20 | // 21 | // 3. Create, meaning spawn multiple instances of the same app to take 22 | // advantage of multi-core processors. 23 | // 24 | // IF the cluster is the master one, then this is the cluster 25 | // that ins control. This means that the master will spawn 26 | // children processes, while also listening for crashes. 27 | // 28 | // If a process exits, then we span a new process in it place 29 | // so we can make sure that we constantly have X amount of 30 | // processes. 31 | // 32 | // ELSE The spawned processes on the other hand are those that creates 33 | // servers and do the heavy lifting. 34 | // 35 | if(cluster.isMaster) { 36 | 37 | // 38 | // 1. Count the machine's CPUs. 39 | // 40 | let cpuCount = 1 //require('os').cpus().length; 41 | 42 | // 43 | // 3. Create a worker for each CPU core. 44 | // 45 | while(cpuCount--) 46 | { 47 | cluster.fork(); 48 | } 49 | 50 | // 51 | // 4. Listen for when the process exits because of a crash, and spawn 52 | // a new process in it place. 53 | // 54 | cluster.on('exit', function (worker) { 55 | 56 | // 57 | // 1. If a worker dies, lets create a new one to replace him 58 | // 59 | cluster.fork(); 60 | 61 | // 62 | // -> Log what happened 63 | // 64 | console.log('Worker %d died :(', worker.id); 65 | 66 | }); 67 | } 68 | else 69 | { 70 | // 71 | // 1. Create HTTP server. 72 | // 73 | server = http.createServer(app); 74 | 75 | // 76 | // 2. Listen on provided port, on all network interfaces. 77 | // 78 | server.listen(port); 79 | 80 | // 81 | // 3. React to error events 82 | // 83 | server.on('error', onError); 84 | 85 | // 86 | // 4. listen to incoming requests 87 | // 88 | server.on('listening', onListening); 89 | } 90 | 91 | // 92 | // Event listener for HTTP server "error" event. 93 | // 94 | function onError(error) 95 | { 96 | if(error.syscall !== 'listen') { 97 | throw error; 98 | } 99 | 100 | // 101 | // handle specific listen errors with friendly messages 102 | // 103 | switch(error.code) 104 | { 105 | case 'EACCES': 106 | console.error("Port %d requires elevated privileges", port); 107 | process.exit(1); 108 | break; 109 | case 'EADDRINUSE': 110 | console.error("Port %d is already in use", port); 111 | process.exit(1); 112 | break; 113 | default: 114 | throw error; 115 | } 116 | } 117 | 118 | // 119 | // Event listener for HTTP server "listening" event. 120 | // 121 | function onListening() { 122 | 123 | // 124 | // -> display in the console where to look for the server 125 | // 126 | console.log("Worker %d is listening on port %d", cluster.worker.id, port); 127 | } 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "how-to-stream-a-movie-using-nodejs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "6.9.1" 7 | }, 8 | "scripts": { 9 | "start": "nf start" 10 | }, 11 | "dependencies": { 12 | "body-parser": "1.15.1", 13 | "cors": "^2.8.1", 14 | "express": "4.13.4", 15 | "hjs": "0.0.6", 16 | "morgan": "1.9.1", 17 | "webtorrent": "^0.107.16" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "1.11.0", 21 | "foreman": "3.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | 3 | let router = express.Router(); 4 | 5 | router.get('/', function(req, res, next) { 6 | 7 | // 8 | // -> Display the index view with the video tag 9 | // 10 | res.render("index", { 11 | base_url: process.env.BASE_URL 12 | }); 13 | 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /routes/video.js: -------------------------------------------------------------------------------- 1 | let fs = require("fs") 2 | let path = require("path"); 3 | let express = require('express'); 4 | let WebTorrent = require('webtorrent') 5 | 6 | let router = express.Router(); 7 | 8 | // 9 | // 1. When the server starts create a WebTorrent client 10 | // 11 | let client = new WebTorrent(); 12 | 13 | // 14 | // 2. The object that holds the client stats to be displayed in the front end 15 | // using an API call every n amount of time using jQuery. 16 | // 17 | let stats = { 18 | progress: 0, 19 | downloadSpeed: 0, 20 | ratio: 0 21 | } 22 | 23 | // 24 | // 3. The variable that holds the error message from the client. Farly crude but 25 | // I don't expect to much happening hear aside the potential to add the same 26 | // Magnet Hash twice. 27 | // 28 | let error_message = ""; 29 | 30 | // 31 | // 4. Listen for any potential client error and update the above variable so 32 | // the front end can display it in the browser. 33 | // 34 | client.on('error', function(err) { 35 | 36 | error_message = err.message; 37 | 38 | }); 39 | 40 | // 41 | // 5. Emitted by the client whenever data is downloaded. Useful for reporting the 42 | // current torrent status of the client. 43 | // 44 | client.on('download', function(bytes) { 45 | 46 | // 47 | // 1. Update the object with fresh data 48 | // 49 | stats = { 50 | progress: Math.round(client.progress * 100 * 100) / 100, 51 | downloadSpeed: client.downloadSpeed, 52 | ratio: client.ratio 53 | } 54 | 55 | }); 56 | 57 | // 58 | // API call that adds a new Magnet Hash to the client so it can start 59 | // downloading it. 60 | // 61 | // magnet -> Magnet Hash 62 | // 63 | // return <- An array with a list of files 64 | // 65 | router.get('/add/:magnet', function(req, res) { 66 | 67 | // 68 | // 1. Extract the magnet Hash and save it in a meaningful variable. 69 | // 70 | let magnet = req.params.magnet; 71 | 72 | // 73 | // 2. Add the magnet Hash to the client 74 | // 75 | client.add(magnet, function (torrent) { 76 | 77 | // 78 | // 1. The array that will hold the content of the Magnet Hash. 79 | // 80 | let files = []; 81 | 82 | // 83 | // 2. Loop over all the file that are inside the Magnet Hash and add 84 | // them to the above variable. 85 | // 86 | torrent.files.forEach(function(data) { 87 | 88 | files.push({ 89 | name: data.name, 90 | length: data.length 91 | }); 92 | 93 | }); 94 | 95 | // 96 | // -> Once we have all the data send it back to the browser to be 97 | // displayed. 98 | // 99 | res.status(200) 100 | res.json(files); 101 | 102 | }); 103 | 104 | }); 105 | 106 | // 107 | // The API call to start streaming the selected file to the video tag. 108 | // 109 | // magnet -> Magnet Hash 110 | // file_name -> the selected file name that is within the Magnet Hash 111 | // 112 | // return <- A chunk of the video file as buffer (binary data) 113 | // 114 | router.get('/stream/:magnet/:file_name', function(req, res, next) { 115 | 116 | // 117 | // 1. Extract the magnet Hash and save it in a meaningful variable. 118 | // 119 | let magnet = req.params.magnet; 120 | 121 | // 122 | // 2. Returns the torrent with the given torrentId. Convenience method. 123 | // Easier than searching through the client.torrents array. Returns 124 | // null if no matching torrent found. 125 | // 126 | var tor = client.get(magnet); 127 | 128 | // 129 | // 3. Variable that will store the user selected file 130 | // 131 | let file = {}; 132 | 133 | // 134 | // 4. Loop over all the files contained inside a Magnet Hash and find the one 135 | // the user selected. 136 | // 137 | for(i = 0; i < tor.files.length; i++) 138 | { 139 | if(tor.files[i].name == req.params.file_name) 140 | { 141 | file = tor.files[i]; 142 | } 143 | } 144 | 145 | // 146 | // 5. Save the range the browser is asking for in a clear and 147 | // reusable variable 148 | // 149 | // The range tells us what part of the file the browser wants 150 | // in bytes. 151 | // 152 | // EXAMPLE: bytes=65534-33357823 153 | // 154 | let range = req.headers.range; 155 | 156 | console.log(range); 157 | 158 | // 159 | // 6. Make sure the browser ask for a range to be sent. 160 | // 161 | if(!range) 162 | { 163 | // 164 | // 1. Create the error 165 | // 166 | let err = new Error("Wrong range"); 167 | err.status = 416; 168 | 169 | // 170 | // -> Send the error and stop the request. 171 | // 172 | return next(err); 173 | } 174 | 175 | // 176 | // 7. Convert the string range in to an array for easy use. 177 | // 178 | let positions = range.replace(/bytes=/, "").split("-"); 179 | 180 | // 181 | // 8. Convert the start value in to an integer 182 | // 183 | let start = parseInt(positions[0], 10); 184 | 185 | // 186 | // 9. Save the total file size in to a clear variable 187 | // 188 | let file_size = file.length; 189 | 190 | // 191 | // 10. IF the end parameter is present we convert it in to an 192 | // integer, the same way we did the start position 193 | // 194 | // ELSE We use the file_size variable as the last part to be 195 | // sent. 196 | // 197 | let end = positions[1] ? parseInt(positions[1], 10) : file_size - 1; 198 | 199 | // 200 | // 11. Calculate the amount of bits will be sent back to the 201 | // browser. 202 | // 203 | let chunksize = (end - start) + 1; 204 | 205 | // 206 | // 12. Create the header for the video tag so it knows what is 207 | // receiving. 208 | // 209 | let head = { 210 | "Content-Range": "bytes " + start + "-" + end + "/" + file_size, 211 | "Accept-Ranges": "bytes", 212 | "Content-Length": chunksize, 213 | "Content-Type": "video/mp4" 214 | } 215 | 216 | // 217 | // 13. Send the custom header 218 | // 219 | res.writeHead(206, head); 220 | 221 | // 222 | // 14. Create the createReadStream option object so createReadStream 223 | // knows how much data it should be read from the file. 224 | // 225 | let stream_position = { 226 | start: start, 227 | end: end 228 | } 229 | 230 | // 231 | // 15. Create a stream chunk based on what the browser asked us for 232 | // 233 | let stream = file.createReadStream(stream_position) 234 | 235 | // 236 | // 16. Pipe the video chunk to the request back to the request 237 | // 238 | stream.pipe(res); 239 | 240 | // 241 | // -> If there was an error while opening a stream we stop the 242 | // request and display it. 243 | // 244 | stream.on("error", function(err) { 245 | 246 | return next(err); 247 | 248 | }); 249 | 250 | }); 251 | 252 | // 253 | // The API call that gets all the Magnet Hashes that the client is actually 254 | // having. 255 | // 256 | // return <- An array with all the Magnet Hashes 257 | // 258 | router.get('/list', function(req, res, next) { 259 | 260 | // 261 | // 1. Loop over all the Magnet Hashes 262 | // 263 | let torrent = client.torrents.reduce(function(array, data) { 264 | 265 | array.push({ 266 | hash: data.infoHash 267 | }); 268 | 269 | return array; 270 | 271 | }, []); 272 | 273 | // 274 | // -> Return the Magnet Hashes 275 | // 276 | res.status(200); 277 | res.json(torrent); 278 | 279 | }); 280 | 281 | // 282 | // The API call that sends back the stats of the client 283 | // 284 | // return <- A object with the client stats 285 | // 286 | router.get('/stats', function(req, res, next) { 287 | 288 | res.status(200); 289 | res.json(stats); 290 | 291 | }); 292 | 293 | // 294 | // The API call that gets errors that occurred with the client 295 | // 296 | // return <- A a string with the error 297 | // 298 | router.get('/errors', function(req, res, next) { 299 | 300 | res.status(200); 301 | res.json(error_message); 302 | 303 | }); 304 | 305 | // 306 | // The API call to delete a Magnet Hash from the client. 307 | // 308 | // magnet -> Magnet Hash 309 | // 310 | // return <- Just the status of the request 311 | // 312 | router.get('/delete/:magnet', function(req, res, next) { 313 | 314 | // 315 | // 1. Extract the magnet Hash and save it in a meaningful variable. 316 | // 317 | let magnet = req.params.magnet; 318 | 319 | // 320 | // 2. Remove the Magnet Hash from the client. 321 | // 322 | client.remove(magnet, function() { 323 | 324 | res.status(200); 325 | res.end(); 326 | 327 | }); 328 | 329 | }); 330 | 331 | module.exports = router; 332 | -------------------------------------------------------------------------------- /views/index.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 🍿 Movie Time 10 | 11 | 12 | 13 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

Samples Torrents:

33 | 34 | 42 | 43 |

Magnet Hash

44 | 45 |

Magnet Contents:

46 | 47 | 48 | 49 |

Client Magnets:

50 | 51 | 54 | 55 |

Client Stats:

56 | 57 | 62 | 63 |

Client Errors:

64 | 65 | 68 | 69 | 70 | 71 | 452 | 453 | --------------------------------------------------------------------------------