├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── README.md ├── bin └── rss-watcher ├── package.json ├── src ├── .gitkeep ├── cli.coffee └── watcher.coffee └── test └── test.coffee /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: 4yoG9YoXQFnuOqQcEMM0Y9lea4eB7uuDq 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | test/test.js 3 | .DS_Store 4 | node_modules 5 | .npmignore 6 | lib/ 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - npm install -g grunt grunt-cli 4 | before_script: 5 | - grunt coffee 6 | node_js: 7 | - "7" 8 | - "10" 9 | script: 10 | - grunt travis 11 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (grunt) -> 4 | 5 | _ = require 'underscore' 6 | require 'coffee-errors' 7 | 8 | grunt.loadNpmTasks 'grunt-contrib-watch' 9 | grunt.loadNpmTasks 'grunt-contrib-coffee' 10 | grunt.loadNpmTasks 'grunt-coffeelint' 11 | grunt.loadNpmTasks 'grunt-mocha-test' 12 | grunt.loadNpmTasks 'grunt-notify' 13 | 14 | grunt.registerTask 'test', [ 'coffeelint','coffee', 'mochaTest:spec' ] 15 | grunt.registerTask 'travis', [ 'test' ] 16 | grunt.registerTask 'default', [ 'test', 'watch' ] 17 | 18 | grunt.initConfig 19 | 20 | coffeelint: 21 | options: 22 | max_line_length: 23 | value: 100 24 | indentation: 25 | value: 2 26 | newlines_after_classes: 27 | level: 'error' 28 | no_empty_param_list: 29 | level: 'error' 30 | no_unnecessary_fat_arrows: 31 | level: 'ignore' 32 | dist: 33 | files: [ 34 | { expand: yes, cwd: 'test/', src: [ '*.coffee' ] } 35 | { expand: yes, cwd: './', src: [ '*.coffee' ] } 36 | { expand: yes, cwd: 'models/', src: [ '**/*.coffee' ] } 37 | { expand: yes, cwd: 'config/', src: [ '**/*.coffee' ] } 38 | { expand: yes, cwd: 'events/', src: [ '**/*.coffee' ] } 39 | { expand: yes, cwd: 'src/', src: [ '**/*.coffee' ] } 40 | { expand: yes, cwd: 'public/', src: [ '**/*.coffee' ] } 41 | ] 42 | 43 | watch: 44 | options: 45 | interrupt: yes 46 | dist: 47 | files: [ 48 | '*.coffee' 49 | 'models/**/*.coffee' 50 | 'events/**/*.coffee' 51 | 'config/**/*.coffee' 52 | 'src/**/*.coffee' 53 | 'public/**/*.{coffee,js,jade}' 54 | 'test/**/*.coffee' 55 | ] 56 | tasks: [ 'coffeelint','coffee','mochaTest:spec' ] 57 | 58 | coffee: 59 | multiple: 60 | expand:true 61 | cwd:'src' 62 | src:'*.coffee' 63 | dest:'lib/' 64 | ext:'.js' 65 | 66 | mochaTest: 67 | spec: 68 | options: 69 | reporter:"spec" 70 | timeout: 50000 71 | colors: true 72 | src: ['test/**/*.coffee'] 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rss-watcher [![Build Status](https://travis-ci.org/nikezono/node-rss-watcher.png)](https://travis-ci.org/nikezono/node-rss-watcher)[![Coverage Status](https://coveralls.io/repos/nikezono/node-rss-watcher/badge.png)](https://coveralls.io/r/nikezono/node-rss-watcher) 2 | --- 3 | 4 | [![NPM](https://nodei.co/npm/rss-watcher.png)](https://nodei.co/npm/rss-watcher/) 5 | 6 | ![gyazo](http://gyazo.com/35357bf10711857403eaa7abe6b70037.png) 7 | 8 | ## What is it 9 | `RSS-Watcher` is Tinu Library/Executable for RSS/Atom Feed Reader 10 | 11 | ##install 12 | 13 | ####NPM INSTALL: 14 | 15 | npm install rss-watcher 16 | 17 | ####package.json: 18 | 19 | ``` 20 | { 21 | "dependencies":{ 22 | "rss-watcher": "*" 23 | } 24 | } 25 | ``` 26 | 27 | ## Usage 28 | 29 | Watcher = require 'rss-watcher' 30 | feed = 'http://github.com/nikezono.atom' 31 | 32 | watcher = new Watcher(feed) 33 | 34 | watcher.on 'new article',(article)-> 35 | console.log article 36 | 37 | watcher.run (err,articles)-> 38 | console.error err if err 39 | console.log articles # current articles 40 | 41 | 42 | ### option 43 | 44 | watcher = new Watcher(feed) 45 | watcher.set 46 | feed:feed # feed url 47 | interval: 60 # request per interval seconds. default:average update frequency 48 | 49 | ### exposed events 50 | 51 | watcher.on "error",(error)-> 52 | console.error error 53 | 54 | watcher.on "new article",(article)-> 55 | console.log article # article object 56 | 57 | watcher.on "stop", -> 58 | console.log 'stop' 59 | 60 | watcher.stop() 61 | 62 | ## CLI tool 63 | 64 | > rss-watcher -f 'http://github.com/nikezono.atom' -i 20000 # 20000s interval 65 | 66 | 67 | ## Test 68 | 69 | Spec Report: 70 | 71 | npm -i -g grunt grunt-cli 72 | grunt test 73 | 74 | Coverage dump: 75 | 76 | grunt coverage 77 | open coverage.html 78 | 79 | -------------------------------------------------------------------------------- /bin/rss-watcher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rss-watcher", 3 | "version": "1.4.0", 4 | "description": "RSS reader/watcher", 5 | "main": "lib/watcher.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "bin": { 10 | "rss-watcher": "./bin/rss-watcher" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/nikezono/node-rss-watcher.git" 15 | }, 16 | "dependencies": { 17 | "async": "~2.6.1", 18 | "colors": "~1.3.0", 19 | "commander": "~2.16.0", 20 | "moment": "~2.22.2", 21 | "parse-rss": "*" 22 | }, 23 | "devDependencies": { 24 | "coffee-errors": "^0.8.6", 25 | "coffee-script": "*", 26 | "coffeelint": "^2.1.0", 27 | "grunt": "^1.0.3", 28 | "grunt-cli": "^1.2.0", 29 | "grunt-coffeelint": "0.0.16", 30 | "grunt-contrib-coffee": "^2.0.0", 31 | "grunt-contrib-watch": "^1.1.0", 32 | "grunt-mocha-test": "^0.13.3", 33 | "grunt-notify": "^0.4.5", 34 | "mocha": "^5.2.0", 35 | "underscore": "^1.6.0" 36 | }, 37 | "keywords": [ 38 | "rss", 39 | "atom", 40 | "reader", 41 | "watcher", 42 | "watch", 43 | "read" 44 | ], 45 | "author": "nikezono", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/nikezono/node-rss-watcher/issues" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikezono/node-rss-watcher/63c6a94412a3a339b2f6efb6db567880d571cfcb/src/.gitkeep -------------------------------------------------------------------------------- /src/cli.coffee: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # CLI.coffee 4 | # 5 | #### 6 | 7 | 8 | ### Module DEPENDENCIES ### 9 | program = require "commander" 10 | colors = require 'colors' 11 | moment = require 'moment' 12 | 13 | # Color Schema 14 | colors.setTheme 15 | time: 'grey' 16 | verbose: 'cyan' 17 | prompt: 'grey' 18 | info: 'blue' 19 | 20 | color = ["white",'yellow', 'cyan', 'magenta', 'red', 'green', 'blue' ] 21 | cnumber = 0 22 | 23 | # define Option 24 | program.version("1.2.0") 25 | .option("-f, --feed [String]", "RSS/Atom feed URL (required)") 26 | .option("-i, --interval [Number]", "fetch interval (optional)",parseInt) 27 | .option("-u, --nourl [Bool]", "Don't show articles url (optional)") 28 | .option("-s, --site [Bool]", "show sitename (optional)") 29 | .parse process.argv 30 | 31 | path = require 'path' 32 | Watcher = require './watcher' 33 | watcher = new Watcher program.feed 34 | 35 | watcher.on 'error',(error)-> 36 | console.error error 37 | 38 | watcher.on 'new article',(article)-> 39 | rendering(article) 40 | 41 | if program.interval 42 | watcher.set 43 | interval:program.interval 44 | 45 | watcher.run (err,articles)-> 46 | throw new Error(err) if err 47 | 48 | for article in articles 49 | rendering(article) 50 | 51 | rendering = (article)-> 52 | seed = article.pubDate/1000 53 | cnumber = seed%color.length 54 | date = "[#{moment(article.pubDate).format("(ddd) HH:mm")}]" 55 | title = article.title 56 | site = article.meta.title 57 | url = article.meta.link 58 | 59 | text = "" 60 | 61 | text = "#{date.time} #{title[color[cnumber]]}" 62 | text = text + " - #{site}" if program.site? 63 | text = text + " #{url.underline}" unless program.nourl? 64 | console.log text 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/watcher.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # 3 | # watcher.coffee 4 | # 5 | # Author:@nikezono 6 | # 7 | #### 8 | 9 | {EventEmitter} = require 'events' 10 | parser = require 'parse-rss' 11 | 12 | fetchFeed = (feedUrl,callback)=> 13 | parser feedUrl,(err,articles)=> 14 | return callback err,null if err? 15 | 16 | articles.sort (a,b)-> 17 | return a.pubDate/1000 - b.pubDate/1000 18 | 19 | return callback null,articles 20 | 21 | 22 | # Feed Watcher. Allocate one watcher per single feed 23 | class Watcher extends EventEmitter 24 | 25 | constructor:(feedUrl)-> 26 | throw new Error("arguments error.") if not feedUrl or feedUrl is undefined 27 | super() 28 | 29 | @feedUrl = feedUrl 30 | @interval = null 31 | @lastPubDate = null 32 | @lastPubTitles = [] 33 | @timer = null 34 | @watch = => 35 | 36 | fetch = => 37 | fetchFeed @feedUrl,(err,articles)=> 38 | return @emit 'error', err if err 39 | 40 | for article in articles 41 | if @isNewArticle(article) 42 | @emit 'new article',article 43 | @updateLastPubArticle(article) 44 | 45 | return setInterval -> 46 | fetch(@feedUrl) 47 | ,@interval*1000 48 | 49 | 50 | set:(obj)-> 51 | flag = false 52 | if obj.feedUrl? 53 | @feedUrl = obj.feedUrl if obj.feedUrl? 54 | flag = true 55 | if obj.interval? 56 | @interval = obj.interval if obj.interval? 57 | flag = true 58 | return flag 59 | 60 | updateLastPubArticle:(article)=> 61 | newPubDate = article.pubDate / 1000 62 | if @lastPubDate == newPubDate 63 | @lastPubTitles.push(article.title) 64 | else 65 | @lastPubTitles = [article.title] 66 | @lastPubDate = newPubDate 67 | 68 | isNewArticle:(article)=> 69 | return (@lastPubDate is null and @lastPubTitles.length == 0) or 70 | (@lastPubDate <= article.pubDate/1000 and article.title not in @lastPubTitles) 71 | 72 | run:(callback)=> 73 | 74 | initialize = (callback)=> 75 | fetchFeed @feedUrl,(err,articles)=> 76 | return callback new Error(err),null if err? and callback? 77 | @lastPubDate = articles[articles.length-1].pubDate / 1000 78 | @lastPubTitle = articles[articles.length-1].title 79 | @timer = @watch() 80 | return callback null, articles if callback? 81 | 82 | if not @interval 83 | @interval = 60 * 5 # 5 minutes... it's heuristic 84 | 85 | return initialize(callback) 86 | 87 | stop:=> 88 | if not @timer 89 | throw new Error("RSS-Watcher isnt running now") 90 | 91 | clearInterval(@timer) 92 | @emit 'stop' 93 | 94 | 95 | 96 | module.exports = Watcher 97 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # 3 | # test.coffee 4 | # Author:@nikezono, 2014/06/27 5 | # 6 | ### 7 | 8 | 9 | # dependency 10 | path = require 'path' 11 | assert = require 'assert' 12 | 13 | # Feed to test 14 | # FIXME use something does not emit http request, 15 | # such as mock or stub 16 | feed = "http://nikezono.net/atom.xml" 17 | 18 | Watcher = require '../lib/watcher' 19 | 20 | describe "rss-watcher",-> 21 | 22 | it "can compile",(done)-> 23 | watcher = new Watcher(feed) 24 | assert.notEqual watcher,null 25 | done() 26 | 27 | it "can raise error if feed url is null",-> 28 | assert.throws -> 29 | watcher = new Watcher() 30 | ,Error 31 | 32 | it "can return error if feed url is invalid",(done)-> 33 | watcher = new Watcher("hoge") 34 | watcher.run (err,articles)-> 35 | assert.ok(err instanceof Error) 36 | done() 37 | 38 | it "does not emit any event at first launch",(done)-> 39 | watcher = new Watcher(feed) 40 | watcher.run (err,articles)-> 41 | assert.ok(0 < articles.length) 42 | done() 43 | 44 | it "can pass option 'interval' for fetch interval",(done)-> 45 | watcher = new Watcher(feed) 46 | begin = Date.now() 47 | assert.ok watcher.set 48 | feedUrl:feed 49 | interval:1000 50 | watcher.run (err,articles)-> 51 | done() 52 | 53 | it "can't pass negative value as option 'interval'",(done)-> 54 | watcher = new Watcher(feed) 55 | watcher.set 56 | interval:(freq)-> 57 | return -1000 58 | watcher.run (err,articles)-> 59 | assert.ok err instanceof Error 60 | done() 61 | 62 | it "can't pass function that returns not a number",(done)-> 63 | watcher = new Watcher(feed) 64 | watcher.set 65 | interval:(freq)-> 66 | return "hoge" 67 | watcher.run (err,articles)-> 68 | assert.ok err instanceof Error 69 | done() 70 | 71 | it "tracks multiple articles with the same pubDate",(done)-> 72 | watcher = new Watcher(feed) 73 | article1 = 74 | title: 'first title' 75 | pubDate: new Date('Wed, 18 Jul 2018 22:45:19 +0000') 76 | article2 = 77 | title: 'second title' 78 | pubDate: article1.pubDate 79 | article3 = 80 | title: 'third title' 81 | pubDate: new Date('Wed, 18 Jul 2018 22:45:20 +0000') 82 | assert(watcher.isNewArticle(article1),'expected article1 to be new') 83 | 84 | watcher.updateLastPubArticle(article1) 85 | assert(!watcher.isNewArticle(article1),'expected article1 not to be new') 86 | assert(watcher.isNewArticle(article2),'expected article2 to be new') 87 | 88 | watcher.updateLastPubArticle(article2) 89 | assert(!watcher.isNewArticle(article1),'expected article1 not to be new') 90 | assert(!watcher.isNewArticle(article2),'expected article2 not to be new') 91 | 92 | assert(watcher.isNewArticle(article3),'expected article3 to be new') 93 | watcher.updateLastPubArticle(article3) 94 | assert(!watcher.isNewArticle(article1),'expected article1 not to be new') 95 | assert(!watcher.isNewArticle(article2),'expected article2 not to be new') 96 | assert(!watcher.isNewArticle(article3),'expected article3 not to be new') 97 | done() 98 | 99 | it "stop",(done)-> 100 | watcher = new Watcher(feed) 101 | watcher.run -> 102 | watcher.on "stop",-> 103 | done() 104 | watcher.stop() 105 | 106 | it "stop raise error",-> 107 | watcher = new Watcher(feed) 108 | assert.throws -> 109 | watcher.stop() 110 | ,Error 111 | 112 | --------------------------------------------------------------------------------