<commit_before>'use strict';

const fs = require('fs');
const url = require('url');
const path = require('path');
const iOSUtils = require('ios-utils');
const EventEmitter = require('events');
const childProcess = require('child_process');

const _ = require('./helper');
const pkg = require('../package');
const XCProxy = require('./proxy');
const logger = require('./logger');
const XCTestWD = require('./xctestwd');

const {
  detectPort
} = _;
const TEST_URL = pkg.site;
const projectPath = XCTestWD.projectPath;
const SERVER_URL_REG = XCTestWD.SERVER_URL_REG;
const simulatorLogFlag = XCTestWD.simulatorLogFlag;

class XCTest extends EventEmitter {
  constructor(options) {
    super();
    this.proxy = null;
    this.capabilities = null;
    this.sessionId = null;
    this.device = null;
    this.deviceLogProc = null;
    this.runnerProc = null;
    this.iproxyProc = null;
    Object.assign(this, {
      proxyHost: '127.0.0.1',
      proxyPort: 8001,
      urlBase: 'wd/hub'
    }, options || {});
    this.init();
  }

  init() {
    this.checkProjectPath();
    process.on('uncaughtException', (e) => {
      logger.error(`Uncaught Exception: ${e.stack}`);
      this.stop();
      process.exit(1);
    });
    process.on('exit', () => {
      this.stop();
    });
  }

  checkProjectPath() {
    if (_.isExistedDir(projectPath)) {
      logger.debug(`project path: ${projectPath}`);
    } else {
      logger.error('project path not found');
    }
  }

  configUrl(str) {
    const urlObj = url.parse(str);
    this.proxyHost = urlObj.hostname;
    this.proxyPort = urlObj.port;
  }

  initProxy() {
    this.proxy = new XCProxy({
      proxyHost: this.proxyHost,
      proxyPort: this.proxyPort,
      urlBase: this.urlBase
    });
  }

  * startSimLog() {
    const logPath = yield this.startBootstrap(false);

    if (this.proxyHost && this.proxyPort) {
      return;
    }

    const logDir = path.resolve(logPath);

    return _.retry(() => {
      return new Promise((resolve, reject) => {
        let logTxtFile = path.join(logDir, '..', 'StandardOutputAndStandardError.txt');
        logTxtFile = logDir.replace(/(\s)/, '\\ ');

        logger.info(`Read simulator log at: ${logTxtFile}`);

        if (!_.isExistedFile(logTxtFile)) {
          return reject();
        }
        let args = `-f -n 0 ${logTxtFile}`.split(' ');
        var proc = childProcess.spawn('tail', args, {});
        this.deviceLogProc = proc;

        proc.stderr.setEncoding('utf8');
        proc.stdout.setEncoding('utf8');

        proc.stdout.on('data', data => {

          // avoid logout long data such as bitmap
          if (data.length <= 300 && logger.debugMode) {
            logger.debug(data);
          }

          let match = SERVER_URL_REG.exec(data);
          if (match) {
            const url = match[1];
            if (url.startsWith('http://')) {
              this.configUrl(url);
              resolve();
            }
          }
        });

        proc.stderr.on('data', data => {
          logger.debug(data);
        });

        proc.stdout.on('error', (err) => {
          logger.warn(`simulator log process error with ${err}`);
        });

        proc.on('exit', (code, signal) => {
          logger.warn(`simulator log process exit with code: ${code}, signal: ${signal}`);
          reject();
        });
      });
    }, 1000, Infinity);
  }

  * startDeviceLog() {
    yield this.startBootstrap(true);

    if (this.proxyHost && this.proxyPort) {
      return;
    }

    var proc = childProcess.spawn(iOSUtils.devicelog.binPath, [this.device.deviceId], {});
    this.deviceLogProc = proc;

    proc.stderr.setEncoding('utf8');
    proc.stdout.setEncoding('utf8');

    return new Promise((resolve, reject) => {
      proc.stdout.on('data', data => {

        // avoid logout long data such as bitmap
        if (data.length <= 300 && logger.debugMode) {
          logger.debug(data);
        }

        let match = SERVER_URL_REG.exec(data);
        if (match) {
          const url = match[1];
          if (url.startsWith('http://')) {
            this.configUrl(url);
            resolve();
          }
        }
      });

      proc.stderr.on('data', data => {
        logger.debug(data);
      });

      proc.stdout.on('error', (err) => {
        logger.warn(`devicelog error with ${err}`);
      });

      proc.on('exit', (code, signal) => {
        logger.warn(`devicelog exit with code: ${code}, signal: ${signal}`);
        reject();
      });
    });
  }

  * startBootstrap(isDevice) {
    return new Promise((resolve, reject) => {
      logger.info(`XCTestWD version: ${XCTestWD.version}`);

      var args = `clean test -project ${XCTestWD.projectPath} -scheme XCTestWDUITests -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');

      // check potential optimization provided by .xctestrun file, which allows xctestwd can be executed concurrently on multiple devices
      let xctestrun_path = path.join(__dirname, '..', 'XCTestWD', 'build', 'Build', 'Products');
      let xctestrun = fs.readdirSync(xctestrun_path).filter(fn => fn.match(isDevice ? '.*device.*\.xctestrun' : '.*simulator.*\.xctestrun')).shift();
      if (xctestrun) {
        args = `test-without-building -xctestrun ${xctestrun_path}/${xctestrun} -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');
      }

      var env = _.merge({}, process.env, {
        XCTESTWD_PORT: this.proxyPort
      });

      var proc = childProcess.spawn('xcodebuild', args, {
        env: env
      });
      this.runnerProc = proc;
      proc.stderr.setEncoding('utf8');
      proc.stdout.setEncoding('utf8');

      proc.stdout.on('data', data => {
        // logger.debug(data);
        if (xctestrun) {
          let match = SERVER_URL_REG.exec(data);
          if (match) {
            const url = match[1];
            if (url.startsWith('http://')) {
              logger.debug('hitted for xctestrun mode');
              this.configUrl(url);
              resolve();
            }
          }
        }
      });

      proc.stderr.on('data', data => {

        // logger.debug(data);
        if (!xctestrun) {
          if (~data.indexOf(simulatorLogFlag)) {
            const list = data.split(simulatorLogFlag);
            const res = list[1].trim();
            logger.debug('hitted for default mode');
            resolve(res);
          } else {
            logger.debug(`please check project: ${projectPath}`);
          }
        }
      });

      proc.stdout.on('error', (err) => {
        logger.warn(`xctest client error with ${err}`);
        logger.debug(`please check project: ${projectPath}`);
      });

      proc.on('exit', (code, signal) => {
        this.stop();
        logger.warn(`xctest client exit with code: ${code}, signal: ${signal}`);
      });

    });
  }

  * startIproxy() {
    let args = [this.proxyPort, this.proxyPort, this.device.deviceId];

    const IOS_USBMUXD_IPROXY = 'iproxy';
    const binPath = yield _.exec(`which ${IOS_USBMUXD_IPROXY}`);

    var proc = childProcess.spawn(binPath, args);

    this.iproxyProc = proc;
    proc.stderr.setEncoding('utf8');
    proc.stdout.setEncoding('utf8');

    proc.stdout.on('data', () => {
    });

    proc.stderr.on('data', () => {
      // logger.debug(data);
    });

    proc.stdout.on('error', (err) => {
      logger.warn(`${IOS_USBMUXD_IPROXY} error with ${err}`);
    });

    proc.on('exit', (code, signal) => {
      logger.warn(`${IOS_USBMUXD_IPROXY} exit with code: ${code}, signal: ${signal}`);
    });
  }

  * start(caps) {
    try {
      this.proxyPort = yield detectPort(this.proxyPort);

      this.capabilities = caps;
      const xcodeVersion = yield iOSUtils.getXcodeVersion();

      logger.debug(`xcode version: ${xcodeVersion}`);

      var deviceInfo = iOSUtils.getDeviceInfo(this.device.deviceId);

      if (deviceInfo.isRealIOS) {
        yield this.startDeviceLog();
        yield this.startIproxy();
      } else {
        yield this.startSimLog();
      }

      logger.info(`${pkg.name} start with port: ${this.proxyPort}`);

      this.initProxy();

      if (caps.desiredCapabilities.browserName === 'Safari') {
        var promise = this.proxy.send(`/${this.urlBase}/session`, 'POST', {
          desiredCapabilities: {
            bundleId: 'com.apple.mobilesafari'
          }
        });
        return yield Promise.all([this.device.openURL(TEST_URL), promise]);
      } else {
        return yield this.proxy.send(`/${this.urlBase}/session`, 'POST', caps);
      }
    } catch (err) {
      logger.debug(`Fail to start xctest: ${err}`);
      this.stop();
      throw err;
    }
  }

  stop() {
    if (this.deviceLogProc) {
      logger.debug(`killing deviceLogProc pid: ${this.deviceLogProc.pid}`);
      this.deviceLogProc.kill('SIGKILL');
      this.deviceLogProc = null;
    }
    if (this.runnerProc) {
      logger.debug(`killing runnerProc pid: ${this.runnerProc.pid}`);
      this.runnerProc.kill('SIGKILL');
      this.runnerProc = null;
    }

    if (this.iproxyProc) {
      logger.debug(`killing iproxyProc pid: ${this.iproxyProc.pid}`);
      this.iproxyProc.kill('SIGKILL');
      this.iproxyProc = null;
    }
  }

  sendCommand(url, method, body) {
    return this.proxy.send(url, method, body);
  }
}

module.exports = XCTest;
module.exports.XCTestWD = XCTestWD;
<commit_msg>feat(log): add more logs (#177)<commit_after>'use strict';

const fs = require('fs');
const url = require('url');
const path = require('path');
const iOSUtils = require('ios-utils');
const EventEmitter = require('events');
const childProcess = require('child_process');

const _ = require('./helper');
const pkg = require('../package');
const XCProxy = require('./proxy');
const logger = require('./logger');
const XCTestWD = require('./xctestwd');

const {
  detectPort
} = _;
const TEST_URL = pkg.site;
const projectPath = XCTestWD.projectPath;
const SERVER_URL_REG = XCTestWD.SERVER_URL_REG;
const simulatorLogFlag = XCTestWD.simulatorLogFlag;

class XCTest extends EventEmitter {
  constructor(options) {
    super();
    this.proxy = null;
    this.capabilities = null;
    this.sessionId = null;
    this.device = null;
    this.deviceLogProc = null;
    this.runnerProc = null;
    this.iproxyProc = null;
    Object.assign(this, {
      proxyHost: '127.0.0.1',
      proxyPort: 8001,
      urlBase: 'wd/hub'
    }, options || {});
    this.init();
  }

  init() {
    this.checkProjectPath();
    process.on('uncaughtException', (e) => {
      logger.error(`Uncaught Exception: ${e.stack}`);
      this.stop();
      process.exit(1);
    });
    process.on('exit', () => {
      this.stop();
    });
  }

  checkProjectPath() {
    if (_.isExistedDir(projectPath)) {
      logger.debug(`project path: ${projectPath}`);
    } else {
      logger.error('project path not found');
    }
  }

  configUrl(str) {
    const urlObj = url.parse(str);
    this.proxyHost = urlObj.hostname;
    this.proxyPort = urlObj.port;
  }

  initProxy() {
    this.proxy = new XCProxy({
      proxyHost: this.proxyHost,
      proxyPort: this.proxyPort,
      urlBase: this.urlBase
    });
  }

  * startSimLog() {
    const logPath = yield this.startBootstrap(false);

    if (this.proxyHost && this.proxyPort) {
      return;
    }

    const logDir = path.resolve(logPath);

    return _.retry(() => {
      return new Promise((resolve, reject) => {
        let logTxtFile = path.join(logDir, '..', 'StandardOutputAndStandardError.txt');
        logTxtFile = logDir.replace(/(\s)/, '\\ ');

        logger.info(`Read simulator log at: ${logTxtFile}`);

        if (!_.isExistedFile(logTxtFile)) {
          return reject();
        }
        let args = `-f -n 0 ${logTxtFile}`.split(' ');
        var proc = childProcess.spawn('tail', args, {});
        this.deviceLogProc = proc;

        proc.stderr.setEncoding('utf8');
        proc.stdout.setEncoding('utf8');

        proc.stdout.on('data', data => {

          // avoid logout long data such as bitmap
          if (data.length <= 300 && logger.debugMode) {
            logger.debug(data);
          }

          let match = SERVER_URL_REG.exec(data);
          if (match) {
            const url = match[1];
            if (url.startsWith('http://')) {
              this.configUrl(url);
              resolve();
            }
          }
        });

        proc.stderr.on('data', data => {
          logger.debug(data);
        });

        proc.stdout.on('error', (err) => {
          logger.warn(`simulator log process error with ${err}`);
        });

        proc.on('exit', (code, signal) => {
          logger.warn(`simulator log process exit with code: ${code}, signal: ${signal}`);
          reject();
        });
      });
    }, 1000, Infinity);
  }

  * startDeviceLog() {
    yield this.startBootstrap(true);

    if (this.proxyHost && this.proxyPort) {
      return;
    }

    var proc = childProcess.spawn(iOSUtils.devicelog.binPath, [this.device.deviceId], {});
    this.deviceLogProc = proc;

    proc.stderr.setEncoding('utf8');
    proc.stdout.setEncoding('utf8');

    return new Promise((resolve, reject) => {
      proc.stdout.on('data', data => {

        // avoid logout long data such as bitmap
        if (data.length <= 300 && logger.debugMode) {
          logger.debug(data);
        }

        let match = SERVER_URL_REG.exec(data);
        if (match) {
          const url = match[1];
          if (url.startsWith('http://')) {
            this.configUrl(url);
            resolve();
          }
        }
      });

      proc.stderr.on('data', data => {
        logger.debug(data);
      });

      proc.stdout.on('error', (err) => {
        logger.warn(`devicelog error with ${err}`);
      });

      proc.on('exit', (code, signal) => {
        logger.warn(`devicelog exit with code: ${code}, signal: ${signal}`);
        reject();
      });
    });
  }

  * startBootstrap(isDevice) {
    return new Promise((resolve, reject) => {
      logger.info(`XCTestWD version: ${XCTestWD.version}`);

      var args = `clean test -project ${XCTestWD.projectPath} -scheme XCTestWDUITests -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');

      // check potential optimization provided by .xctestrun file, which allows xctestwd can be executed concurrently on multiple devices
      let xctestrun_path = path.join(__dirname, '..', 'XCTestWD', 'build', 'Build', 'Products');
      let xctestrun = fs.readdirSync(xctestrun_path).filter(fn => fn.match(isDevice ? '.*device.*\.xctestrun' : '.*simulator.*\.xctestrun')).shift();
      if (xctestrun) {
        args = `test-without-building -xctestrun ${xctestrun_path}/${xctestrun} -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');
      }

      var env = _.merge({}, process.env, {
        XCTESTWD_PORT: this.proxyPort
      });

      var proc = childProcess.spawn('xcodebuild', args, {
        env: env
      });
      this.runnerProc = proc;
      proc.stderr.setEncoding('utf8');
      proc.stdout.setEncoding('utf8');

      proc.stdout.on('data', data => {
        logger.debug(data);
        if (xctestrun) {
          let match = SERVER_URL_REG.exec(data);
          if (match) {
            const url = match[1];
            if (url.startsWith('http://')) {
              logger.debug('hitted for xctestrun mode');
              this.configUrl(url);
              resolve();
            }
          }
        }
      });

      proc.stderr.on('data', data => {
        if (data.length > 1000) {
          logger.warn(data.slice(0, 1000) + '...');
        } else {
          logger.warn(data);
        }
        if (!xctestrun) {
          if (~data.indexOf(simulatorLogFlag)) {
            const list = data.split(simulatorLogFlag);
            const res = list[1].trim();
            logger.debug('hitted for default mode');
            resolve(res);
          } else {
            logger.debug(`please check project: ${projectPath}`);
          }
        }
      });

      proc.stdout.on('error', (err) => {
        logger.warn(`xctest client error with ${err}`);
        logger.debug(`please check project: ${projectPath}`);
      });

      proc.on('exit', (code, signal) => {
        this.stop();
        logger.warn(`xctest client exit with code: ${code}, signal: ${signal}`);
      });

    });
  }

  * startIproxy() {
    let args = [this.proxyPort, this.proxyPort, this.device.deviceId];

    const IOS_USBMUXD_IPROXY = 'iproxy';
    const binPath = yield _.exec(`which ${IOS_USBMUXD_IPROXY}`);

    var proc = childProcess.spawn(binPath, args);

    this.iproxyProc = proc;
    proc.stderr.setEncoding('utf8');
    proc.stdout.setEncoding('utf8');

    proc.stdout.on('data', () => {
    });

    proc.stderr.on('data', (data) => {
      if (data.length > 1000) {
        logger.warn(data.slice(0, 1000) + '...');
      } else {
        logger.warn(data);
      }
    });

    proc.stdout.on('error', (err) => {
      logger.warn(`${IOS_USBMUXD_IPROXY} error with ${err}`);
    });

    proc.on('exit', (code, signal) => {
      logger.warn(`${IOS_USBMUXD_IPROXY} exit with code: ${code}, signal: ${signal}`);
    });
  }

  * start(caps) {
    try {
      this.proxyPort = yield detectPort(this.proxyPort);

      this.capabilities = caps;
      const xcodeVersion = yield iOSUtils.getXcodeVersion();

      logger.debug(`xcode version: ${xcodeVersion}`);

      var deviceInfo = iOSUtils.getDeviceInfo(this.device.deviceId);

      if (deviceInfo.isRealIOS) {
        yield this.startDeviceLog();
        yield this.startIproxy();
      } else {
        yield this.startSimLog();
      }

      logger.info(`${pkg.name} start with port: ${this.proxyPort}`);

      this.initProxy();

      if (caps.desiredCapabilities.browserName === 'Safari') {
        var promise = this.proxy.send(`/${this.urlBase}/session`, 'POST', {
          desiredCapabilities: {
            bundleId: 'com.apple.mobilesafari'
          }
        });
        return yield Promise.all([this.device.openURL(TEST_URL), promise]);
      } else {
        return yield this.proxy.send(`/${this.urlBase}/session`, 'POST', caps);
      }
    } catch (err) {
      logger.debug(`Fail to start xctest: ${err}`);
      this.stop();
      throw err;
    }
  }

  stop() {
    if (this.deviceLogProc) {
      logger.debug(`killing deviceLogProc pid: ${this.deviceLogProc.pid}`);
      this.deviceLogProc.kill('SIGKILL');
      this.deviceLogProc = null;
    }
    if (this.runnerProc) {
      logger.debug(`killing runnerProc pid: ${this.runnerProc.pid}`);
      this.runnerProc.kill('SIGKILL');
      this.runnerProc = null;
    }

    if (this.iproxyProc) {
      logger.debug(`killing iproxyProc pid: ${this.iproxyProc.pid}`);
      this.iproxyProc.kill('SIGKILL');
      this.iproxyProc = null;
    }
  }

  sendCommand(url, method, body) {
    return this.proxy.send(url, method, body);
  }
}

module.exports = XCTest;
module.exports.XCTestWD = XCTestWD;
<|endoftext|>


<commit_before>var emit = require('stereo/emit');
var on = require('stereo/on');
var when = require('stereo/when');
var snatch = require('stereo/catch');
var asObject = require('as/object');

function getOptions(rootChildren) {
  var dropdown = rootChildren[2];
  if (!dropdown || dropdown.tagName !== 'UL') return {error: {message:
    'bare-select: ' +
    'Dropdown element not found. The third element in a `<bare-select>` ' +
    'should be the dropdown – a `<ul>` element.'
  }};

  var options = Array.prototype.slice.call(dropdown.children)
    .filter(function(element) {return element.tagName === 'LI';})
  ;

  if (!options.length) return {error: {message: 'bare-select: ' +
    'No options found in the dropdown. Every option should be a `<li>` ' +
    'element with a radio button.'
  }};

  else if (options.some(function(item) {
    return (
      !item.children[0] ||
      item.children[0].tagName !== 'INPUT'
    );
  })) return {error: {message: 'bare-select: ' +
    'Wrong option markup. The first element in every dropdown option ' +
    '(`<li>`) should be an `<input>` element – a radio button or checkbox.'
  }};

  return {value: asObject(
    options.map(function(item) {
      return {
        key: item.children[0].value,
        value: {
          node: item,
        },
      };
    })
  )};
}

module.exports = function view (rootElement) {

  // Initialize internal DOM queries.
  var rootChildren = rootElement.children;

  // Initialize the input channel `selection`.
  var selection = Object.freeze({
    emit: emit(),
  });

  // Initialize the input channel `captionContent`.
  var captionContent = Object.freeze({
    emit: emit(),
  });

  // Initialize the input channel `unfolded`.
  var unfolded = Object.freeze({
    emit: emit(),
  });

  // Initialize the output channel `options`.
  var emitOptions = emit();
  var options = Object.freeze({
    on: on(emitOptions),
    when: when(emitOptions),
    catch: snatch(emitOptions),
  });

  // Emit an initial `update` or `error` to `options`.
  var optionsMessage = getOptions(rootChildren);
  if (optionsMessage.error) emitOptions('error', optionsMessage.error);
  else emitOptions('update', optionsMessage.value);

  // Initialize the output channel `captionElement`.
  var emitCaptionElement = emit();
  var captionElement = Object.freeze({
    on: on(emitCaptionElement),
  });

  // Return the channels.
  return Object.freeze({
    selection: selection,
    captionContent: captionContent,
    unfolded: unfolded,
    options: options,
    captionElement: captionElement,
  });
};
<commit_msg>Fix typo<commit_after>var emit = require('stereo/emit');
var on = require('stereo/on');
var when = require('stereo/when');
var snatch = require('stereo/catch');
var asObject = require('as/object');

function getOptions(rootChildren) {
  var dropdown = rootChildren[2];
  if (!dropdown || dropdown.tagName !== 'UL') return {error: {message:
    'bare-select: ' +
    'Dropdown element not found. The third element in a `<bare-select>` ' +
    'should be the dropdown – a `<ul>` element.'
  }};

  var options = Array.prototype.slice.call(dropdown.children)
    .filter(function(element) {return element.tagName === 'LI';})
  ;

  if (!options.length) return {error: {message: 'bare-select: ' +
    'No options found in the dropdown. Every option should be a `<li>` ' +
    'element with a radio button.'
  }};

  else if (options.some(function(item) {
    return (
      !item.children[0] ||
      item.children[0].tagName !== 'INPUT'
    );
  })) return {error: {message: 'bare-select: ' +
    'Wrong option markup. The first element in every dropdown option ' +
    '(`<li>`) should be an `<input>` element – a radio button or checkbox.'
  }};

  return {value: asObject(
    options.map(function(item) {
      return {
        key: item.children[0].value,
        value: {
          node: item,
        },
      };
    })
  )};
}

module.exports = function view(rootElement) {

  // Initialize internal DOM queries.
  var rootChildren = rootElement.children;

  // Initialize the input channel `selection`.
  var selection = Object.freeze({
    emit: emit(),
  });

  // Initialize the input channel `captionContent`.
  var captionContent = Object.freeze({
    emit: emit(),
  });

  // Initialize the input channel `unfolded`.
  var unfolded = Object.freeze({
    emit: emit(),
  });

  // Initialize the output channel `options`.
  var emitOptions = emit();
  var options = Object.freeze({
    on: on(emitOptions),
    when: when(emitOptions),
    catch: snatch(emitOptions),
  });

  // Emit an initial `update` or `error` to `options`.
  var optionsMessage = getOptions(rootChildren);
  if (optionsMessage.error) emitOptions('error', optionsMessage.error);
  else emitOptions('update', optionsMessage.value);

  // Initialize the output channel `captionElement`.
  var emitCaptionElement = emit();
  var captionElement = Object.freeze({
    on: on(emitCaptionElement),
  });

  // Return the channels.
  return Object.freeze({
    selection: selection,
    captionContent: captionContent,
    unfolded: unfolded,
    options: options,
    captionElement: captionElement,
  });
};
<|endoftext|>


<commit_before>module.exports = {
	events: {
		message: function ( bot, nick, to, text ) {
			if ( text === 'lol' && Math.random() > 0.8 ) { // 1 in 5
				if ( Math.random() > 0.8 ) { // 1 in 25
					if ( Math.random() > 0.8 ) { // 1 in 125
						var interjections = [
							'LMAO',
							'ROFL',
							'doublekek',
							'doublepluskek',
							'hahahaha XD',
							'kek',
							'keke',
							'lel',
							'lmao',
							'lulz',
							'psml',
							'rofl',
							'roflmao',
							'rwtsh lol',
							'i can\'t believe you\'ve done this'
						];
						return bot.shout( to, interjections[Math.floor( Math.random() * interjections.length )] );
					}
					return bot.shout( to, 'LOL' );
				}
				return bot.shout( to, 'lol' );
			} else if ( text === 'LOL' && Math.random() > 0.8 ) {
				if ( Math.random() > 0.8 ) {
					return bot.shout( to, 'ＬＯＬ' );
				}
				if ( Math.random() > 0.4 ) {
					return bot.shout( to, 'LOL' );
				}
				return bot.shout( to, 'lol' );
			}
			return false;
		}
	}
};
<commit_msg>Raise interjection chance<commit_after>module.exports = {
	events: {
		message: function ( bot, nick, to, text ) {
			if ( text === 'lol' && Math.random() > 0.8 ) { // 1 in 5
				if ( Math.random() > 0.8 ) { // 1 in 25
					if ( Math.random() > 0.5 ) { // 1 in 50
						var interjections = [
							'LMAO',
							'ROFL',
							'doublekek',
							'doublepluskek',
							'hahahaha XD',
							'kek',
							'keke',
							'lel',
							'lmao',
							'lulz',
							'psml',
							'rofl',
							'roflmao',
							'rwtsh lol',
							'i can\'t believe you\'ve done this'
						];
						return bot.shout( to, interjections[Math.floor( Math.random() * interjections.length )] );
					}
					return bot.shout( to, 'LOL' );
				}
				return bot.shout( to, 'lol' );
			} else if ( text === 'LOL' && Math.random() > 0.8 ) {
				if ( Math.random() > 0.8 ) {
					return bot.shout( to, 'ＬＯＬ' );
				}
				if ( Math.random() > 0.4 ) {
					return bot.shout( to, 'LOL' );
				}
				return bot.shout( to, 'lol' );
			}
			return false;
		}
	}
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var async = require('async-chainable');
var bwmNg = require('bwm-ng');
var wirelessTools = require('wireless-tools');
var which = require('which');

var bandwidthUsage = {}; // Holder for polled bandwidth info

module.exports = {
	settings: {
		net: {
			bwmNg: true,
			ignoreNoIP: true,
		},
	},
	poll: function(finish) {
		async()
			.set('settings', this.settings)
			.set('bandwidthUsage', {})
			.parallel({
				adapters: function(next) {
					wirelessTools.ifconfig.status(next);
				},
				iwconfig: function(next) {
					wirelessTools.iwconfig.status(next);
				},
				bandwidth: function(next) {
					var self = this;
					bwmNg.check(function(iface, bytesDown, bytesUp) {
						bandwidthUsage[iface] = {
							downSpeed: bytesDown,
							upSpeed: bytesUp,
						};
					});
					next();
				},
			})
			.forEach('adapters', function(next, adapter) {
				// Merge .net + Wlan adapter info {{{
				var wlan = _.find(this.iwconfig, {interface: adapter.interface });
				if (wlan) { // Matching wlan adapter
					adapter.type = 'wireless';
					_.merge(adapter, wlan);
				} else { // Boring ethernet
					adapter.type = 'ethernet';
				}
				// }}}
				// Match against ifSpeeds to provide bandwidth speeds {{{
				if (bandwidthUsage[adapter.interface]) {
					adapter.downSpeed = bandwidthUsage[adapter.interface].downSpeed;
					adapter.upSpeed = bandwidthUsage[adapter.interface].upSpeed;
				}
				// }}}
				next();
			})
			.then('adapters', function(next) {
				// Filter out adapters based on `ignore*` criteria {{{
				if (_.get(this.settings, 'net.ignoreNoIP'))
					this.adapters = this.adapters.filter(function(adapter) {
						return (adapter.ipv4_address || adapter.ipv6_address);
					});

				return next(null, this.adapters);
				// }}}
			})
			.end(function(err) {
				if (err) return finish(err);
				finish(null, {
					net: this.adapters,
				});
			});
	},
	register: function(finish, parentStats) {
		var settings = this.settings;
		which('bwm-ng', function(err, binPath) {
			if (err) { // If no binary - disable BWM-NG polling
				parentStats.emit('debug', 'Binary `bwm-ng` is not in path - disabling bandwith statistics');
				_.set(settings, 'net.bwmNg', false);
			}
			finish();
		});
	},
};
<commit_msg>Now possible to ignore specific device interfaces<commit_after>var _ = require('lodash');
var async = require('async-chainable');
var bwmNg = require('bwm-ng');
var wirelessTools = require('wireless-tools');
var which = require('which');

var bandwidthUsage = {}; // Holder for polled bandwidth info

module.exports = {
	settings: {
		net: {
			bwmNg: true,
			ignoreNoIP: true,
			ignoreDevice: [],
		},
	},
	poll: function(finish) {
		async()
			.set('settings', this.settings)
			.set('bandwidthUsage', {})
			.parallel({
				adapters: function(next) {
					wirelessTools.ifconfig.status(next);
				},
				iwconfig: function(next) {
					wirelessTools.iwconfig.status(next);
				},
				bandwidth: function(next) {
					var self = this;
					bwmNg.check(function(iface, bytesDown, bytesUp) {
						bandwidthUsage[iface] = {
							downSpeed: bytesDown,
							upSpeed: bytesUp,
						};
					});
					next();
				},
			})
			.forEach('adapters', function(next, adapter) {
				// Merge .net + Wlan adapter info {{{
				var wlan = _.find(this.iwconfig, {interface: adapter.interface });
				if (wlan) { // Matching wlan adapter
					adapter.type = 'wireless';
					_.merge(adapter, wlan);
				} else { // Boring ethernet
					adapter.type = 'ethernet';
				}
				// }}}
				// Match against ifSpeeds to provide bandwidth speeds {{{
				if (bandwidthUsage[adapter.interface]) {
					adapter.downSpeed = bandwidthUsage[adapter.interface].downSpeed;
					adapter.upSpeed = bandwidthUsage[adapter.interface].upSpeed;
				}
				// }}}
				next();
			})
			.then('adapters', function(next) {
				var self = this;
				// Filter out adapters based on `ignore*` criteria {{{
				if (_.get(self.settings, 'net.ignoreNoIP'))
					this.adapters = this.adapters.filter(function(adapter) {
						return (adapter.ipv4_address || adapter.ipv6_address);
					});

				if (_.get(self.settings, 'net.ignoreDevice'))
					this.adapters = this.adapters.filter(function(adapter) {
						return (!_.contains(self.settings.net.ignoreDevice, adapter.interface));
					});

				return next(null, this.adapters);
				// }}}
			})
			.end(function(err) {
				if (err) return finish(err);
				finish(null, {
					net: this.adapters,
				});
			});
	},
	register: function(finish, parentStats) {
		var settings = this.settings;
		which('bwm-ng', function(err, binPath) {
			if (err) { // If no binary - disable BWM-NG polling
				parentStats.emit('debug', 'Binary `bwm-ng` is not in path - disabling bandwith statistics');
				_.set(settings, 'net.bwmNg', false);
			}
			finish();
		});
	},
};
<|endoftext|>


<commit_before>(function(root) {

var fs = null
var gametree = root.gametree
var setting = root.setting
var helper = root.helper

if (typeof require != 'undefined') {
    fs = require('fs')
    gametree = require('./gametree')
    setting = require('./setting')
    helper = require('./helper')
    iconv = require('iconv-lite')
}

var context = typeof module != 'undefined' ? module.exports : (window.sgf = {})
var alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

/* The default encoding and list of properties that should be interpreted as
 * being encoded by the file's CA[] property is defined in the SGF spec at
 * http://www.red-bean.com/sgf/properties.html#CA
 */
var defaultEncoding = 'ISO-8859-1'
var encodedProperties = ['C', 'N', 'AN', 'BR', 'BT', 'CP', 'DT', 'EV', 'GN',
                          'ON', 'OT', 'PB', 'PC', 'PW', 'RE', 'RO', 'RU', 'SO',
                          'US', 'WR', 'WT', 'GC']

context.meta = {
    name: 'Smart Game Format',
    extensions: ['sgf']
}

context.tokenize = function(input) {
    input = helper.normalizeEndings(input)

    var tokens = []
    var rules = {
        ignore: /^\s+/,
        parenthesis: /^(\(|\))/,
        semicolon: /^;/,
        prop_ident: /^[A-Za-z]+/,
        c_value_type: /^\[([^\\\]]|\\[^])*\]/
    }

    while (input.length > 0) {
        var token = null
        var length = 1

        for (var type in rules) {
            var matches = rules[type].exec(input)
            if (!matches) continue

            var value = matches[0]
            length = value.length
            token = [type, value]

            break
        }

        if (token && token[0] != 'ignore') tokens.push(token)
        input = input.substr(length)
    }

    return tokens
}

context.parse = function(tokens, callback, start, depth, encoding) {
    if (!callback) callback = function(progress) {}
    if (!start) start = [0]
    if (isNaN(depth)) depth = 0
    if (!encoding) encoding = defaultEncoding

    var i = start[0]
    var node, property, tree = gametree.new()
    tree.collapsed = tokens.length >= setting.get('graph.collapse_tokens_count')
        && depth > setting.get('graph.collapse_min_depth')

    while (i < tokens.length) {
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == '(') break
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == ')') return tree

        var type = tokens[i][0], value = tokens[i][1]

        if (type == 'semicolon') {
            node = {}
            tree.nodes.push(node)
        } else if (type == 'prop_ident') {
            var id = value.split('').filter(function(x) {
                return x.toUpperCase() == x
            }).join('')

            if (id != '') {
                node[id] = []
                property = node[id]
            }
        } else if (type == 'c_value_type') {
            var encodedValue = value.substr(1, value.length - 2)
            if (id == 'CA' && iconv.encodingExists(encodedValue) && encodedValue != defaultEncoding) {
                encoding = encodedValue
                property.push(encodedValue)
                /* We may have already incorrectly parsed some values in this root node
                 * already, so we have to go back and re-parse them now.
                 */
                for (k in node) {
                    if (k != 'CA' && encodedProperties.indexOf(k) > -1) {
                        decodedValues = []
                        for (v in node[k]) {
                            decodedValues.push(iconv.decode(Buffer.from(node[k][v], 'binary'), encoding))
                        }
                        node[k] = decodedValues
                    }
                }
            } else if (encodedProperties.indexOf(id) > -1 && encoding != defaultEncoding) {
                decodedValue = iconv.decode(Buffer.from(encodedValue, 'binary'), encoding)
                property.push(context.unescapeString(decodedValue))
            } else {
                property.push(context.unescapeString(encodedValue))
            }
        }

        start[0] = ++i
    }

    while (i < tokens.length) {
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == '(') {
            start[0] = i + 1

            t = context.parse(tokens, callback, start, depth + Math.min(tree.subtrees.length, 1), encoding)

            if (t.nodes.length > 0) {
                t.parent = tree
                tree.subtrees.push(t)
                tree.current = 0
            }

            i = start[0]
        } else if (tokens[i][0] == 'parenthesis' && tokens[i][1] == ')') {
            start[0] = i
            callback(i / tokens.length)
            break
        }

        i++
    }

    return tree
}

context.parseFile = function(filename, callback) {
    if (!fs) return null

    var input = fs.readFileSync(filename, { encoding: 'binary' })
    var tokens = context.tokenize(input)

    return context.parse(tokens, callback)
}

context.string2dates = function(input) {
    if (!input.match(/^(\d{4}(-\d{1,2}(-\d{1,2})?)?(\s*,\s*(\d{4}|(\d{4}-)?\d{1,2}(-\d{1,2})?))*)?$/))
        return null
    if (input.trim() == '')
        return []

    var dates = input.split(',').map(function(x) {
        return x.trim().split('-')
    })

    for (var i = 1; i < dates.length; i++) {
        var date = dates[i]
        var prev = dates[i - 1]

        if (date[0].length != 4) {
            // No year

            if (date.length == 1 && prev.length == 3) {
                // Add month
                date.unshift(prev[1])
            }

            // Add year
            date.unshift(prev[0])
        }
    }

    return dates.map(function(x) {
        return x.map(function(y) { return +y })
    })
}

context.dates2string = function(dates) {
    if (dates.length == 0) return ''

    var datesCopy = [dates[0].slice()]

    for (var i = 1; i < dates.length; i++) {
        var date = dates[i]
        var prev = dates[i - 1]

        var k = 0
        for (var j = 0; j < date.length; j++) {
            if (date[j] == prev[j] && k == j) k++
            else break
        }

        datesCopy.push(date.slice(k))
    }

    return datesCopy.map(function(x) {
        return x.map(function(y) {
            return y > 9 ? '' + y : '0' + y
        }).join('-')
    }).join(',')
}

context.point2vertex = function(point) {
    if (point.length != 2) return [-1, -1]
    return point.split('').map(function(x) { return alpha.indexOf(x) })
}

context.vertex2point = function(vertex) {
    var x = vertex[0], y = vertex[1]
    if (Math.min(x, y) < 0 || Math.max(x, y) >= alpha.length)
        return ''
    return alpha[x] + alpha[y]
}

context.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [context.point2vertex(compressed)]

    var v1 = context.point2vertex(compressed.slice(0, colon))
    var v2 = context.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = Math.min(v1[0], v2[0]); i <= Math.max(v1[0], v2[0]); i++) {
        for (var j = Math.min(v1[1], v2[1]); j <= Math.max(v1[1], v2[1]); j++) {
            list.push([i, j])
        }
    }

    return list
}

context.stringify = function(tree) {
    var output = ''

    tree.nodes.forEach(function(node) {
        output += ';'

        for (var id in node) {
            if (id.toUpperCase() != id) continue
            if (id == 'CA') {
                /* Since we are outputting UTF8-encoded strings no matter what the input
                 * encoding was, we need to intercept the CA property and reset it.
                 */
                output += 'CA[UTF-8]'
                continue;
            }
            output += id + '[' + node[id].map(context.escapeString).join('][') + ']'
        }

        output += '\n'
    })

    for (var i = 0; i < tree.subtrees.length; i++) {
        output += '(' + context.stringify(tree.subtrees[i]) + ')'
    }

    return output
}

context.escapeString = function(input) {
    return input.toString().replace(/\\/g, '\\\\').replace(/\]/g, '\\]')
}

context.unescapeString = function(input) {
    var result = ''
    var inBackslash = false

    input = helper.normalizeEndings(input)

    for (var i = 0; i < input.length; i++) {
        if (!inBackslash) {
            if (input[i] != '\\')
                result += input[i]
            else if (input[i] == '\\')
                inBackslash = true
        } else {
            if (input[i] != '\n')
                result += input[i]

            inBackslash = false
        }
    }

    return result
}

}).call(null, typeof module != 'undefined' ? module : window)
<commit_msg>Formatting<commit_after>(function(root) {

var fs = null
var gametree = root.gametree
var setting = root.setting
var helper = root.helper

if (typeof require != 'undefined') {
    fs = require('fs')
    gametree = require('./gametree')
    setting = require('./setting')
    helper = require('./helper')
    iconv = require('iconv-lite')
}

var context = typeof module != 'undefined' ? module.exports : (window.sgf = {})
var alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

// The default encoding and list of properties that should be interpreted as
// being encoded by the file's CA[] property is defined in the SGF spec at
// http://www.red-bean.com/sgf/properties.html#CA

var defaultEncoding = 'ISO-8859-1'
var encodedProperties = ['C', 'N', 'AN', 'BR', 'BT', 'CP', 'DT', 'EV', 'GN',
                         'ON', 'OT', 'PB', 'PC', 'PW', 'RE', 'RO', 'RU', 'SO',
                         'US', 'WR', 'WT', 'GC']

context.meta = {
    name: 'Smart Game Format',
    extensions: ['sgf']
}

context.tokenize = function(input) {
    input = helper.normalizeEndings(input)

    var tokens = []
    var rules = {
        ignore: /^\s+/,
        parenthesis: /^(\(|\))/,
        semicolon: /^;/,
        prop_ident: /^[A-Za-z]+/,
        c_value_type: /^\[([^\\\]]|\\[^])*\]/
    }

    while (input.length > 0) {
        var token = null
        var length = 1

        for (var type in rules) {
            var matches = rules[type].exec(input)
            if (!matches) continue

            var value = matches[0]
            length = value.length
            token = [type, value]

            break
        }

        if (token && token[0] != 'ignore') tokens.push(token)
        input = input.substr(length)
    }

    return tokens
}

context.parse = function(tokens, callback, start, depth, encoding) {
    if (!callback) callback = function(progress) {}
    if (!start) start = [0]
    if (isNaN(depth)) depth = 0
    if (!encoding) encoding = defaultEncoding

    var i = start[0]
    var node, property, tree = gametree.new()
    tree.collapsed = tokens.length >= setting.get('graph.collapse_tokens_count')
        && depth > setting.get('graph.collapse_min_depth')

    while (i < tokens.length) {
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == '(') break
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == ')') return tree

        var type = tokens[i][0], value = tokens[i][1]

        if (type == 'semicolon') {
            node = {}
            tree.nodes.push(node)
        } else if (type == 'prop_ident') {
            var id = value.split('').filter(function(x) {
                return x.toUpperCase() == x
            }).join('')

            if (id != '') {
                node[id] = []
                property = node[id]
            }
        } else if (type == 'c_value_type') {
            var encodedValue = context.unescapeString(value.substr(1, value.length - 2))

            if (id == 'CA' && iconv.encodingExists(encodedValue) && encodedValue != defaultEncoding) {
                encoding = encodedValue
                property.push(encodedValue)

                // We may have already incorrectly parsed some values in this root node
                // already, so we have to go back and re-parse them now.

                for (k in node) {
                    if (encodedProperties.indexOf(k) >= 0) {
                        decodedValues = []
                        for (v in node[k]) {
                            decodedValues.push(iconv.decode(Buffer.from(node[k][v], 'binary'), encoding))
                        }
                        node[k] = decodedValues
                    }
                }
            } else if (encodedProperties.indexOf(id) > -1 && encoding != defaultEncoding) {
                decodedValue = iconv.decode(Buffer.from(encodedValue, 'binary'), encoding)
                property.push(decodedValue)
            } else {
                property.push(encodedValue)
            }
        }

        start[0] = ++i
    }

    while (i < tokens.length) {
        if (tokens[i][0] == 'parenthesis' && tokens[i][1] == '(') {
            start[0] = i + 1

            t = context.parse(tokens, callback, start, depth + Math.min(tree.subtrees.length, 1), encoding)

            if (t.nodes.length > 0) {
                t.parent = tree
                tree.subtrees.push(t)
                tree.current = 0
            }

            i = start[0]
        } else if (tokens[i][0] == 'parenthesis' && tokens[i][1] == ')') {
            start[0] = i
            callback(i / tokens.length)
            break
        }

        i++
    }

    return tree
}

context.parseFile = function(filename, callback) {
    if (!fs) return null

    var input = fs.readFileSync(filename, { encoding: 'binary' })
    var tokens = context.tokenize(input)

    return context.parse(tokens, callback)
}

context.string2dates = function(input) {
    if (!input.match(/^(\d{4}(-\d{1,2}(-\d{1,2})?)?(\s*,\s*(\d{4}|(\d{4}-)?\d{1,2}(-\d{1,2})?))*)?$/))
        return null
    if (input.trim() == '')
        return []

    var dates = input.split(',').map(function(x) {
        return x.trim().split('-')
    })

    for (var i = 1; i < dates.length; i++) {
        var date = dates[i]
        var prev = dates[i - 1]

        if (date[0].length != 4) {
            // No year

            if (date.length == 1 && prev.length == 3) {
                // Add month
                date.unshift(prev[1])
            }

            // Add year
            date.unshift(prev[0])
        }
    }

    return dates.map(function(x) {
        return x.map(function(y) { return +y })
    })
}

context.dates2string = function(dates) {
    if (dates.length == 0) return ''

    var datesCopy = [dates[0].slice()]

    for (var i = 1; i < dates.length; i++) {
        var date = dates[i]
        var prev = dates[i - 1]

        var k = 0
        for (var j = 0; j < date.length; j++) {
            if (date[j] == prev[j] && k == j) k++
            else break
        }

        datesCopy.push(date.slice(k))
    }

    return datesCopy.map(function(x) {
        return x.map(function(y) {
            return y > 9 ? '' + y : '0' + y
        }).join('-')
    }).join(',')
}

context.point2vertex = function(point) {
    if (point.length != 2) return [-1, -1]
    return point.split('').map(function(x) { return alpha.indexOf(x) })
}

context.vertex2point = function(vertex) {
    var x = vertex[0], y = vertex[1]
    if (Math.min(x, y) < 0 || Math.max(x, y) >= alpha.length)
        return ''
    return alpha[x] + alpha[y]
}

context.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [context.point2vertex(compressed)]

    var v1 = context.point2vertex(compressed.slice(0, colon))
    var v2 = context.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = Math.min(v1[0], v2[0]); i <= Math.max(v1[0], v2[0]); i++) {
        for (var j = Math.min(v1[1], v2[1]); j <= Math.max(v1[1], v2[1]); j++) {
            list.push([i, j])
        }
    }

    return list
}

context.stringify = function(tree) {
    var output = ''

    tree.nodes.forEach(function(node) {
        output += ';'

        for (var id in node) {
            if (id.toUpperCase() != id) continue

            if (id == 'CA') {
                // Since we are outputting UTF8-encoded strings no matter what the input
                // encoding was, we need to intercept the CA property and reset it.

                output += 'CA[UTF-8]'
                continue
            }

            output += id + '[' + node[id].map(context.escapeString).join('][') + ']'
        }

        output += '\n'
    })

    for (var i = 0; i < tree.subtrees.length; i++) {
        output += '(' + context.stringify(tree.subtrees[i]) + ')'
    }

    return output
}

context.escapeString = function(input) {
    return input.toString().replace(/\\/g, '\\\\').replace(/\]/g, '\\]')
}

context.unescapeString = function(input) {
    var result = ''
    var inBackslash = false

    input = helper.normalizeEndings(input)

    for (var i = 0; i < input.length; i++) {
        if (!inBackslash) {
            if (input[i] != '\\')
                result += input[i]
            else if (input[i] == '\\')
                inBackslash = true
        } else {
            if (input[i] != '\n')
                result += input[i]

            inBackslash = false
        }
    }

    return result
}

}).call(null, typeof module != 'undefined' ? module : window)
<|endoftext|>


<commit_before>const bs = require('binary-search');
const EventEmitter = require('events');
const mp4 = require('mp4-stream');
const Box = require('mp4-box-encoding');
const RangeSliceStream = require('range-slice-stream');

class MP4Remuxer extends EventEmitter {
    constructor(file) {
        this._tracks = []
        this._fragmentSequence = 1
        this._file = file
        this._decoder = null
        this._findMoov(0)
    }

    _findMoov(offset) {
        const self = this;

        if (self._decoder) {
            self._decoder.destroy()
        }

        self._decoder = mp4.decode()
        const fileStream = self._file.createReadStream({
            start: offset
        });
        fileStream.pipe(self._decoder)

        self._decoder.once('box', headers => {
            if (headers.type === 'moov') {
                self._decoder.decode(moov => {
                    fileStream.destroy()
                    try {
                        self._processMoov(moov)
                    } catch (err) {
                        err.message = `Cannot parse mp4 file: ${err.message}`
                        self.emit('error', err)
                    }
                })
            } else {
                fileStream.destroy()
                self._findMoov(offset + headers.length)
            }
        })
    }

    _processMoov(moov) {
        const self = this;

        const traks = moov.traks;
        self._tracks = []
        self._hasVideo = false
        self._hasAudio = false
        for (let i = 0; i < traks.length; i++) {
            const trak = traks[i];
            const stbl = trak.mdia.minf.stbl;
            const stsdEntry = stbl.stsd.entries[0];
            const handlerType = trak.mdia.hdlr.handlerType;
            let codec;
            let mime;
            if (handlerType === 'vide' && stsdEntry.type === 'avc1') {
                if (self._hasVideo) {
                    continue
                }
                self._hasVideo = true
                codec = 'avc1'
                if (stsdEntry.avcC) {
                    codec += `.${stsdEntry.avcC.mimeCodec}`
                }
                mime = `video/mp4; codecs="${codec}"`
            } else if (handlerType === 'soun' && stsdEntry.type === 'mp4a') {
                if (self._hasAudio) {
                    continue
                }
                self._hasAudio = true
                codec = 'mp4a'
                if (stsdEntry.esds && stsdEntry.esds.mimeCodec) {
                    codec += `.${stsdEntry.esds.mimeCodec}`
                }
                mime = `audio/mp4; codecs="${codec}"`
            } else {
                continue
            }

            const samples = [];
            let sample = 0;

            // Chunk/position data
            let sampleInChunk = 0;
            let chunk = 0;
            let offsetInChunk = 0;
            let sampleToChunkIndex = 0;

            // Time data
            let dts = 0;
            const decodingTimeEntry = new RunLengthIndex(stbl.stts.entries);
            let presentationOffsetEntry = null;
            if (stbl.ctts) {
                presentationOffsetEntry = new RunLengthIndex(stbl.ctts.entries)
            }

            // Sync table index
            let syncSampleIndex = 0;

            while (true) {
                var currChunkEntry = stbl.stsc.entries[sampleToChunkIndex]

                // Compute size
                const size = stbl.stsz.entries[sample];

                // Compute time data
                const duration = decodingTimeEntry.value.duration;
                const presentationOffset = presentationOffsetEntry ? presentationOffsetEntry.value.compositionOffset : 0;

                // Compute sync
                let sync = true;
                if (stbl.stss) {
                    sync = stbl.stss.entries[syncSampleIndex] === sample + 1
                }

                // Create new sample entry
                const chunkOffsetTable = stbl.stco || stbl.co64;
                samples.push({
                    size,
                    duration,
                    dts,
                    presentationOffset,
                    sync,
                    offset: offsetInChunk + chunkOffsetTable.entries[chunk]
                })

                // Go to next sample
                sample++
                if (sample >= stbl.stsz.entries.length) {
                    break
                }

                // Move position/chunk
                sampleInChunk++
                offsetInChunk += size
                if (sampleInChunk >= currChunkEntry.samplesPerChunk) {
                    // Move to new chunk
                    sampleInChunk = 0
                    offsetInChunk = 0
                    chunk++
                    // Move sample to chunk box index
                    const nextChunkEntry = stbl.stsc.entries[sampleToChunkIndex + 1];
                    if (nextChunkEntry && chunk + 1 >= nextChunkEntry.firstChunk) {
                        sampleToChunkIndex++
                    }
                }

                // Move time forward
                dts += duration
                decodingTimeEntry.inc()
                presentationOffsetEntry && presentationOffsetEntry.inc()

                // Move sync table index
                if (sync) {
                    syncSampleIndex++
                }
            }

            trak.mdia.mdhd.duration = 0
            trak.tkhd.duration = 0

            const defaultSampleDescriptionIndex = currChunkEntry.sampleDescriptionId;

            const trackMoov = {
                type: 'moov',
                mvhd: moov.mvhd,
                traks: [{
                    tkhd: trak.tkhd,
                    mdia: {
                        mdhd: trak.mdia.mdhd,
                        hdlr: trak.mdia.hdlr,
                        elng: trak.mdia.elng,
                        minf: {
                            vmhd: trak.mdia.minf.vmhd,
                            smhd: trak.mdia.minf.smhd,
                            dinf: trak.mdia.minf.dinf,
                            stbl: {
                                stsd: stbl.stsd,
                                stts: empty(),
                                ctts: empty(),
                                stsc: empty(),
                                stsz: empty(),
                                stco: empty(),
                                stss: empty()
                            }
                        }
                    }
                }],
                mvex: {
                    mehd: {
                        fragmentDuration: moov.mvhd.duration
                    },
                    trexs: [{
                        trackId: trak.tkhd.trackId,
                        defaultSampleDescriptionIndex,
                        defaultSampleDuration: 0,
                        defaultSampleSize: 0,
                        defaultSampleFlags: 0
                    }]
                }
            };

            self._tracks.push({
                trackId: trak.tkhd.trackId,
                timeScale: trak.mdia.mdhd.timeScale,
                samples,
                currSample: null,
                currTime: null,
                moov: trackMoov,
                mime
            })
        }

        if (self._tracks.length === 0) {
            self.emit('error', new Error('no playable tracks'))
            return
        }

        // Must be set last since this is used above
        moov.mvhd.duration = 0

        self._ftyp = {
            type: 'ftyp',
            brand: 'iso5',
            brandVersion: 0,
            compatibleBrands: [
                'iso5'
            ]
        }

        const ftypBuf = Box.encode(self._ftyp);
        const data = self._tracks.map(track => {
            const moovBuf = Box.encode(track.moov);
            return {
                mime: track.mime,
                init: Buffer.concat([ftypBuf, moovBuf])
            }
        });

        self.emit('ready', data)
    }

    seek(time) {
        const self = this;
        if (!self._tracks) {
            throw new Error('Not ready yet; wait for \'ready\' event')
        }

        if (self._fileStream) {
            self._fileStream.destroy()
            self._fileStream = null
        }

        let startOffset = -1;
        self._tracks.map((track, i) => {
            // find the keyframe before the time
            // stream from there
            if (track.outStream) {
                track.outStream.destroy()
            }
            if (track.inStream) {
                track.inStream.destroy()
                track.inStream = null
            }
            const outStream = track.outStream = mp4.encode();
            const fragment = self._generateFragment(i, time);
            if (!fragment) {
                return outStream.finalize()
            }

            if (startOffset === -1 || fragment.ranges[0].start < startOffset) {
                startOffset = fragment.ranges[0].start
            }

            writeFragment(fragment)

            function writeFragment (frag) {
                if (outStream.destroyed) return
                outStream.box(frag.moof, err => {
                    if (err) return self.emit('error', err)
                    if (outStream.destroyed) return
                    const slicedStream = track.inStream.slice(frag.ranges);
                    slicedStream.pipe(outStream.mediaData(frag.length, err => {
                        if (err) return self.emit('error', err)
                        if (outStream.destroyed) return
                        const nextFrag = self._generateFragment(i);
                        if (!nextFrag) {
                            return outStream.finalize()
                        }
                        writeFragment(nextFrag)
                    }))
                })
            }
        })

        if (startOffset >= 0) {
            const fileStream = self._fileStream = self._file.createReadStream({
                start: startOffset
            });

            self._tracks.forEach(track => {
                track.inStream = new RangeSliceStream(startOffset, {
                    // Allow up to a 10MB offset between audio and video,
                    // which should be fine for any reasonable interleaving
                    // interval and bitrate
                    highWaterMark: 10000000
                })
                fileStream.pipe(track.inStream)
            })
        }

        return self._tracks.map(track => {
            return track.outStream
        });
    }

    _findSampleBefore(trackInd, time) {
        const self = this;

        const track = self._tracks[trackInd];
        const scaledTime = Math.floor(track.timeScale * time);
        let sample = bs(track.samples, scaledTime, (sample, t) => {
            const pts = sample.dts + sample.presentationOffset;// - track.editShift
            return pts - t
        });
        if (sample === -1) {
            sample = 0
        } else if (sample < 0) {
            sample = -sample - 2
        }
        // sample is now the last sample with dts <= time
        // Find the preceeding sync sample
        while (!track.samples[sample].sync) {
            sample--
        }
        return sample
    }

    _generateFragment(track, time) {
        const self = this;
        /*
        1. Find correct sample
        2. Process backward until sync sample found
        3. Process forward until next sync sample after MIN_FRAGMENT_DURATION found
        */
        const currTrack = self._tracks[track];
        let firstSample;
        if (time !== undefined) {
            firstSample = self._findSampleBefore(track, time)
        } else {
            firstSample = currTrack.currSample
        }

        if (firstSample >= currTrack.samples.length)
            return null

        const startDts = currTrack.samples[firstSample].dts;

        let totalLen = 0;
        const ranges = [];
        for (var currSample = firstSample; currSample < currTrack.samples.length; currSample++) {
            const sample = currTrack.samples[currSample];
            if (sample.sync && sample.dts - startDts >= currTrack.timeScale * MIN_FRAGMENT_DURATION) {
                break // This is a reasonable place to end the fragment
            }

            totalLen += sample.size
            const currRange = ranges.length - 1;
            if (currRange < 0 || ranges[currRange].end !== sample.offset) {
                // Push a new range
                ranges.push({
                    start: sample.offset,
                    end: sample.offset + sample.size
                })
            } else {
                ranges[currRange].end += sample.size
            }
        }

        currTrack.currSample = currSample

        return {
            moof: self._generateMoof(track, firstSample, currSample),
            ranges,
            length: totalLen
        };
    }

    _generateMoof(track, firstSample, lastSample) {
        const self = this;

        const currTrack = self._tracks[track];

        const entries = [];
        let trunVersion = 0;
        for (let j = firstSample; j < lastSample; j++) {
            const currSample = currTrack.samples[j];
            if (currSample.presentationOffset < 0)
                trunVersion = 1
            entries.push({
                sampleDuration: currSample.duration,
                sampleSize: currSample.size,
                sampleFlags: currSample.sync ? 0x2000000 : 0x1010000,
                sampleCompositionTimeOffset: currSample.presentationOffset
            })
        }

        const moof = {
            type: 'moof',
            mfhd: {
                sequenceNumber: self._fragmentSequence++
            },
            trafs: [{
                tfhd: {
                    flags: 0x20000, // default-base-is-moof
                    trackId: currTrack.trackId
                },
                tfdt: {
                    baseMediaDecodeTime: currTrack.samples[firstSample].dts
                },
                trun: {
                    flags: 0xf01,
                    dataOffset: 8, // The moof size has to be added to this later as well
                    entries,
                    version: trunVersion
                }
            }]
        };

        // Update the offset
        moof.trafs[0].trun.dataOffset += Box.encodingLength(moof)

        return moof
    }
}

class RunLengthIndex {
    constructor(entries, countName) {
        const self = this;
        self._entries = entries
        self._countName = countName || 'count'
        self._index = 0
        self._offset = 0

        self.value = self._entries[0]
    }

    inc() {
        const self = this;
        self._offset++
        if (self._offset >= self._entries[self._index][self._countName]) {
            self._index++
            self._offset = 0
        }

        self.value = self._entries[self._index]
    }
}

function empty () {
	return {
		version: 0,
		flags: 0,
		entries: []
	}
}

const MIN_FRAGMENT_DURATION = 1; // second

module.exports = MP4Remuxer
<commit_msg>Standard.js fix<commit_after>const bs = require('binary-search')
const EventEmitter = require('events')
const mp4 = require('mp4-stream')
const Box = require('mp4-box-encoding')
const RangeSliceStream = require('range-slice-stream')

class MP4Remuxer extends EventEmitter {
  constructor (file) {
    super()

    this._tracks = []
    this._fragmentSequence = 1
    this._file = file
    this._decoder = null
    this._findMoov(0)
  }

  _findMoov (offset) {
    const self = this

    if (self._decoder) {
      self._decoder.destroy()
    }

    self._decoder = mp4.decode()
    const fileStream = self._file.createReadStream({
      start: offset
    })
    fileStream.pipe(self._decoder)

    self._decoder.once('box', headers => {
      if (headers.type === 'moov') {
        self._decoder.decode(moov => {
          fileStream.destroy()
          try {
            self._processMoov(moov)
          } catch (err) {
            err.message = `Cannot parse mp4 file: ${err.message}`
            self.emit('error', err)
          }
        })
      } else {
        fileStream.destroy()
        self._findMoov(offset + headers.length)
      }
    })
  }

  _processMoov (moov) {
    const self = this

    const traks = moov.traks
    self._tracks = []
    self._hasVideo = false
    self._hasAudio = false
    for (let i = 0; i < traks.length; i++) {
      const trak = traks[i]
      const stbl = trak.mdia.minf.stbl
      const stsdEntry = stbl.stsd.entries[0]
      const handlerType = trak.mdia.hdlr.handlerType
      let codec
      let mime
      if (handlerType === 'vide' && stsdEntry.type === 'avc1') {
        if (self._hasVideo) {
          continue
        }
        self._hasVideo = true
        codec = 'avc1'
        if (stsdEntry.avcC) {
          codec += `.${stsdEntry.avcC.mimeCodec}`
        }
        mime = `video/mp4; codecs="${codec}"`
      } else if (handlerType === 'soun' && stsdEntry.type === 'mp4a') {
        if (self._hasAudio) {
          continue
        }
        self._hasAudio = true
        codec = 'mp4a'
        if (stsdEntry.esds && stsdEntry.esds.mimeCodec) {
          codec += `.${stsdEntry.esds.mimeCodec}`
        }
        mime = `audio/mp4; codecs="${codec}"`
      } else {
        continue
      }

      const samples = []
      let sample = 0

      // Chunk/position data
      let sampleInChunk = 0
      let chunk = 0
      let offsetInChunk = 0
      let sampleToChunkIndex = 0

      // Time data
      let dts = 0
      const decodingTimeEntry = new RunLengthIndex(stbl.stts.entries)
      let presentationOffsetEntry = null
      if (stbl.ctts) {
        presentationOffsetEntry = new RunLengthIndex(stbl.ctts.entries)
      }

      // Sync table index
      let syncSampleIndex = 0

      while (true) {
        var currChunkEntry = stbl.stsc.entries[sampleToChunkIndex]

        // Compute size
        const size = stbl.stsz.entries[sample]

        // Compute time data
        const duration = decodingTimeEntry.value.duration
        const presentationOffset = presentationOffsetEntry ? presentationOffsetEntry.value.compositionOffset : 0

        // Compute sync
        let sync = true
        if (stbl.stss) {
          sync = stbl.stss.entries[syncSampleIndex] === sample + 1
        }

        // Create new sample entry
        const chunkOffsetTable = stbl.stco || stbl.co64
        samples.push({
          size,
          duration,
          dts,
          presentationOffset,
          sync,
          offset: offsetInChunk + chunkOffsetTable.entries[chunk]
        })

        // Go to next sample
        sample++
        if (sample >= stbl.stsz.entries.length) {
          break
        }

        // Move position/chunk
        sampleInChunk++
        offsetInChunk += size
        if (sampleInChunk >= currChunkEntry.samplesPerChunk) {
          // Move to new chunk
          sampleInChunk = 0
          offsetInChunk = 0
          chunk++
          // Move sample to chunk box index
          const nextChunkEntry = stbl.stsc.entries[sampleToChunkIndex + 1]
          if (nextChunkEntry && chunk + 1 >= nextChunkEntry.firstChunk) {
            sampleToChunkIndex++
          }
        }

        // Move time forward
        dts += duration
        decodingTimeEntry.inc()
        presentationOffsetEntry && presentationOffsetEntry.inc()

        // Move sync table index
        if (sync) {
          syncSampleIndex++
        }
      }

      trak.mdia.mdhd.duration = 0
      trak.tkhd.duration = 0

      const defaultSampleDescriptionIndex = currChunkEntry.sampleDescriptionId

      const trackMoov = {
        type: 'moov',
        mvhd: moov.mvhd,
        traks: [{
          tkhd: trak.tkhd,
          mdia: {
            mdhd: trak.mdia.mdhd,
            hdlr: trak.mdia.hdlr,
            elng: trak.mdia.elng,
            minf: {
              vmhd: trak.mdia.minf.vmhd,
              smhd: trak.mdia.minf.smhd,
              dinf: trak.mdia.minf.dinf,
              stbl: {
                stsd: stbl.stsd,
                stts: empty(),
                ctts: empty(),
                stsc: empty(),
                stsz: empty(),
                stco: empty(),
                stss: empty()
              }
            }
          }
        }],
        mvex: {
          mehd: {
            fragmentDuration: moov.mvhd.duration
          },
          trexs: [{
            trackId: trak.tkhd.trackId,
            defaultSampleDescriptionIndex,
            defaultSampleDuration: 0,
            defaultSampleSize: 0,
            defaultSampleFlags: 0
          }]
        }
      }

      self._tracks.push({
        trackId: trak.tkhd.trackId,
        timeScale: trak.mdia.mdhd.timeScale,
        samples,
        currSample: null,
        currTime: null,
        moov: trackMoov,
        mime
      })
    }

    if (self._tracks.length === 0) {
      self.emit('error', new Error('no playable tracks'))
      return
    }

    // Must be set last since this is used above
    moov.mvhd.duration = 0

    self._ftyp = {
      type: 'ftyp',
      brand: 'iso5',
      brandVersion: 0,
      compatibleBrands: [
        'iso5'
      ]
    }

    const ftypBuf = Box.encode(self._ftyp)
    const data = self._tracks.map(track => {
      const moovBuf = Box.encode(track.moov)
      return {
        mime: track.mime,
        init: Buffer.concat([ftypBuf, moovBuf])
      }
    })

    self.emit('ready', data)
  }

  seek (time) {
    const self = this
    if (!self._tracks) {
      throw new Error('Not ready yet; wait for \'ready\' event')
    }

    if (self._fileStream) {
      self._fileStream.destroy()
      self._fileStream = null
    }

    let startOffset = -1
    self._tracks.map((track, i) => {
      // find the keyframe before the time
      // stream from there
      if (track.outStream) {
        track.outStream.destroy()
      }
      if (track.inStream) {
        track.inStream.destroy()
        track.inStream = null
      }
      const outStream = track.outStream = mp4.encode()
      const fragment = self._generateFragment(i, time)
      if (!fragment) {
        return outStream.finalize()
      }

      if (startOffset === -1 || fragment.ranges[0].start < startOffset) {
        startOffset = fragment.ranges[0].start
      }

      writeFragment(fragment)

      function writeFragment (frag) {
        if (outStream.destroyed) return
        outStream.box(frag.moof, err => {
          if (err) return self.emit('error', err)
          if (outStream.destroyed) return
          const slicedStream = track.inStream.slice(frag.ranges)
          slicedStream.pipe(outStream.mediaData(frag.length, err => {
            if (err) return self.emit('error', err)
            if (outStream.destroyed) return
            const nextFrag = self._generateFragment(i)
            if (!nextFrag) {
              return outStream.finalize()
            }
            writeFragment(nextFrag)
          }))
        })
      }
    })

    if (startOffset >= 0) {
      const fileStream = self._fileStream = self._file.createReadStream({
        start: startOffset
      })

      self._tracks.forEach(track => {
        track.inStream = new RangeSliceStream(startOffset, {
          // Allow up to a 10MB offset between audio and video,
          // which should be fine for any reasonable interleaving
          // interval and bitrate
          highWaterMark: 10000000
        })
        fileStream.pipe(track.inStream)
      })
    }

    return self._tracks.map(track => {
      return track.outStream
    })
  }

  _findSampleBefore (trackInd, time) {
    const self = this

    const track = self._tracks[trackInd]
    const scaledTime = Math.floor(track.timeScale * time)
    let sample = bs(track.samples, scaledTime, (sample, t) => {
      const pts = sample.dts + sample.presentationOffset// - track.editShift
      return pts - t
    })
    if (sample === -1) {
      sample = 0
    } else if (sample < 0) {
      sample = -sample - 2
    }
    // sample is now the last sample with dts <= time
    // Find the preceeding sync sample
    while (!track.samples[sample].sync) {
      sample--
    }
    return sample
  }

  _generateFragment (track, time) {
    const self = this
    /*
        1. Find correct sample
        2. Process backward until sync sample found
        3. Process forward until next sync sample after MIN_FRAGMENT_DURATION found
        */
    const currTrack = self._tracks[track]
    let firstSample
    if (time !== undefined) {
      firstSample = self._findSampleBefore(track, time)
    } else {
      firstSample = currTrack.currSample
    }

    if (firstSample >= currTrack.samples.length) { return null }

    const startDts = currTrack.samples[firstSample].dts

    let totalLen = 0
    const ranges = []
    for (var currSample = firstSample; currSample < currTrack.samples.length; currSample++) {
      const sample = currTrack.samples[currSample]
      if (sample.sync && sample.dts - startDts >= currTrack.timeScale * MIN_FRAGMENT_DURATION) {
        break // This is a reasonable place to end the fragment
      }

      totalLen += sample.size
      const currRange = ranges.length - 1
      if (currRange < 0 || ranges[currRange].end !== sample.offset) {
        // Push a new range
        ranges.push({
          start: sample.offset,
          end: sample.offset + sample.size
        })
      } else {
        ranges[currRange].end += sample.size
      }
    }

    currTrack.currSample = currSample

    return {
      moof: self._generateMoof(track, firstSample, currSample),
      ranges,
      length: totalLen
    }
  }

  _generateMoof (track, firstSample, lastSample) {
    const self = this

    const currTrack = self._tracks[track]

    const entries = []
    let trunVersion = 0
    for (let j = firstSample; j < lastSample; j++) {
      const currSample = currTrack.samples[j]
      if (currSample.presentationOffset < 0) { trunVersion = 1 }
      entries.push({
        sampleDuration: currSample.duration,
        sampleSize: currSample.size,
        sampleFlags: currSample.sync ? 0x2000000 : 0x1010000,
        sampleCompositionTimeOffset: currSample.presentationOffset
      })
    }

    const moof = {
      type: 'moof',
      mfhd: {
        sequenceNumber: self._fragmentSequence++
      },
      trafs: [{
        tfhd: {
          flags: 0x20000, // default-base-is-moof
          trackId: currTrack.trackId
        },
        tfdt: {
          baseMediaDecodeTime: currTrack.samples[firstSample].dts
        },
        trun: {
          flags: 0xf01,
          dataOffset: 8, // The moof size has to be added to this later as well
          entries,
          version: trunVersion
        }
      }]
    }

    // Update the offset
    moof.trafs[0].trun.dataOffset += Box.encodingLength(moof)

    return moof
  }
}

class RunLengthIndex {
  constructor (entries, countName) {
    const self = this
    self._entries = entries
    self._countName = countName || 'count'
    self._index = 0
    self._offset = 0

    self.value = self._entries[0]
  }

  inc () {
    const self = this
    self._offset++
    if (self._offset >= self._entries[self._index][self._countName]) {
      self._index++
      self._offset = 0
    }

    self.value = self._entries[self._index]
  }
}

function empty () {
  return {
    version: 0,
    flags: 0,
    entries: []
  }
}

const MIN_FRAGMENT_DURATION = 1 // second

module.exports = MP4Remuxer
<|endoftext|>


<commit_before>var _ = require('underscore'),
    Backbone = require('backbone'),
    async = require('async'),
    ModelStore = require('./store/model_store'),
    CollectionStore = require('./store/collection_store'),
    isServer = (typeof window === 'undefined');

if (!isServer) {
  Backbone.$ = window.$ || require('jquery');
}

module.exports = Fetcher;

function Fetcher(options) {
  this.options = options;
  this.app = this.options.app;
  this.modelUtils = this.app.modelUtils;
  this.modelStore = new ModelStore({
    app: this.app,
    modelUtils: this.modelUtils
  });
  this.collectionStore = new CollectionStore({
    app: this.app,
    modelUtils: this.modelUtils
  });
}

Fetcher.prototype.buildOptions = function(additionalOptions, params) {
  var options = {app: this.app, parse: true};
  _.defaults(options, additionalOptions);
  _.defaults(options, params);
  return options;
};

/**
 * Returns an instance of Model or Collection.
 */
Fetcher.prototype.getModelOrCollectionForSpec = function(spec, attrsOrModels, options, callback) {
  if (spec.model) {
    return this.getModelForSpec(spec, attrsOrModels, options, callback);
  } else {
    return this.getCollectionForSpec(spec, attrsOrModels, options, callback);
  }
};

/**
 * Returns an instance of Collection.
 */
Fetcher.prototype.getCollectionForSpec = function(spec, models, options, callback) {
  var collectionOptions = this.buildOptions(options, spec.params);
  models = models || [];
  return this.modelUtils.getCollection(spec.collection, models, collectionOptions, callback);
};

/**
 * Returns an instance of Model.
 */
Fetcher.prototype.getModelForSpec = function(spec, attributes, options, callback) {
  var modelOptions = this.buildOptions(options);

  attributes = attributes || {};
  _.defaults(attributes, spec.params);

  return this.modelUtils.getModel(spec.model, attributes, modelOptions, callback);
};

/**
 * Used to hold timestamps of when 'checkFresh()' was called on a model/collection.
 * We use this to throttle it in 'shouldCheckFresh()'.
 */
Fetcher.prototype.checkedFreshTimestamps = {};

/**
 * Only once every ten seconds. Smarter?
 */
Fetcher.prototype.checkedFreshRate = 10000;

Fetcher.prototype.shouldCheckFresh = function(spec) {
  var key = this.checkedFreshKey(spec),
      timestamp = this.checkedFreshTimestamps[key];
  if (!timestamp) {
    return true;
  }
  if (new Date().getTime() - timestamp > this.checkedFreshRate) {
    return true;
  }
  return false;
};

Fetcher.prototype.didCheckFresh = function(spec) {
  var key = this.checkedFreshKey(spec);
  this.checkedFreshTimestamps[key] = new Date().getTime();
};

Fetcher.prototype.checkedFreshKey = function(spec) {
  var meta;
  meta = {
    name: spec.model || spec.collection,
    params: spec.params
  };
  return JSON.stringify(meta);
};

/**
 * map fetchSpecs to models and fetch data in parallel
 */
Fetcher.prototype._retrieve = function(fetchSpecs, options, callback) {
  var batchedRequests = {};

  _.each(fetchSpecs, function(spec, name) {
    batchedRequests[name] = function(cb) {
      var model;

      if (!options.readFromCache) {
        this.fetchFromApi(spec, options, cb);
      } else {
        model = null;

        // First, see if we have stored the model or collection.
        if (spec.model != null) {

          this._retrieveModel(spec, function(err, model) {
            this._refreshData(spec, model, options, cb);
          }.bind(this));

        } else if (spec.collection != null) {
          this.collectionStore.get(spec.collection, spec.params, function(collection) {
            this._refreshData(spec, collection, options, cb);
          }.bind(this));

        }

      }
    }.bind(this);
  }, this);
  async.parallel(batchedRequests, callback);
};

Fetcher.prototype._refreshData = function(spec, modelOrCollection, options, cb) {

  // If we found the model/collection in the store, then return that.
  if (!this.needsFetch(modelOrCollection, spec)) {
    /**
     * If 'checkFresh' is set (and we're in the client), then before we
     * return the cached object we fire off a fetch, compare the results,
     * and if the data is different, we trigger a 'refresh' event.
     */
    if (spec.checkFresh && !isServer && this.shouldCheckFresh(spec)) {
      modelOrCollection.checkFresh();
      this.didCheckFresh(spec);
    }

    cb(null, modelOrCollection);
  } else {
    /**
     * Else, fetch anew.
     */
    this.fetchFromApi(spec, options, cb);
  }
}

Fetcher.prototype._retrieveModel = function(spec, callback) {
  var fetcher = this;

  // Attempt to fetch from the modelStore based on the idAttribute
  this.modelUtils.modelIdAttribute(spec.model, function(idAttribute) {
    var model = fetcher.modelStore.get(spec.model, spec.params[idAttribute]);
    if (model) return callback(null, model);

    // if there are no other keys than the id in the params, return null;
    if (_.isEmpty(_.omit(spec.params, idAttribute)))
      return callback(null, null);

    // Attempt to fetch the model in the modelStore based on the other params
    return callback(null, fetcher.modelStore.find(spec.model, spec.params));
  });
};

Fetcher.prototype.needsFetch = function(modelOrCollection, spec) {
  if (modelOrCollection == null) return true;

  if (this.modelUtils.isModel(modelOrCollection) && this.isMissingKeys(modelOrCollection.attributes, spec.ensureKeys)) {
    return true;
  }

  if (spec.needsFetch === true) return true;
  if (typeof spec.needsFetch === 'function' && spec.needsFetch(modelOrCollection)) return true;
  return false;
};

Fetcher.prototype.isMissingKeys = function(modelData, keys) {
  var key;

  if (keys == null) {
    return false;
  }

  if (!_.isArray(keys)) {
    keys = [keys];
  }

  for (var i = 0, len = keys.length; i < len; i++) {
    key = keys[i];
    if (modelData[key] == null) {
      return true;
    }
  }
  return false;
};

Fetcher.prototype.fetchFromApi = function(spec, options, callback) {
  var model = this.getModelOrCollectionForSpec(spec),
      fetcher = this;

  model.fetch({
    headers: options.headers || {},
    data: spec.params,
    success: function(model, body) {
      callback(null, model);
    },
    error: function(model, resp, options) {
      var body, respOutput, err;

      body = resp.body;
      resp.body = typeof body === 'string' ? body.slice(0, 150) : body;
      respOutput = JSON.stringify(resp);
      err = new Error("ERROR fetching model '" + fetcher.modelUtils.modelName(model.constructor) + "' with options '" + JSON.stringify(options) + "'. Response: " + respOutput);
      err.status = resp.status;
      err.body = body;
      callback(err);
    }
  });
};

Fetcher.prototype.retrieveModelsForCollectionName = function(collectionName, modelIds) {
  var modelName = this.modelUtils.getModelNameForCollectionName(collectionName);
  return this.retrieveModels(modelName, modelIds);
};

Fetcher.prototype.retrieveModels = function(modelName, modelIds) {
  return modelIds.map(function(id) {
    return this.modelStore.get(modelName, id);
  }, this);
};

Fetcher.prototype.summarize = function(modelOrCollection) {
  var summary = {},
      idAttribute;

  if (this.modelUtils.isCollection(modelOrCollection)) {
    idAttribute = modelOrCollection.model.prototype.idAttribute;
    summary = {
      collection: this.modelUtils.modelName(modelOrCollection.constructor),
      ids: modelOrCollection.pluck(idAttribute),
      params: modelOrCollection.params,
      meta: modelOrCollection.meta
    };
  } else if (this.modelUtils.isModel(modelOrCollection)) {
    idAttribute = modelOrCollection.idAttribute;
    summary = {
      model: this.modelUtils.modelName(modelOrCollection.constructor),
      id: modelOrCollection.get(idAttribute)
    };
  }
  return summary;
};

Fetcher.prototype.storeResults = function(results) {
  _.each(results, function(modelOrCollection) {
    modelOrCollection.store();
  });
};

Fetcher.prototype.bootstrapData = function(modelMap) {
  var results = {},
      fetcher = this;

  async.forEach(_.keys(modelMap), function(name, cb) {
    var map = modelMap[name];
    fetcher.getModelOrCollectionForSpec(map.summary, map.data, _.pick(map.summary, 'params', 'meta'), function(modelOrCollection) {
      results[name] = modelOrCollection;
      cb(null);
    });
  }, function(err) {
    fetcher.storeResults(results);
  });
};

Fetcher.prototype.hydrate = function(summaries, options, callback) {
  var results = {},
      fetcher = this;

  /**
   * Support both (summaries, options, callback)
   * and (summaries, callback).
   */
  if (arguments.length === 2) {
    callback = options;
    options = {};
  } else {
    options = options || {};
  }

  async.forEach(_.keys(summaries), function(name, cb) {
    var summary = summaries[name];
    if (summary.model != null) {
      results[name] = fetcher.modelStore.get(summary.model, summary.id);

      if ((results[name] != null) && (options.app != null)) {
        results[name].app = options.app;
      }

      cb(null);

    } else if (summary.collection != null) {
      // Also support getting all models for a collection.
      fetcher.collectionStore.get(summary.collection, summary.params, function(collection) {
        if (collection == null) {
          throw new Error("Collection of type \"" + summary.collection + "\" not found for params: " + JSON.stringify(summary.params));
        }

        results[name] = collection;

        cb(null);
      });
    }
  }, function(err) {
    callback(err, results);
  });
};

Fetcher.prototype.pendingFetches = 0;

Fetcher.prototype.fetch = function(fetchSpecs, options, callback) {
  var fetcher = this;

  /**
   * Support both (fetchSpecs, options, callback)
   * and (fetchSpecs, callback).
   */
  if (arguments.length === 2) {
    callback = options;
    options = {};
  } else {
    options = options || {};
  }

  // Different defaults for client v server.
  if (isServer) {
    if (options.readFromCache == null) {
      options.readFromCache = false;
    }
    if (options.writeToCache == null) {
      options.writeToCache = false;
    }
  } else {
    if (options.readFromCache == null) {
      options.readFromCache = true;
    }
    if (options.writeToCache == null) {
      options.writeToCache = true;
    }
  }

  this.pendingFetches++;
  this.trigger('fetch:start', fetchSpecs);
  this._retrieve(fetchSpecs, options, function(err, results) {
    fetcher.pendingFetches--;
    fetcher.trigger('fetch:end', fetchSpecs, err, results);
    if (err) return callback(err);
    if (options.writeToCache) {
      fetcher.storeResults(results);
    }
    callback(null, results);
  });
};

// Mixin Backbone.Events for events that work in client & server.
_.extend(Fetcher.prototype, Backbone.Events);
<commit_msg>Added ability to overwrite readFromCache for an individual spec<commit_after>var _ = require('underscore'),
    Backbone = require('backbone'),
    async = require('async'),
    ModelStore = require('./store/model_store'),
    CollectionStore = require('./store/collection_store'),
    isServer = (typeof window === 'undefined');

if (!isServer) {
  Backbone.$ = window.$ || require('jquery');
}

module.exports = Fetcher;

function Fetcher(options) {
  this.options = options;
  this.app = this.options.app;
  this.modelUtils = this.app.modelUtils;
  this.modelStore = new ModelStore({
    app: this.app,
    modelUtils: this.modelUtils
  });
  this.collectionStore = new CollectionStore({
    app: this.app,
    modelUtils: this.modelUtils
  });
}

Fetcher.prototype.buildOptions = function(additionalOptions, params) {
  var options = {app: this.app, parse: true};
  _.defaults(options, additionalOptions);
  _.defaults(options, params);
  return options;
};

/**
 * Returns an instance of Model or Collection.
 */
Fetcher.prototype.getModelOrCollectionForSpec = function(spec, attrsOrModels, options, callback) {
  if (spec.model) {
    return this.getModelForSpec(spec, attrsOrModels, options, callback);
  } else {
    return this.getCollectionForSpec(spec, attrsOrModels, options, callback);
  }
};

/**
 * Returns an instance of Collection.
 */
Fetcher.prototype.getCollectionForSpec = function(spec, models, options, callback) {
  var collectionOptions = this.buildOptions(options, spec.params);
  models = models || [];
  return this.modelUtils.getCollection(spec.collection, models, collectionOptions, callback);
};

/**
 * Returns an instance of Model.
 */
Fetcher.prototype.getModelForSpec = function(spec, attributes, options, callback) {
  var modelOptions = this.buildOptions(options);

  attributes = attributes || {};
  _.defaults(attributes, spec.params);

  return this.modelUtils.getModel(spec.model, attributes, modelOptions, callback);
};

/**
 * Used to hold timestamps of when 'checkFresh()' was called on a model/collection.
 * We use this to throttle it in 'shouldCheckFresh()'.
 */
Fetcher.prototype.checkedFreshTimestamps = {};

/**
 * Only once every ten seconds. Smarter?
 */
Fetcher.prototype.checkedFreshRate = 10000;

Fetcher.prototype.shouldCheckFresh = function(spec) {
  var key = this.checkedFreshKey(spec),
      timestamp = this.checkedFreshTimestamps[key];
  if (!timestamp) {
    return true;
  }
  if (new Date().getTime() - timestamp > this.checkedFreshRate) {
    return true;
  }
  return false;
};

Fetcher.prototype.didCheckFresh = function(spec) {
  var key = this.checkedFreshKey(spec);
  this.checkedFreshTimestamps[key] = new Date().getTime();
};

Fetcher.prototype.checkedFreshKey = function(spec) {
  var meta;
  meta = {
    name: spec.model || spec.collection,
    params: spec.params
  };
  return JSON.stringify(meta);
};

/**
 * map fetchSpecs to models and fetch data in parallel
 */
Fetcher.prototype._retrieve = function(fetchSpecs, options, callback) {
  var batchedRequests = {};

  _.each(fetchSpecs, function(spec, name) {
    batchedRequests[name] = function(cb) {
      var model;

      var readFromCache = options.readFromCache;

      // If present, the individual spec can overwrite the fetchSpec.
      if (!_.isUndefined(spec.readFromCache) && !_.isNull(spec.readFromCache)) {
        readFromCache = spec.readFromCache;
      }

      if (!readFromCache) {
        this.fetchFromApi(spec, options, cb);
      } else {
        model = null;

        // First, see if we have stored the model or collection.
        if (spec.model != null) {

          this._retrieveModel(spec, function(err, model) {
            this._refreshData(spec, model, options, cb);
          }.bind(this));

        } else if (spec.collection != null) {
          this.collectionStore.get(spec.collection, spec.params, function(collection) {
            this._refreshData(spec, collection, options, cb);
          }.bind(this));

        }

      }
    }.bind(this);
  }, this);
  async.parallel(batchedRequests, callback);
};

Fetcher.prototype._refreshData = function(spec, modelOrCollection, options, cb) {

  // If we found the model/collection in the store, then return that.
  if (!this.needsFetch(modelOrCollection, spec)) {
    /**
     * If 'checkFresh' is set (and we're in the client), then before we
     * return the cached object we fire off a fetch, compare the results,
     * and if the data is different, we trigger a 'refresh' event.
     */
    if (spec.checkFresh && !isServer && this.shouldCheckFresh(spec)) {
      modelOrCollection.checkFresh();
      this.didCheckFresh(spec);
    }

    cb(null, modelOrCollection);
  } else {
    /**
     * Else, fetch anew.
     */
    this.fetchFromApi(spec, options, cb);
  }
}

Fetcher.prototype._retrieveModel = function(spec, callback) {
  var fetcher = this;

  // Attempt to fetch from the modelStore based on the idAttribute
  this.modelUtils.modelIdAttribute(spec.model, function(idAttribute) {
    var model = fetcher.modelStore.get(spec.model, spec.params[idAttribute]);
    if (model) return callback(null, model);

    // if there are no other keys than the id in the params, return null;
    if (_.isEmpty(_.omit(spec.params, idAttribute)))
      return callback(null, null);

    // Attempt to fetch the model in the modelStore based on the other params
    return callback(null, fetcher.modelStore.find(spec.model, spec.params));
  });
};

Fetcher.prototype.needsFetch = function(modelOrCollection, spec) {
  if (modelOrCollection == null) return true;

  if (this.modelUtils.isModel(modelOrCollection) && this.isMissingKeys(modelOrCollection.attributes, spec.ensureKeys)) {
    return true;
  }

  if (spec.needsFetch === true) return true;
  if (typeof spec.needsFetch === 'function' && spec.needsFetch(modelOrCollection)) return true;
  return false;
};

Fetcher.prototype.isMissingKeys = function(modelData, keys) {
  var key;

  if (keys == null) {
    return false;
  }

  if (!_.isArray(keys)) {
    keys = [keys];
  }

  for (var i = 0, len = keys.length; i < len; i++) {
    key = keys[i];
    if (modelData[key] == null) {
      return true;
    }
  }
  return false;
};

Fetcher.prototype.fetchFromApi = function(spec, options, callback) {
  var model = this.getModelOrCollectionForSpec(spec),
      fetcher = this;

  model.fetch({
    headers: options.headers || {},
    data: spec.params,
    success: function(model, body) {
      callback(null, model);
    },
    error: function(model, resp, options) {
      var body, respOutput, err;

      body = resp.body;
      resp.body = typeof body === 'string' ? body.slice(0, 150) : body;
      respOutput = JSON.stringify(resp);
      err = new Error("ERROR fetching model '" + fetcher.modelUtils.modelName(model.constructor) + "' with options '" + JSON.stringify(options) + "'. Response: " + respOutput);
      err.status = resp.status;
      err.body = body;
      callback(err);
    }
  });
};

Fetcher.prototype.retrieveModelsForCollectionName = function(collectionName, modelIds) {
  var modelName = this.modelUtils.getModelNameForCollectionName(collectionName);
  return this.retrieveModels(modelName, modelIds);
};

Fetcher.prototype.retrieveModels = function(modelName, modelIds) {
  return modelIds.map(function(id) {
    return this.modelStore.get(modelName, id);
  }, this);
};

Fetcher.prototype.summarize = function(modelOrCollection) {
  var summary = {},
      idAttribute;

  if (this.modelUtils.isCollection(modelOrCollection)) {
    idAttribute = modelOrCollection.model.prototype.idAttribute;
    summary = {
      collection: this.modelUtils.modelName(modelOrCollection.constructor),
      ids: modelOrCollection.pluck(idAttribute),
      params: modelOrCollection.params,
      meta: modelOrCollection.meta
    };
  } else if (this.modelUtils.isModel(modelOrCollection)) {
    idAttribute = modelOrCollection.idAttribute;
    summary = {
      model: this.modelUtils.modelName(modelOrCollection.constructor),
      id: modelOrCollection.get(idAttribute)
    };
  }
  return summary;
};

Fetcher.prototype.storeResults = function(results) {
  _.each(results, function(modelOrCollection) {
    modelOrCollection.store();
  });
};

Fetcher.prototype.bootstrapData = function(modelMap) {
  var results = {},
      fetcher = this;

  async.forEach(_.keys(modelMap), function(name, cb) {
    var map = modelMap[name];
    fetcher.getModelOrCollectionForSpec(map.summary, map.data, _.pick(map.summary, 'params', 'meta'), function(modelOrCollection) {
      results[name] = modelOrCollection;
      cb(null);
    });
  }, function(err) {
    fetcher.storeResults(results);
  });
};

Fetcher.prototype.hydrate = function(summaries, options, callback) {
  var results = {},
      fetcher = this;

  /**
   * Support both (summaries, options, callback)
   * and (summaries, callback).
   */
  if (arguments.length === 2) {
    callback = options;
    options = {};
  } else {
    options = options || {};
  }

  async.forEach(_.keys(summaries), function(name, cb) {
    var summary = summaries[name];
    if (summary.model != null) {
      results[name] = fetcher.modelStore.get(summary.model, summary.id);

      if ((results[name] != null) && (options.app != null)) {
        results[name].app = options.app;
      }

      cb(null);

    } else if (summary.collection != null) {
      // Also support getting all models for a collection.
      fetcher.collectionStore.get(summary.collection, summary.params, function(collection) {
        if (collection == null) {
          throw new Error("Collection of type \"" + summary.collection + "\" not found for params: " + JSON.stringify(summary.params));
        }

        results[name] = collection;

        cb(null);
      });
    }
  }, function(err) {
    callback(err, results);
  });
};

Fetcher.prototype.pendingFetches = 0;

Fetcher.prototype.fetch = function(fetchSpecs, options, callback) {
  var fetcher = this;

  /**
   * Support both (fetchSpecs, options, callback)
   * and (fetchSpecs, callback).
   */
  if (arguments.length === 2) {
    callback = options;
    options = {};
  } else {
    options = options || {};
  }

  // Different defaults for client v server.
  if (isServer) {
    if (options.readFromCache == null) {
      options.readFromCache = false;
    }
    if (options.writeToCache == null) {
      options.writeToCache = false;
    }
  } else {
    if (options.readFromCache == null) {
      options.readFromCache = true;
    }
    if (options.writeToCache == null) {
      options.writeToCache = true;
    }
  }

  this.pendingFetches++;
  this.trigger('fetch:start', fetchSpecs);
  this._retrieve(fetchSpecs, options, function(err, results) {
    fetcher.pendingFetches--;
    fetcher.trigger('fetch:end', fetchSpecs, err, results);
    if (err) return callback(err);
    if (options.writeToCache) {
      fetcher.storeResults(results);
    }
    callback(null, results);
  });
};

// Mixin Backbone.Events for events that work in client & server.
_.extend(Fetcher.prototype, Backbone.Events);
<|endoftext|>