The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── app.js
├── index.js
├── package.json
└── screenshot.png


/.gitignore:
--------------------------------------------------------------------------------
1 | torrents
2 | node_modules
3 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | sudo: false
 2 | language: node_js
 3 | node_js:
 4 |   - "6"
 5 |   - "5"
 6 |   - "4"
 7 |   - "0.12"
 8 |   - "0.11"
 9 |   - "0.10"
10 |   - "iojs"
11 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2013 Mathias Buus Madsen <mathiasbuus@gmail.com>
2 | 
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | 
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | 
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # peerflix
 2 | 
 3 | Streaming torrent client for Node.js
 4 | 
 5 | ```
 6 | npm install -g peerflix
 7 | ```
 8 | 
 9 | [![build status](http://img.shields.io/travis/mafintosh/peerflix.svg?style=flat)](http://travis-ci.org/mafintosh/peerflix)
10 | 
11 | ## Usage
12 | 
13 | Peerflix can be used with a magnet link or a torrent file.
14 | To stream a video with its magnet link use the following command.
15 | 
16 | ```
17 | peerflix "magnet:?xt=urn:btih:ef330b39f4801d25b4245212e75a38634bfc856e" --vlc
18 | ```
19 | 
20 | Remember to put `"` around your magnet link since they usually contain `&`.
21 | `peerflix` will print a terminal interface. The first line contains an address to a http server. The `--vlc` flag ensures vlc is opened when the torrent is ready to stream.
22 | 
23 | ![peerflix](https://raw.github.com/mafintosh/peerflix/master/screenshot.png)
24 | 
25 | To stream music with a torrent file use the following command.
26 | 
27 | ```
28 | peerflix "http://some-torrent/music.torrent" -a --vlc
29 | ```
30 | 
31 | The `-a` flag ensures that all files in the music repository are played with vlc.
32 | Otherwise if the torrent contains multiple files, `peerflix` will choose the biggest one.
33 | To get a full list of available options run peerflix with the help flag.
34 | 
35 | ```
36 | peerflix --help
37 | ```
38 | 
39 | Examples of usage of could be
40 | 
41 | ```
42 | peerflix magnet-link --list # Select from a list of files to download
43 | peerflix magnet-link --vlc -- --fullscreen # will pass --fullscreen to vlc
44 | peerflix magnet-link --mplayer --subtitles subtitle-file.srt # play in mplayer with subtitles
45 | peerflix magnet-link --connection 200 # set max connection to 200
46 | ```
47 | 
48 | 
49 | ## Programmatic usage
50 | 
51 | If you want to build your own app using streaming bittorrent in Node you should checkout [torrent-stream](https://github.com/mafintosh/torrent-stream)
52 | 
53 | ## Chromebook users
54 | 
55 | Chromebooks are set to refuse all incoming connections by default - to change this:  
56 | 
57 | ```
58 | sudo iptables -P INPUT ACCEPT
59 | ```
60 | 
61 | ## Chromecast
62 | 
63 | If you wanna use peerflix on your chromecast checkout [peercast](https://github.com/mafintosh/peercast)
64 | or [castnow](https://github.com/xat/castnow)
65 | 
66 | ## License
67 | 
68 | MIT
69 | 


--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env node
  2 | 
  3 | var optimist = require('optimist')
  4 | var rc = require('rc')
  5 | var clivas = require('clivas')
  6 | var numeral = require('numeral')
  7 | var os = require('os')
  8 | var address = require('network-address')
  9 | var proc = require('child_process')
 10 | var peerflix = require('./')
 11 | var keypress = require('keypress')
 12 | var openUrl = require('open')
 13 | var inquirer = require('inquirer')
 14 | var parsetorrent = require('parse-torrent')
 15 | var bufferFrom = require('buffer-from')
 16 | 
 17 | var path = require('path')
 18 | 
 19 | process.title = 'peerflix'
 20 | 
 21 | var argv = rc('peerflix', {}, optimist
 22 |   .usage('Usage: $0 magnet-link-or-torrent [options]')
 23 |   .alias('c', 'connections').describe('c', 'max connected peers').default('c', os.cpus().length > 1 ? 100 : 30)
 24 |   .alias('p', 'port').describe('p', 'change the http port').default('p', 8888)
 25 |   .alias('i', 'index').describe('i', 'changed streamed file (index)')
 26 |   .alias('l', 'list').describe('l', 'list available files with corresponding index').boolean('l')
 27 |   .alias('t', 'subtitles').describe('t', 'load subtitles file')
 28 |   .alias('q', 'quiet').describe('q', 'be quiet').boolean('v')
 29 |   .alias('v', 'vlc').describe('v', 'autoplay in vlc*').boolean('v')
 30 |   .alias('s', 'airplay').describe('s', 'autoplay via AirPlay').boolean('a')
 31 |   .alias('m', 'mplayer').describe('m', 'autoplay in mplayer*').boolean('m')
 32 |   .alias('g', 'smplayer').describe('g', 'autoplay in smplayer*').boolean('g')
 33 |   .describe('mpchc', 'autoplay in MPC-HC player*').boolean('boolean')
 34 |   .describe('potplayer', 'autoplay in Potplayer*').boolean('boolean')
 35 |   .alias('k', 'mpv').describe('k', 'autoplay in mpv*').boolean('k')
 36 |   .alias('o', 'omx').describe('o', 'autoplay in omx**').boolean('o')
 37 |   .alias('w', 'webplay').describe('w', 'autoplay in webplay').boolean('w')
 38 |   .alias('j', 'jack').describe('j', 'autoplay in omx** using the audio jack').boolean('j')
 39 |   .alias('f', 'path').describe('f', 'change buffer file path')
 40 |   .alias('b', 'blocklist').describe('b', 'use the specified blocklist')
 41 |   .alias('n', 'no-quit').describe('n', 'do not quit peerflix on vlc exit').boolean('n')
 42 |   .alias('a', 'all').describe('a', 'select all files in the torrent').boolean('a')
 43 |   .alias('r', 'remove').describe('r', 'remove files on exit').boolean('r')
 44 |   .alias('h', 'hostname').describe('h', 'host name or IP to bind the server to')
 45 |   .alias('e', 'peer').describe('e', 'add peer by ip:port')
 46 |   .alias('x', 'peer-port').describe('x', 'set peer listening port')
 47 |   .alias('d', 'not-on-top').describe('d', 'do not float video on top').boolean('d')
 48 |   .describe('on-downloaded', 'script to call when file is 100% downloaded')
 49 |   .describe('on-listening', 'script to call when server goes live')
 50 |   .describe('version', 'prints current version').boolean('boolean')
 51 |   .argv)
 52 | 
 53 | if (argv.version) {
 54 |   console.error(require('./package').version)
 55 |   process.exit(0)
 56 | }
 57 | 
 58 | var filename = argv._[0]
 59 | var onTop = !argv.d
 60 | 
 61 | if (!filename) {
 62 |   optimist.showHelp()
 63 |   console.error('Options passed after -- will be passed to your player')
 64 |   console.error('')
 65 |   console.error('  "peerflix magnet-link --vlc -- --fullscreen" will pass --fullscreen to vlc')
 66 |   console.error('')
 67 |   console.error('* Autoplay can take several seconds to start since it needs to wait for the first piece')
 68 |   console.error('** OMX player is the default Raspbian video player\n')
 69 |   process.exit(1)
 70 | }
 71 | 
 72 | var VLC_ARGS = '-q' + (onTop ? ' --video-on-top' : '') + ' --play-and-exit'
 73 | var OMX_EXEC = argv.jack ? 'omxplayer -r -o local ' : 'omxplayer -r -o hdmi '
 74 | var MPLAYER_EXEC = 'mplayer ' + (onTop ? '-ontop' : '') + ' -really-quiet -noidx -loop 0 '
 75 | var SMPLAYER_EXEC = 'smplayer ' + (onTop ? '-ontop' : '')
 76 | var MPV_EXEC = 'mpv ' + (onTop ? '--ontop' : '') + ' --really-quiet --loop=no '
 77 | var MPC_HC_ARGS = '/play'
 78 | var POTPLAYER_ARGS = ''
 79 | 
 80 | var enc = function (s) {
 81 |   return /\s/.test(s) ? JSON.stringify(s) : s
 82 | }
 83 | 
 84 | if (argv.t) {
 85 |   VLC_ARGS += ' --sub-file=' + (process.platform === 'win32' ? argv.t : enc(argv.t))
 86 |   OMX_EXEC += ' --subtitles ' + enc(argv.t)
 87 |   MPLAYER_EXEC += ' -sub ' + enc(argv.t)
 88 |   SMPLAYER_EXEC += ' -sub ' + enc(argv.t)
 89 |   MPV_EXEC += ' --sub-file=' + enc(argv.t)
 90 |   POTPLAYER_ARGS += ' ' + enc(argv.t)
 91 | }
 92 | 
 93 | if (argv._.length > 1) {
 94 |   var _args = argv._
 95 |   _args.shift()
 96 |   var playerArgs = _args.join(' ')
 97 |   VLC_ARGS += ' ' + playerArgs
 98 |   OMX_EXEC += ' ' + playerArgs
 99 |   MPLAYER_EXEC += ' ' + playerArgs
100 |   SMPLAYER_EXEC += ' ' + playerArgs
101 |   MPV_EXEC += ' ' + playerArgs
102 |   MPC_HC_ARGS += ' ' + playerArgs
103 |   POTPLAYER_ARGS += ' ' + playerArgs
104 | }
105 | 
106 | var watchVerifying = function (engine) {
107 |   var showVerifying = function (i) {
108 |     var percentage = Math.round(((i + 1) / engine.torrent.pieces.length) * 100.0)
109 |     clivas.clear()
110 |     clivas.line('{yellow:Verifying downloaded:} ' + percentage + '%')
111 |   }
112 | 
113 |   var startShowVerifying = function () {
114 |     showVerifying(-1)
115 |     engine.on('verify', showVerifying)
116 |   }
117 | 
118 |   var stopShowVerifying = function () {
119 |     clivas.clear()
120 |     engine.removeListener('verify', showVerifying)
121 |     engine.removeListener('verifying', startShowVerifying)
122 |   }
123 | 
124 |   engine.on('verifying', startShowVerifying)
125 |   engine.on('ready', stopShowVerifying)
126 | }
127 | 
128 | var ontorrent = function (torrent) {
129 |   if (argv['peer-port']) argv.peerPort = Number(argv['peer-port'])
130 | 
131 |   var engine = peerflix(torrent, argv)
132 |   var hotswaps = 0
133 |   var verified = 0
134 |   var invalid = 0
135 |   var airplayServer = null
136 |   var downloadedPercentage = 0
137 | 
138 |   engine.on('verify', function () {
139 |     verified++
140 |     downloadedPercentage = Math.floor(verified / engine.torrent.pieces.length * 100)
141 |   })
142 | 
143 |   engine.on('invalid-piece', function () {
144 |     invalid++
145 |   })
146 | 
147 |   var bytes = function (num) {
148 |     return numeral(num).format('0.0b')
149 |   }
150 | 
151 |   if (argv.list) {
152 |     var interactive = process.stdout.isTTY && process.stdin.isTTY && !!process.stdin.setRawMode
153 | 
154 |     var onready = function () {
155 |       if (interactive) {
156 |         var filenamesInOriginalOrder = engine.files.map(file => file.path)
157 |         inquirer.prompt([{
158 |           type: 'list',
159 |           name: 'file',
160 |           message: 'Choose one file',
161 |           choices: Array.from(engine.files)
162 |             .sort((file1, file2) => file1.path.localeCompare(file2.path))
163 |             .map(function (file, i) {
164 |               return {
165 |                 name: file.name + ' : ' + bytes(file.length),
166 |                 value: filenamesInOriginalOrder.indexOf(file.path)
167 |               }
168 |             })
169 |         }]).then(function (answers) {
170 |           argv.index = answers.file
171 |           delete argv.list
172 |           ontorrent(torrent)
173 |         })
174 |       } else {
175 |         engine.files.forEach(function (file, i, files) {
176 |           clivas.line('{3+bold:' + i + '} : {magenta:' + file.name + '} : {blue:' + bytes(file.length) + '}')
177 |         })
178 |         process.exit(0)
179 |       }
180 |     }
181 | 
182 |     if (engine.torrent) onready()
183 |     else {
184 |       watchVerifying(engine)
185 |       engine.on('ready', onready)
186 |     }
187 |     return
188 |   }
189 | 
190 |   engine.on('hotswap', function () {
191 |     hotswaps++
192 |   })
193 | 
194 |   var started = Date.now()
195 |   var wires = engine.swarm.wires
196 |   var swarm = engine.swarm
197 | 
198 |   var active = function (wire) {
199 |     return !wire.peerChoking
200 |   }
201 | 
202 |   var peers = [].concat(argv.peer || [])
203 |   peers.forEach(function (peer) {
204 |     engine.connect(peer)
205 |   })
206 | 
207 |   if (argv['on-downloaded']) {
208 |     var downloaded = false
209 |     engine.on('uninterested', function () {
210 |       if (!downloaded) proc.exec(argv['on-downloaded'])
211 |       downloaded = true
212 |     })
213 |   }
214 | 
215 |   engine.server.on('listening', function () {
216 |     var host = argv.hostname || address()
217 |     var href = 'http://' + host + ':' + engine.server.address().port + '/'
218 |     var localHref = 'http://localhost:' + engine.server.address().port + '/'
219 |     var filename = engine.server.index.name.split('/').pop().replace(/\{|\}/g, '')
220 |     var filelength = engine.server.index.length
221 |     var player = null
222 |     var paused = false
223 |     var timePaused = 0
224 |     var pausedAt = null
225 | 
226 |     VLC_ARGS += ' --meta-title="' + filename.replace(/"/g, '\\"') + '"'
227 | 
228 |     if (argv.all) {
229 |       filename = engine.torrent.name
230 |       filelength = engine.torrent.length
231 |       href += '.m3u'
232 |       localHref += '.m3u'
233 |     }
234 | 
235 |     var registry = function (hive, key, name, cb) {
236 |       var Registry = require('winreg')
237 |       var regKey = new Registry({
238 |         hive: Registry[hive],
239 |         key: key
240 |       })
241 |       regKey.get(name, cb)
242 |     }
243 | 
244 |     if (argv.vlc && process.platform === 'win32') {
245 |       player = 'vlc'
246 |       var runVLC = function (regItem) {
247 |         VLC_ARGS = VLC_ARGS.split(' ')
248 |         VLC_ARGS.unshift(localHref)
249 |         proc.execFile(regItem.value + path.sep + 'vlc.exe', VLC_ARGS)
250 |       }
251 |       registry('HKLM', '\\Software\\VideoLAN\\VLC', 'InstallDir', function (err, regItem) {
252 |         if (err) {
253 |           registry('HKLM', '\\Software\\WOW6432Node\\VideoLAN\\VLC', 'InstallDir', function (err, regItem) {
254 |             if (err) return
255 |             runVLC(regItem)
256 |           })
257 |         } else {
258 |           runVLC(regItem)
259 |         }
260 |       })
261 |     } else if (argv.mpchc && process.platform === 'win32') {
262 |       player = 'mph-hc'
263 |       registry('HKCU', '\\Software\\MPC-HC\\MPC-HC', 'ExePath', function (err, regItem) {
264 |         if (err) return
265 |         proc.exec('"' + regItem.value + '" "' + localHref + '" ' + MPC_HC_ARGS)
266 |       })
267 |     } else if (argv.potplayer && process.platform === 'win32') {
268 |       player = 'potplayer'
269 |       var runPotPlayer = function (regItem) {
270 |         proc.exec('"' + regItem.value + '" "' + localHref + '" ' + POTPLAYER_ARGS)
271 |       }
272 |       registry('HKCU', '\\Software\\DAUM\\PotPlayer64', 'ProgramPath', function (err, regItem) {
273 |         if (err) {
274 |           registry('HKCU', '\\Software\\DAUM\\PotPlayer', 'ProgramPath', function (err, regItem) {
275 |             if (err) return
276 |             runPotPlayer(regItem)
277 |           })
278 |         } else {
279 |           runPotPlayer(regItem)
280 |         }
281 |       })
282 |     } else {
283 |       if (argv.vlc) {
284 |         player = 'vlc'
285 |         var root = '/Applications/VLC.app/Contents/MacOS/VLC'
286 |         var home = (process.env.HOME || '') + root
287 |         var vlc = proc.exec('vlc ' + VLC_ARGS + ' ' + localHref + ' || ' + root + ' ' + VLC_ARGS + ' ' + localHref + ' || ' + home + ' ' + VLC_ARGS + ' ' + localHref, function (error, stdout, stderror) {
288 |           if (error) {
289 |             process.exit(0)
290 |           }
291 |         })
292 | 
293 |         vlc.on('exit', function () {
294 |           if (!argv.n && argv.quit !== false) process.exit(0)
295 |         })
296 |       }
297 |     }
298 | 
299 |     if (argv.omx) {
300 |       player = 'omx'
301 |       var omx = proc.exec(OMX_EXEC + ' ' + localHref)
302 |       omx.on('exit', function () {
303 |         if (!argv.n && argv.quit !== false) process.exit(0)
304 |       })
305 |     }
306 |     if (argv.mplayer) {
307 |       player = 'mplayer'
308 |       var mplayer = proc.exec(MPLAYER_EXEC + ' ' + localHref)
309 |       mplayer.on('exit', function () {
310 |         if (!argv.n && argv.quit !== false) process.exit(0)
311 |       })
312 |     }
313 |     if (argv.smplayer) {
314 |       player = 'smplayer'
315 |       var smplayer = proc.exec(SMPLAYER_EXEC + ' ' + localHref)
316 |       smplayer.on('exit', function () {
317 |         if (!argv.n && argv.quit !== false) process.exit(0)
318 |       })
319 |     }
320 |     if (argv.mpv) {
321 |       player = 'mpv'
322 |       var mpv = proc.exec(MPV_EXEC + ' ' + localHref)
323 |       mpv.on('exit', function () {
324 |         if (!argv.n && argv.quit !== false) process.exit(0)
325 |       })
326 |     }
327 |     if (argv.webplay) {
328 |       player = 'webplay'
329 |       openUrl('https://85d514b3e548d934d8ff7c45a54732e65a3162fe.htmlb.in/#' + localHref)
330 |     }
331 |     if (argv.airplay) {
332 |       var list = require('airplayer')()
333 |       list.once('update', function (player) {
334 |         airplayServer = player
335 |         list.destroy()
336 |         player.play(href)
337 |       })
338 |     }
339 | 
340 |     if (argv['on-listening']) proc.exec(argv['on-listening'] + ' ' + href)
341 | 
342 |     if (argv.quiet) return console.log('server is listening on ' + href)
343 | 
344 |     process.stdout.write(bufferFrom('G1tIG1sySg==', 'base64')) // clear for drawing
345 | 
346 |     var interactive = !player && process.stdin.isTTY && !!process.stdin.setRawMode
347 | 
348 |     if (interactive) {
349 |       keypress(process.stdin)
350 |       process.stdin.on('keypress', function (ch, key) {
351 |         if (!key) return
352 |         if (key.name === 'c' && key.ctrl === true) return process.kill(process.pid, 'SIGINT')
353 |         if (key.name === 'l' && key.ctrl === true) {
354 |           var command = 'xdg-open'
355 |           if (process.platform === 'win32') { command = 'explorer' }
356 |           if (process.platform === 'darwin') { command = 'open' }
357 | 
358 |           return proc.exec(command + ' ' + engine.path)
359 |         }
360 |         if (key.name !== 'space') return
361 | 
362 |         if (player) return
363 |         if (paused === false) {
364 |           if (!argv.all) {
365 |             engine.server.index.deselect()
366 |           } else {
367 |             engine.files.forEach(function (file) {
368 |               file.deselect()
369 |             })
370 |           }
371 |           paused = true
372 |           pausedAt = Date.now()
373 |           draw()
374 |           return
375 |         }
376 | 
377 |         if (!argv.all) {
378 |           engine.server.index.select()
379 |         } else {
380 |           engine.files.forEach(function (file) {
381 |             file.select()
382 |           })
383 |         }
384 | 
385 |         paused = false
386 |         timePaused += Date.now() - pausedAt
387 |         draw()
388 |       })
389 |       process.stdin.setRawMode(true)
390 |     }
391 | 
392 |     var draw = function () {
393 |       var unchoked = engine.swarm.wires.filter(active)
394 |       var timeCurrentPause = 0
395 |       if (paused === true) {
396 |         timeCurrentPause = Date.now() - pausedAt
397 |       }
398 |       var runtime = Math.floor((Date.now() - started - timePaused - timeCurrentPause) / 1000)
399 |       var linesremaining = clivas.height
400 |       var peerslisted = 0
401 | 
402 |       clivas.clear()
403 |       if (argv.airplay) {
404 |         if (airplayServer) clivas.line('{green:streaming to} {bold:' + airplayServer.name + '} {green:using airplay}')
405 |         else clivas.line('{green:streaming} {green:using airplay}')
406 |       } else {
407 |         clivas.line('{green:open} {bold:' + (player || 'vlc') + '} {green:and enter} {bold:' + href + '} {green:as the network address}')
408 |       }
409 |       clivas.line('')
410 |       clivas.line('{yellow:info} {green:streaming} {bold:' + filename + ' (' + bytes(filelength) + ')} {green:-} {bold:' + bytes(swarm.downloadSpeed()) + '/s} {green:from} {bold:' + unchoked.length + '/' + wires.length + '} {green:peers}    ')
411 |       clivas.line('{yellow:info} {green:path} {cyan:' + engine.path + '}')
412 |       clivas.line('{yellow:info} {green:downloaded} {bold:' + bytes(swarm.downloaded) + '} (' + downloadedPercentage + '%) {green:and uploaded }{bold:' + bytes(swarm.uploaded) + '} {green:in }{bold:' + runtime + 's} {green:with} {bold:' + hotswaps + '} {green:hotswaps}     ')
413 |       clivas.line('{yellow:info} {green:verified} {bold:' + verified + '} {green:pieces and received} {bold:' + invalid + '} {green:invalid pieces}')
414 |       clivas.line('{yellow:info} {green:peer queue size is} {bold:' + swarm.queued + '}')
415 |       clivas.line('{80:}')
416 | 
417 |       if (interactive) {
418 |         var openLoc = ' or CTRL+L to open download location}'
419 |         if (paused) clivas.line('{yellow:PAUSED} {green:Press SPACE to continue download' + openLoc)
420 |         else clivas.line('{50+green:Press SPACE to pause download' + openLoc)
421 |       }
422 | 
423 |       clivas.line('')
424 |       linesremaining -= 9
425 | 
426 |       wires.every(function (wire) {
427 |         var tags = []
428 |         if (wire.peerChoking) tags.push('choked')
429 |         clivas.line('{25+magenta:' + wire.peerAddress + '} {10:' + bytes(wire.downloaded) + '} {10 + cyan:' + bytes(wire.downloadSpeed()) + '/s} {15 + grey:' + tags.join(', ') + '}   ')
430 |         peerslisted++
431 |         return linesremaining - peerslisted > 4
432 |       })
433 |       linesremaining -= peerslisted
434 | 
435 |       if (wires.length > peerslisted) {
436 |         clivas.line('{80:}')
437 |         clivas.line('... and ' + (wires.length - peerslisted) + ' more     ')
438 |       }
439 | 
440 |       clivas.line('{80:}')
441 |       clivas.flush()
442 |     }
443 | 
444 |     setInterval(draw, 500)
445 |     draw()
446 |   })
447 | 
448 |   engine.server.once('error', function () {
449 |     engine.server.listen(0, argv.hostname)
450 |   })
451 | 
452 |   var onmagnet = function () {
453 |     clivas.clear()
454 |     clivas.line('{green:fetching torrent metadata from} {bold:' + engine.swarm.wires.length + '} {green:peers}')
455 |   }
456 | 
457 |   if (typeof torrent === 'string' && torrent.indexOf('magnet:') === 0 && !argv.quiet) {
458 |     onmagnet()
459 |     engine.swarm.on('wire', onmagnet)
460 |   }
461 | 
462 |   engine.on('ready', function () {
463 |     engine.swarm.removeListener('wire', onmagnet)
464 |     if (!argv.all) return
465 |     engine.files.forEach(function (file) {
466 |       file.select()
467 |     })
468 |   })
469 | 
470 |   var onexit = function () {
471 |     // we're doing some heavy lifting so it can take some time to exit... let's
472 |     // better output a status message so the user knows we're working on it :)
473 |     clivas.line('')
474 |     clivas.line('{yellow:info} {green:peerflix is exiting...}')
475 |   }
476 | 
477 |   watchVerifying(engine)
478 | 
479 |   if (argv.remove) {
480 |     var remove = function () {
481 |       onexit()
482 |       engine.remove(function () {
483 |         process.exit()
484 |       })
485 |     }
486 | 
487 |     process.on('SIGINT', remove)
488 |     process.on('SIGTERM', remove)
489 |   } else {
490 |     process.on('SIGINT', function () {
491 |       onexit()
492 |       process.exit()
493 |     })
494 |   }
495 | }
496 | 
497 | parsetorrent.remote(filename, function (err, parsedtorrent) {
498 |   if (err) {
499 |     console.error(err.message)
500 |     process.exit(1)
501 |   }
502 |   ontorrent(parsedtorrent)
503 | })
504 | 


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
  1 | var torrentStream = require('torrent-stream')
  2 | var http = require('http')
  3 | var fs = require('fs')
  4 | var rangeParser = require('range-parser')
  5 | var xtend = require('xtend')
  6 | var url = require('url')
  7 | var mime = require('mime')
  8 | var pump = require('pump')
  9 | 
 10 | var parseBlocklist = function (filename) {
 11 |   // TODO: support gzipped files
 12 |   var blocklistData = fs.readFileSync(filename, { encoding: 'utf8' })
 13 |   var blocklist = []
 14 |   blocklistData.split('\n').forEach(function (line) {
 15 |     var match = null
 16 |     if ((match = /^\s*[^#].*?\s*:\s*([a-f0-9.:]+?)\s*-\s*([a-f0-9.:]+?)\s*$/.exec(line))) {
 17 |       blocklist.push({
 18 |         start: match[1],
 19 |         end: match[2]
 20 |       })
 21 |     }
 22 |   })
 23 |   return blocklist
 24 | }
 25 | 
 26 | var truthy = function () {
 27 |   return true
 28 | }
 29 | 
 30 | var createServer = function (e, opts) {
 31 |   var server = http.createServer()
 32 |   var index = opts.index
 33 |   var getType = opts.type || mime.getType.bind(mime)
 34 |   var filter = opts.filter || truthy
 35 | 
 36 |   var onready = function () {
 37 |     if (typeof index !== 'number') {
 38 |       index = e.files.reduce(function (a, b) {
 39 |         return a.length > b.length ? a : b
 40 |       })
 41 |       index = e.files.indexOf(index)
 42 |     }
 43 | 
 44 |     e.files[index].select()
 45 |     server.index = e.files[index]
 46 | 
 47 |     if (opts.sort) e.files.sort(opts.sort)
 48 |   }
 49 | 
 50 |   if (e.torrent) onready()
 51 |   else e.on('ready', onready)
 52 | 
 53 |   server.on('request', function (request, response) {
 54 |     var u = url.parse(request.url)
 55 |     var host = request.headers.host || 'localhost'
 56 | 
 57 |     var toPlaylist = function () {
 58 |       var toEntry = function (file, i) {
 59 |         return '#EXTINF:-1,' + file.path + '\n' + 'http://' + host + '/' + i
 60 |       }
 61 | 
 62 |       return '#EXTM3U\n' + e.files.filter(filter).map(toEntry).join('\n')
 63 |     }
 64 | 
 65 |     var toJSON = function () {
 66 |       var totalPeers = e.swarm.wires
 67 | 
 68 |       var activePeers = totalPeers.filter(function (wire) {
 69 |         return !wire.peerChoking
 70 |       })
 71 | 
 72 |       var totalLength = e.files.reduce(function (prevFileLength, currFile) {
 73 |         return prevFileLength + currFile.length
 74 |       }, 0)
 75 | 
 76 |       var toEntry = function (file, i) {
 77 |         return {
 78 |           name: file.name,
 79 |           url: 'http://' + host + '/' + i,
 80 |           length: file.length
 81 |         }
 82 |       }
 83 | 
 84 |       var swarmStats = {
 85 |         totalLength: totalLength,
 86 |         downloaded: e.swarm.downloaded,
 87 |         uploaded: e.swarm.uploaded,
 88 |         downloadSpeed: parseInt(e.swarm.downloadSpeed(), 10),
 89 |         uploadSpeed: parseInt(e.swarm.uploadSpeed(), 10),
 90 |         totalPeers: totalPeers.length,
 91 |         activePeers: activePeers.length,
 92 |         files: e.files.filter(filter).map(toEntry)
 93 |       }
 94 | 
 95 |       return JSON.stringify(swarmStats, null, '  ')
 96 |     }
 97 | 
 98 |     // Allow CORS requests to specify arbitrary headers, e.g. 'Range',
 99 |     // by responding to the OPTIONS preflight request with the specified
100 |     // origin and requested headers.
101 |     if (request.method === 'OPTIONS' && request.headers['access-control-request-headers']) {
102 |       response.setHeader('Access-Control-Allow-Origin', request.headers.origin)
103 |       response.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
104 |       response.setHeader(
105 |           'Access-Control-Allow-Headers',
106 |           request.headers['access-control-request-headers'])
107 |       response.setHeader('Access-Control-Max-Age', '1728000')
108 | 
109 |       response.end()
110 |       return
111 |     }
112 | 
113 |     if (request.headers.origin) response.setHeader('Access-Control-Allow-Origin', request.headers.origin)
114 |     if (u.pathname === '/') u.pathname = '/' + index
115 | 
116 |     if (u.pathname === '/favicon.ico') {
117 |       response.statusCode = 404
118 |       response.end()
119 |       return
120 |     }
121 | 
122 |     if (u.pathname === '/.json') {
123 |       var json = toJSON()
124 |       response.setHeader('Content-Type', 'application/json; charset=utf-8')
125 |       response.setHeader('Content-Length', Buffer.byteLength(json))
126 |       response.end(json)
127 |       return
128 |     }
129 | 
130 |     if (u.pathname === '/.m3u') {
131 |       var playlist = toPlaylist()
132 |       response.setHeader('Content-Type', 'application/x-mpegurl; charset=utf-8')
133 |       response.setHeader('Content-Length', Buffer.byteLength(playlist))
134 |       response.end(playlist)
135 |       return
136 |     }
137 | 
138 |     e.files.forEach(function (file, i) {
139 |       if (u.pathname.slice(1) === file.name) u.pathname = '/' + i
140 |     })
141 | 
142 |     var i = Number(u.pathname.slice(1))
143 | 
144 |     if (isNaN(i) || i >= e.files.length) {
145 |       response.statusCode = 404
146 |       response.end()
147 |       return
148 |     }
149 | 
150 |     var file = e.files[i]
151 |     var range = request.headers.range
152 |     range = range && rangeParser(file.length, range)[0]
153 |     response.setHeader('Accept-Ranges', 'bytes')
154 |     response.setHeader('Content-Type', getType(file.name))
155 |     response.setHeader('transferMode.dlna.org', 'Streaming')
156 |     response.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000')
157 |     if (!range) {
158 |       response.setHeader('Content-Length', file.length)
159 |       if (request.method === 'HEAD') return response.end()
160 |       pump(file.createReadStream(), response)
161 |       return
162 |     }
163 | 
164 |     response.statusCode = 206
165 |     response.setHeader('Content-Length', range.end - range.start + 1)
166 |     response.setHeader('Content-Range', 'bytes ' + range.start + '-' + range.end + '/' + file.length)
167 |     if (request.method === 'HEAD') return response.end()
168 |     pump(file.createReadStream(range), response)
169 |   })
170 | 
171 |   server.on('connection', function (socket) {
172 |     socket.setTimeout(36000000)
173 |   })
174 | 
175 |   return server
176 | }
177 | 
178 | module.exports = function (torrent, opts) {
179 |   if (!opts) opts = {}
180 | 
181 |   // Parse blocklist
182 |   if (opts.blocklist) opts.blocklist = parseBlocklist(opts.blocklist)
183 | 
184 |   var engine = torrentStream(torrent, xtend(opts, {port: opts.peerPort}))
185 | 
186 |   // Just want torrent-stream to list files.
187 |   if (opts.list) return engine
188 | 
189 |   // Pause/Resume downloading as needed
190 |   engine.on('uninterested', function () {
191 |     engine.swarm.pause()
192 |   })
193 | 
194 |   engine.on('interested', function () {
195 |     engine.swarm.resume()
196 |   })
197 | 
198 |   engine.server = createServer(engine, opts)
199 | 
200 |   // Listen when torrent-stream is ready, by default a random port.
201 |   engine.on('ready', function () {
202 |     engine.server.listen(opts.port || 0, opts.hostname)
203 |   })
204 | 
205 |   engine.listen()
206 | 
207 |   return engine
208 | }
209 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "peerflix",
 3 |   "description": "Streaming torrent client for Node.js",
 4 |   "version": "0.39.0",
 5 |   "author": "Mathias Buus (@mafintosh)",
 6 |   "license": "MIT",
 7 |   "homepage": "https://github.com/mafintosh/peerflix",
 8 |   "main": "index.js",
 9 |   "bugs": {
10 |     "url": "https://github.com/mafintosh/peerflix/issues"
11 |   },
12 |   "repository": {
13 |     "type": "git",
14 |     "url": "git://github.com/mafintosh/peerflix.git"
15 |   },
16 |   "bin": {
17 |     "peerflix": "./app.js"
18 |   },
19 |   "dependencies": {
20 |     "airplayer": "^2.0.0",
21 |     "buffer-from": "^1.0.0",
22 |     "clivas": "^0.2.0",
23 |     "inquirer": "^5.0.1",
24 |     "keypress": "^0.2.1",
25 |     "mime": "^2.2.0",
26 |     "network-address": "^1.1.0",
27 |     "numeral": "^2.0.6",
28 |     "open": "0.0.5",
29 |     "optimist": "^0.6.1",
30 |     "parse-torrent": "^5.4.0",
31 |     "pump": "^2.0.0",
32 |     "range-parser": "^1.0.0",
33 |     "rc": "^1.1.6",
34 |     "torrent-stream": "^1.0.1",
35 |     "winreg": "1.2.4",
36 |     "xtend": "^4.0.0"
37 |   },
38 |   "devDependencies": {
39 |     "standard": "^10.0.3"
40 |   },
41 |   "optionalDependencies": {
42 |     "airplayer": "^2.0.0"
43 |   },
44 |   "scripts": {
45 |     "test": "standard"
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafintosh/peerflix/1b580420671677650ce4fd7d37daeddb58350b22/screenshot.png


--------------------------------------------------------------------------------