<commit_before>var log = function () {
  'use strict';
  var args = [
    '<InfiniLoad>'
  ];
  // For some reason using `let...of` will throw an error.
  for (let key in arguments) {
    args.push(arguments[key]);
  }
  console.log.apply(console, args);
};
var resolveGenerator = function (mixed, args) {
  'use strict';
  // If the test variable is a function, call it with the arguments and return
  // its result. Otherwise return that variable.
  return (typeof mixed === 'function') ? mixed.apply(null, args) : mixed;
};
var timeValueConversion = {};
timeValueConversion['number'] = function (numValue) {
  return numValue;
};
timeValueConversion['date'] = function (numValue) {
  return new Date(numValue);
};
var allowedTimeFieldType = Object.getOwnPropertyNames(timeValueConversion);

InfiniLoad = function (collection, options) {
  'use strict';

  var _statsCollName, _contentCollName,
      _id, _pubId, _selector, _sort, _fields,
      _timeFieldName, _timeFieldType,
      _affiliation,
      _verbose, _slowdown, // These are debug options.
      _countingSort, _countingFields;

  // Make sure we get a valid collection.
  check(collection, Mongo.Collection);

  // Check necessary parameters in options.
  check(options, Match.Optional(Match.ObjectIncluding({
    'id': Match.Optional(String),
    'selector': Match.Optional(Match.OneOf(Object, Function)),
    'sort': Match.Optional(Match.OneOf(Object, Function)),
    'fields': Match.Optional(Match.OneOf(Object, Function)),
    'timeField': Match.Optional(Match.OneOf(String, {
      'name': Match.Optional(String),
      'type': Match.Optional(String)
    })),
    'affiliation': Match.Optional(Function),
    'verbose': Match.Optional(Boolean),
    'slowdown': Match.Optional(Number)
  })));

  options = options || {};

  // Fetch options.
  _pubId = options['id'] || 'default';
  _selector = options['selector'] || {};
  _sort = options['sort'] || {};
  _fields = options['fields'] || {};
  if (typeof options['timeField'] === 'string') {
    options['timeField'] = {
      'name': options['timeField']
    }
  } else {
    options['timeField'] = options['timeField'] || {};
  }
  _timeFieldName = options['timeField']['name'] || 'createTime';
  _timeFieldType = options['timeField']['type'] || 'number';
  _affiliation = options['affiliation'] || null;
  _verbose = options['verbose'] || false;
  _slowdown = options['slowdown'] || 0;

  _id = collection['_name'] + '__' + _pubId;
  _statsCollName = '__InfiniLoad-Stats-' + _id;
  _contentCollName = '__InfiniLoad-Content-' + _id;

  // Sort options for counting and detecting new documents.
  _countingSort = {};
  _countingSort[_timeFieldName] = -1;
  // Fields to return for counting documents.
  _countingFields = {};
  _countingFields[_timeFieldName] = 1;

  // Verify configurations.
  if (allowedTimeFieldType.indexOf(_timeFieldType) < 0) {
    throw new Error('Error when initializing InfiniLoad ' + _id + '. ' +
      _timeFieldType + ' is not a supported type.');
  }

  if (_verbose) {
    log('Initializing InfiniLoad ' + _id, {
      'statsCollName': _statsCollName,
      'contentCollName': _contentCollName,
      'selector': _selector,
      'sort': _sort,
      'fields': _fields,
      'timeFieldName': _timeFieldName,
      'timeFieldConstructor': _timeFieldType,
      'affiliation': _affiliation,
      'countingSort': _countingSort,
      'countingFields': _countingFields
    });
  }

  Meteor.publish(_statsCollName, function (options) {
    var now, self, initializing, selector,
        totalDocCount, totalDocCursor, totalDocHandle,
        latestDocTime, latestDocCursor,
        lastLoadTimeComparingValue,
        newDocCount, newDocCursor, newDocHandle,
        newDocSelector,
        GetReturnObject, Changed;

    if (_verbose) {
      log(_id, 'Publish request', _statsCollName, options);
    }

    check(options, Match.Optional(Match.ObjectIncluding({
      'args': Match.Optional(Object),
      'lastLoadTime': Match.Optional(Number)
    })));

    // `Date.now()` is faster than `new Date().getTime()`.
    now = Date.now();

    options = options || {};

    options['args'] = options['args'] || {};
    // If `lastLoadTime` is not specified, it is `now`.
    options['lastLoadTime'] = options['lastLoadTime'] || now;

    if (_verbose) {
      log(_id, 'Accepted publish request', options);
    }

    self = this;
    initializing = true;

    selector = resolveGenerator(_selector, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'selector', selector);
    }

    totalDocCount = 0;
    totalDocCursor = collection.find(selector, {
      'sort': _countingSort,
      'fields': _countingFields
    });

    // Get the newest document.
    latestDocTime = 0;
    latestDocCursor = collection.find(selector, {
      'sort': _countingSort,
      'limit': 1,
      'fields': _countingFields
    });
    if (latestDocCursor.count() === 0) {
      if (_verbose) {
        log(_id, 'selection is empty');
      }
      latestDocTime = now;
    } else {
      // Time field value must be able to be converted to a number.
      let timeValue = Number(latestDocCursor.fetch()[0][_timeFieldName]);
      latestDocTime = timeValue || now;
    }

    // Get what's new since last loading.
    lastLoadTimeComparingValue = timeValueConversion[_timeFieldType](options['lastLoadTime'])
    newDocCount = 0;
    newDocSelector = {};
    newDocSelector[_timeFieldName] = {
      '$gt': lastLoadTimeComparingValue
    };
    newDocCursor = collection.find({
      '$and': [
        selector,
        newDocSelector
      ]
    }, {
      'sort': _countingSort,
      'fields': _countingFields
    });

    GetReturnObject = function () {
      return {
        'totalDocCount': totalDocCount,
        'latestDocTime': latestDocTime,
        'newDocCount': newDocCount,
        'selector': selector
      };
    };
    Changed = function () {
      self.changed(_statsCollName, 0, GetReturnObject());
    };

    totalDocHandle = totalDocCursor.observeChanges({
      'added': function (id, fields) {
        totalDocCount++;
        // Time field value must be able to be converted to a number.
        let timeValue = Number(fields[_timeFieldName]);
        if (timeValue > latestDocTime) {
          latestDocTime = timeValue;
        }
        if (!initializing) Changed();
      },
      'removed': function (id) {
        totalDocCount--;
        Changed();
      }
    });
    newDocHandle = newDocCursor.observeChanges({
      'added': function (id) {
        newDocCount++;
        if (!initializing) Changed();
      },
      'removed': function (id) {
        newDocCount--;
        Changed();
      }
    });

    initializing = false;
    self.added(_statsCollName, 0, GetReturnObject());
    self.ready();

    self.onStop(function () {
      totalDocHandle.stop();
      newDocHandle.stop();
    });

    return;
  });

  Meteor.publish(_contentCollName, function (options) {
    var now, selector, sort, fields, oldDocSelector, oldDocCursor,
        lastLoadTimeComparingValue, returningCursors;

    if (_verbose) {
      log(_id, 'Publish request', _contentCollName, options);
    }

    check(options, Match.Optional(Match.ObjectIncluding({
      'args': Match.Optional(Object),
      'limit': Match.Optional(Number),
      'lastLoadTime': Match.Optional(Number)
    })));

    // `Date.now()` is faster than `new Date().getTime()`.
    now = Date.now();

    options = options || {};

    options['args'] = options['args'] || {};
    options['limit'] = options['limit'] || 0;
    options['lastLoadTime'] = options['lastLoadTime'] || now;

    if (_verbose) {
      log(_id, 'Accepted publish request', options);
    }

    selector = resolveGenerator(_selector, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'selector', selector);
    }

    sort = resolveGenerator(_sort, [this.userId, options['args']]);
    sort[_timeFieldName] = -1;
    if (_verbose) {
      log(_id, 'sort', sort);
    }

    fields = resolveGenerator(_fields, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'fields', fields);
    }

    lastLoadTimeComparingValue = timeValueConversion[_timeFieldType](options['lastLoadTime'])
    oldDocSelector = {};
    oldDocSelector[_timeFieldName] = {
      '$lte': lastLoadTimeComparingValue
    };

    oldDocCursor = collection.find({
      '$and': [
        selector,
        oldDocSelector
      ]
    }, {
      'sort': sort,
      'limit': options.limit,
      'fields': fields
    });

    if (_verbose) {
      log(_id, 'Results found', oldDocCursor.count());
    }

    returningCursors = [oldDocCursor];
    // Handle affiliation and see if it has more cursors to be returned.
    if (_affiliation) {
      let affiliatedCursors = _affiliation(oldDocCursor);
      // Proceed only if returns anything.
      if (affiliatedCursors) {
        // Make it an array.
        if (!Array.isArray(affiliatedCursors)) {
          affiliatedCursors = [affiliatedCursors];
        }
        // Examine the array and collect Mongo.Cursor items.
        for (let cursor of affiliatedCursors) {
          // `cursor instanceof Mongo.Cursor` doesn't work.
          if (cursor.fetch) {
            returningCursors.push(cursor);
          }
        }
      }
    }

    if (_slowdown > 0) Meteor._sleepForMs(_slowdown);

    return returningCursors;
  });
  return;
};
<commit_msg>Combined total and new doc observers so changes for new docs cause only one update.<commit_after>var log = function () {
  'use strict';
  var args = [
    '<InfiniLoad>'
  ];
  // For some reason using `let...of` will throw an error.
  for (let key in arguments) {
    args.push(arguments[key]);
  }
  console.log.apply(console, args);
};
var resolveGenerator = function (mixed, args) {
  'use strict';
  // If the test variable is a function, call it with the arguments and return
  // its result. Otherwise return that variable.
  return (typeof mixed === 'function') ? mixed.apply(null, args) : mixed;
};
var timeValueConversion = {};
timeValueConversion['number'] = function (numValue) {
  return numValue;
};
timeValueConversion['date'] = function (numValue) {
  return new Date(numValue);
};
var allowedTimeFieldType = Object.getOwnPropertyNames(timeValueConversion);

InfiniLoad = function (collection, options) {
  'use strict';

  var _statsCollName, _contentCollName,
      _id, _pubId, _selector, _sort, _fields,
      _timeFieldName, _timeFieldType,
      _affiliation,
      _verbose, _slowdown, // These are debug options.
      _countingSort, _countingFields;

  // Make sure we get a valid collection.
  check(collection, Mongo.Collection);

  // Check necessary parameters in options.
  check(options, Match.Optional(Match.ObjectIncluding({
    'id': Match.Optional(String),
    'selector': Match.Optional(Match.OneOf(Object, Function)),
    'sort': Match.Optional(Match.OneOf(Object, Function)),
    'fields': Match.Optional(Match.OneOf(Object, Function)),
    'timeField': Match.Optional(Match.OneOf(String, {
      'name': Match.Optional(String),
      'type': Match.Optional(String)
    })),
    'affiliation': Match.Optional(Function),
    'verbose': Match.Optional(Boolean),
    'slowdown': Match.Optional(Number)
  })));

  options = options || {};

  // Fetch options.
  _pubId = options['id'] || 'default';
  _selector = options['selector'] || {};
  _sort = options['sort'] || {};
  _fields = options['fields'] || {};
  if (typeof options['timeField'] === 'string') {
    options['timeField'] = {
      'name': options['timeField']
    }
  } else {
    options['timeField'] = options['timeField'] || {};
  }
  _timeFieldName = options['timeField']['name'] || 'createTime';
  _timeFieldType = options['timeField']['type'] || 'number';
  _affiliation = options['affiliation'] || null;
  _verbose = options['verbose'] || false;
  _slowdown = options['slowdown'] || 0;

  _id = collection['_name'] + '__' + _pubId;
  _statsCollName = '__InfiniLoad-Stats-' + _id;
  _contentCollName = '__InfiniLoad-Content-' + _id;

  // Sort options for counting and detecting new documents.
  _countingSort = {};
  _countingSort[_timeFieldName] = -1;
  // Fields to return for counting documents.
  _countingFields = {};
  _countingFields[_timeFieldName] = 1;

  // Verify configurations.
  if (allowedTimeFieldType.indexOf(_timeFieldType) < 0) {
    throw new Error('Error when initializing InfiniLoad ' + _id + '. ' +
      _timeFieldType + ' is not a supported type.');
  }

  if (_verbose) {
    log('Initializing InfiniLoad ' + _id, {
      'statsCollName': _statsCollName,
      'contentCollName': _contentCollName,
      'selector': _selector,
      'sort': _sort,
      'fields': _fields,
      'timeFieldName': _timeFieldName,
      'timeFieldConstructor': _timeFieldType,
      'affiliation': _affiliation,
      'countingSort': _countingSort,
      'countingFields': _countingFields
    });
  }

  Meteor.publish(_statsCollName, function (options) {
    var now, self, initializing, selector,
        totalDocCount, totalDocCursor, totalDocHandle,
        latestDocTime, latestDocCursor,
        newDocCount,
        newDocSelector,
        GetReturnObject, Changed;

    if (_verbose) {
      log(_id, 'Publish request', _statsCollName, options);
    }

    check(options, Match.Optional(Match.ObjectIncluding({
      'args': Match.Optional(Object),
      'lastLoadTime': Match.Optional(Number)
    })));

    // `Date.now()` is faster than `new Date().getTime()`.
    now = Date.now();

    options = options || {};

    options['args'] = options['args'] || {};
    // If `lastLoadTime` is not specified, it is `now`.
    options['lastLoadTime'] = options['lastLoadTime'] || now;

    if (_verbose) {
      log(_id, 'Accepted publish request', options);
    }

    self = this;
    initializing = true;

    selector = resolveGenerator(_selector, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'selector', selector);
    }

    totalDocCount = 0;
    totalDocCursor = collection.find(selector, {
      'sort': _countingSort,
      'fields': _countingFields
    });

    // Get the newest document.
    latestDocTime = 0;
    latestDocCursor = collection.find(selector, {
      'sort': _countingSort,
      'limit': 1,
      'fields': _countingFields
    });
    if (latestDocCursor.count() === 0) {
      if (_verbose) {
        log(_id, 'selection is empty');
      }
      latestDocTime = now;
    } else {
      // Time field value must be able to be converted to a number.
      let timeValue = Number(latestDocCursor.fetch()[0][_timeFieldName]);
      latestDocTime = timeValue || now;
    }

    // Get what's new since last loading.
    newDocCount = 0;

    GetReturnObject = function () {
      return {
        'totalDocCount': totalDocCount,
        'latestDocTime': latestDocTime,
        'newDocCount': newDocCount,
        'selector': selector
      };
    };
    Changed = function () {
      self.changed(_statsCollName, 0, GetReturnObject());
    };

    totalDocHandle = totalDocCursor.observe({
      'added': function (doc) {
        totalDocCount++;
        // Time field value must be able to be converted to a number.
        let timeValue = Number(doc[_timeFieldName]);
        // See if this doc is the latest on server.
        if (timeValue > latestDocTime) {
          latestDocTime = timeValue;
        }
        // If this doc is new to the client, update new count.
        if (timeValue > options['lastLoadTime']) {
          newDocCount++;
        }
        if (!initializing) Changed();
      },
      'removed': function (doc) {
        totalDocCount--;
        // If this doc is new to the client, update new count.
        if (timeValue > options['lastLoadTime']) {
          newDocCount--;
        }
        Changed();
      }
    });

    initializing = false;
    self.added(_statsCollName, 0, GetReturnObject());
    self.ready();

    self.onStop(function () {
      totalDocHandle.stop();
    });

    return;
  });

  Meteor.publish(_contentCollName, function (options) {
    var now, selector, sort, fields, oldDocSelector, oldDocCursor,
        lastLoadTimeComparingValue, returningCursors;

    if (_verbose) {
      log(_id, 'Publish request', _contentCollName, options);
    }

    check(options, Match.Optional(Match.ObjectIncluding({
      'args': Match.Optional(Object),
      'limit': Match.Optional(Number),
      'lastLoadTime': Match.Optional(Number)
    })));

    // `Date.now()` is faster than `new Date().getTime()`.
    now = Date.now();

    options = options || {};

    options['args'] = options['args'] || {};
    options['limit'] = options['limit'] || 0;
    options['lastLoadTime'] = options['lastLoadTime'] || now;

    if (_verbose) {
      log(_id, 'Accepted publish request', options);
    }

    selector = resolveGenerator(_selector, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'selector', selector);
    }

    sort = resolveGenerator(_sort, [this.userId, options['args']]);
    sort[_timeFieldName] = -1;
    if (_verbose) {
      log(_id, 'sort', sort);
    }

    fields = resolveGenerator(_fields, [this.userId, options['args']]);
    if (_verbose) {
      log(_id, 'fields', fields);
    }

    lastLoadTimeComparingValue = timeValueConversion[_timeFieldType](options['lastLoadTime'])
    oldDocSelector = {};
    oldDocSelector[_timeFieldName] = {
      '$lte': lastLoadTimeComparingValue
    };

    oldDocCursor = collection.find({
      '$and': [
        selector,
        oldDocSelector
      ]
    }, {
      'sort': sort,
      'limit': options.limit,
      'fields': fields
    });

    if (_verbose) {
      log(_id, 'Results found', oldDocCursor.count());
    }

    returningCursors = [oldDocCursor];
    // Handle affiliation and see if it has more cursors to be returned.
    if (_affiliation) {
      let affiliatedCursors = _affiliation(oldDocCursor);
      // Proceed only if returns anything.
      if (affiliatedCursors) {
        // Make it an array.
        if (!Array.isArray(affiliatedCursors)) {
          affiliatedCursors = [affiliatedCursors];
        }
        // Examine the array and collect Mongo.Cursor items.
        for (let cursor of affiliatedCursors) {
          // `cursor instanceof Mongo.Cursor` doesn't work.
          if (cursor.fetch) {
            returningCursors.push(cursor);
          }
        }
      }
    }

    if (_slowdown > 0) Meteor._sleepForMs(_slowdown);

    return returningCursors;
  });
  return;
};
<|endoftext|>


<commit_before>/* eslint-disable standard/no-callback-literal */
import fs from 'fs'
import url from 'url'
import path from 'path'
import each from 'async/each'
import mkdirp from 'mkdirp'
import request from 'request-promise'

/**
 * @description Validate a Playlist
 * @param {string} playlistContent
 * @returns {boolean}
 */
function isValidPlaylist (playlistContent) {
  return playlistContent.match(/^#EXTM3U/im) !== null
}

/**
 * @description Validate a URL
 * @param {string} url URL to validate
 * @returns {boolean}
 */
function validateURL (url) {
  var urlRegex = new RegExp(
    '^' +
    // protocol identifier
    '(?:(?:https?)://)' +
    // user:pass authentication
    '(?:\\S+(?::\\S*)?@)?' +
    '(?:' +
    // IP address exclusion
    // private & local networks
    '(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
    '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
    '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
    // IP address dotted notation octets
    // excludes loopback network 0.0.0.0
    // excludes reserved space >= 224.0.0.0
    // excludes network & broacast addresses
    // (first & last IP address of each class)
    '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
    '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
    '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
    '|' +
    // host name
    '(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
    // domain name
    '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
    // TLD identifier
    '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
    ')' +
    // port number
    '(?::\\d{2,5})?' +
    // resource path
    '(?:/\\S*)?' +
    '$', 'i'
  )

  return urlRegex.test(url)
}

/**
 * Strip first slash from a url
 * @param  {String} url URL to strip the slash
 * @return {String} Stripped url
 */
function stripFirstSlash (url) {
  return url.substr(0, 1).replace('/', '') + url.substring(1)
}

/**
 * HLSDownloader Class
 */
class HLSDownloader {
  /**
   * @constructor HLSParser
   * @param  {Object} playlistInfo playlist information to download
   * @return {Object} Error object if any required piece is missing
   */
  constructor (playlistInfo) {
    if (typeof playlistInfo === 'object' &&
      (playlistInfo.playlistURL === null ||
      playlistInfo.playlistURL === 'undefined' ||
      playlistInfo.playlistURL === '' ||
      !validateURL(playlistInfo.playlistURL))) {
      console.log('ERR_VALIDATION: playListURL is required ' +
        'or check if your URL is valid or not!!')
    }

    this.playlistURL = playlistInfo.playlistURL
    this.destination = playlistInfo.destination || null
    const urls = url.parse(this.playlistURL, true, true)
    this.hostName = urls.hostname
    this.items = []
    this.errors = []
  }

  /**
   * @description initiate download
   * @method {function} startDownload
   * @param {function} callback
   */
  startDownload (callback) {
    return this.getPlaylist(callback)
  }

  /**
   * @description Download master playlist
   * @method getPlaylist
   * @param {function} callback
   */
  getPlaylist (callback) {
    request.get(this.playlistURL).then((body) => {
      if (!isValidPlaylist(body)) {
        return callback(new Error("This playlist isn't a valid m3u8 playlist"))
      }

      this.items.push(this.playlistURL)
      this.parseMasterPlaylist(body, callback)
    }).catch((err) => {
      if (err) {
        const error = new Error('VariantDownloadError')
        error.statusCode = err.statusCode
        error.uri = err.options.uri
        return callback(error)
      }
    })
  }

  /**
   * @description Parse master playlist content
   * @method parseMasterPlaylist
   * @param {string} playlistContent
   * @param {function} callback
   */
  parseMasterPlaylist (playlistContent, callback) {
    if (playlistContent.match(/^#EXT-X-TARGETDURATION:\d+/im)) {
      this.parseVariantPlaylist(playlistContent)
      this.downloadItems(callback)
    } else {
      try {
        const replacedPlaylistContent = playlistContent.replace(/^#[\s\S].*/igm, '')
        const variants = replacedPlaylistContent.split('\n').filter(item => item !== '')

        let errorCounter = 0
        const variantCount = variants.length

        each(variants, (item, cb) => {
          const variantUrl = url.resolve(this.playlistURL, item)
          request.get(variantUrl).then(body => {
            if (isValidPlaylist(body)) {
              this.items.push(variantUrl)
              this.parseVariantPlaylist(body)
              return cb(null)
            }
          }).catch(err => {
            this.errors.push(err.options.uri)

            // check if all variants has error
            if (err && ++errorCounter === variantCount) {
              return cb(true)
            }

            return cb(null)
          })
        }, err => err ? callback({
          playlistURL: this.playlistURL,
          message: 'No valid Downloadable variant exists in master playlist'
        }) : this.downloadItems(callback))
      } catch (exception) {
        // Catch any syntax error
        return callback(exception)
      }
    }
  }

  /**
   * @description Parse variant playlist content and index the TS chunk to download.
   * @method parseVariantPlaylist
   * @param {string} variantPath
   * @param {string} playlistContent
   */
  parseVariantPlaylist (playlistContent) {
    const replacedPlaylistContent = playlistContent.replace(/^#[\s\S].*/igm, '')
    let items = replacedPlaylistContent
      .split('\n')
      .filter((item) => item !== '')
      .map((item) => url.resolve(this.playlistURL, item))

    this.items = this.items.concat(items)
  }

  /**
   * @description Download indexed chunks and playlist.
   * @method downloadItems
   * @param {function} callback
   */
  downloadItems (callback) {
    each(this.items, (variantUrl, cb) => {
      request.get(variantUrl).then(downloadedItem => {
        if (this.destination !== null &&
          this.destination !== '' &&
          this.destination !== 'undefined') {
          return this.createItems(variantUrl, downloadedItem, cb)
        }

        downloadedItem = null
        return cb()
      }).catch(err => {
        this.errors.push(err.options.uri)
        return cb(null)
      })
    }, err => {
      if (err) {
        return callback({
          playlistURL: this.playlistURL,
          message: 'Internal Server Error from remote'
        })
      }

      if (this.errors.length > 0) {
        return callback(null, {
          message: 'Download done with some errors',
          playlistURL: this.playlistURL,
          errors: this.errors
        })
      }

      return callback(null, {
        message: 'Downloaded successfully',
        playlistURL: this.playlistURL
      })
    })
  }

  /**
   * @description Download indexed chunks and playlist.
   * @method downloadItems
   * @param {function} callback
   */
  createItems (variantURL, content, cb) {
    const itemPath = url.parse(variantURL).pathname
    const destDirectory = this.destination + path.dirname(itemPath)
    const filePath = this.destination + '/' + stripFirstSlash(itemPath)

    mkdirp(destDirectory, err => (err) ? cb(err) : fs.writeFile(filePath, content, cb))
  }
}

export const downloader = HLSDownloader
export default HLSDownloader
<commit_msg>little refactoring in syntax<commit_after>/* eslint-disable standard/no-callback-literal */
import fs from 'fs'
import url from 'url'
import path from 'path'
import each from 'async/each'
import mkdirp from 'mkdirp'
import request from 'request-promise'

/**
 * @description Validate a Playlist
 * @param {string} playlistContent
 * @returns {boolean}
 */
function isValidPlaylist (playlistContent) {
  return playlistContent.match(/^#EXTM3U/im) !== null
}

/**
 * @description Validate a URL
 * @param {string} url URL to validate
 * @returns {boolean}
 */
function validateURL (url) {
  var urlRegex = new RegExp(
    '^' +
    // protocol identifier
    '(?:(?:https?)://)' +
    // user:pass authentication
    '(?:\\S+(?::\\S*)?@)?' +
    '(?:' +
    // IP address exclusion
    // private & local networks
    '(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
    '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
    '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
    // IP address dotted notation octets
    // excludes loopback network 0.0.0.0
    // excludes reserved space >= 224.0.0.0
    // excludes network & broacast addresses
    // (first & last IP address of each class)
    '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
    '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
    '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
    '|' +
    // host name
    '(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
    // domain name
    '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
    // TLD identifier
    '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
    ')' +
    // port number
    '(?::\\d{2,5})?' +
    // resource path
    '(?:/\\S*)?' +
    '$', 'i'
  )

  return urlRegex.test(url)
}

/**
 * Strip first slash from a url
 * @param  {String} url URL to strip the slash
 * @return {String} Stripped url
 */
function stripFirstSlash (url) {
  return url.substr(0, 1).replace('/', '') + url.substring(1)
}

/**
 * HLSDownloader Class
 */
class HLSDownloader {
  /**
   * @constructor HLSParser
   * @param  {Object} playlistInfo playlist information to download
   * @return {Object} Error object if any required piece is missing
   */
  constructor (playlistInfo) {
    if (typeof playlistInfo === 'object' &&
      (playlistInfo.playlistURL === null ||
      playlistInfo.playlistURL === 'undefined' ||
      playlistInfo.playlistURL === '' ||
      !validateURL(playlistInfo.playlistURL))) {
      console.log('ERR_VALIDATION: playListURL is required ' +
        'or check if your URL is valid or not!!')
    }

    this.playlistURL = playlistInfo.playlistURL
    this.destination = playlistInfo.destination || null
    const urls = url.parse(this.playlistURL, true, true)
    this.hostName = urls.hostname
    this.items = []
    this.errors = []
  }

  /**
   * @description initiate download
   * @method {function} startDownload
   * @param {function} callback
   */
  startDownload (callback) {
    return this.getPlaylist(callback)
  }

  /**
   * @description Download master playlist
   * @method getPlaylist
   * @param {function} callback
   */
  getPlaylist (callback) {
    request(this.playlistURL).then((body) => {
      if (!isValidPlaylist(body)) {
        return callback(new Error("This playlist isn't a valid m3u8 playlist"))
      }

      this.items.push(this.playlistURL)
      this.parseMasterPlaylist(body, callback)
    }).catch((err) => {
      if (err) {
        const error = new Error('VariantDownloadError')
        error.statusCode = err.statusCode
        error.uri = err.options.uri
        return callback(error)
      }
    })
  }

  /**
   * @description Parse master playlist content
   * @method parseMasterPlaylist
   * @param {string} playlistContent
   * @param {function} callback
   */
  parseMasterPlaylist (playlistContent, callback) {
    if (playlistContent.match(/^#EXT-X-TARGETDURATION:\d+/im)) {
      this.parseVariantPlaylist(playlistContent)
      this.downloadItems(callback)
    } else {
      try {
        const replacedPlaylistContent = playlistContent.replace(/^#[\s\S].*/igm, '')
        const variants = replacedPlaylistContent.split('\n').filter(item => item !== '')

        let errorCounter = 0
        const variantCount = variants.length

        each(variants, (item, cb) => {
          const variantUrl = url.resolve(this.playlistURL, item)
          request(variantUrl).then(body => {
            if (isValidPlaylist(body)) {
              this.items.push(variantUrl)
              this.parseVariantPlaylist(body)
              return cb(null)
            }
          }).catch(err => {
            this.errors.push(err.options.uri)

            // check if all variants has error
            if (err && ++errorCounter === variantCount) {
              return cb(true)
            }

            return cb(null)
          })
        }, err => err ? callback({
          playlistURL: this.playlistURL,
          message: 'No valid Downloadable variant exists in master playlist'
        }) : this.downloadItems(callback))
      } catch (exception) {
        // Catch any syntax error
        return callback(exception)
      }
    }
  }

  /**
   * @description Parse variant playlist content and index the TS chunk to download.
   * @method parseVariantPlaylist
   * @param {string} variantPath
   * @param {string} playlistContent
   */
  parseVariantPlaylist (playlistContent) {
    const replacedPlaylistContent = playlistContent.replace(/^#[\s\S].*/igm, '')
    let items = replacedPlaylistContent
      .split('\n')
      .filter((item) => item !== '')
      .map((item) => url.resolve(this.playlistURL, item))

    this.items = this.items.concat(items)
  }

  /**
   * @description Download indexed chunks and playlist.
   * @method downloadItems
   * @param {function} callback
   */
  downloadItems (callback) {
    each(this.items, (variantUrl, cb) => {
      request(variantUrl).then(downloadedItem => {
        if (this.destination !== null &&
          this.destination !== '' &&
          this.destination !== 'undefined') {
          return this.createItems(variantUrl, downloadedItem, cb)
        }

        downloadedItem = null
        return cb()
      }).catch(err => {
        this.errors.push(err.options.uri)
        return cb(null)
      })
    }, err => {
      if (err) {
        return callback({
          playlistURL: this.playlistURL,
          message: 'Internal Server Error from remote'
        })
      }

      if (this.errors.length > 0) {
        return callback(null, {
          message: 'Download done with some errors',
          playlistURL: this.playlistURL,
          errors: this.errors
        })
      }

      return callback(null, {
        message: 'Downloaded successfully',
        playlistURL: this.playlistURL
      })
    })
  }

  /**
   * @description Download indexed chunks and playlist.
   * @method downloadItems
   * @param {function} callback
   */
  createItems (variantURL, content, cb) {
    const itemPath = url.parse(variantURL).pathname
    const destDirectory = this.destination + path.dirname(itemPath)
    const filePath = this.destination + '/' + stripFirstSlash(itemPath)

    mkdirp(destDirectory, err => (err) ? cb(err) : fs.writeFile(filePath, content, cb))
  }
}

export const downloader = HLSDownloader
export default HLSDownloader
<|endoftext|>


<commit_before>'use strict';

const flatten = require('lodash.flatten');
const ON = 'on';
const BEFORE = 'before';
const AFTER = 'after';
const HIERARCHY_SEPARATOR = '.';

/**
 * Listeners by event.
 *
 * [message]: {
 *   before: ...Function,
 *   on: ...Function,
 *   after: ...Function,
 * }
 *
 * @var Object
 */
let listeners = {};

/**
 * An objet containing all the methods that hot-press can possibly namespace.
 *
 * @var Object
 */
let HP = {};

/**
 * Returns all the listeners for a given message. The returned value will be an
 * object whose keys are 'before', 'on' and 'after' of which each will contain
 * an array of functions/listeners.
 *
 * @param String message
 * @return Object
 */
function getListenersFor(message) {
  if (!listeners[message]) listeners[message] = {
    [BEFORE]: [],
    [ON]: [],
    [AFTER]: []
  };
  return listeners[message];
}

/**
 * Returns a list of messages that should be called in order for a specific
 * message
 *
 * @param String message
 * @return String[]
 */
function getHierarchy(message) {
  let parts = message.split(HIERARCHY_SEPARATOR);
  let hierarchy = [parts[0], '*'];
  parts.reduce((message, part) => {
    let prefix = message + HIERARCHY_SEPARATOR;
    let newMessage = prefix + part;
    let wildcard = prefix + '*';
    hierarchy.unshift(newMessage, wildcard);
    return newMessage;
  });
  return hierarchy;
}

/**
 * Adds a listener to a specific part of an event's lifecycle.
 *
 * @param String part
 * @param String message
 * @param Function fn
 */
function onPart(part, message, fn) {
  getListenersFor(message)[part].push(fn);
}

/**
 * Adds a listener to the event.
 *
 * @param String message
 * @param Function fn
 */
HP.on = onPart.bind(null, ON);

/**
 * Adds a listener to the beginning of the event lifecycle.
 *
 * @param String message
 * @param Function fn
 */
HP.before = onPart.bind(null, BEFORE);

/**
 * Adds a listener to the end of the event lifecycle.
 *
 * @param String message
 * @param Function fn
 */
HP.after = onPart.bind(null, AFTER);

/**
 * Will call the listener once each of the specified events have been emitted.
 * The events are given as an object, where each key will specify the part of
 * the event lifecycle and the value is an array of event names/messages.
 *
 * ```
 * all({
 *   before: ['foo', 'bar'],
 *   on: ['another'],
 *   after: ['event']
 * }, ({foo, bar, another, event}) => {
 *   // You'll receive data for each event that fired
 * });
 * ```
 *
 * @param Object<before: String[], on: String[], after: String[]> messages
 * @param Function fn
 */
HP.all = (messages, fn) => {
  let toDo;
  let dataCollection;
  let size = Object
    .keys(messages)
    .reduce((size, part) => size + messages[part].length, 0);

  init();

  function subscriber(message, ...data) {
    dataCollection[message] = data;
    toDo--;
    if (!toDo) {
      fn(dataCollection);
      init();
    }
  }

  function registerSubscribers(prop, method) {
    if (messages[prop]) messages[prop].forEach(message => {
      method(message, subscriber);
    });
  }

  function init() {
    toDo = size;
    dataCollection = {};
    registerSubscribers('before', HP.onceBefore);
    registerSubscribers('on', HP.once);
    registerSubscribers('after', HP.onceAfter);
  }
};

/**
 * Removes a listener from a given event.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
function removeListener(message, fn) {
  let listeners = getListenersFor(message);
  let removed = 0;
  for (let key in listeners) {
    let set = listeners[key];
    let index = set.indexOf(fn);
    if (index !== -1) {
      set.splice(index, 1);
      removed++;
      break;
    }
  }
  return removed;
}

/**
 * Removes all listeners from a given event.
 *
 * @param String message
 * @return Number
 */
function removeAllListeners(message) {
  let all = listeners[message];
  let amount = all ? all.length : 0;
  delete listeners[message];
  return amount;
}

/**
 * Removes the specified listener from a given event. If no listener is
 * specified, all will be removed.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
HP.off = (message, fn) => {
  return fn ? removeListener(message, fn) : removeAllListeners(message);
};

/**
 * Adds a listener to a specific part of the event lifecycle and removes it
 * as soon as it's been called.
 *
 * @param Function subscribe
 * @param String message
 * @param Function fn
 */
function oncePart(subscribe, message, fn) {
  let subscriber = (...args) => {
    HP.off(message, subscriber);
    return fn(...args);
  };
  subscribe(message, subscriber);
}

/**
 * Adds the listener to the event for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.once = oncePart.bind(null, HP.on);

/**
 * Adds a listener to the beginning of the event lifecycle for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.onceBefore = oncePart.bind(null, HP.before);

/**
 * Adds a listener to the end of the event lifecycle for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.onceAfter = oncePart.bind(null, HP.after);

/**
 * Creates an emitter based on the event name/message and data. The emitter
 * can then be used to emit each part of the lifecycle.
 *
 * @param String message
 * @param Any[] data
 * @return Function
 */
function createEmitter(message, data) {
  let call = fn => fn(message, ...data);
  let hierarchy = getHierarchy(message);
  return part => Promise.all(flatten(
    hierarchy.map(message => getListenersFor(message)[part].map(call))
  ));
}

/**
 * Fires off the lifecycle of an event.
 *
 * @param String message
 * @param Any ...data
 * @return Promise
 */
HP.emit = (message, ...data) => {
  let emit = createEmitter(message, data);
  return emit(BEFORE)
    .then(() => emit(ON))
    .then(() => emit(AFTER));
};

/**
 * Subscribes emittion of an array of events to another event. All data will be
 * passed to the emittions.
 *
 * @param Function subscribe
 * @param String message
 * @param String[] triggers
 */
function triggersPart(subscribe, message, triggers) {
  subscribe(message, (_, ...data) => Promise.all(
    triggers.map(message => HP.emit(message, ...data))
  ));
}

/**
 * Registering that one event will trigger another, passing all data.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggers = triggersPart.bind(null, HP.on);

/**
 * Registering that one event will trigger another, after the lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersAfter = triggersPart.bind(null, HP.after);

/**
 * Registering that one event will trigger another, before the lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersBefore = triggersPart.bind(null, HP.before);

/**
 * Registering that one event will trigger another, just once.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnce = triggersPart.bind(null, HP.once);

/**
 * Registering that one event will trigger another, just once, after the
 * lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnceAfter = triggersPart.bind(null, HP.onceAfter);

/**
 * Registering that one event will trigger another, just once, before the
 * lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnceBefore = triggersPart.bind(null, HP.onceBefore);

/**
 * Decorates an argument of a function call.
 *
 * @param Function method
 * @param Number nthArg
 * @param Function decorate
 * @return Function
 */
function decorateArg(method, nthArg, decorate) {
  return (...args) => {
    args.splice(nthArg, 1, decorate(args[nthArg]));
    return method(...args);
  };
}

/**
 * Returns a version of hot-press of which all event names are prefixed with a
 * namespace.
 *
 * @param String ns
 * @return Object
 */
function ns(ns) {
  let decorateMessage = message => `${ns}.${message}`;
  let decorateMessages = ms => ms.map(decorateMessage);
  let methods = Object.keys(HP);

  let object = methods.reduce((object, method) => Object.assign(object, {
    [method]: decorateArg(HP[method], 0, decorateMessage)
  }), {});

  return methods
    .filter(method => method.startsWith('triggers'))
    .reduce((object, method) => Object.assign(object, {
      [method]: decorateArg(object[method], 1, decorateMessages)
    }), object);
}

Object.assign(exports, HP, {ns});
<commit_msg>:art:<commit_after>'use strict';

const flatten = require('lodash.flatten');
const ON = 'on';
const BEFORE = 'before';
const AFTER = 'after';
const HIERARCHY_SEPARATOR = '.';

/**
 * Listeners by event.
 *
 * [message]: {
 *   before: ...Function,
 *   on: ...Function,
 *   after: ...Function,
 * }
 *
 * @var Object
 */
let listeners = {};

/**
 * An objet containing all the methods that hot-press can possibly namespace.
 *
 * @var Object
 */
let HP = {};

/**
 * Returns all the listeners for a given message. The returned value will be an
 * object whose keys are 'before', 'on' and 'after' of which each will contain
 * an array of functions/listeners.
 *
 * @param String message
 * @return Object
 */
function getListenersFor(message) {
  if (!listeners[message]) listeners[message] = {
    [BEFORE]: [],
    [ON]: [],
    [AFTER]: []
  };
  return listeners[message];
}

/**
 * Returns a list of messages that should be called in order for a specific
 * message
 *
 * @param String message
 * @return String[]
 */
function getHierarchy(message) {
  let parts = message.split(HIERARCHY_SEPARATOR);
  let hierarchy = [parts[0], '*'];
  parts.reduce((message, part) => {
    let prefix = message + HIERARCHY_SEPARATOR;
    let newMessage = prefix + part;
    let wildcard = prefix + '*';
    hierarchy.unshift(newMessage, wildcard);
    return newMessage;
  });
  return hierarchy;
}

/**
 * Adds a listener to a specific part of an event's lifecycle.
 *
 * @param String part
 * @param String message
 * @param Function fn
 */
function onPart(part, message, fn) {
  getListenersFor(message)[part].push(fn);
}

/**
 * Adds a listener to the event.
 *
 * @param String message
 * @param Function fn
 */
HP.on = onPart.bind(null, ON);

/**
 * Adds a listener to the beginning of the event lifecycle.
 *
 * @param String message
 * @param Function fn
 */
HP.before = onPart.bind(null, BEFORE);

/**
 * Adds a listener to the end of the event lifecycle.
 *
 * @param String message
 * @param Function fn
 */
HP.after = onPart.bind(null, AFTER);

/**
 * Will call the listener once each of the specified events have been emitted.
 * The events are given as an object, where each key will specify the part of
 * the event lifecycle and the value is an array of event names/messages.
 *
 * ```
 * all({
 *   before: ['foo', 'bar'],
 *   on: ['another'],
 *   after: ['event']
 * }, ({foo, bar, another, event}) => {
 *   // You'll receive data for each event that fired
 * });
 * ```
 *
 * @param Object<before: String[], on: String[], after: String[]> messages
 * @param Function fn
 */
HP.all = (messages, fn) => {
  let toDo;
  let dataCollection;
  let size = Object
    .keys(messages)
    .reduce((size, part) => size + messages[part].length, 0);

  init();

  function subscriber(message, ...data) {
    dataCollection[message] = data;
    toDo--;
    if (!toDo) {
      fn(dataCollection);
      init();
    }
  }

  function registerSubscribers(prop, method) {
    if (messages[prop]) messages[prop].forEach(message => {
      method(message, subscriber);
    });
  }

  function init() {
    toDo = size;
    dataCollection = {};
    registerSubscribers(BEFORE, HP.onceBefore);
    registerSubscribers(ON, HP.once);
    registerSubscribers(AFTER, HP.onceAfter);
  }
};

/**
 * Removes a listener from a given event.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
function removeListener(message, fn) {
  let listeners = getListenersFor(message);
  let removed = 0;
  for (let key in listeners) {
    let set = listeners[key];
    let index = set.indexOf(fn);
    if (index !== -1) {
      set.splice(index, 1);
      removed++;
      break;
    }
  }
  return removed;
}

/**
 * Removes all listeners from a given event.
 *
 * @param String message
 * @return Number
 */
function removeAllListeners(message) {
  let all = listeners[message];
  let amount = all ? all.length : 0;
  delete listeners[message];
  return amount;
}

/**
 * Removes the specified listener from a given event. If no listener is
 * specified, all will be removed.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
HP.off = (message, fn) => {
  return fn ? removeListener(message, fn) : removeAllListeners(message);
};

/**
 * Adds a listener to a specific part of the event lifecycle and removes it
 * as soon as it's been called.
 *
 * @param Function subscribe
 * @param String message
 * @param Function fn
 */
function oncePart(subscribe, message, fn) {
  let subscriber = (...args) => {
    HP.off(message, subscriber);
    return fn(...args);
  };
  subscribe(message, subscriber);
}

/**
 * Adds the listener to the event for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.once = oncePart.bind(null, HP.on);

/**
 * Adds a listener to the beginning of the event lifecycle for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.onceBefore = oncePart.bind(null, HP.before);

/**
 * Adds a listener to the end of the event lifecycle for just one emittion.
 *
 * @param String message
 * @param Function fn
 */
HP.onceAfter = oncePart.bind(null, HP.after);

/**
 * Creates an emitter based on the event name/message and data. The emitter
 * can then be used to emit each part of the lifecycle.
 *
 * @param String message
 * @param Any[] data
 * @return Function
 */
function createEmitter(message, data) {
  let call = fn => fn(message, ...data);
  let hierarchy = getHierarchy(message);
  return part => Promise.all(flatten(
    hierarchy.map(message => getListenersFor(message)[part].map(call))
  ));
}

/**
 * Fires off the lifecycle of an event.
 *
 * @param String message
 * @param Any ...data
 * @return Promise
 */
HP.emit = (message, ...data) => {
  let emit = createEmitter(message, data);
  return emit(BEFORE)
    .then(() => emit(ON))
    .then(() => emit(AFTER));
};

/**
 * Subscribes emittion of an array of events to another event. All data will be
 * passed to the emittions.
 *
 * @param Function subscribe
 * @param String message
 * @param String[] triggers
 */
function triggersPart(subscribe, message, triggers) {
  subscribe(message, (_, ...data) => Promise.all(
    triggers.map(message => HP.emit(message, ...data))
  ));
}

/**
 * Registering that one event will trigger another, passing all data.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggers = triggersPart.bind(null, HP.on);

/**
 * Registering that one event will trigger another, after the lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersAfter = triggersPart.bind(null, HP.after);

/**
 * Registering that one event will trigger another, before the lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersBefore = triggersPart.bind(null, HP.before);

/**
 * Registering that one event will trigger another, just once.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnce = triggersPart.bind(null, HP.once);

/**
 * Registering that one event will trigger another, just once, after the
 * lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnceAfter = triggersPart.bind(null, HP.onceAfter);

/**
 * Registering that one event will trigger another, just once, before the
 * lifecycle.
 *
 * @param String trigger
 * @param String[] messages
 */
HP.triggersOnceBefore = triggersPart.bind(null, HP.onceBefore);

/**
 * Decorates an argument of a function call.
 *
 * @param Function method
 * @param Number nthArg
 * @param Function decorate
 * @return Function
 */
function decorateArg(method, nthArg, decorate) {
  return (...args) => {
    args.splice(nthArg, 1, decorate(args[nthArg]));
    return method(...args);
  };
}

/**
 * Returns a version of hot-press of which all event names are prefixed with a
 * namespace.
 *
 * @param String ns
 * @return Object
 */
function ns(ns) {
  let decorateMessage = message => `${ns}.${message}`;
  let decorateMessages = ms => ms.map(decorateMessage);
  let methods = Object.keys(HP);

  let object = methods.reduce((object, method) => Object.assign(object, {
    [method]: decorateArg(HP[method], 0, decorateMessage)
  }), {});

  return methods
    .filter(method => method.startsWith('triggers'))
    .reduce((object, method) => Object.assign(object, {
      [method]: decorateArg(object[method], 1, decorateMessages)
    }), object);
}

Object.assign(exports, HP, {ns});
<|endoftext|>


<commit_before>/**
 * Created by Obscurity on 2017/1/21.
 */

(function ($) {

    'use strict';

    var large = {
        size: "large",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var medium = {
        size: "small",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var small = {
        size: "small",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var sizes = ["small", "medium", "large"];

    var defaults = medium;

    var hShare = function (options) {

        var url = encodeURIComponent(location.href);
        var title = encodeURIComponent(document.title);

        var opts = $.extend({}, defaults, options);

        var _render = function (platform, icon, text, customize, html) {
            if (customize) {
                return html;
            }
            else {
                switch(platform) {
                    case "qzone":
                        return $(_renderQzone(icon, text));
                        break;
                    case "wechat":
                        var wechatEntry = $(_renderWechat(icon, text));
                        wechatEntry.on('click', _renderWechatQECode);
                        return wechatEntry;
                        break;
                    case "sinaweibo":
                        return $(_renderSinaWeibo(icon, text));
                        break;
                    case "renren":
                        return $(_renderRenren(icon, text));
                        break;
                    case "douban":
                        return $(_renderDouban(icon, text));
                        break;
                    default:
                        throw Error("invalid platform");
                }
            }
        };

        var _renderWechatQECode = function () {
            console.log($(this).offset());
        };

        var _renderWechat = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "<\/a>";
            }
            else {
                return "<\/a>";
            }
        };

        var _renderQzone = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "<\/a>";
            }
            else {
                return "<\/a>";
            }
        };

        var _renderDouban = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "<a class='hshare hshare-" + size + "'  href=\"javascript:void(function(){var w=window,d=document,e=encodeURIComponent,s1=window.getSelection,s2=d.getSelection,s3=d.selection,s=s1?s1():s2?s2():s3?s3.createRange().text:'',r='https://www.douban.com/recommend/?url='+e(w.location.href)+'&title='+e(d.title)+'&sel='+e(s)+'&v=1',w=450,h=330,x=function(){if(!window.open(r,'douban','toolbar=0,resizable=1,scrollbars=yes,status=1,width='+w+',height='+h+',left='+(screen.width-w)/2+',top='+(screen.height-h)/2))location.href=r+'&r=1'};if(/Firefox/.test(navigator.userAgent)){setTimeout(x,0)}else{x()}})()\">" + text + "";
            }
            else {
                return "<a class='hshare hshare-" + size + "'  href=\"javascript:void(function(){var w=window,d=document,e=encodeURIComponent,s1=window.getSelection,s2=d.getSelection,s3=d.selection,s=s1?s1():s2?s2():s3?s3.createRange().text:'',r='https://www.douban.com/recommend/?url='+e(w.location.href)+'&title='+e(d.title)+'&sel='+e(s)+'&v=1',w=450,h=330,x=function(){if(!window.open(r,'douban','toolbar=0,resizable=1,scrollbars=yes,status=1,width='+w+',height='+h+',left='+(screen.width-w)/2+',top='+(screen.height-h)/2))location.href=r+'&r=1'};if(/Firefox/.test(navigator.userAgent)){setTimeout(x,0)}else{x()}})()\">";
            }
        };

        var _renderRenren = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "";
            }
            else {
                return "";
            }
        };

        var _renderSinaWeibo = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "";
            }
            else {
                return "";
            }
        };

        return this.each(function () {
            var $this = $(this);

            var platforms = opts.shares;

            platforms.forEach(function (plt) {
                var platform = plt.platform || "";
                var icon = plt.icon || "";
                var text = plt.text || "";
                var isCustomized = !!plt.customize;
                var customize = plt.customize || "";
                $this.append($(_render(platform, icon, text, isCustomized, customize)));
            });

        });
    };

    $.fn.extend({
        hshare: hShare
    });

})(jQuery);<commit_msg>增加了一个demo<commit_after>/**
 * Created by Obscurity on 2017/1/21.
 */

(function ($) {

    'use strict';

    var large = {
        size: "large",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var medium = {
        size: "small",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var small = {
        size: "small",
        renderText: false,
        shares: [{
            platform: "qzone",
            icon: "https://qzonestyle.gtimg.cn/ac/qzone_v5/app/app_share/qz_logo.png",
            text: "QQ空间"
        }, {
            platform: "qq"
        }, {
            platform: "wechat",
            icon: "https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_appwx_logo.png",
            text: "微信"
        }, {
            platform: "sinaweibo",
            icon: "https://img.t.sinajs.cn/t4/appstyle/widget/images/shareButton/share_icon_mini.png?id=1375775002754",
            text: "新浪微博"
        }, {
            platform: "douban",
            icon: "https://img3.doubanio.com/pics/fw2douban_s.png",
            text: "豆瓣"
        }]
    };

    var sizes = ["small", "medium", "large"];

    var defaults = medium;

    var hShare = function (options) {

        var url = encodeURIComponent(location.href);
        var title = encodeURIComponent(document.title);

        var opts = $.extend({}, defaults, options);

        var _render = function (platform, icon, text, customize, html) {
            if (customize) {
                return html;
            }
            else {
                switch(platform) {
                    case "qzone":
                        return $(_renderQzone(icon, text));
                        break;
                    case "wechat":
                        var wechatEntry = $(_renderWechat(icon, text));
                        wechatEntry.on('click', _renderWechatQECode);
                        return wechatEntry;
                        break;
                    case "sinaweibo":
                        return $(_renderSinaWeibo(icon, text));
                        break;
                    case "renren":
                        return $(_renderRenren(icon, text));
                        break;
                    case "douban":
                        return $(_renderDouban(icon, text));
                        break;
                    default:
                        throw Error("invalid platform");
                }
            }
        };

        var _renderWechatQECode = function () {
            console.log($(this).offset());
        };

        var _renderWechat = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "<\/a>";
            }
            else {
                return "<\/a>";
            }
        };

        var _renderQzone = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "<\/a>";
            }
            else {
                return "<\/a>";
            }
        };

        var _renderDouban = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "<a class='hshare hshare-" + size + "'  href=\"javascript:void(function(){var w=window,d=document,e=encodeURIComponent,s1=window.getSelection,s2=d.getSelection,s3=d.selection,s=s1?s1():s2?s2():s3?s3.createRange().text:'',r='https://www.douban.com/recommend/?url='+e(w.location.href)+'&title='+e(d.title)+'&sel='+e(s)+'&v=1',w=450,h=330,x=function(){if(!window.open(r,'douban','toolbar=0,resizable=1,scrollbars=yes,status=1,width='+w+',height='+h+',left='+(screen.width-w)/2+',top='+(screen.height-h)/2))location.href=r+'&r=1'};if(/Firefox/.test(navigator.userAgent)){setTimeout(x,0)}else{x()}})()\">" + text + "";
            }
            else {
                return "<a class='hshare hshare-" + size + "'  href=\"javascript:void(function(){var w=window,d=document,e=encodeURIComponent,s1=window.getSelection,s2=d.getSelection,s3=d.selection,s=s1?s1():s2?s2():s3?s3.createRange().text:'',r='https://www.douban.com/recommend/?url='+e(w.location.href)+'&title='+e(d.title)+'&sel='+e(s)+'&v=1',w=450,h=330,x=function(){if(!window.open(r,'douban','toolbar=0,resizable=1,scrollbars=yes,status=1,width='+w+',height='+h+',left='+(screen.width-w)/2+',top='+(screen.height-h)/2))location.href=r+'&r=1'};if(/Firefox/.test(navigator.userAgent)){setTimeout(x,0)}else{x()}})()\">";
            }
        };

        var _renderRenren = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "";
            }
            else {
                return "";
            }
        };

        var _renderSinaWeibo = function (icon, text) {
            var size = sizes.includes(opts.size) ? opts.size : "medium";
            if (opts.renderText) {
                return "" + text + "";
            }
            else {
                return "";
            }
        };

        return this.each(function () {
            var $this = $(this);

            var platforms = opts.shares;

            platforms.forEach(function (plt) {
                var platform = plt.platform || "";
                var icon = plt.icon || "";
                var text = plt.text || "";
                var isCustomized = !!plt.customize;
                var customize = plt.customize || "";
                $this.append($(_render(platform, icon, text, isCustomized, customize)));
            });

        });
    };

    $.fn.extend({
        hshare: hShare
    });

})(jQuery);<|endoftext|>


<commit_before>var page = require('webpage').create();

if (phantom.args.length < 2) {
	console.log('11');
	console.log('incorrect args');
    phantom.exit();
} else {
    page.viewportSize = {
		width: phantom.args[2] || 1440,
		height: phantom.args[3] || 2880
	};
	page.paperSize = {
		'format': phantom.args[4] || 'A4',
		'orientation': phantom.args[5] || 'portrait',
		'margin': {
			'top': phantom.args[6],
			'right': phantom.args[7],
			'bottom': phantom.args[8],
			'left': phantom.args[9]
		}
	};
	page.zoomFactor = phantom.args[10] || 1;
    page.open(phantom.args[0], function (status) {
		if(status !== 'success'){
			console.log('error');
			console.log('Unable to load the address!');
		} else {
			window.setTimeout(function(){
				page.render(phantom.args[1]);
				console.log('1');
				phantom.exit();
			}, 400);
		};
	});
};<commit_msg>[REF] added margin fallbacks when params aren't set<commit_after>var page = require('webpage').create();

if (phantom.args.length < 2) {
	console.log('11');
	console.log('incorrect args');
    phantom.exit();
} else {
    page.viewportSize = {
		width: phantom.args[2] || 1440,
		height: phantom.args[3] || 2880
	};
	page.paperSize = {
		'format': phantom.args[4] || 'A4',
		'orientation': phantom.args[5] || 'portrait',
		'margin': {
			'top': phantom.args[6] || '1cm',
			'right': phantom.args[7] || '1cm',
			'bottom': phantom.args[8] || '1cm',
			'left': phantom.args[9] || '1cm'
		}
	};
	page.zoomFactor = phantom.args[10] || 1;
    page.open(phantom.args[0], function (status) {
		if(status !== 'success'){
			console.log('error');
			console.log('Unable to load the address!');
		} else {
			window.setTimeout(function(){
				page.render(phantom.args[1]);
				console.log('1');
				phantom.exit();
			}, 400);
		};
	});
};<|endoftext|>


<commit_before>const dgram = require('dgram')
const os = require('os')
const path = require('path')
const fs = require('fs')
const EventEmitter = require('events')
const clone = require('clone')
const uuidV4 = require('uuid/v4')
const progress = require('progress-stream')

var ProgressBar = require('progressbar.js')
const dragDrop = require('drag-drop')
const body = require('stream-body')
const { remote } = require('electron')
const dialog = remote.dialog
const win = remote.getCurrentWindow()

const httpServer = require('./server')
const httpClient = require('https').request

const client = dgram.createSocket({type: 'udp4', reuseAddr: true})
const server = dgram.createSocket({type: 'udp4', reuseAddr: true})

const PORT = 4321
const MC = '224.0.0.1'

const transfers = {
/*
 * id: {
 *   id,
 *   started,
 *   filename,
 *   filesize,
 *   finished
 *   error: object|null
 *   progress: https://www.npmjs.com/package/progress-stream#progress
 * }
 */
}
const transfersEmitter = new EventEmitter()

function updateTransfer (id, fields) {
  if (!transfers[id]) {
    transfers[id] = fields
  }
  const old = clone(transfers[id])
  Object.assign(transfers[id], fields)
  if (transfers[id].progress) {
	  console.log(`${Math.round(transfers[id].progress.percentage)}%  ${transfers[id].filename}`)
  }
  transfersEmitter.emit('change', old, transfers[id])
}

function humanHostname (hostname) {
  return hostname.replace(/\.local/g, '')
}

function send (o) {
  const message = Buffer.from(JSON.stringify(o))
  client.send(message, 0, message.length, PORT, MC)
}

document.body.classList.add(process.platform)

//
// Dont accept arbitrary drops
//
dragDrop(document.body, () => {})

//
// Advertise a message
//
setInterval(ping, 1500)

function ping (extraAttrs = {}) {
  const attrs = Object.assign({}, {
    event: 'join',
    name: humanHostname(os.hostname()),
    platform: os.platform(),
    ctime: Date.now()
  }, extraAttrs)

  send(attrs)
}

//
// Add a close button
//
const close = document.querySelector('.close')
close.addEventListener('click', () => {
  remote.app.exit()
})

const me = document.querySelector('#me')
try {
  const d = fs.readFileSync(path.join(os.homedir(), 'avatar'))
  me.style.backgroundImage = 'url("' + d + '")'
  me.textContent = ''
} catch (ex) {
  me.textContent = os.hostname()
}

//
// Drop your avatar
//
dragDrop(me, (files) => {
  const reader = new window.FileReader()
  reader.onerror = err => {
    console.error(err)
  }
  reader.onload = e => {
    me.style.backgroundImage = 'url("' + e.target.result + '")'
    me.textContent = ''

    fs.writeFileSync(path.join(os.homedir(), 'avatar'), e.target.result)
    ping({refreshAvatar: true})
  }
  reader.readAsDataURL(files[0])
})

//
// Add our hostname to the `me` icon.
//

httpServer((req, res) => {
  const filename = req.headers['x-filename']

  if (req.url === '/upload') {
    const message = [
      'Do you want to accept the file',
      filename,
      'from',
      humanHostname(req.headers['x-from']) + '?'
    ].join(' ')

    const opts = {
      type: 'question',
      buttons: ['Ok', 'Cancel'],
      title: 'Confirm',
      message
    }

    const result = dialog.showMessageBox(win, opts)

    if (result === 0) {
      const dest = path.join(remote.app.getPath('downloads'), filename)
      const writeStream = fs.createWriteStream(dest)
      const transfer = {
        id: uuidV4(),
        started: Date.now(),
        filename: req.headers['x-filename'],
        filesize: req.headers['x-filesize'],
        from: humanHostname(req.headers['x-from']),
        error: null,
        progress: null,
        finished: false
      }
      updateTransfer(transfer.id, transfer)

      const progressStream = progress({
        length: transfer.filesize,
        time: 500 /* ms */
      })
      progressStream.on('progress', (progress) => {
        updateTransfer(transfer.id, {progress})
      })
      console.log(`Staring download from ${transfer.from}, filename: ${transfer.filename}, size ${transfer.filesize} bytes, id ${transfer.id}`)
      req.pipe(progressStream).pipe(writeStream)
    } else if (result === 1) {
      res.statusCode = 403
      return res.end('User rejected the upload')
    }
  } else if (req.url === '/avatar') {
    const filepath = path.join(os.homedir(), 'avatar')
    fs.readFile(filepath, (err, data) => {
      if (err) {
        res.statusCode = 404
        return res.end('')
      }
      res.end(data)
    })
  } else if (req.url === '/linux') {
    // TODO serve the app so people can download it
  } else if (req.url === '/mac') {

  } else if (req.url === '/win') {

  }
})

const registry = {}

function getData (src, cb) {
  const reader = new window.FileReader()
  reader.onerror = err => cb(err)
  reader.onload = e => cb(null, e.target.result)
  reader.readAsArrayBuffer(src)
}

function onFilesDropped (msg, files) {
  files.forEach(file => {
    const opts = {
      host: msg.ip,
      port: 9988,
      path: '/upload',
      method: 'POST',
      rejectUnauthorized: false,
      headers: {
        'Content-Type': file.type,
        'x-filename': file.name,
        'x-from': os.hostname(),
        'x-filesize': file.size
      }
    }

    getData(file, (err, data) => {
      if (err) return console.error(err)
      const req = httpClient(opts, res => {
        if (res.statusCode === 403) {
          showRejectedMessage(msg)
          return
        }
        if (res.statusCode !== 200) {
          res.on('data', data => {
            console.error(res.statusCode, data)
          })
        }
      })

      req.end(Buffer.from(data))
    })
  })
}

//
// Create a tcp server
//

function joined (msg, rinfo) {
  const me = humanHostname(os.hostname())
  msg.name = humanHostname(msg.name)

  //
  // Don't show me my own machine as a peer
  //
  if (!process.env['DEBUG'] && msg.name === me) {
    return
  }

  //
  // If the peer is already rendered, just return
  //
  const selector = `[data-name="${msg.name}"]`
  if (document.querySelector(selector)) return

  //
  // Otherwise, create a peer and add it to the list.
  //
  const peers = document.querySelector('#peers')
  const barElement = document.createElement('div')
  barElement.className = 'bar'
  const peer = document.createElement('div')
  peer.appendChild(barElement)

  peer.className = 'peer adding adding-anim'
  peer.setAttribute('data-name', msg.name)
  peer.setAttribute('data-ip', rinfo.address)
  peer.setAttribute('data-platform', msg.platform)

  const avatar = document.createElement('div')
  avatar.className = 'avatar ' + msg.platform
  peer.appendChild(avatar)

  const name = document.createElement('address')
  name.textContent = msg.name
  name.address = rinfo.address
  peer.appendChild(name)

  const progressBar = new ProgressBar.Circle(barElement, {
    strokeWidth: 6,
    easing: 'easeInOut',
    duration: 500,
    color: '#ED6A5A',
    trailColor: 'transparent',
    trailWidth: 1,
    svgStyle: null
  })

  transfersEmitter.on('change', (old, current) => {
    if (current.from !== msg.name) {
      return
    }
    const activeForMe = Object.values(transfers).filter(t => t.from === msg.name && t.progress && (t.progress.percentage < 100 || old.progress === null || old.id === t.id) && !t.finished)
    if (activeForMe.length === 0) {
      return
    }
    const progress = activeForMe.reduce((a, v) => a + v.progress.transferred, 0) / activeForMe.reduce((a, v) => a + v.progress.length, 0)
    progressBar.animate(progress, () => {
      if (current.progress && current.progress.percentage == 100 && !current.finished) {
        const opts = {
          type: 'info',
          buttons: ['Ok'],
          title: 'Downloaded',
          message: `✅ The file ${current.filename} was successfully downloaded.`
        }
        dialog.showMessageBox(win, opts)
        updateTransfer(current.id, {finished: true})
      }
      if (progress >= 1.0) {
        progressBar.set(0);
      }
    })
  })

  peers.appendChild(peer)

  window.requestAnimationFrame(() => requestAnimationFrame(() => peer.classList.remove('adding')))
  peer.addEventListener('transitionend', e => {
    if (e.propertyName !== 'transform') return
    peer.classList.remove('adding-anim')
  })
  //
  // Add a drag drop event to the peer
  //
  dragDrop(peer, (files) => {
    onFilesDropped(registry[peer.getAttribute('data-name')], files)
  })

  //
  // Get the avatar from the user who joined
  //
  loadAvatar(rinfo.address, peer)

  //
  // remove inital empty message when finding peers
  //
  const selectorEmptyState = document.querySelector('#empty-message')
  selectorEmptyState.classList.remove('show')
}

function parted (msg) {
  const selector = `.peer[data-name="${msg.name}"]`
  const peer = document.querySelector(selector)
  if (peer) peer.parentNode.removeChild(peer)
}

function showRejectedMessage (msg) {
  const opts = {
    type: 'info',
    buttons: ['Ok'],
    title: 'Rejected',
    message: `${msg.name} rejected the file.`
  }

  dialog.showMessageBox(win, opts)
}

function cleanUp () {
  for (var key in registry) {
    if (registry[key] && (Date.now() - registry[key].ctime) > 9000) {
      parted(registry[key])
      registry[key] = null
    }
  }
}

server.on('error', (err) => {
  console.error(err)
  server.close()
})

server.on('message', (msg, rinfo) => {
  msg = JSON.parse(msg)
  if (!registry[msg.name] && msg.event === 'join') {
    joined(msg, rinfo)
  }

  if (msg.refreshAvatar) {
    const selector = `.peer[data-name="${msg.name}"]`
    loadAvatar(rinfo.address, document.querySelector(selector))
  }
  msg.ip = rinfo.address
  registry[msg.name] = msg
})

function loadAvatar (address, peerEl) {
  const opts = {
    host: address,
    port: 9988,
    path: '/avatar',
    rejectUnauthorized: false
  }

  const req = httpClient(opts, res => {
    if (res.statusCode !== 200) {
      return console.error('Unable to get avatar')
    }

    body.parse(res, (err, data) => {
      if (err) return console.error('unable to get avatar')

      const avatar = peerEl.querySelector('.avatar')
      avatar.className = 'avatar'

      peerEl.style.backgroundImage = 'url("' + data + '")'
    })
  })

  req.end()
}

server.on('listening', () => {
  console.log(`Listening on ${PORT}`)
})

server.bind(PORT)

setInterval(cleanUp, 5000)
<commit_msg>Decrease animation duration<commit_after>const dgram = require('dgram')
const os = require('os')
const path = require('path')
const fs = require('fs')
const EventEmitter = require('events')
const clone = require('clone')
const uuidV4 = require('uuid/v4')
const progress = require('progress-stream')

var ProgressBar = require('progressbar.js')
const dragDrop = require('drag-drop')
const body = require('stream-body')
const { remote } = require('electron')
const dialog = remote.dialog
const win = remote.getCurrentWindow()

const httpServer = require('./server')
const httpClient = require('https').request

const client = dgram.createSocket({type: 'udp4', reuseAddr: true})
const server = dgram.createSocket({type: 'udp4', reuseAddr: true})

const PORT = 4321
const MC = '224.0.0.1'
const PROGRESS_REPORT_INTERVAL = 500
// should be a lot less than the report interval above, because if we
// interrupt an animation before it's ended the animation callback is not
// run and the success messages for files, except for the last one are not
// shown
const PROGRESS_ANIMATION_DURATION = 100

const transfers = {
/*
 * id: {
 *   id,
 *   started,
 *   filename,
 *   filesize,
 *   finished
 *   error: object|null
 *   progress: https://www.npmjs.com/package/progress-stream#progress
 * }
 */
}
const transfersEmitter = new EventEmitter()

function updateTransfer (id, fields) {
  if (!transfers[id]) {
    transfers[id] = fields
  }
  const old = clone(transfers[id])
  Object.assign(transfers[id], fields)
  if (transfers[id].progress) {
	  console.log(`${Math.round(transfers[id].progress.percentage)}%  ${transfers[id].filename}`)
  }
  transfersEmitter.emit('change', old, transfers[id])
}

function humanHostname (hostname) {
  return hostname.replace(/\.local/g, '')
}

function send (o) {
  const message = Buffer.from(JSON.stringify(o))
  client.send(message, 0, message.length, PORT, MC)
}

document.body.classList.add(process.platform)

//
// Dont accept arbitrary drops
//
dragDrop(document.body, () => {})

//
// Advertise a message
//
setInterval(ping, 1500)

function ping (extraAttrs = {}) {
  const attrs = Object.assign({}, {
    event: 'join',
    name: humanHostname(os.hostname()),
    platform: os.platform(),
    ctime: Date.now()
  }, extraAttrs)

  send(attrs)
}

//
// Add a close button
//
const close = document.querySelector('.close')
close.addEventListener('click', () => {
  remote.app.exit()
})

const me = document.querySelector('#me')
try {
  const d = fs.readFileSync(path.join(os.homedir(), 'avatar'))
  me.style.backgroundImage = 'url("' + d + '")'
  me.textContent = ''
} catch (ex) {
  me.textContent = os.hostname()
}

//
// Drop your avatar
//
dragDrop(me, (files) => {
  const reader = new window.FileReader()
  reader.onerror = err => {
    console.error(err)
  }
  reader.onload = e => {
    me.style.backgroundImage = 'url("' + e.target.result + '")'
    me.textContent = ''

    fs.writeFileSync(path.join(os.homedir(), 'avatar'), e.target.result)
    ping({refreshAvatar: true})
  }
  reader.readAsDataURL(files[0])
})

//
// Add our hostname to the `me` icon.
//

httpServer((req, res) => {
  const filename = req.headers['x-filename']

  if (req.url === '/upload') {
    const message = [
      'Do you want to accept the file',
      filename,
      'from',
      humanHostname(req.headers['x-from']) + '?'
    ].join(' ')

    const opts = {
      type: 'question',
      buttons: ['Ok', 'Cancel'],
      title: 'Confirm',
      message
    }

    const result = dialog.showMessageBox(win, opts)

    if (result === 0) {
      const dest = path.join(remote.app.getPath('downloads'), filename)
      const writeStream = fs.createWriteStream(dest)
      const transfer = {
        id: uuidV4(),
        started: Date.now(),
        filename: req.headers['x-filename'],
        filesize: req.headers['x-filesize'],
        from: humanHostname(req.headers['x-from']),
        error: null,
        progress: null,
        finished: false
      }
      updateTransfer(transfer.id, transfer)

      const progressStream = progress({
        length: transfer.filesize,
        time: PROGRESS_REPORT_INTERVAL, /* ms */
      })
      progressStream.on('progress', (progress) => {
        updateTransfer(transfer.id, {progress})
      })
      console.log(`Staring download from ${transfer.from}, filename: ${transfer.filename}, size ${transfer.filesize} bytes, id ${transfer.id}`)
      req.pipe(progressStream).pipe(writeStream)
    } else if (result === 1) {
      res.statusCode = 403
      return res.end('User rejected the upload')
    }
  } else if (req.url === '/avatar') {
    const filepath = path.join(os.homedir(), 'avatar')
    fs.readFile(filepath, (err, data) => {
      if (err) {
        res.statusCode = 404
        return res.end('')
      }
      res.end(data)
    })
  } else if (req.url === '/linux') {
    // TODO serve the app so people can download it
  } else if (req.url === '/mac') {

  } else if (req.url === '/win') {

  }
})

const registry = {}

function getData (src, cb) {
  const reader = new window.FileReader()
  reader.onerror = err => cb(err)
  reader.onload = e => cb(null, e.target.result)
  reader.readAsArrayBuffer(src)
}

function onFilesDropped (msg, files) {
  files.forEach(file => {
    const opts = {
      host: msg.ip,
      port: 9988,
      path: '/upload',
      method: 'POST',
      rejectUnauthorized: false,
      headers: {
        'Content-Type': file.type,
        'x-filename': file.name,
        'x-from': os.hostname(),
        'x-filesize': file.size
      }
    }

    getData(file, (err, data) => {
      if (err) return console.error(err)
      const req = httpClient(opts, res => {
        if (res.statusCode === 403) {
          showRejectedMessage(msg)
          return
        }
        if (res.statusCode !== 200) {
          res.on('data', data => {
            console.error(res.statusCode, data)
          })
        }
      })

      req.end(Buffer.from(data))
    })
  })
}

//
// Create a tcp server
//

function joined (msg, rinfo) {
  const me = humanHostname(os.hostname())
  msg.name = humanHostname(msg.name)

  //
  // Don't show me my own machine as a peer
  //
  if (!process.env['DEBUG'] && msg.name === me) {
    return
  }

  //
  // If the peer is already rendered, just return
  //
  const selector = `[data-name="${msg.name}"]`
  if (document.querySelector(selector)) return

  //
  // Otherwise, create a peer and add it to the list.
  //
  const peers = document.querySelector('#peers')
  const barElement = document.createElement('div')
  barElement.className = 'bar'
  const peer = document.createElement('div')
  peer.appendChild(barElement)

  peer.className = 'peer adding adding-anim'
  peer.setAttribute('data-name', msg.name)
  peer.setAttribute('data-ip', rinfo.address)
  peer.setAttribute('data-platform', msg.platform)

  const avatar = document.createElement('div')
  avatar.className = 'avatar ' + msg.platform
  peer.appendChild(avatar)

  const name = document.createElement('address')
  name.textContent = msg.name
  name.address = rinfo.address
  peer.appendChild(name)

  const progressBar = new ProgressBar.Circle(barElement, {
    strokeWidth: 6,
    easing: 'easeInOut',
    duration: PROGRESS_ANIMATION_DURATION,
    color: '#ED6A5A',
    trailColor: 'transparent',
    trailWidth: 1,
    svgStyle: null
  })

  transfersEmitter.on('change', (old, current) => {
    if (current.from !== msg.name) {
      return
    }
    const activeForMe = Object.values(transfers).filter(t => t.from === msg.name && t.progress && (t.progress.percentage < 100 || old.progress === null || old.id === t.id) && !t.finished)
    if (activeForMe.length === 0) {
      return
    }
    const progress = activeForMe.reduce((a, v) => a + v.progress.transferred, 0) / activeForMe.reduce((a, v) => a + v.progress.length, 0)
    progressBar.animate(progress, () => {
      if (current.progress && current.progress.percentage == 100 && !current.finished) {
        const opts = {
          type: 'info',
          buttons: ['Ok'],
          title: 'Downloaded',
          message: `✅ The file ${current.filename} was successfully downloaded.`
        }
        dialog.showMessageBox(win, opts)
        updateTransfer(current.id, {finished: true})
      }
      if (progress >= 1.0) {
        progressBar.set(0);
      }
    })
  })

  peers.appendChild(peer)

  window.requestAnimationFrame(() => requestAnimationFrame(() => peer.classList.remove('adding')))
  peer.addEventListener('transitionend', e => {
    if (e.propertyName !== 'transform') return
    peer.classList.remove('adding-anim')
  })
  //
  // Add a drag drop event to the peer
  //
  dragDrop(peer, (files) => {
    onFilesDropped(registry[peer.getAttribute('data-name')], files)
  })

  //
  // Get the avatar from the user who joined
  //
  loadAvatar(rinfo.address, peer)

  //
  // remove inital empty message when finding peers
  //
  const selectorEmptyState = document.querySelector('#empty-message')
  selectorEmptyState.classList.remove('show')
}

function parted (msg) {
  const selector = `.peer[data-name="${msg.name}"]`
  const peer = document.querySelector(selector)
  if (peer) peer.parentNode.removeChild(peer)
}

function showRejectedMessage (msg) {
  const opts = {
    type: 'info',
    buttons: ['Ok'],
    title: 'Rejected',
    message: `${msg.name} rejected the file.`
  }

  dialog.showMessageBox(win, opts)
}

function cleanUp () {
  for (var key in registry) {
    if (registry[key] && (Date.now() - registry[key].ctime) > 9000) {
      parted(registry[key])
      registry[key] = null
    }
  }
}

server.on('error', (err) => {
  console.error(err)
  server.close()
})

server.on('message', (msg, rinfo) => {
  msg = JSON.parse(msg)
  if (!registry[msg.name] && msg.event === 'join') {
    joined(msg, rinfo)
  }

  if (msg.refreshAvatar) {
    const selector = `.peer[data-name="${msg.name}"]`
    loadAvatar(rinfo.address, document.querySelector(selector))
  }
  msg.ip = rinfo.address
  registry[msg.name] = msg
})

function loadAvatar (address, peerEl) {
  const opts = {
    host: address,
    port: 9988,
    path: '/avatar',
    rejectUnauthorized: false
  }

  const req = httpClient(opts, res => {
    if (res.statusCode !== 200) {
      return console.error('Unable to get avatar')
    }

    body.parse(res, (err, data) => {
      if (err) return console.error('unable to get avatar')

      const avatar = peerEl.querySelector('.avatar')
      avatar.className = 'avatar'

      peerEl.style.backgroundImage = 'url("' + data + '")'
    })
  })

  req.end()
}

server.on('listening', () => {
  console.log(`Listening on ${PORT}`)
})

server.bind(PORT)

setInterval(cleanUp, 5000)
<|endoftext|>


<commit_before>// (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP

import React, { Component,  PropTypes } from 'react';
import jsxToString from 'jsx-to-string';
import debounce from 'lodash/debounce';
import Box from 'grommet/components/Box';

//hjjs configuration
const hljs = require('highlight.js/lib/highlight');
hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'));
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'));
hljs.registerLanguage('javascript',
  require('highlight.js/lib/languages/javascript'));
hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'));

export default class Example extends Component {
  constructor (props) {
    super(props);
    this._highlightCode = props.debounceHighlight
      ? debounce(this._highlightCode.bind(this), 100)
      : this._highlightCode.bind(this);
  }

  componentDidMount () {
    this._highlightCode();
  }

  componentDidUpdate () {
    this._highlightCode();
  }

  _highlightCode () {
    hljs.highlightBlock(this.refs.code);
  }

  _renderCode () {
    const { code, overrides } = this.props;
    let keyValueOverride = {};
    if (overrides) {
      overrides.forEach(override => {
        keyValueOverride[override] = `this._${override}`;
      });
    }
    return jsxToString(code, { keyValueOverride: keyValueOverride })
      .replace('null', 'undefined');
  }

  render () {
    const { align, full, code, control, name, preamble } = this.props;
    let heading;
    if (name) {
      heading =
{name}
;
    }
    let importStatement;
    if (preamble) {
      // Ensure we end with a newline so any code starts on the next line.
      importStatement = preamble.replace(/[^\n]$/, "\n");
    }
    let codeElement;
    if (code) {
      codeElement = this._renderCode();
    }

    return (
      <Box full={full} flex={true} pad={{horizontal: 'medium'}}>
        {heading}
        <Box pad={{ between: 'medium' }} align={align}>
          {control || code}

              {`${importStatement || ''}${codeElement || ''}`}

        </Box>
      </Box>
    );
  }
}

Example.propTypes = {
  align: PropTypes.oneOf(['start', 'center', 'end']),
  code: PropTypes.node,
  control: PropTypes.node, // for LayerDoc
  name: PropTypes.string,
  overrides: PropTypes.arrayOf(PropTypes.string),
  preamble: PropTypes.string,
  debounceHighlight: PropTypes.bool,
  full: PropTypes.oneOf([true, 'horizontal', 'vertical', false])
};
<commit_msg>Use setTimeout to debounce highlight & remove `lodash/debounce`<commit_after>// (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP

import React, { Component,  PropTypes } from 'react';
import jsxToString from 'jsx-to-string';
import Box from 'grommet/components/Box';

//hjjs configuration
const hljs = require('highlight.js/lib/highlight');
hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'));
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'));
hljs.registerLanguage('javascript',
  require('highlight.js/lib/languages/javascript'));
hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'));

export default class Example extends Component {
  constructor (props) {
    super(props);
    this._highlightCode = this._highlightCode.bind(this);
  }

  componentDidMount () {
    this._highlightCode();
  }

  componentDidUpdate () {
    if (this.props.debounceHighlight) {
      clearTimeout(this._highlightTimer);
      this._highlightTimer = setTimeout(this._highlightCode, 100);
    } else {
      this._highlightCode();
    }
  }

  _highlightCode () {
    hljs.highlightBlock(this.refs.code);
  }

  _renderCode () {
    const { code, overrides } = this.props;
    let keyValueOverride = {};
    if (overrides) {
      overrides.forEach(override => {
        keyValueOverride[override] = `this._${override}`;
      });
    }
    return jsxToString(code, { keyValueOverride: keyValueOverride })
      .replace('null', 'undefined');
  }

  render () {
    const { align, full, code, control, name, preamble } = this.props;
    let heading;
    if (name) {
      heading =
{name}
;
    }
    let importStatement;
    if (preamble) {
      // Ensure we end with a newline so any code starts on the next line.
      importStatement = preamble.replace(/[^\n]$/, "\n");
    }
    let codeElement;
    if (code) {
      codeElement = this._renderCode();
    }

    return (
      <Box full={full} flex={true} pad={{horizontal: 'medium'}}>
        {heading}
        <Box pad={{ between: 'medium' }} align={align}>
          {control || code}

              {`${importStatement || ''}${codeElement || ''}`}

        </Box>
      </Box>
    );
  }
}

Example.propTypes = {
  align: PropTypes.oneOf(['start', 'center', 'end']),
  code: PropTypes.node,
  control: PropTypes.node, // for LayerDoc
  name: PropTypes.string,
  overrides: PropTypes.arrayOf(PropTypes.string),
  preamble: PropTypes.string,
  debounceHighlight: PropTypes.bool,
  full: PropTypes.oneOf([true, 'horizontal', 'vertical', false])
};
<|endoftext|>


<commit_before>Doc = Base.extend({
	beans: true,

	initialize: function(canvas) {
		if (canvas) {
			this.canvas = canvas;
			this.ctx = this.canvas.getContext('2d');
			this.size = new Size(canvas.offsetWidth, canvas.offsetHeight);
		}
		Paper.documents.push(this);
		this.activate();
		this.layers = [];
		this.activeLayer = new Layer();
		this.currentStyle = null;
		this.symbols = [];
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle = new PathStyle(this, style);
	},

	activate: function() {
		Paper.activateDocument(this);
	},

	redraw: function() {
		if (this.canvas) {
			// Initial tests conclude that clearing the canvas is always
			// faster than using clearRect:
			// http://jsperf.com/clearrect-vs-setting-width/7
			this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1);
			for (var i = 0, l = this.layers.length; i < l; i++) {
				this.layers[i].draw(this.ctx, {});
			}
		}
	}
});
<commit_msg>Implement new Doc(size) constructor, Doc#views / Doc#activeView and support DocumentView in Doc#redraw.<commit_after>Doc = Base.extend({
	beans: true,

	initialize: function(canvas) {
		if (canvas && canvas instanceof HTMLCanvasElement) {
			this.canvas = canvas;
			this.size = new Size(canvas.offsetWidth, canvas.offsetHeight);
		} else {
			this.size = Size.read(arguments) || new Size(1024, 768);
			this.canvas = document.createElement('canvas');
			this.canvas.width = this.size.width;
			this.canvas.height = this.size.height;
		}
		this.bounds = new Rectangle(new Point(0, 0), this.size);
		this.ctx = this.canvas.getContext('2d');
		Paper.documents.push(this);
		this.activate();
		this.layers = [];
		this.activeLayer = new Layer();
		this.currentStyle = null;
		this.symbols = [];
		this.views = [new DocumentView(this)];
		this.activeView = this.views[0];
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle = new PathStyle(this, style);
	},

	activate: function() {
		Paper.activateDocument(this);
	},

	redraw: function() {
		if (this.canvas) {
			// Initial tests conclude that clearing the canvas using clearRect
			// is always faster than setting canvas.width = canvas.width
			// http://jsperf.com/clearrect-vs-setting-width/
			var view = this.activeView;
			var bounds = view.bounds;
			this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1);
			this.ctx.save();
			view.matrix.applyToContext(this.ctx, true);
			for (var i = 0, l = this.layers.length; i < l; i++) {
				this.layers[i].draw(this.ctx, {});
			}
			this.ctx.restore();
		}
	}
});
<|endoftext|>