├── index.js ├── lib ├── remote │ ├── config.js │ ├── statistic.js │ ├── schedule.js │ └── api.js ├── rank.js ├── actions │ ├── statistic.js │ └── live.js ├── channel_factory.js ├── scene.js ├── channel.js ├── util.js ├── scenes │ ├── rank.js │ ├── live.js │ ├── list.js │ └── statistic.js ├── app.js ├── action.js └── channels │ └── hupu.js ├── img ├── list.png ├── live.png └── statistic.png ├── .gitignore ├── README.md ├── bin └── nbalive.js ├── package.json └── LICENSE /index.js: -------------------------------------------------------------------------------- 1 | var Live = require("./lib/app"); 2 | Live("hupu", null); 3 | -------------------------------------------------------------------------------- /lib/remote/config.js: -------------------------------------------------------------------------------- 1 | exports.REMOTE_SERVER = "http://mangix.me/nba"; 2 | -------------------------------------------------------------------------------- /img/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mangix/nbalive/HEAD/img/list.png -------------------------------------------------------------------------------- /img/live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mangix/nbalive/HEAD/img/live.png -------------------------------------------------------------------------------- /img/statistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mangix/nbalive/HEAD/img/statistic.png -------------------------------------------------------------------------------- /lib/rank.js: -------------------------------------------------------------------------------- 1 | var RankScene = require("./scenes/rank"); 2 | 3 | module.exports = function () { 4 | new RankScene().start(); 5 | }; -------------------------------------------------------------------------------- /lib/actions/statistic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Statistic Action 3 | * */ 4 | 5 | var Action = require("../action"); 6 | 7 | module.exports = function (App, channel, date, gameInfo) { 8 | var action = new Action(); 9 | 10 | action.add("b", "返回", function () { 11 | App.list(channel, date); 12 | }); 13 | 14 | return action; 15 | }; -------------------------------------------------------------------------------- /lib/remote/statistic.js: -------------------------------------------------------------------------------- 1 | var api = require("./api"); 2 | 3 | module.exports = function (game, cb) { 4 | return api({ 5 | url: "/statistic/fetch", 6 | data: { 7 | id: game.id 8 | }, 9 | onSuc: function (data) { 10 | cb(null, data); 11 | }, 12 | onError: function (error) { 13 | cb(error); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/channel_factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Channel Factory 3 | * */ 4 | var channels = ["hupu"]; 5 | var DEFAULT_CHANNEL = channels[0]; 6 | 7 | 8 | var ChannelFactory = { 9 | 10 | create: function (channelName, gameInfo) { 11 | if (!~channels.indexOf(channelName)) { 12 | channelName = DEFAULT_CHANNEL; 13 | } 14 | 15 | return new (require("./channels/" + channelName))(gameInfo); 16 | } 17 | }; 18 | 19 | module.exports = ChannelFactory; -------------------------------------------------------------------------------- /lib/remote/schedule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取某天的赛程 3 | * */ 4 | 5 | var api = require("./api"); 6 | 7 | module.exports = function (channel, date, cb) { 8 | return api({ 9 | url: "/schedule/fetch", 10 | data: { 11 | channel: channel, 12 | date: date || "" 13 | }, 14 | onSuc: function (data) { 15 | cb(null, data); 16 | }, 17 | onError: function (err) { 18 | cb(err); 19 | } 20 | }); 21 | }; -------------------------------------------------------------------------------- /lib/scene.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Scene Base 3 | * */ 4 | var EventEmitter = require("events").EventEmitter; 5 | var _ = require("underscore"); 6 | 7 | var Scene = module.exports = function () { 8 | EventEmitter.call(this); 9 | }; 10 | require("util").inherits(Scene, EventEmitter); 11 | 12 | _.extend(Scene.prototype, { 13 | constructor: Scene, 14 | 15 | /** 16 | * start the scene 17 | * @override 18 | * */ 19 | start: function () { 20 | }, 21 | 22 | /** 23 | * stop the scene 24 | * @override 25 | * */ 26 | stop: function () { 27 | } 28 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | #IDE 31 | .idea 32 | *.iml 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nbalive 2 | 3 | 4 | NBA live in terminal. 5 | 6 | ![rect](https://github.com/mangix/nbalive/blob/master/img/list.png) 7 | ![rect](https://github.com/mangix/nbalive/blob/master/img/live.png) 8 | ![rect](https://github.com/mangix/nbalive/blob/master/img/statistic.png) 9 | 10 | ## Installation 11 | 12 | 先安装[Node.js](http://nodejs.org/download/) ,然后 13 | 14 | $ npm install -g nbalive 15 | 16 | ## Usage 17 | ```bash 18 | Usage: nbalive [options] 19 | 20 | Options: 21 | 22 | -h, --help output usage information 23 | -V, --version output the version number 24 | -d, --date [date] choose date 25 | -r, --rank show rank list 26 | ``` 27 | ## Example 28 | $ nbalive //当天赛程 29 | $ nbalive -d 2014-12-01 //指定某天赛程 30 | $ nbalive -r //查看排名 31 | $ nbalive --help //查看帮助 32 | -------------------------------------------------------------------------------- /lib/actions/live.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Action 3 | * */ 4 | 5 | var Action = require("../action"); 6 | var Statistic = require("../scenes/statistic"); 7 | var Load = require("../util").animateLoad; 8 | module.exports = function (App, channel, date, gameInfo) { 9 | var action = new Action(); 10 | 11 | action.add("b", "返回", function () { 12 | App.list(channel, date); 13 | }); 14 | 15 | action.add("s", "查看当前技术统计", function () { 16 | var statistic = new Statistic(gameInfo); 17 | statistic.start(); 18 | statistic.on("finish", function () { 19 | statistic.stop(); 20 | process.stdin.setRawMode(true); 21 | Load.start(); 22 | }); 23 | }); 24 | 25 | action.on("unknown",function(){ 26 | process.stdin.setRawMode(true); 27 | Load.start(); 28 | }); 29 | 30 | return action; 31 | }; -------------------------------------------------------------------------------- /bin/nbalive.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var program = require('commander'); 3 | var util = require("../lib/util"); 4 | var fs = require("fs"); 5 | var path = require("path"); 6 | 7 | var defaultDate = util.format(new Date()); 8 | var Rank = require('../lib/rank'); 9 | var App = require("../lib/app"); 10 | 11 | program 12 | .version(JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"))).version) 13 | .option('-d, --date [date]', 'choose date', checkDate) 14 | .option('-r, --rank', 'show rank list') 15 | .parse(process.argv); 16 | 17 | if (program.rank) { 18 | Rank(); 19 | } else { 20 | App("hupu", program.date || defaultDate); 21 | } 22 | 23 | 24 | function checkDate(aDate) { 25 | if (/^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}/.test(aDate)) { 26 | return util.format(new Date(aDate)); 27 | } else { 28 | return defaultDate; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbalive", 3 | "version": "0.3.2", 4 | "description": "nba live terminal", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "nba", 11 | "terminal" 12 | ], 13 | "author": "mangix", 14 | "license": "MIT", 15 | "dependencies": { 16 | "cheerio": "^0.18.0", 17 | "commander": "^2.5.0", 18 | "keypress": "^0.2.1", 19 | "request": "^2.50.0", 20 | "term-grid": "^0.1.7", 21 | "term-list": "^0.2.1", 22 | "underscore": "^1.7.0" 23 | }, 24 | "bin": { 25 | "nbalive": "./bin/nbalive.js" 26 | }, 27 | "devDependencies": {}, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/mangix/nbalive.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/mangix/nbalive/issues" 34 | }, 35 | "homepage": "https://github.com/mangix/nbalive" 36 | } 37 | -------------------------------------------------------------------------------- /lib/remote/api.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var SERVER = require("./config").REMOTE_SERVER; 3 | var queryString = require("querystring"); 4 | var objToQuery = function (obj) { 5 | var qs = queryString.stringify(obj); 6 | 7 | return qs ? "?" + qs : ""; 8 | 9 | }; 10 | var NOOP = function () { 11 | }; 12 | 13 | module.exports = function (options) { 14 | if (!options.url) { 15 | return; 16 | } 17 | var onSuc = options.onSuc || NOOP; 18 | var onError = options.onError || NOOP; 19 | 20 | var url = SERVER + options.url + objToQuery(options.data); 21 | 22 | return request(url, function (errors, response, body) { 23 | if (!errors && response.statusCode == 200) { 24 | var data; 25 | try { 26 | data = JSON.parse(body).data; 27 | } catch (e) { 28 | onError(new Error("api error")); 29 | } 30 | onSuc(data); 31 | } else { 32 | onError(new Error("api error")); 33 | } 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | * */ 4 | var EventEmitter = require("events").EventEmitter; 5 | var util = require("util"); 6 | var _ = require("underscore"); 7 | 8 | /** 9 | * Channel Base Class 10 | * Channel is an EventEmitter 11 | * @param gameInfo{Object} game info object 12 | * { 13 | * gameId:"", 14 | * host:"", 15 | * visiting:"" 16 | * } 17 | * */ 18 | var Channel = function (gameInfo) { 19 | this.gameInfo = gameInfo; 20 | EventEmitter.call(this); 21 | }; 22 | util.inherits(Channel, EventEmitter); 23 | 24 | 25 | _.extend(Channel.prototype, { 26 | constructor: Channel, 27 | /** 28 | * @override 29 | * start live 30 | * and should emit the 'data' event when fetched data each time 31 | * data formats like { 32 | * time:"", 33 | * team:"", 34 | * content:"", 35 | * score:"" 36 | * } 37 | * */ 38 | startLive: function () { 39 | }, 40 | /** 41 | * @override 42 | * stop Live 43 | * */ 44 | 45 | stopLive: function () { 46 | 47 | } 48 | }); 49 | 50 | 51 | module.exports = Channel; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 mangix 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 | 23 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * filter html tag in string 3 | * */ 4 | exports.escape = function (str) { 5 | return str ? str.replace(/<[/]?\w+>/g, "") : "" 6 | }; 7 | 8 | 9 | /** 10 | * cell 11 | * */ 12 | exports.cell = function (content, width, align) { 13 | var len = countCharacters(content); 14 | if (len >= width) { 15 | return content; 16 | } 17 | 18 | function countCharacters(str) { 19 | var totalCount = 0; 20 | for (var i = 0; i < str.length; i++) { 21 | var c = str.charCodeAt(i); 22 | if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) { 23 | totalCount++; 24 | } 25 | else { 26 | totalCount += 2; 27 | } 28 | } 29 | return totalCount; 30 | } 31 | 32 | function empty(size) { 33 | return size <= 1 ? " " : (new Array(size)).join(" "); 34 | } 35 | 36 | switch (align) { 37 | case "right" : 38 | return empty(width - len) + content; 39 | case "center": 40 | return empty(Math.floor((width - len) / 2)) + content + empty(Math.round((width - len) / 2)); 41 | default : 42 | return content + empty(width - len); 43 | } 44 | }; 45 | 46 | var fixZero = function (num) { 47 | return num < 10 ? "0" + num : num; 48 | }; 49 | /** 50 | * date format 51 | * */ 52 | exports.format = function (date) { 53 | return date.getFullYear() + "-" + fixZero(date.getMonth() + 1) + "-" + fixZero(date.getDate()); 54 | }; 55 | 56 | exports.loading = function (text) { 57 | console.log("\033[35m" + text + "\033[0m"); 58 | }; 59 | 60 | exports.animateLoad = { 61 | timer: null, 62 | start: function () { 63 | this.stop(); 64 | var chars = ["|", "/", "-","\\"]; 65 | var count = 0; 66 | var out = process.stdout; 67 | this.timer = setInterval(function () { 68 | out.write("\b\033[K"); 69 | out.write(chars[count]); 70 | count++; 71 | count = (count > 3)? (count-4):count; 72 | }, 80); 73 | }, 74 | stop: function () { 75 | clearInterval(this.timer); 76 | process.stdout.write("\b\033[K"); 77 | } 78 | }; 79 | 80 | -------------------------------------------------------------------------------- /lib/scenes/rank.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var cheerio = require("cheerio"); 3 | var Grid = require("term-grid"); 4 | 5 | var Scene = require("../scene"); 6 | var util = require("util"); 7 | var _ = require("underscore"); 8 | 9 | var URL = "http://g.hupu.com/nba/standing"; 10 | 11 | 12 | var RankScene = module.exports = function () { 13 | Scene.call(this); 14 | }; 15 | 16 | util.inherits(RankScene, Scene); 17 | 18 | _.extend(RankScene.prototype, { 19 | 20 | start: function () { 21 | var self = this; 22 | this.api = request(URL, function (error, response, body) { 23 | if (!error && response && response.statusCode == 200) { 24 | var $ = cheerio.load(body); 25 | var twoTables = [ 26 | [], 27 | [] 28 | ]; 29 | var index = -1; 30 | $('.rank_data .players_table tr').each(function (i, tr) { 31 | tr = $(tr); 32 | if (tr.hasClass('linglei')) { 33 | index++; 34 | } 35 | if (index < 0) { 36 | return; 37 | } 38 | var row = []; 39 | twoTables[index].push(row); 40 | tr.find("td").each(function (j, td) { 41 | row.push($(td).text().trim().replace("\n", "")); 42 | }); 43 | }); 44 | 45 | self.draw(twoTables); 46 | 47 | } else { 48 | console.log("data error"); 49 | } 50 | 51 | }); 52 | }, 53 | stop: function () { 54 | this.api && this.api.abort(); 55 | }, 56 | draw: function (data) { 57 | data.forEach(function (table) { 58 | var grid = new Grid(table); 59 | grid.setColor(function (content, i, j) { 60 | if (i == 0) { 61 | return "magenta"; 62 | } 63 | if (i == 1) { 64 | return "yellow"; 65 | } 66 | }); 67 | grid.setWidth(1, 20); 68 | var count = grid.getColumnCount(); 69 | for (var i = 2; i < count; i++) { 70 | grid.setAlign(i,"center"); 71 | } 72 | grid.draw(); 73 | }); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | var ListScene = require("./scenes/list"); 2 | var LiveScene = require("./scenes/live"); 3 | var StatisticScene = require("./scenes/statistic"); 4 | var LiveAction = require("./actions/live"); 5 | var StatisticAction = require("./actions/statistic"); 6 | 7 | var loading = require("./util").loading; 8 | 9 | 10 | var App = module.exports = function (channel, date) { 11 | App.list(channel, date); 12 | }; 13 | 14 | /** 15 | * switch scene 16 | * stop the current scene and start the incoming scene 17 | * */ 18 | App.switchScene = function (scene) { 19 | this.currentScene && this.currentScene.stop(); 20 | this.currentScene = scene; 21 | scene.start(); 22 | }; 23 | 24 | /** 25 | * switch action 26 | * stop the current action and start the incoming action 27 | * */ 28 | App.switchAction = function (action) { 29 | this.currentAction && this.currentAction.stop(); 30 | this.currentAction = action; 31 | action && action.start(); 32 | }; 33 | 34 | /** 35 | * initial 36 | * */ 37 | App.currentScene = null; 38 | App.currentAction = null; 39 | 40 | /** 41 | * live 42 | * */ 43 | App.live = function (channel, date, gameInfo) { 44 | var scene = new LiveScene(channel, gameInfo); 45 | App.switchScene(scene); 46 | 47 | var action = LiveAction(App, channel, date, gameInfo); 48 | App.switchAction(action); 49 | 50 | scene.channel.once("data", action.info.bind(action)); 51 | 52 | loading("loading live data..."); 53 | }; 54 | 55 | /** 56 | * statistic 57 | * */ 58 | App.statistic = function (channel, date, gameInfo) { 59 | var scene = new StatisticScene(gameInfo); 60 | App.switchScene(scene); 61 | 62 | var action = StatisticAction(App, channel, date, gameInfo); 63 | App.switchAction(action); 64 | 65 | scene.on("finish", action.info.bind(action)); 66 | loading("loading statistic..."); 67 | }; 68 | 69 | /** 70 | * game list 71 | * */ 72 | App.list = function (channel, date) { 73 | loading("loading game info.."); 74 | 75 | var listScene = new ListScene(channel, date); 76 | 77 | App.switchScene(listScene); 78 | App.switchAction(null); 79 | 80 | listScene.on('choose', function (gameInfo) { 81 | switch (gameInfo.status) { 82 | case 1: 83 | App.live(channel, date, gameInfo); 84 | break; 85 | case 2: 86 | App.statistic(channel, date, gameInfo); 87 | break; 88 | } 89 | }); 90 | }; 91 | 92 | 93 | -------------------------------------------------------------------------------- /lib/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Action 3 | * commander for Scene 4 | * */ 5 | var Grid = require("term-grid"); 6 | var EventEmitter = require("events").EventEmitter; 7 | var util = require("util"); 8 | var _ = require("underscore"); 9 | 10 | var Action = function (prefix) { 11 | this.actions = {}; 12 | this.prefix = prefix || "/"; 13 | this.parse = this.parse.bind(this); 14 | EventEmitter.call(this); 15 | }; 16 | util.inherits(Action, EventEmitter); 17 | 18 | _.extend(Action.prototype, { 19 | /** 20 | * @param name 21 | * @param params .... 22 | * @param description 23 | * @param action 24 | * */ 25 | add: function () { 26 | var actionName , action, desc, params = []; 27 | if (arguments.length < 3) { 28 | return; 29 | } 30 | 31 | actionName = arguments[0]; 32 | action = arguments[arguments.length - 1]; 33 | desc = arguments[arguments.length - 2]; 34 | 35 | params = Array.prototype.slice.call(arguments, 1, arguments.length - 2); 36 | if (actionName) { 37 | this.actions[this.prefix + actionName] = { 38 | params: params, 39 | action: action, 40 | desc: desc 41 | } 42 | } 43 | }, 44 | parse: function (commander) { 45 | commander = commander.toString().trim(); 46 | if (typeof commander !== "string" || !commander || commander == this.prefix) { 47 | return; 48 | } 49 | var args = commander.split(/\s/); 50 | var actionName = args[0]; 51 | var params = args.slice(1); 52 | 53 | if (actionName && actionName[0] != this.prefix) { 54 | actionName = this.prefix + actionName; 55 | } 56 | 57 | if (actionName && actionName in this.actions) { 58 | var action = this.actions[actionName]; 59 | // match a command 60 | var param = {}; 61 | var paramNames = action.params; 62 | paramNames.forEach(function (name, i) { 63 | param[name] = params[i]; 64 | }); 65 | action.action.call(this, param); 66 | } else { 67 | console.error("\033[031munknown command: " + commander + "\033[0m"); 68 | this.info(); 69 | this.emit("unknown"); 70 | } 71 | }, 72 | start: function () { 73 | process.stdin.on("data", this.parse); 74 | process.stdin.resume(); 75 | }, 76 | stop: function () { 77 | process.stdin.removeListener("data", this.parse); 78 | }, 79 | info: function () { 80 | console.log("\033[031m您可以输入:\033[0m"); 81 | var actions = this.actions; 82 | var list = []; 83 | Object.keys(this.actions).forEach(function (action) { 84 | list.push([action + " " + actions[action].params.map(function (param) { 85 | return "{" + param + "}"; 86 | }) , actions[action].desc ]); 87 | }); 88 | 89 | var grid = new Grid(list); 90 | grid.setColor("yellow"); 91 | grid.draw(); 92 | 93 | } 94 | }); 95 | 96 | module.exports = Action; -------------------------------------------------------------------------------- /lib/scenes/live.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Scene 3 | * */ 4 | 5 | var util = require("util"); 6 | var Scene = require("../scene"); 7 | var _ = require("underscore"); 8 | var Grid = require("term-grid"); 9 | 10 | var ChannelFactory = require("../channel_factory"); 11 | 12 | var Load = require("../util").animateLoad; 13 | 14 | var LiveScene = module.exports = function (channelName, gameInfo) { 15 | Scene.call(this); 16 | this.gameInfo = gameInfo; 17 | this.channelName = channelName; 18 | 19 | this.typing = false; 20 | this.initGrid(); 21 | this.initChannel(); 22 | require("keypress")(process.stdin); 23 | this.onKeyPress = this.onKeyPress.bind(this); 24 | }; 25 | 26 | util.inherits(LiveScene, Scene); 27 | 28 | _.extend(LiveScene.prototype, { 29 | onKeyPress: function (ch, key) { 30 | if (ch == "/") { 31 | this.stopLoading(); 32 | this.typing = true; 33 | process.stdout.write("/"); 34 | } 35 | if (key) { 36 | switch (key.name) { 37 | case 'c': 38 | key.ctrl && process.exit(0); 39 | break; 40 | } 41 | } 42 | }, 43 | initGrid: function () { 44 | var grid = this.grid = new Grid(); 45 | grid.setWidth([8, 20]); 46 | grid.setColor(1, "green"); 47 | }, 48 | initChannel: function () { 49 | var channel = this.channel = ChannelFactory.create(this.channelName, this.gameInfo); 50 | var grid = this.grid; 51 | var gameInfo = this.gameInfo; 52 | var self = this; 53 | channel.on('data', function (res) { 54 | self.stopLoading(); 55 | res.forEach(function (data) { 56 | var content = data.content , team = data.team; 57 | if (content && team) { 58 | if (content.indexOf(team) == -1) { 59 | content = team + content; 60 | } 61 | } 62 | grid.appendRow([data.time || "", data.score ? gameInfo.host + " " + data.score + " " + gameInfo.visiting : "", content], true); 63 | }); 64 | if (!self.typing) { 65 | self.loading(); 66 | } 67 | }); 68 | 69 | channel.on("over", function () { 70 | Load.stop(); 71 | process.stdout.write("game over \n"); 72 | }); 73 | 74 | channel.on("error", function () { 75 | process.stdout.write("connection error, retry... \n"); 76 | }); 77 | }, 78 | start: function () { 79 | var self = this; 80 | this.loading(); 81 | this.channel.startLive(); 82 | process.stdin.on('keypress', this.onKeyPress); 83 | process.stdin.on("data", function () { 84 | self.typing = false; 85 | }) 86 | }, 87 | stop: function () { 88 | this.channel.stopLive(); 89 | process.stdin.removeListener("keypress", this.onKeyPress); 90 | Load.stop(); 91 | }, 92 | loading: function () { 93 | process.stdin.setRawMode(true); 94 | Load.start(); 95 | }, 96 | stopLoading: function () { 97 | Load.stop(); 98 | process.stdin.setRawMode(false); 99 | } 100 | }); -------------------------------------------------------------------------------- /lib/scenes/list.js: -------------------------------------------------------------------------------- 1 | var List = require('term-list'); 2 | var Grid = require('term-grid'); 3 | var cell = require("./../util").cell; 4 | var Scene = require("../scene"); 5 | var util = require("util"); 6 | var _ = require("underscore"); 7 | 8 | var schedule = require("../remote/schedule"); 9 | 10 | function fixScore(score) { 11 | var sc = score.split("-"); 12 | sc[0] = cell(sc[0], 3, "right"); 13 | sc[1] = cell(sc[1], 3, "left"); 14 | console.log(sc[0]); 15 | return sc.join(" - "); 16 | } 17 | 18 | var MAP = { 19 | 0: "SOON", 20 | 2: "END", 21 | 1: "LIVE" 22 | }; 23 | var COLOR = { 24 | 0: "yellow", 25 | 2: "red", 26 | 1: "green" 27 | }; 28 | /** 29 | * List scene 30 | * @param channel{String} 31 | * @param date{String} 32 | * */ 33 | var ListScene = module.exports = function (channel, date) { 34 | this.channel = channel; 35 | this.date = date; 36 | Scene.call(this); 37 | }; 38 | 39 | util.inherits(ListScene, Scene); 40 | 41 | _.extend(ListScene.prototype, { 42 | initGrid: function () { 43 | var games = this.games; 44 | var grid = this.grid = new Grid(games.map(function (game) { 45 | return [ 46 | MAP[game.status] || "", 47 | game.time, 48 | game.host, 49 | fixScore(game.score), 50 | game.visiting 51 | ]; 52 | })); 53 | grid.setWidth([6, 8, 10, 20]); 54 | grid.setAlign(["", "left", "right", "center"]); 55 | grid.setColor(function (content, i, j) { 56 | if (j == 0) { 57 | return COLOR[games[i].status]; 58 | } else if (games[i].status == 2) { 59 | var game = games[i]; 60 | if ((game.hostScore > game.visitScore && j == 2 ) || (game.hostScore < game.visitScore && j == 4)) { 61 | return "cyan"; 62 | 63 | } 64 | 65 | } 66 | }); 67 | }, 68 | 69 | initList: function () { 70 | var games = this.games; 71 | var list = this.list = new List({ marker: '\033[36m› \033[0m', markerLength: 2 }); 72 | var self = this; 73 | list.on('keypress', function (key, item) { 74 | if (key.name == "return") { 75 | var gameInfo = games.filter(function (game) { 76 | return game.id == item; 77 | })[0]; 78 | self.emit("choose", gameInfo); 79 | } 80 | }); 81 | this.grid.compile().forEach(function (game, i) { 82 | list.add(games[i].id, game); 83 | }); 84 | }, 85 | start: function () { 86 | var self = this; 87 | this.api = schedule(this.channel, this.date, function (error, games) { 88 | if (error) { 89 | console.log(error); 90 | return; 91 | } 92 | if (!games.length) { 93 | console.log("no games this day"); 94 | return; 95 | } 96 | self.games = games; 97 | self.initGrid(); 98 | self.initList(); 99 | self.list.start(); 100 | }); 101 | }, 102 | stop: function () { 103 | this.list && this.list.stop(); 104 | this.api && this.api.abort(); 105 | } 106 | }); 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /lib/scenes/statistic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Statistic Scene 3 | * */ 4 | 5 | var util = require("util"); 6 | var Scene = require("../scene"); 7 | var _ = require("underscore"); 8 | var getStatistic = require("../remote/statistic"); 9 | var Grid = require("term-grid"); 10 | 11 | var StatisticScene = module.exports = function (gameInfo) { 12 | Scene.call(this); 13 | this.gameInfo = gameInfo; 14 | }; 15 | 16 | util.inherits(StatisticScene, Scene); 17 | 18 | _.extend(StatisticScene.prototype, { 19 | start: function () { 20 | process.stdin.setRawMode(false); 21 | var gameInfo = this.gameInfo; 22 | var self = this; 23 | this.api = getStatistic(gameInfo, function (error, data) { 24 | if (error) { 25 | console.log(error); 26 | } else { 27 | self.createGrid(data); 28 | 29 | self.createGridBaseInfo(); 30 | self.configGridColor(); 31 | self.configGridAlign(); 32 | 33 | self.grid.draw(); 34 | self.emit("finish"); 35 | } 36 | }); 37 | }, 38 | stop: function () { 39 | this.api && this.api.abort(); 40 | }, 41 | createGrid: function (data) { 42 | this.grid = new Grid(data); 43 | }, 44 | createGridBaseInfo: function () { 45 | var grid = this.grid; 46 | var data = grid.data; 47 | 48 | //column count 49 | var count = this.gridColumnCount = grid.getColumnCount(); 50 | 51 | //find max data in each column 52 | var visitingStartRow = 0; 53 | var homeMax = this.homeMax = []; 54 | var visitMax = this.visitMax = []; 55 | data.forEach(function (row, i) { 56 | if (row[0] && ~row[0].toString().indexOf("客队")) { 57 | visitingStartRow = i; 58 | } 59 | }); 60 | for (var i = 0; i < count; i++) { 61 | data.forEach(function (row, j) { 62 | var arr; 63 | var maxRow; 64 | if (i < row.length && /^[\+|-]?\d+$/.test(row[i])) { 65 | if (j < visitingStartRow) { 66 | arr = homeMax; 67 | maxRow = visitingStartRow - 2; 68 | } else { 69 | arr = visitMax; 70 | maxRow = data.length - 2; 71 | } 72 | if (j < maxRow && (arr[i] === undefined || parseInt(row[i]) > parseInt(arr[i]))) { 73 | arr[i] = row[i]; 74 | } 75 | } 76 | }); 77 | } 78 | this.visitingStartRow = visitingStartRow; 79 | }, 80 | configGridColor: function () { 81 | var grid = this.grid; 82 | 83 | var visitingStartRow = this.visitingStartRow; 84 | var homeMax = this.homeMax; 85 | var visitMax = this.visitMax; 86 | 87 | //config color 88 | grid.setColor(function (content, row, column) { 89 | if (column == 0) { 90 | if (/主队|客队/.test(content)) { 91 | return "magenta"; 92 | } else if (/首发|替补/.test(content)) { 93 | return "yellow"; 94 | } 95 | } else { 96 | if (/首发|时间|投篮|3分|罚球|前场|后场|篮板|助攻|犯规|抢断|失误|封盖|得分|(\+\/-)/.test(content)) { 97 | return "yellow"; 98 | } else { 99 | var targetMax; 100 | if (row < visitingStartRow) { 101 | targetMax = homeMax; 102 | } else { 103 | targetMax = visitMax; 104 | } 105 | if (content === targetMax[column]) { 106 | return "green"; 107 | } 108 | } 109 | } 110 | }); 111 | }, 112 | configGridAlign: function () { 113 | //config align 114 | for (var i = 2; i < this.gridColumnCount; i++) { 115 | this.grid.setAlign(i, "right"); 116 | } 117 | } 118 | }); 119 | -------------------------------------------------------------------------------- /lib/channels/hupu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * hupu live 3 | * */ 4 | 5 | var request = require("request"); 6 | 7 | var util = require("util"); 8 | var Channel = require("../channel"); 9 | var HOST = "http://g.hupu.com/"; 10 | 11 | var cheerio = require("cheerio"); 12 | 13 | var ChannelHupu = module.exports = function (gameInfo) { 14 | this.channel = "hupu"; 15 | this.sid = 0; 16 | this.s_count = 0; 17 | 18 | this.lastFetchTime = +new Date(); 19 | this.fetchLag = 10000; 20 | Channel.call(this, gameInfo); 21 | 22 | }; 23 | 24 | util.inherits(ChannelHupu, Channel); 25 | 26 | ChannelHupu.prototype.startLive = function () { 27 | this.live(); 28 | }; 29 | ChannelHupu.prototype.stopLive = function () { 30 | clearTimeout(this.timer); 31 | this.request.abort(); 32 | }; 33 | 34 | ChannelHupu.prototype.live = function () { 35 | var self = this; 36 | self.lastFetchTime = +new Date(); 37 | self.request = self._buildLiveRequest(function (res) { 38 | if (self.isOver(res)) { 39 | self.emit("over"); 40 | } else { 41 | self._parseLiveData(res, self.lagLive); 42 | } 43 | }); 44 | }; 45 | ChannelHupu.prototype.lagLive = function () { 46 | var self = this; 47 | if (+new Date() - self.lastFetchTime > self.fetchLag) { 48 | self.live(); 49 | } else { 50 | self.timer = setTimeout(function () { 51 | self.live(); 52 | }, self.fetchLag - ( +new Date() - self.lastFetchTime)); 53 | } 54 | }; 55 | 56 | /** 57 | * is live finished? 58 | * */ 59 | ChannelHupu.prototype.isOver = function (res) { 60 | return res === "over"; 61 | }; 62 | 63 | /** 64 | * resolve the http response 65 | * @param res{String} http response body 66 | * @param cb{Function} called when finished 67 | * */ 68 | ChannelHupu.prototype._parseLiveData = function (res, cb) { 69 | var self = this; 70 | var $ = cheerio.load("" + res + "
"); 71 | 72 | var trs = $("tr"); 73 | var newSid , datas = []; 74 | if (trs.length) { 75 | newSid = trs.eq(0).attr("sid"); 76 | if (newSid && newSid != self.sid) { 77 | self.sid = newSid; 78 | self.s_count += trs.length; 79 | trs.each(function (i, tr) { 80 | var tds = $(tr).find("td"); 81 | if (tds.length == 4) { 82 | datas.unshift({ 83 | time: $(tds[0]).text(), 84 | team: $(tds[1]).text(), 85 | content: $(tds[2]).text(), 86 | score: $(tds[3]).text() 87 | }); 88 | } else if (tds.length == 1) { 89 | datas.unshift({ 90 | content: $(tds[0]).text() 91 | }); 92 | } 93 | }); 94 | // datas.forEach(function (data) { 95 | self.emit('data', datas); 96 | // }); 97 | } 98 | }else{ 99 | self.emit('empty'); 100 | } 101 | cb.call(self); 102 | }; 103 | 104 | /** 105 | * build http request to hupu live 106 | * @param cb{Function} called when request success 107 | * */ 108 | ChannelHupu.prototype._buildLiveRequest = function (cb) { 109 | var self = this; 110 | return request({ 111 | url: this._buildLiveRequestUrl(), 112 | headers: { 113 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53' 114 | } 115 | }, function (error, response, body) { 116 | if (error) { 117 | self.emit("error", error); 118 | self.lagLive(); 119 | } else { 120 | if (response.statusCode == 200) { 121 | cb(body); 122 | } else { 123 | self.emit("error", new Error("remote error " + response.statusCode)); 124 | self.lagLive(); 125 | } 126 | } 127 | }); 128 | }; 129 | 130 | 131 | /** 132 | * build hupu live url 133 | * @return {String} 134 | * */ 135 | ChannelHupu.prototype._buildLiveRequestUrl = function () { 136 | var gameInfo = this.gameInfo; 137 | var data = { 138 | sid: this.sid, 139 | s_count: this.s_count, 140 | match_id: gameInfo.gameId, 141 | homeTeamName: encodeURIComponent(gameInfo.host), 142 | awayTeamName: encodeURIComponent(gameInfo.visiting) 143 | }; 144 | var url = HOST + "/node/playbyplay/matchLives" + Object.keys(data).reduce(function (sum, current) { 145 | return sum + current + "=" + data[current] + "&"; 146 | }, "?"); 147 | 148 | return url; 149 | }; 150 | --------------------------------------------------------------------------------