From e4a22d89e4d1c83e24573aece88e10087669af0f Mon Sep 17 00:00:00 2001 From: Simon Kusterer Date: Mon, 20 Oct 2014 20:19:09 +0200 Subject: [PATCH] initial commit --- .gitignore | 4 +++ LICENSE | 21 ++++++++++++ README.md | 35 +++++++++++++++++++ index.js | 82 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 42 +++++++++++++++++++++++ plugins/localfile.js | 25 ++++++++++++++ plugins/torrent.js | 18 ++++++++++ plugins/youtube.js | 38 ++++++++++++++++++++ 8 files changed, 265 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 index.js create mode 100644 package.json create mode 100644 plugins/localfile.js create mode 100644 plugins/torrent.js create mode 100644 plugins/youtube.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd5275b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +node_modules/ +.idea/ +test.mp4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..061c5bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Simon Kusterer + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cc265d --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# castnow + +castnow is commandline utility which can be used to playback media files on +your chromecast device. It supports playback of local video files, youtube +clips, videos on the web and torrents. +You can toggle beween play and pause with the space-key. + +### Usage + +``` + +// start playback of a local video file +castnow ./myvideo.mp4 + +// start playback of some mp4 file over the web +castnow http://commondatastorage.googleapis.com/gtv-videos-bucket/ED_1280.mp4 + +// start playback of some youtube clip +castnow https://www.youtube.com/watch?v=pcVRrlmpcWk + +// start playback of some video over torrent +castnow --torrent + +// re-attach to an currently running playback session +castnow + +``` + +### Installation + +`npm install castnow -g` + +## License +Copyright (c) 2014 Simon Kusterer +Licensed under the MIT license. diff --git a/index.js b/index.js new file mode 100755 index 0000000..9ba484b --- /dev/null +++ b/index.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +var player = require('chromecast-player')(); +var opts = require('minimist')(process.argv.slice(2)); +var chalk = require('chalk'); +var keypress = require('keypress'); +var log = require('single-line-log').stdout; + +// Plugins +var localfile = require('./plugins/localfile'); +var torrent = require('./plugins/torrent'); +var youtube = require('./plugins/youtube'); + +if (opts._.length) { + opts.path = opts._[0]; +} + +delete opts._; + +var ctrl = function(err, p, ctx) { + if (err) { + console.log(chalk.red(err)); + process.exit(); + } + + keypress(process.stdin); + process.stdin.setRawMode(true); + process.stdin.resume(); + + var isPlaying = function() { + return p.currentSession.playerState === 'PLAYING'; + }; + + process.stdin.on('keypress', function(ch, key) { + if (key.name === 'space') { + if (isPlaying()) { + p.pause(); + } else { + p.play(); + } + } + if (key && key.ctrl && key.name == 'c') { + process.exit(); + } + }); + +}; + +var circulate = function(arr) { + var len = arr.length, pos = -1; + return !len ? void 0 : function() { + return arr[pos = ++pos % len]; + } +}; + +var logState = (function() { + var inter; + var dots = circulate(['.', '..', '...', '....']); + return function(status) { + if (inter) clearInterval(inter); + inter = setInterval(function() { + log(chalk.grey('player status: ') + chalk.green(status + dots()) + "\n"); + }, 300); + }; +})(); + +player.use(function(ctx, next) { + ctx.on('status', logState) + next(); +}); + +player.use(torrent); +player.use(localfile); +player.use(youtube); + +if (!opts.path) { + player.attach(opts, ctrl); +} else { + player.launch(opts, ctrl); +} + +module.exports = player; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7d9d265 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "castnow", + "version": "0.0.1", + "description": "commandline chromecast player", + "main": "index.js", + "bin": { + "castnow": "./index.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Simon Kusterer", + "license": "MIT", + "repository": { + "type": "git", + "url": "git@github.com:xat/castnow.git" + }, + "keywords": [ + "chromecast", + "media", + "player", + "video", + "torrent", + "peerflix", + "commandline", + "cast" + ], + "dependencies": { + "castv2-client": "0.0.6", + "chalk": "^0.5.1", + "chromecast-player": "0.1.0", + "get-port": "^1.0.0", + "get-youtube-id": "^0.1.3", + "internal-ip": "^1.0.0", + "keypress": "^0.2.1", + "minimist": "^1.1.0", + "peercast": "^1.1.2", + "peerflix": "^0.19.1", + "read-torrent": "^1.0.0", + "single-line-log": "^0.4.1" + } +} diff --git a/plugins/localfile.js b/plugins/localfile.js new file mode 100644 index 0000000..6e81dc8 --- /dev/null +++ b/plugins/localfile.js @@ -0,0 +1,25 @@ +var http = require('http'); +var getPort = require('get-port'); +var internalIp = require('internal-ip'); +var fs = require('fs'); + +var isFile = function(path) { + return fs.existsSync(path) && fs.statSync(path).isFile(); +}; + +var localfile = function(ctx, next) { + if (ctx.mode === 'attach') return next(); + if (!isFile(ctx.options.path)) return next(); + var filePath = ctx.options.path; + + getPort(function(err, port) { + ctx.options.path = 'http://' + internalIp() + ':' + port; + ctx.options.type = 'video/mp4'; + http.createServer(function(req, res) { + fs.createReadStream(filePath).pipe(res); + }).listen(port); + next(); + }); +}; + +module.exports = localfile; diff --git a/plugins/torrent.js b/plugins/torrent.js new file mode 100644 index 0000000..f2dec69 --- /dev/null +++ b/plugins/torrent.js @@ -0,0 +1,18 @@ +var readTorrent = require('read-torrent'); +var peerflix = require('peerflix'); +var internalIp = require('internal-ip'); + +var torrent = function(ctx, next) { + if (ctx.mode === 'attach' || !ctx.options.torrent) return next(); + readTorrent(ctx.options.path, function(err, torrent) { + if (err) return next(); + var engine = peerflix(torrent); + engine.server.once('listening', function() { + ctx.options.path = 'http://'+internalIp()+':'+engine.server.address().port; + ctx.options.type = 'video/mp4'; + next(); + }); + }); +}; + +module.exports = torrent; diff --git a/plugins/youtube.js b/plugins/youtube.js new file mode 100644 index 0000000..f49c9fa --- /dev/null +++ b/plugins/youtube.js @@ -0,0 +1,38 @@ +var Api = require('chromecast-player').api; +var castv2Cli = require('castv2-client'); +var RequestResponseController = castv2Cli.RequestResponseController; +var inherits = require('util').inherits; +var getYouTubeId = require('get-youtube-id'); + +var Yt = function() { + Api.apply(this, arguments); + this.ytreq = this.createController(RequestResponseController, + 'urn:x-cast:com.google.youtube.mdx'); +}; + +Yt.APP_ID = '233637DE'; + +inherits(Yt, Api); + +Yt.prototype.load = function(opts, cb) { + var opts = { + type: 'flingVideo', + data: { + currentTime: 0, + videoId: opts.path + } + }; + this.ytreq.request(opts); + cb(); +}; + +var youtube = function(ctx, next) { + if (ctx.mode === 'attach') return next(); + var id = getYouTubeId(ctx.options.path); + if (!id) return next(); + ctx.api = Yt; + ctx.options.path = id; + next(); +}; + +module.exports = youtube;