├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src ├── Feed.js ├── Monitor.js ├── Param.js └── XmlStreamParser.js └── test ├── feed.js ├── listmounts.xml ├── monitor.js ├── stats.xml └── xmlStreamParser.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | branches: 5 | only: 6 | - master 7 | - develop 8 | script: npm run coverage 9 | addons: 10 | code_climate: 11 | repo_token: 12 | secure: XR6KHiUf/0UIgrOGAzkRCPA90HnAsH7QnyZzyHfav4hxNQGXQHAs1rAwyUE+b0dVN9JQ+DsynVzvdSABgVf4Emi/H8pF4Z1eOIBqqwA/lBxo9I5mM0jdHytoAD9az2s+UmMw5NhSgHOd4jnMHfHM5YeCByN490hHhHK9bqe2c1BqZadPoMJ78rA8I4y/56R+QRc4GFHVCtWycQjLvsRbyGzDfE0V2DvMPtkHz8mN8biDXjKHhzahZ5U71bznkfuYT7lArZ1aMEW8KRjj3KWESjrwYRUSOkBZRamMjX5Bh+oA/AX8PZXjW1PP77kbSe+Vstj5wB7HBgNAk928kggKsVlzw4Dso+GnTaL7nCdSyygMrVwW6eZNNKe0C78CeVHQLGErHSELEMFDWkiT/m+afAnROmXxDcLdUDLKyN3C0bEIDCRujw6YEw+SgvSAFhJQP7GBZxJj01INaFeLAgW+1XHPxNYdVTFFqGs7Wo5gTdlls2M/eUaTptI9lN5qbuQswZfh0zXOO87hY3J+u5J8Pi6AwKbDmGZl89ZolFG809g27+a7O5RBXriSOx3Sczb1PS0tbMrRF/dQfkaMWH8jxhVoKwwsNtNctaUf+O8UBwLwgTo0eSFz2oibqnKE+DDpgNMx5NcJ43u3ksbxmsXbKyBNRVVy/uNyGJtH8vUwevE= 13 | after_success: 14 | - codeclimate-test-reporter < ./coverage/lcov.info -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Vasin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | [![Build Status](https://travis-ci.org/alvassin/nodejs-icecast-monitor.svg?branch=master)](https://travis-ci.org/alvassin/nodejs-icecast-monitor) [![Code Climate](https://codeclimate.com/github/alvassin/nodejs-icecast-monitor/badges/gpa.svg)](https://codeclimate.com/github/alvassin/nodejs-icecast-monitor) [![Test Coverage](https://codeclimate.com/github/alvassin/nodejs-icecast-monitor/badges/coverage.svg)](https://codeclimate.com/github/alvassin/nodejs-icecast-monitor/coverage) 3 | 4 | Powerful & handy interface for icecast-kh monitoring & statistics collection (admin access is required). 5 | 6 | * Able to collect icecast stats in realtime; 7 | * Provides easy access to all stats, available in web admin; 8 | * Can deal with very large amounts of data in memory-effective way; 9 | * Has only one npm dependency. 10 | 11 | To install latest stable version use `npm install icecast-monitor` command. 12 | 13 | * [Options](#options) 14 | * [Methods](#methods) 15 | * [createFeed](#monitorcreatefeed) 16 | * [getServerInfo](#monitorgetserverinfo) 17 | * [getSources](#monitorgetsources) 18 | * [getSource](#monitorgetsource) 19 | * [getListeners](#monitorgetlisteners) 20 | * [createStatsXmlStream](#monitorcreatestatsxmlstream) 21 | * [Feed](#feed) 22 | * [Events](#events) 23 | * [Methods](#methods-1) 24 | * [XmlStreamParser](#xmlstreamparser) 25 | * [Events](#events-1) 26 | 27 | # Options 28 | 29 | To access icecast monitor features create `Monitor` instance: 30 | ```js 31 | var Monitor = require('icecast-monitor'); 32 | var monitor = new Monitor({ 33 | host: 'icecast.dev', 34 | port: 80, 35 | user: 'admin', 36 | password: 'hackme' 37 | }); 38 | ``` 39 | Following constructor parameters are available: 40 | 41 | Parameter | Type | Required | Description 42 | -----------|---------|----------|------------ 43 | `host` | String | Yes | IP or DNS name 44 | `port` | Integer | No | Port number (defaults to `80`) 45 | `user` | String | Yes | Admin username 46 | `password` | String | Yes | Admin password 47 | 48 | # Methods 49 | #### monitor.createFeed 50 | Creates [Monitor.Feed](#feed) instance, which establishes persistent connection with icecast & processes its events feed. 51 | 52 | ```js 53 | monitor.createFeed(function(err, feed) { 54 | if (err) throw err; 55 | 56 | // Handle wildcard events 57 | feed.on('*', function(event, data, raw) { 58 | console.log(event, data, raw); 59 | }); 60 | 61 | // Handle usual events 62 | feed.on('mount.listeners', function(listeners, raw) { 63 | console.log(listeners, raw); 64 | }); 65 | }); 66 | ``` 67 | 68 | #### monitor.getServerInfo 69 | Returns information about icecast server. Please see [Monitor.XmlStreamParser `server` event](#server) data for details. 70 | 71 | ```js 72 | monitor.getServerInfo(function(err, server) { 73 | if (err) throw err; 74 | console.log(server); 75 | }); 76 | ``` 77 | 78 | #### monitor.getSources 79 | Returns array with all audio sources (without detailed listeners information). Please see [Monitor.XmlStreamParser `source` event](#source) for data provided about every source. 80 | 81 | ```js 82 | monitor.getSources(function(err, sources) { 83 | if (err) throw err; 84 | console.log(sources); 85 | }); 86 | ``` 87 | 88 | #### monitor.getSource 89 | Provides detailed information about specified source & its listeners. 90 | 91 | ```js 92 | monitor.getSource('/some-mountpoint', function(err, source) { 93 | if (err) throw err; 94 | console.log(source); 95 | }); 96 | ``` 97 | Returns same data as [Monitor.XmlStreamParser `source` event](#source), with one difference: `listeners` parameter will contain `array` with [information about every listener](#listener). 98 | 99 | #### monitor.getListeners 100 | Returns array with all listeners, connected to icecast server. Please see [Monitor.XmlStreamParser `listener` event](#listener) data for details. *Can produce huge amounts of data, use wisely.* 101 | 102 | ```js 103 | monitor.getListeners(function(err, listeners) { 104 | if (err) throw err; 105 | console.log(listeners); 106 | }); 107 | ``` 108 | 109 | #### monitor.createStatsXmlStream 110 | Performs HTTP request to given icecast url path and returns stream for further processing. Can be useful to process large icecast XML output using [Monitor.XmlStreamParser](#xmlstreamparser). 111 | 112 | We use following icecast url paths: 113 | * `/admin/stats` 114 | * icecast server information 115 | * sources detailed information 116 | * no detailed listeners information 117 | * `/admin/stats?mount=/$mount` 118 | * icecast server information 119 | * specified source information 120 | * detailed information about connected listeners 121 | * `/admin/listmounts?with_listeners` 122 | * no information about icecast server 123 | * minimal information about sources 124 | * and detailed information about all icecast listeners 125 | 126 | ```js 127 | // Collect sources without storing them in a memory 128 | monitor.createStatsXmlStream('/admin/stats', function(err, xmlStream) { 129 | if (err) throw err; 130 | 131 | var xmlParser = new Monitor.XmlStreamParser(); 132 | 133 | xmlParser.on('error', function(err) { 134 | console.log('error', err); 135 | }); 136 | 137 | xmlParser.on('source', function(source) { 138 | // Do work with received source 139 | console.log('source', source); 140 | }); 141 | 142 | // Finish event is being piped from xmlStream 143 | xmlParser.on('finish', function() { 144 | console.log('all sources are processed'); 145 | }); 146 | 147 | xmlStream.pipe(xmlParser); 148 | }); 149 | ``` 150 | 151 | # Feed 152 | Establishes persistent connection with icecast using STATS HTTP method & processes events feed in realtime. Best way to create is to use [monitor.createFeed](#createfeed) method, which injects all necessary parameters. 153 | 154 | ## Events 155 | 156 | For mount.* and server.* events user-callback is provided with following parameters: 157 | 158 | Parameter | Type | Description 159 | ----------|--------|------------ 160 | `event` | String | Event name (present only for wildcard events) 161 | `data` | Mixed | Parsed parameter(s), is described for each event below 162 | `raw` | String | Raw message received from icecast 163 | 164 | * **Internal events** 165 | * `connect`: connection with icecast is established 166 | * `disconnect`: connection with icecast is closed 167 | 168 | * **Wildcard events** 169 | * `*`: groups absolutely all supported events, produces lots of calls 170 | * `mount.*`: groups all mount-related events 171 | * `server.*`: groups all server-related events 172 | 173 | * **Mounts events** 174 | * [`mount.audioCodecId`](#mountaudiocodecid) 175 | * [`mount.audioInfo`](#mountaudioinfo) 176 | * [`mount.authenticator`](#mountauthenticator) 177 | * [`mount.bitrate`](#mountbitrate) 178 | * [`mount.connected`](#mountconnected) 179 | * [`mount.delete`](#mountdelete) 180 | * [`mount.flush`](#mountflush) 181 | * [`mount.genre`](#mountgenre) 182 | * [`mount.incomingBitrate`](#mountincomingbitrate) 183 | * [`mount.listenerConnections`](#mountlistenerconnections) 184 | * [`mount.listenerPeak`](#mountlistenerpeak) 185 | * [`mount.listeners`](#mountlisteners) 186 | * [`mount.listenUrl`](#mountlistenurl) 187 | * [`mount.maxListeners`](#mountmaxlisteners) 188 | * [`mount.metadataUpdated`](#mountmetadataupdated) 189 | * [`mount.mpegChannels`](#mountmpegchannels) 190 | * [`mount.mpegSampleRate`](#mountmpegsamplerate) 191 | * [`mount.new`](#mountnew) 192 | * [`mount.outgoingKBitrate`](#mountoutgoingkbitrate) 193 | * [`mount.public`](#mountpublic) 194 | * [`mount.queueSize`](#mountqueuesize) 195 | * [`mount.serverDescription`](#mountserverdescription) 196 | * [`mount.serverName`](#mountservername) 197 | * [`mount.serverType`](#mountservertype) 198 | * [`mount.serverUrl`](#mountserverurl) 199 | * [`mount.slowListeners`](#mountslowlisteners) 200 | * [`mount.sourceIp`](#mountsourceip) 201 | * [`mount.streamStart`](#mountstreamstart) 202 | * [`mount.title`](#mounttitle) 203 | * [`mount.totalBytesRead`](#mounttotalbytesread) 204 | * [`mount.totalBytesSent`](#mounttotalbytessent) 205 | * [`mount.totalMBytesSent`](#mounttotalmbytessent) 206 | * [`mount.ypCurrentlyPlaying`](#mountypcurrentlyplaying) 207 | 208 | * **Server events** 209 | * [`server.admin`](#serveradmin) 210 | * [`server.bannedIPs`](#serverbannedips) 211 | * [`server.build`](#serverbuild) 212 | * [`server.clientConnections`](#serverclientconnections) 213 | * [`server.clients`](#serverclients) 214 | * [`server.connections`](#serverconnections) 215 | * [`server.fileConnections`](#serverfileconnections) 216 | * [`server.host`](#serverhost) 217 | * [`server.info`](#serverinfo) 218 | * [`server.listenerConnections`](#serverlistenerconnections) 219 | * [`server.listeners`](#serverlisteners) 220 | * [`server.location`](#serverlocation) 221 | * [`server.outgoingKBitrate`](#serveroutgoingkbitrate) 222 | * [`server.serverId`](#serverserverid) 223 | * [`server.serverStart`](#serverserverstart) 224 | * [`server.sourceClientConnections`](#serversourceclientconnections) 225 | * [`server.sourceRelayConnections`](#serversourcerelayconnections) 226 | * [`server.sources`](#serversources) 227 | * [`server.sourceTotalConnections`](#serversourcetotalconnections) 228 | * [`server.stats`](#serverstats) 229 | * [`server.statsConnections`](#serverstatsconnections) 230 | * [`server.streamKBytesRead`](#serverstreamkbytesread) 231 | * [`server.streamKBytesSent`](#serverstreamkbytessent) 232 | 233 | #### mount.audioCodecId 234 | ``` 235 | EVENT /test.mp3 audio_codecid 2 236 | ``` 237 | 238 | Parameter | Type | Description 239 | ----------|---------|------------ 240 | `mount` | String | Mountpoint name 241 | `data` | Integer | Audio codec id: 2 for mp3, 10 for aac 242 | 243 | #### mount.audioInfo 244 | Displays audio encoding information. 245 | ``` 246 | EVENT /test.mp3 audio_info channels=2;samplerate=44100;bitrate=64 247 | ``` 248 | 249 | Parameter | Type | Description 250 | ------------------|---------|------------ 251 | `mount` | String | Mountpoint name 252 | `data` | Object | Audio channel info 253 | `data.channels` | Integer | Number of channels 254 | `data.sampleRate` | Integer | Sample rate 255 | `data.bitrate` | Integer | Bitrate (kbps) 256 | 257 | #### mount.authenticator 258 | ``` 259 | EVENT /test.mp3 authenticator command 260 | ``` 261 | 262 | Parameter | Type | Description 263 | ----------|--------|------------ 264 | `mount` | String | Mountpoint name 265 | `data` | String | Authenticator type 266 | 267 | #### mount.bitrate 268 | ``` 269 | EVENT /test.mp3 bitrate 64 270 | ``` 271 | 272 | Parameter | Type | Description 273 | ----------|---------|------------ 274 | `mount` | String | Mountpoint name 275 | `data` | Integer | Bitrate (kbps), used for stats & YP 276 | 277 | #### mount.connected 278 | ``` 279 | EVENT /test.mp3 connected 180423 280 | ``` 281 | 282 | Parameter | Type | Description 283 | ----------|---------|------------ 284 | `mount` | String | Mountpoint name 285 | `data` | Integer | Connection duration in seconds 286 | 287 | #### mount.delete 288 | Emitted when mount is deleted. Allows to notify relays about deleted source immediately (rather than wait for polling by the slaves). 289 | ``` 290 | DELETE /test.mp3 291 | ``` 292 | 293 | Parameter | Type | Description 294 | ----------|--------|------------ 295 | `mount` | String | Deleted mountpoint name 296 | 297 | #### mount.flush 298 | ``` 299 | FLUSH /test.mp3 300 | ``` 301 | 302 | Parameter | Type | Description 303 | ----------|--------|------------ 304 | `mount` | String | Flushed mountpoint name 305 | 306 | #### mount.genre 307 | ``` 308 | EVENT /test.mp3 genre Misc 309 | ``` 310 | 311 | Parameter | Type | Description 312 | ----------|--------|------------ 313 | `mount` | String | Mountpoint name 314 | `data` | String | Genre name, used for stats & YP 315 | 316 | #### mount.incomingBitrate 317 | ``` 318 | EVENT /test.mp3 incoming_bitrate 127064 319 | ``` 320 | 321 | Parameter | Type | Description 322 | ----------|---------|------------ 323 | `mount` | String | Mountpoint name 324 | `data` | Integer | Source bitrate (bps) 325 | 326 | #### mount.listenerConnections 327 | ``` 328 | EVENT /test.mp3 listener_connections 4 329 | ``` 330 | 331 | Parameter | Type | Description 332 | ----------|---------|------------ 333 | `mount` | String | Mountpoint name 334 | `data` | Integer | Connections number 335 | 336 | #### mount.listenerPeak 337 | ``` 338 | EVENT /test.mp3 listener_peak 2 339 | ``` 340 | 341 | Parameter | Type | Description 342 | ----------|---------|------------ 343 | `mount` | String | Mountpoint name 344 | `data` | Integer | Max detected number of simultaneous listeners 345 | 346 | #### mount.listeners 347 | ``` 348 | EVENT /test.mp3 listeners 2 349 | ``` 350 | 351 | Parameter | Type | Description 352 | ----------|---------|------------ 353 | `mount` | String | Mountpoint name 354 | `data` | Integer | Current listeners number 355 | 356 | #### mount.listenUrl 357 | ``` 358 | EVENT /11-31.mp3 listenurl http://icecast.dev:80/test.mp3 359 | ``` 360 | 361 | Parameter | Type | Description 362 | ----------|--------|------------ 363 | `mount` | String | Mountpoint name 364 | `data` | String | Audio stream url 365 | 366 | #### mount.maxListeners 367 | ``` 368 | EVENT /11-31.mp3 max_listeners -1 369 | ``` 370 | 371 | Parameter | Type | Description 372 | ----------|---------|------------ 373 | `mount` | String | Mountpoint name 374 | `data` | Integer | Simultanious listeners limit 375 | 376 | #### mount.metadataUpdated 377 | 378 | Is emitted when track is updated. 379 | ``` 380 | EVENT /test.mp3 metadata_updated 06/Aug/2015:14:05:05 +0300 381 | ``` 382 | 383 | Parameter | Type | Description 384 | ----------|--------|------------ 385 | `mount` | String | Mountpoint name 386 | `data` | String | Date when metadata was updated 387 | 388 | #### mount.mpegChannels 389 | ``` 390 | EVENT /test.mp3 mpeg_channels 2 391 | ``` 392 | 393 | Parameter | Type | Description 394 | ----------|---------|------------ 395 | `mount` | String | Mountpoint name 396 | `data` | Integer | Number of audio channels 397 | 398 | #### mount.mpegSampleRate 399 | ``` 400 | EVENT /test.mp3 mpeg_samplerate 44100 401 | ``` 402 | 403 | Parameter | Type | Description 404 | ----------|---------|------------ 405 | `mount` | String | Mountpoint name 406 | `data` | Integer | Sample rate 407 | 408 | #### mount.new 409 | Emitted when new mount is created. Allows to notify relays about new source immediately (rather than wait for polling by the slaves). 410 | ``` 411 | NEW audio/mpeg /229-682.mp3 412 | ``` 413 | 414 | Parameter | Type | Description 415 | -------------|--------|------------ 416 | `mount` | String | Mountpoint 417 | `data` | String | Mime type 418 | 419 | #### mount.outgoingKBitrate 420 | ``` 421 | EVENT /test.mp3 outgoing_kbitrate 0 422 | ``` 423 | 424 | Parameter | Type | Description 425 | ----------|---------|------------ 426 | `mount` | String | Mountpoint name 427 | `data` | Integer | Outgoing bitrate (kbps) 428 | 429 | #### mount.public 430 | Displays mount visibility (advertisement) setting. 431 | ``` 432 | EVENT /test.mp3 public 1 433 | ``` 434 | 435 | Parameter | Type | Description 436 | ----------|---------|------------ 437 | `mount` | String | Mountpoint name 438 | `data` | Integer | Possible values: `-1` (up to source client / relay) , `0` (disable), `1` (force advertisement) 439 | 440 | #### mount.queueSize 441 | ``` 442 | EVENT /test.mp3 queue_size 65828 443 | ``` 444 | 445 | Parameter | Type | Description 446 | ----------|---------|------------ 447 | `mount` | String | Mountpoint name 448 | `data` | Integer | Queue size 449 | 450 | #### mount.serverDescription 451 | ``` 452 | EVENT /test.mp3 server_description My station description 453 | ``` 454 | 455 | Parameter | Type | Description 456 | ----------|--------|------------ 457 | `mount` | String | Mountpoint name 458 | `data` | String | User-defined station description 459 | 460 | #### mount.serverName 461 | ``` 462 | EVENT /test.mp3 server_name TestFM 463 | ``` 464 | 465 | Parameter | Type | Description 466 | ----------|--------|------------ 467 | `mount` | String | Mountpoint name 468 | `data` | String | User-defined station name 469 | 470 | #### mount.serverType 471 | ``` 472 | EVENT /test.mp3 server_type audio/mpeg 473 | ``` 474 | 475 | Parameter | Type | Description 476 | ----------|--------|------------ 477 | `mount` | String | Mountpoint name 478 | `data` | String | Mime type 479 | 480 | #### mount.serverUrl 481 | ``` 482 | EVENT /test.mp3 server_url http://example.com/ 483 | ``` 484 | 485 | Parameter | Type | Description 486 | ----------|--------|------------ 487 | `mount` | String | Mountpoint name 488 | `data` | String | User-defined url 489 | 490 | #### mount.slowListeners 491 | ``` 492 | EVENT /test.mp3 slow_listeners 0 493 | ``` 494 | 495 | Parameter | Type | Description 496 | ----------|---------|------------ 497 | `mount` | String | Mountpoint name 498 | `data` | Integer | Slow listeners number 499 | 500 | #### mount.sourceIp 501 | ``` 502 | EVENT /test.mp3 source_ip icecast.dev 503 | ``` 504 | 505 | Parameter | Type | Description 506 | ----------|--------|------------ 507 | `mount` | String | Mountpoint name 508 | `data` | String | Mounpoint stream source host or ip address 509 | 510 | #### mount.streamStart 511 | ``` 512 | EVENT /test.mp3 stream_start 04/Aug/2015:12:00:31 +0300 513 | ``` 514 | 515 | Parameter | Type | Description 516 | ----------|--------|------------ 517 | `mount` | String | Mountpoint name 518 | `data` | String | Date, when mount started streaming 519 | 520 | #### mount.title 521 | ``` 522 | EVENT /test.mp3 title Werkdiscs - Helena Hauff - 'Sworn To Secrecy Part II' 523 | ``` 524 | 525 | Parameter | Type | Description 526 | ----------|--------|------------ 527 | `mount` | String | Mountpoint name 528 | `data` | String | Track name 529 | 530 | #### mount.totalBytesRead 531 | ``` 532 | EVENT /test.mp3 total_bytes_read 1443575627 533 | ``` 534 | 535 | Parameter | Type | Description 536 | ----------|---------|------------ 537 | `mount` | String | Mountpoint name 538 | `data` | Integer | Source (incoming) traffic in bytes 539 | 540 | #### mount.totalBytesSent 541 | ``` 542 | EVENT /test.mp3 total_bytes_sent 256000 543 | ``` 544 | 545 | Parameter | Type | Description 546 | ----------|---------|------------ 547 | `mount` | String | Mountpoint name 548 | `data` | Integer | Source (outgoing) traffic in bytes 549 | 550 | #### mount.totalMBytesSent 551 | ``` 552 | EVENT /test.mp3 total_mbytes_sent 0 553 | ``` 554 | 555 | Parameter | Type | Description 556 | ----------|---------|------------ 557 | `mount` | String | Mountpoint name 558 | `data` | Integer | Source (outgoing) traffic in bytes 559 | 560 | #### mount.ypCurrentlyPlaying 561 | ``` 562 | EVENT /test.mp3 yp_currently_playing Nickelback - How You Remind Me 563 | ``` 564 | 565 | Parameter | Type | Description 566 | ----------|--------|------------ 567 | `mount` | String | Mountpoint name 568 | `data` | String | Track, that is displayed in YP 569 | 570 | #### server.admin 571 | Displays administrator's email. 572 | ``` 573 | EVENT global admin email@example.com 574 | ``` 575 | 576 | Parameter | Type | Description 577 | ----------|--------|------------ 578 | `data` | String | Administrator email 579 | 580 | #### server.bannedIPs 581 | ``` 582 | EVENT global banned_IPs 0 583 | ``` 584 | 585 | Parameter | Type | Description 586 | ----------|---------|------------ 587 | `data` | Integer | Banned ip addresses number 588 | 589 | #### server.build 590 | ``` 591 | EVENT global build 20150616004931 592 | ``` 593 | 594 | Parameter | Type | Description 595 | ----------|---------|------------ 596 | `data` | Integer | Build number 597 | 598 | #### server.clientConnections 599 | ``` 600 | EVENT global client_connections 1029675 601 | ``` 602 | 603 | Parameter | Type | Description 604 | ----------|---------|------------ 605 | `data` | Integer | Client connections number 606 | 607 | #### server.clients 608 | ``` 609 | EVENT global clients 62 610 | ``` 611 | 612 | Parameter | Type | Description 613 | ----------|---------|------------ 614 | `data` | Integer | Connected clients 615 | 616 | #### server.connections 617 | ``` 618 | EVENT global connections 1178553 619 | ``` 620 | 621 | Parameter | Type | Description 622 | ----------|---------|------------ 623 | `data` | Integer | Connections number 624 | 625 | #### server.fileConnections 626 | ``` 627 | EVENT global file_connections 3534 628 | ``` 629 | 630 | Parameter | Type | Description 631 | ----------|---------|------------ 632 | `data` | Integer | File connections number 633 | 634 | #### server.host 635 | Configuration icecast.hostname setting value. Is used for the stream directory lookups or playlist generation possibily if a Host header is not provided. 636 | ``` 637 | EVENT global host icecast.dev 638 | ``` 639 | 640 | Parameter | Type | Description 641 | ----------|--------|------------ 642 | `data` | String | Server DNS name or IP address 643 | 644 | #### server.info 645 | Identifies the end of the big list at the beginning. When initially connected, you get a snapshot (a blast of content), and this just marks the end of it. After this then the stats are generated since the snapshot. 646 | ``` 647 | INFO full list end 648 | ``` 649 | 650 | #### server.listenerConnections 651 | ``` 652 | EVENT global listener_connections 220589 653 | ``` 654 | 655 | Parameter | Type | Description 656 | ----------|---------|------------ 657 | `data` | Integer | Listener connections number 658 | 659 | #### server.listeners 660 | ``` 661 | EVENT global listeners 16 662 | ``` 663 | 664 | Parameter | Type | Description 665 | ----------|---------|------------ 666 | `data` | Integer | Current listeners number 667 | 668 | #### server.location 669 | Configuration icecast.location setting value, is also displayed in web interface. 670 | ``` 671 | EVENT global location RU 672 | ``` 673 | 674 | Parameter | Type | Description 675 | ----------|--------|------------ 676 | `data` | String | Server location 677 | 678 | #### server.outgoingKBitrate 679 | ``` 680 | EVENT global outgoing_kbitrate 4411 681 | ``` 682 | 683 | Parameter | Type | Description 684 | ----------|---------|------------ 685 | `data` | Integer | Outgoing bitrate (kbps) 686 | 687 | #### server.serverId 688 | Icecast server identifier. Can be overrided in config file. 689 | ``` 690 | EVENT global server_id Icecast 2.4.0-kh1 691 | ``` 692 | 693 | Parameter | Type | Description 694 | ----------|--------|------------ 695 | `data` | String | Server identifier (icecast followed by a version number or user-defined value) 696 | 697 | #### server.serverStart 698 | ``` 699 | EVENT global server_start 06/Jul/2015:00:19:34 +0300 700 | ``` 701 | 702 | Parameter | Type | Description 703 | ----------|--------|------------ 704 | `data` | String | Server start date 705 | 706 | #### server.sourceClientConnections 707 | ``` 708 | EVENT global source_client_connections 0 709 | ``` 710 | 711 | Parameter | Type | Description 712 | ----------|---------|------------ 713 | `data` | Integer | Source client connections number 714 | 715 | #### server.sourceRelayConnections 716 | ``` 717 | EVENT global source_relay_connections 1317 718 | ``` 719 | 720 | Parameter | Type | Description 721 | ----------|---------|------------ 722 | `data` | Integer | Source relay connections number 723 | 724 | #### server.sources 725 | ``` 726 | EVENT global sources 45 727 | ``` 728 | 729 | Parameter | Type | Description 730 | ----------|---------|------------ 731 | `data` | Integer | Sources number 732 | 733 | #### server.sourceTotalConnections 734 | ``` 735 | EVENT global source_total_connections 1318 736 | ``` 737 | 738 | Parameter | Type | Description 739 | ----------|---------|------------ 740 | `data` | Integer | Source total connections number 741 | 742 | #### server.stats 743 | ``` 744 | EVENT global stats 0 745 | ``` 746 | 747 | Parameter | Type | Description 748 | ----------|---------|------------ 749 | `data` | Integer | ? 750 | 751 | #### server.statsConnections 752 | ``` 753 | EVENT global stats_connections 2 754 | ``` 755 | 756 | Parameter | Type | Description 757 | ----------|---------|------------ 758 | `data` | Integer | ? 759 | 760 | #### server.streamKBytesRead 761 | ``` 762 | EVENT global stream_kbytes_read 2414225600 763 | ``` 764 | 765 | Parameter | Type | Description 766 | ----------|---------|------------ 767 | `data` | Integer | Stream incoming traffic (kbytes) 768 | 769 | #### server.streamKBytesSent 770 | ``` 771 | EVENT global stream_kbytes_sent 1102687068 772 | ``` 773 | 774 | Parameter | Type | Description 775 | ----------|---------|------------ 776 | `data` | Integer | Stream outgoing traffic (kbytes) 777 | 778 | ## Methods 779 | 780 | #### feed.connect 781 | Establishes connection, once connected emits `connect` event. If you use [createFeed](#monitorcreatefeed) method, it will call `feed.connect` automatically, so this method can be used to handle disconnects like shown below: 782 | ```js 783 | monitor.createFeed(function(err, feed) { 784 | if (err) throw err; 785 | 786 | // Handle disconnects 787 | feed.on('disconnect', function() { 788 | feed.connect(); 789 | }); 790 | }); 791 | ``` 792 | 793 | #### feed.disconnect 794 | Closes icecast connection, once disconnected emits `disconnect` event. 795 | ```js 796 | monitor.createFeed(function(err, feed) { 797 | if (err) throw err; 798 | feed.on('connect', function() { 799 | 800 | // Disconnect with 5 seconds delay 801 | setTimeout(feed.disconnect, 5000); 802 | }); 803 | }); 804 | ``` 805 | 806 | # XmlStreamParser 807 | [Writeable stream](https://nodejs.org/api/stream.html#stream_class_stream_writable), that allows to retrieve sources, listeners & server information from icecast xml stream. Icecast xml stream can be retrieved using [monitor.createStatsXmlStream](#monitorcreatestatsxmlstream) method. 808 | 809 | Using XmlStreamParser directly can be more memory-effective when dealing with large icecast output, then using [monitor.getServerInfo](#monitorgetserverinfo), [monitor.getSources](#monitorgetsources), [monitor.getSource](#monitorgetsource) and [monitor.getListeners](#monitorgetlisteners) methods, because those methods have to store information in memory before it is returned in callback. 810 | 811 | ```js 812 | // Collect sources without storing them in a memory 813 | monitor.createStatsXmlStream('/admin/stats', function(err, xmlStream) { 814 | if (err) throw err; 815 | 816 | var xmlParser = new Monitor.XmlStreamParser(); 817 | 818 | // Handle errors 819 | xmlParser.on('error', function(err) { 820 | console.log('error', err); 821 | }); 822 | 823 | // Handle server info 824 | xmlParser.on('server', function(server) { 825 | console.log('server', server); 826 | }); 827 | 828 | // Handle sources 829 | xmlParser.on('source', function(source) { 830 | console.log('source', source); 831 | }); 832 | 833 | // Handle listeners 834 | xmlParser.on('listener', function(listener) { 835 | console.log('listener', listener); 836 | }); 837 | 838 | // Xml stream finished 839 | xmlParser.on('finish', function() { 840 | console.log('data is finished'); 841 | }); 842 | 843 | xmlStream.pipe(xmlParser); 844 | }); 845 | ``` 846 | 847 | ## Events 848 | * [`error`](#error) 849 | * [`server`](#server) 850 | * [`source`](#source) 851 | * [`listener`](#listener) 852 | * [`finish`](https://nodejs.org/api/stream.html#stream_event_finish) 853 | 854 | #### error 855 | Represents error, that happened while parsing xml stream. 856 | 857 | #### server 858 | Is emitted when xml stream processing is finished. Returns following information about icecast server: 859 | 860 | Parameter | Type | Description 861 | --------------------------|---------|------------ 862 | `admin` | String | Administrator's email 863 | `bannedIPs` | Integer | Banned ip addresses number 864 | `build` | Integer | Build number 865 | `clientConnections` | Integer | Total client (sources, listeners, web requests, etc) connections number 866 | `clients` | Integer | Current clients (sources, listeners, web requests, etc) number 867 | `connections` | Integer | ? 868 | `fileConnections` | Integer | File connections number 869 | `host` | String | Host DNS or IP address (is defined by `hostname` setting in icecast config) 870 | `listenerConnections` | Integer | Listeners connections number 871 | `listeners` | Integer | Listeners number 872 | `location` | String | Server location (is defined by `location` setting in icecast config) 873 | `outgoingKBitrate` | Integer | Outgoing bitrate in Kbps 874 | `serverId` | String | Server identifier (is defined by `server-id` setting in icecast config) 875 | `serverStart` | String | Server start date 876 | `sourceClientConnections` | Integer | Source clients connections number 877 | `sourceRelayConnections` | Integer | Source relays connections number 878 | `sources` | Integer | Sources (mountpoints) number 879 | `sourceTotalConnections` | Integer | Total connections number 880 | `stats` | Integer | Number currently connected clients using STATS HTTP method (like [Monitor.Feed](#feed) 881 | `statsConnections` | Integer | STATS HTTP method total connections number 882 | `streamKBytesRead` | Integer | Streaming incoming traffic (KB) 883 | `streamKBytesSent` | Integer | Streaming outgoing traffic (KB) 884 | 885 | #### source 886 | Is emitted when source processing is finished. Returns following information for every source: 887 | 888 | Parameter | Type | Description 889 | ----------------------|---------|------------ 890 | `mount` | String | Mountpoint 891 | `audioCodecId` | Integer | Audio codec id: 2 for mp3, 10 for aac 892 | `audioInfo` | String | Audio encoding information 893 | `authenticator` | String | Authentication scheme 894 | `bitrate` | Integer | User-defined bitrate (Kbps) 895 | `connected` | Integer | Connected time in seconds 896 | `genre` | String | User-defined genre 897 | `incomingBitrate` | Integer | Source stream bitrate (bps) 898 | `listenerConnections` | Integer | Listener connections number 899 | `listenerPeak` | Integer | Maximum detected number of simultaneous users 900 | `listeners` | Integer | Current listeners number 901 | `listenUrl` | String | Audio stream url 902 | `maxListeners` | Integer | Listeners limit 903 | `metadataUpdated` | String | Last metadata update date 904 | `mpegChannels` | Integer | Mpeg channels number 905 | `mpegSampleRate` | Integer | Mpeg sample rate 906 | `outputKBitrate` | Integer | Outgoing bitrate for all listeners (Kbps) 907 | `public` | Integer | Source advertisement: `-1` - source client or relay determines if mountpoint should be advertised, `0` - disables advertisement, `1` - forces advertisement 908 | `queueSize` | Integer | Can vary (typically) because lagging clients cause the size to increase until they either get kicked off or they catch up 909 | `serverDescription` | String | User-defined description 910 | `serverName` | String | User-defined name 911 | `serverType` | String | Mime type 912 | `serverUrl` | String | User-defined url 913 | `slowListeners` | Integer | Slow listeners number 914 | `sourceIp` | String | Source ip address 915 | `streamStart` | String | Date, when stream started 916 | `title` | String | Track name 917 | `totalBytesRead` | Integer | Incoming traffic 918 | `totalBytesSent` | Integer | Outgoing traffic (Bytes) 919 | `totalMBytesSent` | Integer | Outgoing traffic (MBytes) 920 | `ypCurrentlyPlaying` | String | YP track title 921 | 922 | #### listener 923 | Is emitted when listener processing is finished. Returns following information for every listener: 924 | 925 | Parameter | Type | Description 926 | ------------|---------|------------ 927 | `id` | Integer | Icecast internal id, can be used to kick listeners, move them between mounts, etc. 928 | `ip` | String | Listener's ip address 929 | `userAgent` | String | Listener's user agent 930 | `referrer` | String | Url, where listener came from 931 | `lag` | Integer | ? 932 | `connected` | Integer | Connected time in seconds 933 | `mount` | String | Source mounpoint 934 | 935 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | var Monitor = require(__dirname + '/src/Monitor'); 5 | 6 | /** 7 | * Exports 8 | */ 9 | module.exports = Monitor; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icecast-monitor", 3 | "version": "1.0.2", 4 | "description": "Icecast realtime statistics processor with handy nodejs interface.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha", 11 | "coverage": "istanbul cover _mocha -- -R spec" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/alvassin/nodejs-icecast-monitor.git" 16 | }, 17 | "author": "Alexander Vasin ", 18 | "license": "MIT", 19 | "keywords": [ 20 | "icecast", 21 | "radio", 22 | "listener", 23 | "monitoring", 24 | "stats", 25 | "realtime" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/alvassin/nodejs-icecast-monitor/issues" 29 | }, 30 | "homepage": "https://github.com/alvassin/nodejs-icecast-monitor#readme", 31 | "devDependencies": { 32 | "codeclimate-test-reporter": "^0.1.1", 33 | "istanbul": "^0.3.22", 34 | "mocha": "^2.2.5" 35 | }, 36 | "dependencies": { 37 | "sax": "^1.1.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Feed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | var EventEmitter = require('events').EventEmitter; 5 | var http = require('http'); 6 | var Param = require(__dirname + '/Param'); 7 | var util = require('util'); 8 | 9 | /** 10 | * Module exports 11 | */ 12 | module.exports = Feed; 13 | 14 | /** 15 | * Constructor. 16 | * 17 | * @param {object} options 18 | */ 19 | function Feed(options) { 20 | 21 | /** 22 | * Configuration 23 | * @var {object} 24 | */ 25 | this.config = { 26 | host: null, 27 | port: 80, 28 | user: null, 29 | password: null 30 | }; 31 | 32 | /** 33 | * Chunk buffer 34 | * @var {string} 35 | */ 36 | this.chunkBuffer = ''; 37 | 38 | // Handle options input 39 | for (var key in options) { 40 | if (this.config.hasOwnProperty(key)) { 41 | this.config[key] = options[key]; 42 | } 43 | } 44 | 45 | // Check that all config options are defined 46 | for (var key in this.config) { 47 | if (this.config[key] === null) { 48 | throw new Error('Option "' + key + '" is required'); 49 | } 50 | } 51 | } 52 | util.inherits(Feed, EventEmitter); 53 | 54 | /** 55 | * Connect to specified server & start processing data. 56 | */ 57 | Feed.prototype.connect = function() { 58 | 59 | var feed = this; 60 | 61 | this.req = http.request({ 62 | hostname: feed.config.host, 63 | port: feed.config.port, 64 | path: '/', 65 | method: 'STATS', 66 | auth: feed.config.user + ':' + feed.config.password 67 | }, function(res){ 68 | res.on('data', function (chunk) { 69 | feed.handleChunk(chunk.toString()); 70 | }); 71 | 72 | feed.req.socket.on('close', function() { 73 | feed.emit('disconnect'); 74 | }) 75 | }); 76 | 77 | this.req.end(); 78 | }; 79 | 80 | /** 81 | * Processes data chunk 82 | * 83 | * @param {string} chunk 84 | */ 85 | Feed.prototype.handleChunk = function(chunk) { 86 | var lines = chunk.split(/\r\n|\r|\n/g); 87 | 88 | // Add buffer to the first line if present 89 | if (this.chunkBuffer) { 90 | lines[0] = this.chunkBuffer + lines[0]; 91 | this.chunkBuffer = ''; 92 | } 93 | 94 | // Save last chunk to buffer if necessary 95 | if (/\n$/.test(chunk) === false) { 96 | this.chunkBuffer = lines.pop(); 97 | } 98 | 99 | for (var i = 0; i < lines.length; i++) { 100 | lines[i] = lines[i].trim(); 101 | if (lines[i]) this.handleRawEvent(lines[i]); 102 | } 103 | }; 104 | 105 | /** 106 | * Processes raw icecast event. 107 | * 108 | * @param {string} rawEvent 109 | */ 110 | Feed.prototype.handleRawEvent = function(rawEvent) { 111 | 112 | var event = this.parse(rawEvent); 113 | 114 | // Build event params 115 | var params = [event.data]; 116 | if (event.mount) { 117 | params.unshift(event.mount); 118 | } 119 | 120 | // Emit wilcard (*) event 121 | this.emit.apply(this, ['*', event.name].concat(params)); 122 | 123 | // Emit server.* / mount.* events 124 | if (event.mount) { 125 | this.emit.apply(this, ['mount.*', event.name].concat(params)); 126 | } else { 127 | this.emit.apply(this, ['server.*', event.name].concat(params)); 128 | } 129 | 130 | // Emit event 131 | this.emit.apply(this, [event.name].concat(params)) 132 | }; 133 | 134 | /** 135 | * Parse event from raw text line. 136 | * 137 | * @param {string} line 138 | * @return {object} 139 | */ 140 | Feed.prototype.parse = function(line) { 141 | 142 | var chunks = line.split(' '); 143 | 144 | /** 145 | * Event name prefix 146 | * @var {string} 147 | */ 148 | var prefix; 149 | 150 | /** 151 | * Event name 152 | * @var {string} 153 | */ 154 | var name = ''; 155 | 156 | /** 157 | * Mountpoint 158 | * @var {string} 159 | */ 160 | var mount = null; 161 | 162 | /** 163 | * Event params 164 | * @var {Array} 165 | */ 166 | var params = []; 167 | 168 | // Parse event & data 169 | switch(chunks[0]) { 170 | case 'NEW': 171 | name = chunks.shift().toLowerCase(); 172 | mount = chunks.pop(); 173 | params = chunks; 174 | break; 175 | 176 | case 'DELETE': 177 | case 'FLUSH': 178 | name = chunks.shift().toLowerCase(); 179 | mount = chunks.shift(); 180 | break; 181 | 182 | case 'EVENT': 183 | var type = chunks[1] === 'global' ? 'server' : 'mount'; 184 | if (type === 'mount') { 185 | mount = chunks[1]; 186 | } 187 | name = Param.normalizeName(chunks[2]); 188 | params = chunks.slice(3); 189 | break; 190 | 191 | case 'INFO': 192 | default: 193 | name = chunks.shift().toLowerCase(); 194 | params = [chunks.join(' ')]; 195 | break; 196 | } 197 | 198 | // Retrieve event name prefix 199 | prefix = mount ? 'mount' : 'server'; 200 | 201 | return { 202 | name : prefix + '.' + name, 203 | mount : mount, 204 | data : Param.normalizeData(name, params) 205 | }; 206 | }; 207 | 208 | /** 209 | * Disconnects from the server. 210 | */ 211 | Feed.prototype.disconnect = function() { 212 | this.req.connection.destroy(); 213 | }; 214 | -------------------------------------------------------------------------------- /src/Monitor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | var Feed = require(__dirname + '/Feed'); 5 | var http = require('http'); 6 | var XmlStreamParser = require(__dirname + '/XmlStreamParser'); 7 | 8 | /** 9 | * Exports 10 | */ 11 | module.exports = Monitor; 12 | 13 | /** 14 | * Constructor 15 | * 16 | * @param {object} options 17 | */ 18 | function Monitor(options) { 19 | 20 | this.config = { 21 | host: null, 22 | port: 80, 23 | user: null, 24 | password: null 25 | }; 26 | 27 | // Handle options input 28 | for (var key in options) { 29 | if (this.config.hasOwnProperty(key)) { 30 | this.config[key] = options[key]; 31 | } 32 | } 33 | 34 | // Check that all config options are defined 35 | for (var key in this.config) { 36 | if (this.config[key] === null) { 37 | throw new Error('Option "' + key + '" is required'); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Feed constructor 44 | */ 45 | Monitor.Feed = Feed; 46 | 47 | /** 48 | * XmlStreamParser constructor 49 | */ 50 | Monitor.XmlStreamParser = XmlStreamParser; 51 | 52 | /** 53 | * Create stats feed. 54 | * 55 | * @param {function} callback 56 | */ 57 | Monitor.prototype.createFeed = function(callback) { 58 | try { 59 | var feed = new Feed(this.config); 60 | feed.connect(); 61 | callback(null, feed); 62 | } catch (err) { 63 | callback(err); 64 | } 65 | }; 66 | 67 | /** 68 | * Returns information about server. 69 | * 70 | * @param {function} callback 71 | */ 72 | Monitor.prototype.getServerInfo = function(callback) { 73 | this.createStatsXmlStream('/admin/stats', function(err, xmlStream) { 74 | 75 | if (err) { 76 | callback(err); 77 | return; 78 | } 79 | 80 | var xmlParser = new XmlStreamParser(); 81 | 82 | xmlParser.on('error', function(err) { 83 | callback(err); 84 | }); 85 | 86 | xmlParser.on('server', function(server) { 87 | callback(null, server); 88 | }); 89 | 90 | xmlStream.pipe(xmlParser); 91 | }); 92 | }; 93 | 94 | /** 95 | * Get sources list. 96 | * 97 | * @param {function} callback 98 | */ 99 | Monitor.prototype.getSources = function(callback) { 100 | this.createStatsXmlStream('/admin/stats', function(err, xmlStream) { 101 | 102 | if (err) { 103 | callback(err); 104 | return; 105 | } 106 | 107 | var xmlParser = new XmlStreamParser(); 108 | var sources = []; 109 | 110 | xmlParser.on('error', function(err) { 111 | callback(err); 112 | }); 113 | 114 | xmlParser.on('source', function(source) { 115 | sources.push(source); 116 | }); 117 | 118 | // Finish event is being piped from xmlStream 119 | xmlParser.on('finish', function() { 120 | callback(null, sources); 121 | }); 122 | 123 | xmlStream.pipe(xmlParser); 124 | }); 125 | }; 126 | 127 | /** 128 | * Returns information with listeners for specified mount. 129 | * 130 | * @param {string} mount 131 | * @param {function} callback 132 | */ 133 | Monitor.prototype.getSource = function(mount, callback) { 134 | this.createStatsXmlStream('/admin/stats?mount=' + mount, function(err, xmlStream) { 135 | 136 | if (err) { 137 | callback(err); 138 | return; 139 | } 140 | 141 | var xmlParser = new XmlStreamParser(); 142 | var source; 143 | var listeners = []; 144 | 145 | // Handle errors 146 | xmlParser.on('error', function(err) { 147 | callback(err); 148 | }); 149 | 150 | // Retrieve source data 151 | xmlParser.on('source', function(data) { 152 | source = data; 153 | }); 154 | 155 | // Retrieve listeners data 156 | xmlParser.on('listener', function(listener) { 157 | listeners.push(listener); 158 | }); 159 | 160 | // Finish event is being piped from xmlStream 161 | xmlParser.on('finish', function() { 162 | if (source) { 163 | source.listeners = listeners; 164 | callback(null, source); 165 | } else { 166 | callback(new Error('Mount "' + mount + '" not found')); 167 | } 168 | }); 169 | 170 | xmlStream.pipe(xmlParser); 171 | }); 172 | }; 173 | 174 | /** 175 | * Returns listeners information 176 | * @param {function} callback 177 | */ 178 | Monitor.prototype.getListeners = function(callback) { 179 | this.createStatsXmlStream('/admin/listmounts?with_listeners', function(err, xmlStream) { 180 | 181 | if (err) { 182 | callback(err); 183 | return; 184 | } 185 | 186 | var xmlParser = new XmlStreamParser(); 187 | var listeners = []; 188 | 189 | xmlParser.on('error', function(err) { 190 | callback(err); 191 | }); 192 | 193 | xmlParser.on('listener', function(listener) { 194 | listeners.push(listener); 195 | }); 196 | 197 | // Finish event is being piped from xmlStream 198 | xmlParser.on('finish', function() { 199 | callback(null, listeners); 200 | }); 201 | 202 | xmlStream.pipe(xmlParser); 203 | }); 204 | }; 205 | 206 | /** 207 | * Returns XML stream for further processing. 208 | * 209 | * @param {string} urlPath 210 | * @param {function} callback 211 | */ 212 | Monitor.prototype.createStatsXmlStream = function(urlPath, callback) { 213 | 214 | var req = http.request({ 215 | hostname: this.config.host, 216 | port: this.config.port, 217 | path: urlPath, 218 | auth: this.config.user + ':' + this.config.password 219 | }, function(res) { 220 | 221 | if (res.statusCode !== 200) { 222 | callback(new Error('Server responded with ' + res.statusCode + ' HTTP status code')); 223 | return; 224 | } 225 | 226 | res.setEncoding('utf8'); 227 | callback(null, res); 228 | 229 | }); 230 | 231 | req.on('error', function(error) { 232 | callback(error); 233 | }); 234 | 235 | req.end(); 236 | }; -------------------------------------------------------------------------------- /src/Param.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalize param name. 3 | * 4 | * @param {string} name 5 | * @return {string} 6 | */ 7 | module.exports.normalizeName = function(name) { 8 | 9 | /** 10 | * Exceptions, where camelCase is not enough 11 | * @var {object} 12 | */ 13 | var exceptions = { 14 | 'audio_codecid' : 'audioCodecId', 15 | 'ID' : 'id', 16 | 'IP' : 'ip', 17 | 'listenurl' : 'listenUrl', 18 | 'mpeg_samplerate' : 'mpegSampleRate', 19 | 'outgoing_kbitrate' : 'outgoingKBitrate', 20 | 'total_mbytes_sent' : 'totalMBytesSent', 21 | 'stream_kbytes_read' : 'streamKBytesRead', 22 | 'stream_kbytes_sent' : 'streamKBytesSent' 23 | }; 24 | 25 | if (typeof exceptions[name] === 'string') { 26 | return exceptions[name]; 27 | } 28 | 29 | // Camelize param name 30 | return name.replace(/[\s_-](.)/g, function($1) { return $1.toUpperCase(); }) 31 | .replace(/\s|_|-/g, '') 32 | .replace(/^(.)/, function($1) { return $1.toLowerCase(); }); 33 | }; 34 | 35 | /** 36 | * Normalize param data. 37 | * 38 | * @param {string} name 39 | * @param {Array} params 40 | * @return {number|string} 41 | */ 42 | module.exports.normalizeData = function(name, params) { 43 | 44 | /** 45 | * Normalized data 46 | * @var {mixed} 47 | */ 48 | var result; 49 | 50 | switch(name) { 51 | 52 | case 'audioInfo': 53 | var items = params.shift().split(';'); 54 | result = {}; 55 | 56 | for (var i in items) { 57 | items[i] = items[i].split('='); 58 | result[items[i][0]] = parseInt(items[i][1]); 59 | } 60 | break; 61 | 62 | // Nothing to do 63 | case 'delete': 64 | case 'flush': 65 | break; 66 | 67 | // Integer values 68 | case 'audioCodecId': 69 | case 'bannedIPs': 70 | case 'bitrate': 71 | case 'build': 72 | case 'clientConnections': 73 | case 'clients': 74 | case 'connected': 75 | case 'connections': 76 | case 'fileConnections': 77 | case 'incomingBitrate': 78 | case 'lag': 79 | case 'listenerConnections': 80 | case 'listenerPeak': 81 | case 'listeners': 82 | case 'maxListeners': 83 | case 'mpegChannels': 84 | case 'mpegSampleRate': 85 | case 'outgoingKBitrate': 86 | case 'public': 87 | case 'queueSize': 88 | case 'slowListeners': 89 | case 'sourceClientConnections': 90 | case 'sourceRelayConnections': 91 | case 'sources': 92 | case 'sourceTotalConnections': 93 | case 'stats': 94 | case 'statsConnections': 95 | case 'streamKBytesRead': 96 | case 'streamKBytesSent': 97 | case 'totalBytesRead': 98 | case 'totalBytesSent': 99 | case 'totalMBytesSent': 100 | result = parseInt(params.shift()); 101 | break; 102 | 103 | // String values 104 | case 'admin': 105 | case 'authenticator': 106 | case 'genre': 107 | case 'host': 108 | case 'info': 109 | case 'listenUrl': 110 | case 'location': 111 | case 'metadataUpdated': 112 | case 'new': 113 | case 'serverDescription': 114 | case 'serverId': 115 | case 'serverName': 116 | case 'serverStart': 117 | case 'serverType': 118 | case 'serverUrl': 119 | case 'sourceIp': 120 | case 'streamStart': 121 | case 'title': 122 | case 'ypCurrentlyPlaying': 123 | default: 124 | result = params.join(' '); 125 | break; 126 | } 127 | 128 | return result; 129 | }; -------------------------------------------------------------------------------- /src/XmlStreamParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | var Param = require(__dirname + '/Param'); 5 | var sax = require('sax'); 6 | var stream = require('stream'); 7 | var util = require('util'); 8 | 9 | /** 10 | * Module exports 11 | */ 12 | module.exports = XmlStreamParser; 13 | 14 | var config = { 15 | 16 | /** 17 | * Default tags we need to handle. They are used instead of camelCased parameters 18 | * to optimize additional processing during xmlStream parsing. 19 | * @var {object} 20 | */ 21 | tags : { 22 | 23 | /** 24 | * Tags to collect about server 25 | */ 26 | server: [ 27 | 'admin', 28 | 'banned_IPs', 29 | 'build', 30 | 'client_connections', 31 | 'clients', 32 | 'connections', 33 | 'file_connections', 34 | 'host', 35 | 'listener_connections', 36 | 'listeners', 37 | 'location', 38 | 'outgoing_kbitrate', 39 | 'server_id', 40 | 'server_start', 41 | 'source_client_connections', 42 | 'source_relay_connections', 43 | 'sources', 44 | 'source_total_connections', 45 | 'stats', 46 | 'stats_connections', 47 | 'stream_kbytes_read', 48 | 'stream_kbytes_sent' 49 | ], 50 | 51 | /** 52 | * Tags to collect about every source 53 | */ 54 | source: [ 55 | 'audio_codecid', 56 | 'audio_info', 57 | 'authenticator', 58 | 'bitrate', 59 | 'connected', 60 | 'genre', 61 | 'incoming_bitrate', 62 | 'listener_connections', 63 | 'listener_peak', 64 | 'listeners', 65 | 'listenurl', 66 | 'max_listeners', 67 | 'metadata_updated', 68 | 'mpeg_channels', 69 | 'mpeg_samplerate', 70 | 'outgoing_kbitrate', 71 | 'public', 72 | 'queue_size', 73 | 'server_description', 74 | 'server_name', 75 | 'server_type', 76 | 'server_url', 77 | 'slow_listeners', 78 | 'source_ip', 79 | 'stream_start', 80 | 'title', 81 | 'total_bytes_read', 82 | 'total_bytes_sent', 83 | 'total_mbytes_sent', 84 | 'yp_currently_playing' 85 | ], 86 | 87 | /** 88 | * Tags to collect about every listener 89 | */ 90 | listener: [ 91 | 'ID', 92 | 'IP', 93 | 'UserAgent', 94 | 'Referer', 95 | 'lag', 96 | 'Connected' 97 | ] 98 | }, 99 | 100 | /** 101 | * Depth level for different data types 102 | * @var {object} 103 | */ 104 | dataDepthLevels: { 105 | 2: 'server', 106 | 3: 'source', 107 | 4: 'listener' 108 | } 109 | }; 110 | 111 | /** 112 | * Creates data item with normalized tag names as keys and null values. 113 | * 114 | * @param {Array} tags 115 | */ 116 | function initDataItem(tags) { 117 | var item = {}; 118 | 119 | tags.forEach(function(tag) { 120 | var param = Param.normalizeName(tag); 121 | item[param] = null; 122 | }); 123 | 124 | return item; 125 | } 126 | 127 | /** 128 | * Constructor 129 | */ 130 | function XmlStreamParser () { 131 | 132 | /** 133 | * Instance link 134 | * @var {XmlStreamParser} 135 | */ 136 | var parser = this; 137 | 138 | /** 139 | * Represents currently processing data item (server, source, listener). 140 | * After processing is finished, event is emitted, data[object] is flushed. 141 | * @var {object} 142 | */ 143 | this.data = {}; 144 | 145 | /** 146 | * Sax writeable stream 147 | * @var {stream.Writeable} 148 | */ 149 | this.saxStream = sax.createStream(true); 150 | 151 | /** 152 | * Current depth level 153 | * @var {integer} 154 | */ 155 | this.currentDepth; 156 | 157 | /** 158 | * Current tag 159 | * @var {string} 160 | */ 161 | this.currentTag; 162 | 163 | /** 164 | * Current source mountpoint 165 | * @var {string} 166 | */ 167 | this.currentMount; 168 | 169 | // Inherit from writeable stream 170 | stream.Writable.call(this); 171 | 172 | // Handle errors 173 | parser.saxStream.on('error', function(error) { 174 | parser.emit('error', error); 175 | }); 176 | 177 | // Handle data 178 | parser.saxStream.on('opentag', function(node) { 179 | parser.handleOpenTag(node); 180 | }); 181 | 182 | parser.saxStream.on('text', function(text) { 183 | parser.handleText(text); 184 | }); 185 | 186 | parser.saxStream.on('closetag', function(tagName) { 187 | parser.handleCloseTag(tagName); 188 | }); 189 | } 190 | util.inherits(XmlStreamParser, stream.Writable); 191 | 192 | /** 193 | * Handle tag opening. 194 | * 195 | * @param {object} node 196 | */ 197 | XmlStreamParser.prototype.handleOpenTag = function(node) { 198 | 199 | // Re-init data containers 200 | if (node.name === 'icestats') { 201 | this.currentDepth = 0; 202 | 203 | this.data = { 204 | server: null, 205 | source: null, 206 | listener: null 207 | }; 208 | } 209 | 210 | // Calculate depth level 211 | this.currentDepth++; 212 | 213 | if (node.name === 'source') { 214 | this.currentMount = node.attributes.mount; 215 | } 216 | 217 | // Check current depth level should be handled 218 | if ( ! config.dataDepthLevels[this.currentDepth]) { 219 | return; 220 | } 221 | 222 | // Get current data type: server, source or listener 223 | var dataType = config.dataDepthLevels[this.currentDepth]; 224 | 225 | // Check that we need to handle this tag for current data type 226 | if (config.tags[dataType].indexOf(node.name) !== -1) { 227 | if ( ! this.data[dataType]) { 228 | 229 | // Create data item and fill it with nullable values 230 | this.data[dataType] = initDataItem(config.tags[dataType]); 231 | 232 | // Set mount name 233 | if (dataType === 'source' || dataType === 'listener') { 234 | this.data[dataType].mount = this.currentMount; 235 | } 236 | } 237 | this.currentTag = node.name; 238 | } 239 | }; 240 | 241 | /** 242 | * Handles text. 243 | * 244 | * @param {string} text 245 | */ 246 | XmlStreamParser.prototype.handleText = function(text) { 247 | 248 | // Check current depth level should be handled 249 | if ( ! config.dataDepthLevels[this.currentDepth]) { 250 | return; 251 | } 252 | 253 | // Check, if we need to handle current tag 254 | if ( ! this.currentTag) { 255 | return; 256 | } 257 | 258 | // Get current data type: server, source or listener 259 | var dataType = config.dataDepthLevels[this.currentDepth]; 260 | 261 | // Fill with normalized parameter & data 262 | var param = Param.normalizeName(this.currentTag); 263 | 264 | this.data[dataType][param] = Param.normalizeData(param, [text]); 265 | }; 266 | 267 | /** 268 | * Handles tag closing. 269 | * 270 | * @param {string} tagName 271 | */ 272 | XmlStreamParser.prototype.handleCloseTag = function(tagName) { 273 | 274 | this.currentDepth--; 275 | this.currentTag = null; 276 | 277 | switch(tagName) { 278 | 279 | // Source is finished 280 | case 'source': 281 | this.emit('source', this.data.source); 282 | this.data.source = null; 283 | this.currentMount = null; 284 | break; 285 | 286 | // Listener is finished 287 | case 'listener': 288 | this.emit('listener', this.data.listener); 289 | this.data.listener = null; 290 | break; 291 | 292 | // Server info is ready (xml file is finished) 293 | case 'icestats': 294 | this.emit('server', this.data.server); 295 | this.data.server = null; 296 | break; 297 | } 298 | }; 299 | 300 | /** 301 | * Handle input data. 302 | * 303 | * @param {Buffer|string} chunk 304 | * @param {string} encoding 305 | * @param {function} done 306 | */ 307 | XmlStreamParser.prototype._write = function (chunk, encoding, done) { 308 | this.saxStream.write(chunk); 309 | done(); 310 | }; 311 | -------------------------------------------------------------------------------- /test/feed.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Feed = require(__dirname + '/../index').Feed; 3 | 4 | describe('Monitor.Feed', function() { 5 | 6 | /** 7 | * @var {Feed} 8 | */ 9 | var feed; 10 | 11 | beforeEach(function() { 12 | feed = new Feed({ 13 | host: 'localhost', 14 | user: 'admin', 15 | password: 'hackme' 16 | }); 17 | }); 18 | 19 | afterEach(function() { 20 | feed = null; 21 | }); 22 | 23 | /** 24 | * Sets up err timeouts 25 | * @param {Array} expectedEvents 26 | */ 27 | function setupErrTimeouts(expectedEvents) { 28 | var timeouts = {}; 29 | 30 | expectedEvents.forEach(function(event){ 31 | timeouts[event] = setTimeout(function() { 32 | assert(false, 'Event ' + event + ' not fired') 33 | }, 1000); 34 | }); 35 | 36 | return timeouts; 37 | } 38 | 39 | /** 40 | * Asserts that given data is equal, 41 | * uses strict comparison for simple types, deep comparison for objects. 42 | * @param v1 43 | * @param v2 44 | * @param {string} message 45 | */ 46 | function assertDataIsEqual(v1, v2, message) { 47 | var type = typeof v1; 48 | if (type === 'object') { 49 | return assert.deepEqual(v1, v2, message); 50 | } 51 | 52 | return assert.strictEqual(v1, v2, message); 53 | } 54 | 55 | /** 56 | * Sets up server event 57 | * @param {string} event 58 | * @param data 59 | * @param {function} done 60 | */ 61 | function setupServerEvent(event, data, done) { 62 | 63 | var counter = 3; 64 | 65 | // Setup err timeouts 66 | var errTimeouts = setupErrTimeouts([event, 'server.*', '*']); 67 | 68 | // Listen to normal event 69 | feed.on(event, function(eventData) { 70 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 71 | 72 | clearTimeout(errTimeouts[event]); 73 | if (--counter === 0) done(); 74 | }); 75 | 76 | // Listen to type.* wildcard event 77 | feed.on('server.*', function(eventName, eventData) { 78 | assert.strictEqual(event, eventName, 'Event name mismatch'); 79 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 80 | 81 | clearTimeout(errTimeouts['server.*']); 82 | if (--counter === 0) done(); 83 | }); 84 | 85 | // Listen to * wildcard event 86 | feed.on('*', function(eventName, eventData) { 87 | assert.strictEqual(event, eventName, 'Event name mismatch'); 88 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 89 | 90 | clearTimeout(errTimeouts['*']); 91 | if (--counter === 0) done(); 92 | }); 93 | } 94 | 95 | /** 96 | * Sets up mount event 97 | * @param {string} event 98 | * @param {string} mount 99 | * @param data 100 | * @param {function} done 101 | */ 102 | function setupMountEvent(event, mount, data, done) { 103 | 104 | var counter = 3; 105 | 106 | // Setup err timeouts 107 | var errTimeouts = setupErrTimeouts([event, 'mount.*', '*']); 108 | 109 | // Listen to normal event 110 | feed.on(event, function(eventMount, eventData) { 111 | assert.strictEqual(mount, eventMount, 'Mount mismatch'); 112 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 113 | 114 | clearTimeout(errTimeouts[event]); 115 | if (--counter === 0) done(); 116 | }); 117 | 118 | // Listen to type.* wildcard event 119 | feed.on('mount.*', function(eventName, eventMount, eventData) { 120 | assert.strictEqual(event, eventName, 'Event name mismatch'); 121 | assert.strictEqual(mount, eventMount, 'Mount mismatch'); 122 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 123 | 124 | clearTimeout(errTimeouts['mount.*']); 125 | if (--counter === 0) done(); 126 | }); 127 | 128 | // Listen to * wildcard event 129 | feed.on('*', function(eventName, eventMount, eventData) { 130 | assert.strictEqual(event, eventName, 'Event name mismatch'); 131 | assert.strictEqual(mount, eventMount, 'Mount mismatch'); 132 | assertDataIsEqual(data, eventData, 'Event data mismatch'); 133 | 134 | clearTimeout(errTimeouts['*']); 135 | if (--counter === 0) done(); 136 | }); 137 | } 138 | 139 | /** 140 | * Server events 141 | */ 142 | describe('server.admin', function () { 143 | it('should emit server.admin, server.* and * events', function (done) { 144 | setupServerEvent('server.admin', 'admin@example.com', done); 145 | feed.handleRawEvent('EVENT global admin admin@example.com'); 146 | }); 147 | }); 148 | 149 | describe('server.bannedIPs', function () { 150 | it('should emit server.bannedIPs, server.* and * events', function (done) { 151 | setupServerEvent('server.bannedIPs', 0, done); 152 | feed.handleRawEvent('EVENT global banned_IPs 0'); 153 | }); 154 | }); 155 | 156 | describe('server.build', function () { 157 | it('should emit server.build, server.* and * events', function (done) { 158 | setupServerEvent('server.build', 20150616004931, done); 159 | feed.handleRawEvent('EVENT global build 20150616004931'); 160 | }); 161 | }); 162 | 163 | describe('server.clientConnections', function () { 164 | it('should emit server.clientConnections, server.* and * events', function (done) { 165 | setupServerEvent('server.clientConnections', 1029675, done); 166 | feed.handleRawEvent('EVENT global client_connections 1029675'); 167 | }); 168 | }); 169 | 170 | describe('server.clients', function () { 171 | it('should emit server.clients, server.* and * events', function (done) { 172 | setupServerEvent('server.clients', 62, done); 173 | feed.handleRawEvent('EVENT global clients 62'); 174 | }); 175 | }); 176 | 177 | describe('server.connections', function () { 178 | it('should emit server.connections, server.* and * events', function (done) { 179 | setupServerEvent('server.connections', 1178553, done); 180 | feed.handleRawEvent('EVENT global connections 1178553'); 181 | }); 182 | }); 183 | 184 | describe('server.fileConnections', function () { 185 | it('should emit server.fileConnections, server.* and * events', function (done) { 186 | setupServerEvent('server.fileConnections', 3534, done); 187 | feed.handleRawEvent('EVENT global file_connections 3534'); 188 | }); 189 | }); 190 | 191 | describe('server.host', function () { 192 | it('should emit server.host, server.* and * events', function (done) { 193 | setupServerEvent('server.host', 'icecast.dev', done); 194 | feed.handleRawEvent('EVENT global host icecast.dev'); 195 | }); 196 | }); 197 | 198 | describe('server.info', function () { 199 | it('should emit server.info, server.* and * events', function (done) { 200 | setupServerEvent('server.info', 'full list end', done); 201 | feed.handleRawEvent('INFO full list end'); 202 | }); 203 | }); 204 | 205 | describe('server.listenerConnections', function () { 206 | it('should emit server.listenerConnections, server.* and * events', function (done) { 207 | setupServerEvent('server.listenerConnections', 220589, done); 208 | feed.handleRawEvent('EVENT global listener_connections 220589'); 209 | }); 210 | }); 211 | 212 | describe('server.listeners', function () { 213 | it('should emit server.listeners, server.* and * events', function (done) { 214 | setupServerEvent('server.listeners', 16, done); 215 | feed.handleRawEvent('EVENT global listeners 16'); 216 | }); 217 | }); 218 | 219 | describe('server.location', function () { 220 | it('should emit server.location, server.* and * events', function (done) { 221 | setupServerEvent('server.location', 'RU', done); 222 | feed.handleRawEvent('EVENT global location RU'); 223 | }); 224 | }); 225 | 226 | describe('server.outgoingKBitrate', function () { 227 | it('should emit server.outgoingKBitrate, server.* and * events', function (done) { 228 | setupServerEvent('server.outgoingKBitrate', 4411, done); 229 | feed.handleRawEvent('EVENT global outgoing_kbitrate 4411'); 230 | }); 231 | }); 232 | 233 | describe('server.serverId', function () { 234 | it('should emit server.serverId, server.* and * events', function (done) { 235 | setupServerEvent('server.serverId', 'Icecast 2.4.0-kh1', done); 236 | feed.handleRawEvent('EVENT global server_id Icecast 2.4.0-kh1'); 237 | }); 238 | }); 239 | 240 | describe('server.serverStart', function () { 241 | it('should emit server.serverStart, server.* and * events', function (done) { 242 | setupServerEvent('server.serverStart', '06/Jul/2015:00:19:34 +0300', done); 243 | feed.handleRawEvent('EVENT global server_start 06/Jul/2015:00:19:34 +0300'); 244 | }); 245 | }); 246 | 247 | describe('server.sourceClientConnections', function () { 248 | it('should emit server.sourceClientConnections, server.* and * events', function (done) { 249 | setupServerEvent('server.sourceClientConnections', 0, done); 250 | feed.handleRawEvent('EVENT global source_client_connections 0'); 251 | }); 252 | }); 253 | 254 | describe('server.sourceRelayConnections', function () { 255 | it('should emit server.sourceRelayConnections, server.* and * events', function (done) { 256 | setupServerEvent('server.sourceRelayConnections', 1317, done); 257 | feed.handleRawEvent('EVENT global source_relay_connections 1317'); 258 | }); 259 | }); 260 | 261 | describe('server.sources', function () { 262 | it('should emit server.sources, server.* and * events', function (done) { 263 | setupServerEvent('server.sources', 45, done); 264 | feed.handleRawEvent('EVENT global sources 45'); 265 | }); 266 | }); 267 | 268 | describe('server.sourceTotalConnections', function () { 269 | it('should emit server.sourceTotalConnections, server.* and * events', function (done) { 270 | setupServerEvent('server.sourceTotalConnections', 1318, done); 271 | feed.handleRawEvent('EVENT global source_total_connections 1318'); 272 | }); 273 | }); 274 | 275 | describe('server.stats', function () { 276 | it('should emit server.stats, server.* and * events', function (done) { 277 | setupServerEvent('server.stats', 0, done); 278 | feed.handleRawEvent('EVENT global stats 0'); 279 | }); 280 | }); 281 | 282 | describe('server.statsConnections', function () { 283 | it('should emit server.statsConnections, server.* and * events', function (done) { 284 | setupServerEvent('server.statsConnections', 2, done); 285 | feed.handleRawEvent('EVENT global stats_connections 2'); 286 | }); 287 | }); 288 | 289 | describe('server.streamKBytesRead', function () { 290 | it('should emit server.streamKBytesRead, server.* and * events', function (done) { 291 | setupServerEvent('server.streamKBytesRead', 2414225600, done); 292 | feed.handleRawEvent('EVENT global stream_kbytes_read 2414225600'); 293 | }); 294 | }); 295 | 296 | describe('server.streamKBytesSent', function () { 297 | it('should emit server.streamKBytesSent, server.* and * events', function (done) { 298 | setupServerEvent('server.streamKBytesSent', 1102687068, done); 299 | feed.handleRawEvent('EVENT global stream_kbytes_sent 1102687068'); 300 | }); 301 | }); 302 | 303 | /** 304 | * Mount events 305 | */ 306 | describe('mount.audioCodecId', function () { 307 | it('should emit mount.audioCodecId, mount.* and * events', function (done) { 308 | setupMountEvent('mount.audioCodecId', '/test.mp3', 2, done); 309 | feed.handleRawEvent('EVENT /test.mp3 audio_codecid 2'); 310 | }); 311 | }); 312 | 313 | describe('mount.audioInfo', function () { 314 | it('should emit mount.audioInfo, mount.* and * events', function (done) { 315 | setupMountEvent('mount.audioInfo', '/test.mp3', { 316 | channels: 2, 317 | samplerate: 44100, 318 | bitrate: 64 319 | }, done); 320 | feed.handleRawEvent('EVENT /test.mp3 audio_info channels=2;samplerate=44100;bitrate=64'); 321 | }); 322 | }); 323 | 324 | describe('mount.authenticator', function () { 325 | it('should emit mount.authenticator, mount.* and * events', function (done) { 326 | setupMountEvent('mount.authenticator', '/test.mp3', 'command', done); 327 | feed.handleRawEvent('EVENT /test.mp3 authenticator command'); 328 | }); 329 | }); 330 | 331 | describe('mount.bitrate', function () { 332 | it('should emit mount.bitrate, mount.* and * events', function (done) { 333 | setupMountEvent('mount.bitrate', '/test.mp3', 64, done); 334 | feed.handleRawEvent('EVENT /test.mp3 bitrate 64'); 335 | }); 336 | }); 337 | 338 | describe('mount.connected', function () { 339 | it('should emit mount.connected, mount.* and * events', function (done) { 340 | setupMountEvent('mount.connected', '/test.mp3', 180423, done); 341 | feed.handleRawEvent('EVENT /test.mp3 connected 180423'); 342 | }); 343 | }); 344 | 345 | describe('mount.delete', function () { 346 | it('should emit mount.delete, mount.* and * events', function (done) { 347 | setupMountEvent('mount.delete', '/test.mp3', undefined, done); 348 | feed.handleRawEvent('DELETE /test.mp3'); 349 | }); 350 | }); 351 | 352 | describe('mount.flush', function () { 353 | it('should emit mount.flush, mount.* and * events', function (done) { 354 | setupMountEvent('mount.flush', '/test.mp3', undefined, done); 355 | feed.handleRawEvent('FLUSH /test.mp3'); 356 | }); 357 | }); 358 | 359 | describe('mount.genre', function () { 360 | it('should emit mount.genre, mount.* and * events', function (done) { 361 | setupMountEvent('mount.genre', '/test.mp3', 'Misc', done); 362 | feed.handleRawEvent('EVENT /test.mp3 genre Misc'); 363 | }); 364 | }); 365 | 366 | describe('mount.incomingBitrate', function () { 367 | it('should emit mount.incomingBitrate, mount.* and * events', function (done) { 368 | setupMountEvent('mount.incomingBitrate', '/test.mp3', 127064, done); 369 | feed.handleRawEvent('EVENT /test.mp3 incoming_bitrate 127064'); 370 | }); 371 | }); 372 | 373 | describe('mount.listenerConnections', function () { 374 | it('should emit mount.listenerConnections, mount.* and * events', function (done) { 375 | setupMountEvent('mount.listenerConnections', '/test.mp3', 4, done); 376 | feed.handleRawEvent('EVENT /test.mp3 listener_connections 4'); 377 | }); 378 | }); 379 | 380 | describe('mount.listenerPeak', function () { 381 | it('should emit mount.listenerPeak, mount.* and * events', function (done) { 382 | setupMountEvent('mount.listenerPeak', '/test.mp3', 2, done); 383 | feed.handleRawEvent('EVENT /test.mp3 listener_peak 2'); 384 | }); 385 | }); 386 | 387 | describe('mount.listeners', function () { 388 | it('should emit mount.listeners, mount.* and * events', function (done) { 389 | setupMountEvent('mount.listeners', '/test.mp3', 2, done); 390 | feed.handleRawEvent('EVENT /test.mp3 listeners 2'); 391 | }); 392 | }); 393 | 394 | describe('mount.listenUrl', function () { 395 | it('should emit mount.listenUrl, mount.* and * events', function (done) { 396 | setupMountEvent('mount.listenUrl', '/test.mp3', 'http://icecast.dev:80/test.mp3', done); 397 | feed.handleRawEvent('EVENT /test.mp3 listenurl http://icecast.dev:80/test.mp3'); 398 | }); 399 | }); 400 | 401 | describe('mount.maxListeners', function () { 402 | it('should emit mount.maxListeners, mount.* and * events', function (done) { 403 | setupMountEvent('mount.maxListeners', '/test.mp3', -1, done); 404 | feed.handleRawEvent('EVENT /test.mp3 max_listeners -1'); 405 | }); 406 | }); 407 | 408 | describe('mount.mpegChannels', function () { 409 | it('should emit mount.mpegChannels, mount.* and * events', function (done) { 410 | setupMountEvent('mount.mpegChannels', '/test.mp3', 2, done); 411 | feed.handleRawEvent('EVENT /test.mp3 mpeg_channels 2'); 412 | }); 413 | }); 414 | 415 | describe('mount.mpegSampleRate', function () { 416 | it('should emit mount.mpegSampleRate, mount.* and * events', function (done) { 417 | setupMountEvent('mount.mpegSampleRate', '/test.mp3', 44100, done); 418 | feed.handleRawEvent('EVENT /test.mp3 mpeg_samplerate 44100'); 419 | }); 420 | }); 421 | 422 | describe('mount.new', function () { 423 | it('should emit mount.new, mount.* and * events', function (done) { 424 | setupMountEvent('mount.new', '/test.mp3', 'audio/mpeg', done); 425 | feed.handleRawEvent('NEW audio/mpeg /test.mp3'); 426 | }); 427 | }); 428 | 429 | describe('mount.outgoingKBitrate', function () { 430 | it('should emit mount.outgoingKBitrate, mount.* and * events', function (done) { 431 | setupMountEvent('mount.outgoingKBitrate', '/test.mp3', 0, done); 432 | feed.handleRawEvent('EVENT /test.mp3 outgoing_kbitrate 0'); 433 | }); 434 | }); 435 | 436 | describe('mount.public', function () { 437 | it('should emit mount.public, mount.* and * events', function (done) { 438 | setupMountEvent('mount.public', '/test.mp3', 1, done); 439 | feed.handleRawEvent('EVENT /test.mp3 public 1'); 440 | }); 441 | }); 442 | 443 | describe('mount.queueSize', function () { 444 | it('should emit mount.queueSize, mount.* and * events', function (done) { 445 | setupMountEvent('mount.queueSize', '/test.mp3', 65828, done); 446 | feed.handleRawEvent('EVENT /test.mp3 queue_size 65828'); 447 | }); 448 | }); 449 | 450 | describe('mount.serverDescription', function () { 451 | it('should emit mount.serverDescription, mount.* and * events', function (done) { 452 | setupMountEvent('mount.serverDescription', '/test.mp3', 'My station description', done); 453 | feed.handleRawEvent('EVENT /test.mp3 server_description My station description'); 454 | }); 455 | }); 456 | 457 | describe('mount.serverName', function () { 458 | it('should emit mount.serverName, mount.* and * events', function (done) { 459 | setupMountEvent('mount.serverName', '/test.mp3', 'TestFM', done); 460 | feed.handleRawEvent('EVENT /test.mp3 server_name TestFM'); 461 | }); 462 | }); 463 | 464 | describe('mount.serverType', function () { 465 | it('should emit mount.serverType, mount.* and * events', function (done) { 466 | setupMountEvent('mount.serverType', '/test.mp3', 'audio/mpeg', done); 467 | feed.handleRawEvent('EVENT /test.mp3 server_type audio/mpeg'); 468 | }); 469 | }); 470 | 471 | describe('mount.serverUrl', function () { 472 | it('should emit mount.serverUrl, mount.* and * events', function (done) { 473 | setupMountEvent('mount.serverUrl', '/test.mp3', 'http://example.com/', done); 474 | feed.handleRawEvent('EVENT /test.mp3 server_url http://example.com/'); 475 | }); 476 | }); 477 | 478 | describe('mount.serverName', function () { 479 | it('should emit mount.serverName, mount.* and * events', function (done) { 480 | setupMountEvent('mount.serverName', '/test.mp3', 'TestFM', done); 481 | feed.handleRawEvent('EVENT /test.mp3 server_name TestFM'); 482 | }); 483 | }); 484 | 485 | describe('mount.slowListeners', function () { 486 | it('should emit mount.slowListeners, mount.* and * events', function (done) { 487 | setupMountEvent('mount.slowListeners', '/test.mp3', 0, done); 488 | feed.handleRawEvent('EVENT /test.mp3 slow_listeners 0'); 489 | }); 490 | }); 491 | 492 | describe('mount.sourceIp', function () { 493 | it('should emit mount.sourceIp, mount.* and * events', function (done) { 494 | setupMountEvent('mount.sourceIp', '/test.mp3', 'icecast.dev', done); 495 | feed.handleRawEvent('EVENT /test.mp3 source_ip icecast.dev'); 496 | }); 497 | }); 498 | 499 | describe('mount.streamStart', function () { 500 | it('should emit mount.streamStart, mount.* and * events', function (done) { 501 | setupMountEvent('mount.streamStart', '/test.mp3', '04/Aug/2015:12:00:31 +0300', done); 502 | feed.handleRawEvent('EVENT /test.mp3 stream_start 04/Aug/2015:12:00:31 +0300'); 503 | }); 504 | }); 505 | 506 | describe('mount.title', function () { 507 | it('should emit mount.title, mount.* and * events', function (done) { 508 | setupMountEvent('mount.title', '/test.mp3', 'Werkdiscs - Helena Hauff - Sworn To Secrecy Part II', done); 509 | feed.handleRawEvent('EVENT /test.mp3 title Werkdiscs - Helena Hauff - Sworn To Secrecy Part II'); 510 | }); 511 | }); 512 | 513 | describe('mount.totalBytesRead', function () { 514 | it('should emit mount.totalBytesRead, mount.* and * events', function (done) { 515 | setupMountEvent('mount.totalBytesRead', '/test.mp3', 1443575627, done); 516 | feed.handleRawEvent('EVENT /test.mp3 total_bytes_read 1443575627'); 517 | }); 518 | }); 519 | 520 | describe('mount.totalBytesSent', function () { 521 | it('should emit mount.totalBytesSent, mount.* and * events', function (done) { 522 | setupMountEvent('mount.totalBytesSent', '/test.mp3', 256000, done); 523 | feed.handleRawEvent('EVENT /test.mp3 total_bytes_sent 256000'); 524 | }); 525 | }); 526 | 527 | describe('mount.totalMBytesSent', function () { 528 | it('should emit mount.totalMBytesSent, mount.* and * events', function (done) { 529 | setupMountEvent('mount.totalMBytesSent', '/test.mp3', 0, done); 530 | feed.handleRawEvent('EVENT /test.mp3 total_mbytes_sent 0'); 531 | }); 532 | }); 533 | 534 | describe('mount.ypCurrentlyPlaying', function () { 535 | it('should emit mount.ypCurrentlyPlaying, mount.* and * events', function (done) { 536 | setupMountEvent('mount.ypCurrentlyPlaying', '/test.mp3', 'Nickelback - How You Remind Me', done); 537 | feed.handleRawEvent('EVENT /test.mp3 yp_currently_playing Nickelback - How You Remind Me'); 538 | }); 539 | }); 540 | }); -------------------------------------------------------------------------------- /test/listmounts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 1 6 | command 7 | 119157 8 | audio/mpeg 9 | 10 | 1541950 11 | 192.168.182.30 12 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 13 | http://example.com/ 14 | 5642 15 | 7692 16 | 17 | 18 | 19 | 0 20 | 0 21 | command 22 | 216477 23 | audio/mpeg 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/monitor.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var Monitor = require(__dirname + '/../index'); 4 | 5 | describe('Monitor', function() { 6 | 7 | /** 8 | * Overrides standard stream creation function. 9 | * 10 | * @param {string} urlPath 11 | * @param {function} callback 12 | */ 13 | Monitor.prototype.createStatsXmlStream = function(urlPath, callback) { 14 | 15 | var filePath; 16 | 17 | switch(urlPath) { 18 | case '/admin/stats': 19 | case '/admin/stats?mount=/test': 20 | filePath = __dirname + '/stats.xml'; 21 | break; 22 | 23 | case '/admin/listmounts?with_listeners': 24 | filePath = __dirname + '/listmounts.xml'; 25 | break; 26 | 27 | default: 28 | throw new Error('Unsupported url "' + urlPath + '"'); 29 | } 30 | 31 | callback(null, fs.createReadStream(filePath)); 32 | }; 33 | 34 | /** 35 | * @var {Monitor} 36 | */ 37 | var monitor; 38 | 39 | /** 40 | * Create new Monitor object for every test 41 | */ 42 | beforeEach(function() { 43 | monitor = new Monitor({ 44 | host: 'localhost', 45 | user: 'admin', 46 | password: 'hackme' 47 | }); 48 | }); 49 | 50 | afterEach(function() { 51 | monitor = null; 52 | }); 53 | 54 | /** 55 | * Test cases 56 | */ 57 | describe('monitor.getServerInfo', function () { 58 | it('should return information about server', function (done) { 59 | 60 | monitor.getServerInfo(function(err, data) { 61 | 62 | assert.ifError(err); 63 | 64 | assert.deepEqual({ 65 | admin: 'admin@example.com', 66 | bannedIPs: 0, 67 | build: 20150616004931, 68 | clientConnections: 1383589, 69 | clients: 56, 70 | connections: 1532635, 71 | fileConnections: 4248, 72 | host: 'icecast.dev', 73 | listenerConnections: 286175, 74 | listeners: 9, 75 | location: 'RU', 76 | outgoingKBitrate: 1363, 77 | serverId: 'Icecast 2.4.0-kh1', 78 | serverStart: '06/Jul/2015:00:19:34 +0300', 79 | sourceClientConnections: 0, 80 | sourceRelayConnections: 1745, 81 | sources: 45, 82 | sourceTotalConnections: 1745, 83 | stats: 0, 84 | statsConnections: 1, 85 | streamKBytesRead: 3748714817, 86 | streamKBytesSent: 1678120564 87 | }, data, 'Returned information does not match expected results'); 88 | 89 | done(); 90 | 91 | }); 92 | }); 93 | }); 94 | 95 | describe('monitor.getSources', function () { 96 | it('should return information about sources', function (done) { 97 | monitor.getSources(function(err, sources) { 98 | 99 | assert.ifError(err); 100 | 101 | assert.deepEqual([{ 102 | audioCodecId: 2, 103 | audioInfo: { 104 | channels: 2, 105 | samplerate: 44100, 106 | bitrate: 128 107 | }, 108 | authenticator: 'command', 109 | bitrate: 128, 110 | connected: 85218, 111 | genre: 'Misc', 112 | incomingBitrate: 128064, 113 | listenerConnections: 36, 114 | listenerPeak: 1, 115 | listeners: 0, 116 | listenUrl: 'http://icecast.dev:80/test.mp3', 117 | maxListeners: -1, 118 | metadataUpdated: '24/Aug/2015:02:07:12 +0300', 119 | mpegChannels: 2, 120 | mpegSampleRate: 44100, 121 | outgoingKBitrate: 0, 122 | public: 1, 123 | queueSize: 64784, 124 | serverDescription: 'My station description', 125 | serverName: 'TestFM', 126 | serverType: 'audio/mpeg', 127 | serverUrl: 'http://example.com/', 128 | slowListeners: 0, 129 | sourceIp: 'icecast.dev', 130 | streamStart: '23/Aug/2015:02:26:52 +0300', 131 | title: 'Metallica - I Disappear', 132 | totalBytesRead: 1363644219, 133 | totalBytesSent: 3789824, 134 | totalMBytesSent: 3, 135 | ypCurrentlyPlaying: 'Metallica - I Disappear', 136 | mount: '/test.mp3' 137 | }], sources, 'Returned information does not match expected results'); 138 | 139 | done(); 140 | 141 | }); 142 | }); 143 | }); 144 | 145 | describe('monitor.getSource', function () { 146 | it('should return information about /test source', function (done) { 147 | monitor.getSource('/test', function(err, source) { 148 | 149 | assert.ifError(err); 150 | 151 | assert.deepEqual({ 152 | audioCodecId: 2, 153 | audioInfo: { 154 | channels: 2, 155 | samplerate: 44100, 156 | bitrate: 128 157 | }, 158 | authenticator: 'command', 159 | bitrate: 128, 160 | connected: 85218, 161 | genre: 'Misc', 162 | incomingBitrate: 128064, 163 | listenerConnections: 36, 164 | listenerPeak: 1, 165 | listeners: [{ 166 | id: '1535115', 167 | ip: '192.168.182.30', 168 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36', 169 | referer: 'http://example.com/', 170 | lag: 5643, 171 | connected: 7895, 172 | mount: '/test.mp3' 173 | }], 174 | listenUrl: 'http://icecast.dev:80/test.mp3', 175 | maxListeners: -1, 176 | metadataUpdated: '24/Aug/2015:02:07:12 +0300', 177 | mpegChannels: 2, 178 | mpegSampleRate: 44100, 179 | outgoingKBitrate: 0, 180 | public: 1, 181 | queueSize: 64784, 182 | serverDescription: 'My station description', 183 | serverName: 'TestFM', 184 | serverType: 'audio/mpeg', 185 | serverUrl: 'http://example.com/', 186 | slowListeners: 0, 187 | sourceIp: 'icecast.dev', 188 | streamStart: '23/Aug/2015:02:26:52 +0300', 189 | title: 'Metallica - I Disappear', 190 | totalBytesRead: 1363644219, 191 | totalBytesSent: 3789824, 192 | totalMBytesSent: 3, 193 | ypCurrentlyPlaying: 'Metallica - I Disappear', 194 | mount: '/test.mp3' 195 | }, source, 'Returned information does not match expected results'); 196 | 197 | done(); 198 | 199 | }); 200 | }); 201 | }); 202 | 203 | describe('monitor.getListeners', function () { 204 | it('should return information about server', function (done) { 205 | monitor.getListeners(function(err, listeners) { 206 | 207 | assert.ifError(err); 208 | 209 | assert.deepEqual([{ 210 | id: '1541950', 211 | ip: '192.168.182.30', 212 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36', 213 | referer: 'http://example.com/', 214 | lag: 5642, 215 | connected: 7692, 216 | mount: '/test.mp3' 217 | }], listeners, 'Returned information does not match expected results'); 218 | 219 | done(); 220 | 221 | }); 222 | }); 223 | }); 224 | 225 | }); -------------------------------------------------------------------------------- /test/stats.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | admin@example.com 4 | 0 5 | 20150616004931 6 | 1383589 7 | 56 8 | 1532635 9 | 4248 10 | icecast.dev 11 | 286175 12 | 9 13 | RU 14 | 1363 15 | Icecast 2.4.0-kh1 16 | 06/Jul/2015:00:19:34 +0300 17 | 0 18 | 1745 19 | 1745 20 | 45 21 | 0 22 | 1 23 | 3748714817 24 | 1678120564 25 | 26 | 2 27 | channels=2;samplerate=44100;bitrate=128 28 | command 29 | 128 30 | 85218 31 | Misc 32 | 128064 33 | 36 34 | 1 35 | 0 36 | http://icecast.dev:80/test.mp3 37 | -1 38 | 24/Aug/2015:02:07:12 +0300 39 | 2 40 | 44100 41 | 0 42 | 1 43 | 64784 44 | My station description 45 | TestFM 46 | audio/mpeg 47 | http://example.com/ 48 | 0 49 | icecast.dev 50 | 23/Aug/2015:02:26:52 +0300 51 | Metallica - I Disappear 52 | 1363644219 53 | 3789824 54 | 3 55 | Metallica - I Disappear 56 | 57 | 1535115 58 | 192.168.182.30 59 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36 60 | http://example.com/ 61 | 5643 62 | 7895 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /test/xmlStreamParser.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var XmlStreamParser = require(__dirname + '/../index').XmlStreamParser; 4 | 5 | describe('Monitor.XmlStreamParser', function() { 6 | 7 | describe('server', function () { 8 | it('should emit server event', function (done) { 9 | 10 | var actual; 11 | var expected = { 12 | admin: 'admin@example.com', 13 | bannedIPs: 0, 14 | build: 20150616004931, 15 | clientConnections: 1383589, 16 | clients: 56, 17 | connections: 1532635, 18 | fileConnections: 4248, 19 | host: 'icecast.dev', 20 | listenerConnections: 286175, 21 | listeners: 9, 22 | location: 'RU', 23 | outgoingKBitrate: 1363, 24 | serverId: 'Icecast 2.4.0-kh1', 25 | serverStart: '06/Jul/2015:00:19:34 +0300', 26 | sourceClientConnections: 0, 27 | sourceRelayConnections: 1745, 28 | sources: 45, 29 | sourceTotalConnections: 1745, 30 | stats: 0, 31 | statsConnections: 1, 32 | streamKBytesRead: 3748714817, 33 | streamKBytesSent: 1678120564 34 | }; 35 | 36 | var xmlParser = new XmlStreamParser(); 37 | var xmlStream = fs.createReadStream(__dirname + '/stats.xml'); 38 | 39 | xmlParser.on('error', function(err) { 40 | assert(false, 'XmlStreamParser error: ' + err); 41 | }); 42 | 43 | xmlParser.on('server', function(server) { 44 | actual = server; 45 | }); 46 | 47 | xmlParser.on('finish', function() { 48 | assert.deepEqual(actual, expected, 'Event was not fired or data does not match'); 49 | done(); 50 | }); 51 | 52 | xmlStream.pipe(xmlParser); 53 | }); 54 | }); 55 | 56 | describe('source', function () { 57 | it('should emit source event', function (done) { 58 | 59 | var actual; 60 | var expected = { 61 | audioCodecId: 2, 62 | audioInfo: { 63 | channels: 2, 64 | samplerate: 44100, 65 | bitrate: 128 66 | }, 67 | authenticator: 'command', 68 | bitrate: 128, 69 | connected: 85218, 70 | genre: 'Misc', 71 | incomingBitrate: 128064, 72 | listenerConnections: 36, 73 | listenerPeak: 1, 74 | listeners: 0, 75 | listenUrl: 'http://icecast.dev:80/test.mp3', 76 | maxListeners: -1, 77 | metadataUpdated: '24/Aug/2015:02:07:12 +0300', 78 | mpegChannels: 2, 79 | mpegSampleRate: 44100, 80 | outgoingKBitrate: 0, 81 | public: 1, 82 | queueSize: 64784, 83 | serverDescription: 'My station description', 84 | serverName: 'TestFM', 85 | serverType: 'audio/mpeg', 86 | serverUrl: 'http://example.com/', 87 | slowListeners: 0, 88 | sourceIp: 'icecast.dev', 89 | streamStart: '23/Aug/2015:02:26:52 +0300', 90 | title: 'Metallica - I Disappear', 91 | totalBytesRead: 1363644219, 92 | totalBytesSent: 3789824, 93 | totalMBytesSent: 3, 94 | ypCurrentlyPlaying: 'Metallica - I Disappear', 95 | mount: '/test.mp3' 96 | }; 97 | 98 | var xmlParser = new XmlStreamParser(); 99 | var xmlStream = fs.createReadStream(__dirname + '/stats.xml'); 100 | 101 | xmlParser.on('error', function(err) { 102 | assert(false, 'XmlStreamParser error: ' + err); 103 | }); 104 | 105 | xmlParser.on('source', function(source) { 106 | actual = source; 107 | }); 108 | 109 | xmlParser.on('finish', function() { 110 | assert.deepEqual(actual, expected, 'Event was not fired or data does not match'); 111 | done(); 112 | }); 113 | 114 | xmlStream.pipe(xmlParser); 115 | }); 116 | }); 117 | 118 | describe('listener', function () { 119 | it('should emit listener event', function (done) { 120 | 121 | var actual; 122 | var expected = { 123 | id: '1541950', 124 | ip: '192.168.182.30', 125 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36', 126 | referer: 'http://example.com/', 127 | lag: 5642, 128 | connected: 7692, 129 | mount: '/test.mp3' 130 | }; 131 | 132 | var xmlParser = new XmlStreamParser(); 133 | var xmlStream = fs.createReadStream(__dirname + '/listmounts.xml'); 134 | 135 | xmlParser.on('error', function(err) { 136 | assert(false, 'XmlStreamParser error: ' + err); 137 | }); 138 | 139 | xmlParser.on('listener', function(listener) { 140 | actual = listener; 141 | }); 142 | 143 | xmlParser.on('finish', function() { 144 | assert.deepEqual(actual, expected, 'Event was not fired or data does not match'); 145 | done(); 146 | }); 147 | 148 | xmlStream.pipe(xmlParser); 149 | }); 150 | }); 151 | 152 | }); --------------------------------------------------------------------------------