diff --git a/README.md b/README.md index 498717ec..7d70cb88 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,6 @@ Things that still have to be implemented or changed. ####Helper scripts - Only start helper scripts of modules that are actually loaded in the UI (config.js) - Notification system, so that not every helper scripts needs it's own socket to the UI. +- `modules/newsfeed/node_helper.js` now spawns it's own epxress webserver on port 8080. We need to create a solution for every module that needs a server side url. diff --git a/config/config.js.sample b/config/config.js.sample index 25565b70..9e301459 100644 --- a/config/config.js.sample +++ b/config/config.js.sample @@ -38,10 +38,11 @@ var config = { } }, { - module: 'helloworld', + module: 'newsfeed', position: 'bottom_bar', config: { - text: 'Magic Mirror V2' + feedUrl: 'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml', + showPublishDate: true, } }, ] diff --git a/modules/newsfeed/newsfeed.js b/modules/newsfeed/newsfeed.js new file mode 100644 index 00000000..f4bbccd7 --- /dev/null +++ b/modules/newsfeed/newsfeed.js @@ -0,0 +1,150 @@ +/* global Module */ + +/* Magic Mirror + * Module: NewsFeed + * + * By Michael Teeuw http://michaelteeuw.nl + * MIT Licensed. + */ + +Module.create({ + + // Default module config. + defaults: { + feedUrl: 'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml', + showPublishDate: true, + reloadInterval: 10 * 60 * 1000, // every 10 minutes + updateInterval: 7.5 * 1000, + animationSpeed: 2.5 * 1000, + + + proxyUrl: 'http://localhost:8080/?url=', + initialLoadDelay: 0, // 5 seconds delay. This delay is used to keep the OpenWeather API happy. + retryDelay: 2500, + }, + + // Define required scripts. + getScripts: function() { + return ['moment.js']; + }, + + // Define start sequence. + start: function() { + Log.info('Starting module: ' + this.name); + + // Set locale. + moment.locale(config.language); + + this.newsItems = []; + this.loaded = false; + this.scheduleFetch(this.config.initialLoadDelay); + + this.fetchTimer = null; + this.activeItem = 0; + }, + + // Override dom generator. + getDom: function() { + var wrapper = document.createElement("div"); + + if (this.activeItem >= this.newsItems.length) { + this.activeItem = 0; + } + + if (this.newsItems.length > 0) { + + if (this.config.showPublishDate) { + var timestamp = document.createElement("div"); + timestamp.className = "light small dimmed"; + timestamp.innerHTML = this.capitalizeFirstLetter(moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow() + ':'); + wrapper.appendChild(timestamp); + } + + var title = document.createElement("div"); + title.className = "bright medium light"; + title.innerHTML = this.newsItems[this.activeItem].title; + wrapper.appendChild(title); + + } else { + wrapper.innerHTML = "Loading news ..."; + wrapper.className = "small dimmed"; + } + + return wrapper; + }, + + /* fetchNews(compliments) + * Requests new data from news proxy. + */ + fetchNews: function() { + var url = this.config.proxyUrl + encodeURIComponent(this.config.feedUrl); + var self = this; + + var newsRequest = new XMLHttpRequest(); + newsRequest.open("GET", url, true); + newsRequest.onreadystatechange = function() { + if(this.readyState === 4) { + if(this.status === 200) { + self.newsItems = JSON.parse(this.response); + + if (!self.loaded) { + self.scheduleUpdateInterval(); + } + + self.loaded = true; + } else { + Log.error(self.name + ": Could not load news."); + } + + self.scheduleFetch((self.loaded) ? -1 : self.config.retryDelay); + + } + }; + newsRequest.send(); + }, + + /* scheduleUpdateInterval() + * Schedule visual update. + */ + scheduleUpdateInterval: function() { + var self = this; + + self.updateDom(self.config.animationSpeed); + + setInterval(function() { + self.activeItem++; + self.updateDom(self.config.animationSpeed); + }, this.config.updateInterval); + }, + + /* scheduleFetch() + * Schedule next news fetch. + * + * argument delay number - Milliseconds before next update. If empty, this.config.reloadInterval is used. + */ + scheduleFetch: function(delay) { + var nextLoad = this.config.reloadInterval; + if (typeof delay !== 'undefined' && delay >= 0) { + nextLoad = delay; + } + + var self = this; + clearTimeout(this.fetchTimer); + this.fetchTimer = setTimeout(function() { + self.fetchNews(); + }, nextLoad); + }, + + /* capitalizeFirstLetter(string) + * Capitalizes the first character of a string. + * + * argument string string - Input string. + * + * return string - Capitalized output string. + */ + capitalizeFirstLetter: function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } +}); + + diff --git a/modules/newsfeed/node_helper.js b/modules/newsfeed/node_helper.js new file mode 100644 index 00000000..cfe00e41 --- /dev/null +++ b/modules/newsfeed/node_helper.js @@ -0,0 +1,74 @@ +// Configuration. +var config = { + port: 8080 +}; + +// Load modules. +var express = require('express'); +var request = require('request'); +var FeedMe = require('feedme'); +var validUrl = require('valid-url'); +var app = express(); + +// Create NewsFetcher. +var NewsFetcher = (function() { + var self = this; + + self.successCallback = function(){}; + self.errorCallback = function(){}; + + self.items = []; + + var parser = new FeedMe(); + + parser.on('item', function(item) { + //console.log(item); + self.items.push({ + title: item.title, + pubdate: item.pubdate, + }); + }); + + parser.on('end', function(item) { + self.successCallback(self.items); + }); + + parser.on('error', function(item) { + self.errorCallback(); + }); + + return { + fetchNews: function(url, success, error) { + self.successCallback = success; + self.errorCallback = error; + request(url).pipe(parser); + } + }; +})(); + +// Create route for fetcher. +app.get('/', function (req, res) { + + if (!validUrl.isUri(req.query.url)){ + res.status(404).send('No valid feed URL.'); + return; + } + + NewsFetcher.fetchNews(req.query.url, function(items) { + res.send(items); + }, function() { + res.status(400).send('Could not parse feed.'); + }); +}); + +// Listen on port. +app.listen(config.port, function () { + console.log('Feed proxy is running on port: ' + config.port); +}); + +console.log('Starting feed proxy on port: ' + config.port); + + + + + diff --git a/package.json b/package.json index 9df003f5..2ed940dd 100755 --- a/package.json +++ b/package.json @@ -1,32 +1,36 @@ { - "name": "Magic-Mirror", - "version": "2.0.0", - "description": "A modular interface for smart mirrors.", - "main": "js/electron.js", - "scripts": { - "start": "electron js/electron.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/MichMich/MagicMirror.git" - }, - "keywords": [ - "magic mirror", - "smart mirror", - "mirror UI", - "modular" - ], - "author": "Michael Teeuw", - "contributors": "https://github.com/MichMich/MagicMirror/graphs/contributors", - "license": "MIT", - "bugs": { - "url": "https://github.com/MichMich/MagicMirror/issues" - }, - "homepage": "https://github.com/MichMich/MagicMirror#readme", - "devDependencies": { - "electron-prebuilt": "latest" - }, - "dependencies": { - "walk": "latest" - } + "name": "Magic-Mirror", + "version": "2.0.0", + "description": "A modular interface for smart mirrors.", + "main": "js/electron.js", + "scripts": { + "start": "electron js/electron.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MichMich/MagicMirror.git" + }, + "keywords": [ + "magic mirror", + "smart mirror", + "mirror UI", + "modular" + ], + "author": "Michael Teeuw", + "contributors": "https://github.com/MichMich/MagicMirror/graphs/contributors", + "license": "MIT", + "bugs": { + "url": "https://github.com/MichMich/MagicMirror/issues" + }, + "homepage": "https://github.com/MichMich/MagicMirror#readme", + "devDependencies": { + "electron-prebuilt": "latest" + }, + "dependencies": { + "express":"latest", + "request":"latest", + "walk": "latest", + "feedme": "latest", + "valid-url": "latest" + } }