UNPKG

castv2-player

Version:
324 lines (260 loc) 9.76 kB
"use strict" //module initialization module.exports = function (logClass) { //Constants const MIME_BLACKLIST = []; //["audio/ogg", "video/ogg"] <- they seem to work if contentType is specified //Includes var EventEmitter = require('events').EventEmitter; var icy = require('icy'); var url = require('url'); var playlist_parsers = require("playlist-parser"); var http = require('http'); var devnull = require('dev-null'); var util = require('util'); var mime = require('mime-types'); var log = logClass ? logClass : require("./dummyLogClass")("MediaInfo"); //Playlist class class MediaInfo extends EventEmitter { constructor (name, url) { super() let that = this; that._name = name; that._url = url; that._items = []; //List of events triggered by MediaInfo: that.EVENT_UPDATE = "mediaInfoUpdate"; } /* * Public methods */ getInfoPromise () { let that = this; return that._getIcyConnectionPromise() .then (function (res) { return that._parseIcyHeaderPromise (res);}); } close () { let that = this; log.info("%s - Closing mediaInfo",that._name); that._closeIcyConnection (); } /* * Private methods */ _getIcyConnectionPromise () { let that = this; return new Promise (function (resolve, reject) { function connect () { try { let con = MediaInfo._getRequestOptions(that._url); that._icyConnection = icy.get(con, function (res) { res.headers = res.headers ? res.headers : res.res && res.res.headers; if (res.statusCode >= 300 && res.statusCode < 400) { log.info("%s - Detected redirection (%d) to %s",that._name, res.statusCode, res.headers.location); that._url = res.headers.location; connect(); } else { resolve([that._url, res]); } }); that._icyConnection.on('error', function (err) { that._closeIcyConnection; reject(Error(err)); }); } catch (e) { reject(Error(e)); } } connect(); }); } _closeIcyConnection () { let that = this; if (that._icyConnection) { try{ that._icyConnection.abort(); } catch (e){}; delete that._icyConnection; } } static _getRequestOptions (theUrl) { //Create a header with user-agent: this is required by some servers //Apache musicindex mod crashes without it let con = url.parse(theUrl); con.headers = { 'User-Agent': 'ChromecastPlayer' }; return con; } _parseIcyHeaderPromise(args) { let baseUrl = args[0]; let res = args[1]; let that = this; /* * Example from http://edge.live.mp3.mdn.newmedia.nacamar.net/ps-dieneue_rock/livestream_hi.mp3 * * {'accept-ranges': 'none', * 'content-type': 'audio/mpeg', * 'icy-br': '128', * 'ice-audio-info': 'ice-samplerate=44100;ice-bitrate=128;ice-channels=2', * 'icy-description': 'BESTER ROCK UND POP', * 'icy-genre': 'Rock', * 'icy-name': 'DIE NEUE 107.7', * 'icy-pub': '1', * 'icy-url': 'http://www.dieneue1077.de', * server: 'Icecast 2.3.3-kh11', * 'cache-control': 'no-cache, no-store', * pragma: 'no-cache', * 'access-control-allow-origin': '*', * 'access-control-allow-headers': 'Origin, Accept, X-Requested-With, Content-Type', * 'access-control-allow-methods': 'GET, OPTIONS, HEAD', * connection: 'close', * expires: 'Mon, 26 Jul 1997 05:00:00 GMT', * 'icy-metaint': '16000' } */ // log the HTTP response headers log.debug("%s - Connected to %s\n%s",that._name, that._url, util.inspect(res.headers, {depth:null, colors:true})); //Remember content if ("content-type" in res.headers) that._contentType = res.headers["content-type"]; else that._contentType = mime.lookup(that._url); log.debug("%s - Detected %s as contentType",that._name,that._contentType); //Check if this is a playlist let parser; if (that._contentType === "audio/x-mpegurl" || that._contentType === "application/x-mpegURL") { //This is a M3U playlist parser = playlist_parsers.M3U; } else if (that._contentType === "audio/x-scpls") { //This is a PLS playlist parser = playlist_parsers.PLS; } else if ( (that._contentType === "video/x-ms-asf") || (that._contentType === "video/x-ms-asx")) { //This is a ASX playlist parser = playlist_parsers.ASX; } if (parser) { log.info("%s - Detected playlist -> parse", that._name); return that._parsePlaylistPromise(baseUrl, res, parser); } else if ("icy-name" in res.headers) { //Try to get title from icy header let title = that._url; if ("icy-name" in res.headers) title = res.headers["icy-name"]; that._addItem (that._url, that._contentType, {albumName: title}); // log any "metadata" events that happen res.on('metadata', that._gotMetadata.bind(that, res)); //Keep reading to get new metadata res.pipe(devnull()); } else { //This is not a playlist and we do not know how to parse it -> add it with some defaults that._addItem (that._url, that._contentType, {title: that._url}); //Close connection that._closeIcyConnection(); } return Promise.resolve(that._items); } static validContentType (contentType) { return (MIME_BLACKLIST.indexOf(contentType) < 0); } _addItem (url, contentType, metadata) { let that = this; if (MediaInfo.validContentType (contentType)) { that._items.push ({ "url": url, "contentType": contentType, "metadata": metadata }); } else log.error("%s - Not supported type (%s) for %s", that._name, contentType, url); } _updateMetadata (metadata) { let that = this; that._items[0].metadata = Object.assign(that._items[0].metadata, metadata);; } //parse playlist _parsePlaylistPromise (baseUrl, res, parser) { let that = this; return new Promise (function (resolve, reject) { let body = ''; res.on('data', function (chunk) { body += chunk; }); res.on('end', function () { //console.log('BODY: ' + body); //We will generate promises for each element in the list //The result value of the promise is not relevant -> the action directly add items to that._items let itemPromises = []; let playlist = parser.parse(body); for (let i in playlist) { //log.info("Item: %s", util.inspect(playlist[i], {depth:null, colors:true})); let theUrl = url.resolve(baseUrl, playlist[i].file); let title = playlist[i].title ? playlist[i].title : playlist[i].file; let artist = playlist[i].artist ? playlist[i].artist: "unknown"; //Try to guess content type based on extension let contentType = mime.lookup(theUrl); if (contentType) { //Got content itemPromises.push( new Promise(function (resolve, reject) { //Add current item directly that._addItem(theUrl, contentType, {title: title, artist:artist}); resolve(); }) ); } else { //It did not work -> we need to access the URL directly (slower) let itemMediaInfo = new MediaInfo(that._name, theUrl); itemPromises.push( //Full process to get the mediaInfo from this URL itemMediaInfo.getInfoPromise() .then(function (itemList) { //Add all founf elements that._items = that._items.concat(itemList); return Promise.resolve(); }) ); } } delete that._icyConnection; //After all promises are executed then we can return that._items resolve( Promise.all(itemPromises) .then(function () { return Promise.resolve(that._items); }) ); }); res.on('error', function (err) { reject(err); delete that._icyConnection; }); }); } _gotMetadata (res, metadata) { let that = this; /* * { StreamTitle: 'BILLY IDOL - WHITE WEDDING', StreamUrl: '&artist=BILLY%20IDOL&title=WHITE%20WEDDING&album=&duration=&songtype=S&overlay=&buycd=&website=&picture' } */ var parsed = icy.parse(metadata); log.debug("%s - ICY got metadata: \n%s", that._name, util.inspect(parsed, {depth:null})); //Get title (if any) let titte = that._url; if (parsed.StreamTitle) that._updateMetadata({title: parsed.StreamTitle}); //Notify that media has been updated that.emit(that.EVENT_UPDATE); }; } //MediaInfo class end var getMediaInfo = function (name, url){ //TBD: cache MediaInfo classes return new MediaInfo (name, url); } //Export MediaInfo factory return {"get": getMediaInfo}; }