<commit_before>import React from "react";
import { Dimensions } from "react-native";

import _ from "lodash";

import DateFns from "./DateFns";

import DateSliderComponent from "./DateSliderComponent";

const END_THRESHOLD = 4;
const EXTEND_WEEKS_BY = 8;

class DateSlider extends React.Component {
  static propTypes = {
    isoStartDay: React.PropTypes.number,
    locale: React.PropTypes.object,
    onDateSelected: React.PropTypes.func,
    onWeekChanged: React.PropTypes.func,
    selectedDate: React.PropTypes.object
  };

  static defaultProps = {
    isoStartDay: 1
  };

  flatlist = undefined;
  selectedIndex = 0;
  viewableIndex = -1;
  isExpanding = false;

  constructor(props) {
    super(props);

    if (props.locale && props.locale.name && props.locale.config) {
      DateFns.initLocale(props.locale.name, props.locale.config);
    }

    let selectedDate;
    if (this.props.selectedDate) {
      selectedDate = DateFns.initMoment(this.props.selectedDate, props.locale);
    } else {
      selectedDate = DateFns.initMoment(new Date(), props.locale);
    }

    // The very first date in the FlatList
    const startingDate = DateFns.getStartOfWeek(
      selectedDate,
      props.isoStartDay
    ).subtract(EXTEND_WEEKS_BY, "weeks");

    this.state = {
      viewWidth: Dimensions.get("window").width,
      arrDates: this._generateDates(
        startingDate,
        EXTEND_WEEKS_BY * 2 + 1,
        selectedDate,
        props.isoStartDay
      ),
      selectedDate: selectedDate
    };
  }

  componentWillReceiveProps(nextProps) {
    if (
      nextProps.locale &&
      nextProps != this.props.locale &&
      nextProps.locale.name &&
      nextProps.locale.config
    ) {
      DateFns.initLocale(nextProps.locale.name, nextProps.locale.config);
    }

    if (
      nextProps.selectedDate &&
      !this.state.selectedDate.isSame(nextProps.selectedDate, "day")
    ) {
      const selectedDate = DateFns.initMoment(
        nextProps.selectedDate,
        nextProps.locale
      );

      if (
        selectedDate.isBefore(this.state.arrDates[0].date) ||
        selectedDate.isAfter(_.last(this.state.arrDates).date.add(6, "days"))
      ) {
        // We need to regenerate the dates
        // The very first date in the FlatList
        const startingDate = DateFns.getStartOfWeek(
          selectedDate,
          nextProps.isoStartDay
        ).subtract(EXTEND_WEEKS_BY, "weeks");

        this.setState({
          arrDates: this._generateDates(
            startingDate,
            EXTEND_WEEKS_BY * 2 + 1,
            selectedDate,
            nextProps.isoStartDay
          ),
          selectedDate: selectedDate
        });
      } else {
        // just set it in the current set of arrDates
        this._setSelectedDate(selectedDate);
      }

      this._scrollTo(this.selectedIndex);
    }
  }

  render() {
    return (
      <DateSliderComponent
        arrDates={this.state.arrDates}
        captureFlatListRef={this._captureFlatListRef}
        viewWidth={this.state.viewWidth}
        getItemLayout={this._getItemLayout}
        onDateSelected={this._onDateSelected}
        onEndReached={this._onEndReached}
        onEndReachedThreshold={END_THRESHOLD}
        onLayout={this._onLayout}
        onViewableItemsChanged={this._onViewableItemsChanged}
      />
    );
  }

  /**
   * Pure function to generate an array of objects that can be used by the FlatList.
   *
   * The FlatList renders a 'week' as a new item. This function
   * creates a number of entries equal to the weeks parameter
   * and starting from the fromDate paramater. The fromDate
   * needs to be set to the correct weekday used to start each WeekView
   * by the callee.
   *
   * @param {moment} fromDate
   *  The starting date for the array of weeks to be generated
   * @param {number} weeks
   *  The number of weeks to be generated
   * @param {moment} selectedDate
   *  The date that is current selected
   * @param {number} isoStartDay
   *  The ISOWeekday number that starts the week being displayed (1 - 7)
   * @returns {Array<Object>}
   *
   * @memberof DateSlider
   */
  _generateDates(fromDate, weeks, selectedDate, isoStartDay) {
    const startOfSelectedWeek = DateFns.getStartOfWeek(
      selectedDate,
      isoStartDay
    );

    return _.map(_.range(weeks), i => {
      const weekStartDate = fromDate.clone().add(i, "weeks");
      if (startOfSelectedWeek.isSame(weekStartDate, "day")) {
        this.selectedIndex = i;
      }
      return {
        key: weekStartDate.toISOString(),
        date: weekStartDate,
        arrSelected: this._generateArrSelected(
          weekStartDate,
          startOfSelectedWeek,
          selectedDate,
          isoStartDay
        )
      };
    });
  }

  /**
   * A pure function to generate an array of boolean values
   * that specify if one of the days of the week is selected.
   *
   * This array is to be used as a prop on the WeekView component.
   *
   * @param {moment} weekStartDate
   *  The start date of the week this is to be generated
   * @param {moment} startOfSelectedWeek
   *  The start of the week that contains the selectedDate
   * @param {moment} selectedDate
   *  The date that is selected
   * @param {number} isoStartDay
   *  The ISOWeekday number that starts the week being displayed (1 - 7)
   *
   * @returns {Array<boolean>}
   *
   * @memberof DateSlider
   */
  _generateArrSelected(
    weekStartDate,
    startOfSelectedWeek,
    selectedDate,
    isoStartDay
  ) {
    let result = [false, false, false, false, false, false, false];
    if (weekStartDate && weekStartDate.isSame(startOfSelectedWeek, "day")) {
      result[DateFns.getSelectedDayOfWeek(selectedDate, isoStartDay)] = true;
    }
    return result;
  }

  /**
   * Captures the FlatList instance ref, so that
   * the scrollTo method can be called on it.
   * This is populated from the DateSliderComponent.
   *
   * @param {FlatList} flatlist
   *   The FlatList instance
   *
   * @memberof DateSlider
   */
  _captureFlatListRef = flatlist => (this.flatlist = flatlist);

  /**
   * ReactNative method called by the FlatList
   * This needs to be here as it uses viewWidth held in state
   *
   * @param {Object} data
   * @param {number} index
   *
   * @memberof DateSlider
   */
  _getItemLayout = (data, index) => {
    return {
      length: this.state.viewWidth,
      offset: this.state.viewWidth * index,
      index
    };
  };

  /**
   * ReactNative method called by the view containing the FlatList
   * This needs to be here as handles setting the viewWidth
   * based on what the containing view reports.
   *
   * @param {Object} event
   *  The event generated from the RN onLayout call
   *
   * @memberof DateSlider
   */
  _onLayout = event => {
    const nativeEvent = event.nativeEvent;
    this.setState({ viewWidth: nativeEvent.layout.width });
    if (this.viewableIndex === -1) {
      this._scrollTo(this.selectedIndex);
    } else {
      this._scrollTo(this.viewableIndex);
    }
  };

  /**
   * Scrolls the flatlist to the specified index.
   * This wraps the call in a setTimeout(_,0) Promise
   * to stop issues when the index to be scrolled to has not yet been
   * rendered.
   *
   * @param {number} index
   *  The index to scroll the flatlist to
   *
   * @memberof DateSlider
   */
  _scrollTo = index => {
    const scrollToPromise = new Promise(resolve => setTimeout(resolve, 0));
    scrollToPromise.then(() => {
      this.flatlist.scrollToIndex({
        viewPosition: 0,
        index: index,
        animated: false
      });
    });
  };

  _onEndReached = () => {
    this.isExpanding = true;
    const lastDate = this.state.arrDates[this.state.arrDates.length - 1].date;
    const appendWeeksFrom = lastDate.clone().add(1, "weeks");
    const arrNewDates = this._generateDates(
      appendWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate,
      this.props.isoStartDay
    );
    this.setState({ arrDates: [...this.state.arrDates, ...arrNewDates] });
    this.isExpanding = false;
  };

  _onStartReached = () => {
    const firstDate = this.state.arrDates[0].date;
    const prependWeeksFrom = firstDate
      .clone()
      .subtract(EXTEND_WEEKS_BY, "weeks");
    const arrNewDates = this._generateDates(
      prependWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate,
      this.props.isoStartDay
    );
    this.setState({ arrDates: [...arrNewDates, ...this.state.arrDates] });
    this.selectedIndex = this.selectedIndex + EXTEND_WEEKS_BY;
    this._scrollTo(END_THRESHOLD - 1 + EXTEND_WEEKS_BY);
    this.isExpanding = false;
  };

  _onViewableItemsChanged = ({ viewableItems, changed }) => {
    if (viewableItems.length === 1) {
      const viewableIndex = viewableItems[0].index;
      const changedIndex = changed[0].index;
      this.viewableIndex = viewableIndex;

      if (changedIndex === viewableIndex - 1) {
        // Week forward
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (changedIndex === viewableIndex + 1) {
        // week back
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (changedIndex <= END_THRESHOLD && viewableIndex === changedIndex - 1) {
        if (!this.isExpanding) {
          this.isExpanding = true;
          this._onStartReached();
        }
      }
    }
  };

  _onDateSelected = date => {
    this._setSelectedDate(date);
    this.props.onDateSelected(date);
  };

  _setSelectedDate = date => {
    const startOfSelectedWeek = DateFns.getStartOfWeek(
      date,
      this.props.isoStartDay
    );

    let arrDates = [...this.state.arrDates];

    const prevSelectedIndex = this.selectedIndex;

    const selectedIndex = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    arrDates[prevSelectedIndex].arrSelected = this._generateArrSelected();
    arrDates[selectedIndex].arrSelected = this._generateArrSelected(
      arrDates[selectedIndex].date,
      startOfSelectedWeek,
      date,
      this.props.isoStartDay
    );

    this.selectedIndex = selectedIndex;

    this.setState({
      arrDates: arrDates,
      selectedDate: date
    });
  };
}

export default DateSlider;
<commit_msg>Stop setSelectedDate from running if selectedIndex is -1<commit_after>import React from "react";
import { Dimensions } from "react-native";

import _ from "lodash";

import DateFns from "./DateFns";

import DateSliderComponent from "./DateSliderComponent";

const END_THRESHOLD = 4;
const EXTEND_WEEKS_BY = 8;

class DateSlider extends React.Component {
  static propTypes = {
    isoStartDay: React.PropTypes.number,
    locale: React.PropTypes.object,
    onDateSelected: React.PropTypes.func,
    onWeekChanged: React.PropTypes.func,
    selectedDate: React.PropTypes.object
  };

  static defaultProps = {
    isoStartDay: 1
  };

  flatlist = undefined;
  selectedIndex = 0;
  viewableIndex = -1;
  isExpanding = false;

  constructor(props) {
    super(props);

    if (props.locale && props.locale.name && props.locale.config) {
      DateFns.initLocale(props.locale.name, props.locale.config);
    }

    let selectedDate;
    if (this.props.selectedDate) {
      selectedDate = DateFns.initMoment(this.props.selectedDate, props.locale);
    } else {
      selectedDate = DateFns.initMoment(new Date(), props.locale);
    }

    // The very first date in the FlatList
    const startingDate = DateFns.getStartOfWeek(
      selectedDate,
      props.isoStartDay
    ).subtract(EXTEND_WEEKS_BY, "weeks");

    this.state = {
      viewWidth: Dimensions.get("window").width,
      arrDates: this._generateDates(
        startingDate,
        EXTEND_WEEKS_BY * 2 + 1,
        selectedDate,
        props.isoStartDay
      ),
      selectedDate: selectedDate
    };
  }

  componentWillReceiveProps(nextProps) {
    if (
      nextProps.locale &&
      nextProps != this.props.locale &&
      nextProps.locale.name &&
      nextProps.locale.config
    ) {
      DateFns.initLocale(nextProps.locale.name, nextProps.locale.config);
    }

    if (
      nextProps.selectedDate &&
      !this.state.selectedDate.isSame(nextProps.selectedDate, "day")
    ) {
      const selectedDate = DateFns.initMoment(
        nextProps.selectedDate,
        nextProps.locale
      );

      if (
        selectedDate.isBefore(this.state.arrDates[0].date) ||
        selectedDate.isAfter(_.last(this.state.arrDates).date.add(6, "days"))
      ) {
        // We need to regenerate the dates
        // The very first date in the FlatList
        const startingDate = DateFns.getStartOfWeek(
          selectedDate,
          nextProps.isoStartDay
        ).subtract(EXTEND_WEEKS_BY, "weeks");

        this.setState({
          arrDates: this._generateDates(
            startingDate,
            EXTEND_WEEKS_BY * 2 + 1,
            selectedDate,
            nextProps.isoStartDay
          ),
          selectedDate: selectedDate
        });
      } else {
        // just set it in the current set of arrDates
        this._setSelectedDate(selectedDate);
      }

      this._scrollTo(this.selectedIndex);
    }
  }

  render() {
    return (
      <DateSliderComponent
        arrDates={this.state.arrDates}
        captureFlatListRef={this._captureFlatListRef}
        viewWidth={this.state.viewWidth}
        getItemLayout={this._getItemLayout}
        onDateSelected={this._onDateSelected}
        onEndReached={this._onEndReached}
        onEndReachedThreshold={END_THRESHOLD}
        onLayout={this._onLayout}
        onViewableItemsChanged={this._onViewableItemsChanged}
      />
    );
  }

  /**
   * Pure function to generate an array of objects that can be used by the FlatList.
   *
   * The FlatList renders a 'week' as a new item. This function
   * creates a number of entries equal to the weeks parameter
   * and starting from the fromDate paramater. The fromDate
   * needs to be set to the correct weekday used to start each WeekView
   * by the callee.
   *
   * @param {moment} fromDate
   *  The starting date for the array of weeks to be generated
   * @param {number} weeks
   *  The number of weeks to be generated
   * @param {moment} selectedDate
   *  The date that is current selected
   * @param {number} isoStartDay
   *  The ISOWeekday number that starts the week being displayed (1 - 7)
   * @returns {Array<Object>}
   *
   * @memberof DateSlider
   */
  _generateDates(fromDate, weeks, selectedDate, isoStartDay) {
    const startOfSelectedWeek = DateFns.getStartOfWeek(
      selectedDate,
      isoStartDay
    );

    return _.map(_.range(weeks), i => {
      const weekStartDate = fromDate.clone().add(i, "weeks");
      if (startOfSelectedWeek.isSame(weekStartDate, "day")) {
        this.selectedIndex = i;
      }
      return {
        key: weekStartDate.toISOString(),
        date: weekStartDate,
        arrSelected: this._generateArrSelected(
          weekStartDate,
          startOfSelectedWeek,
          selectedDate,
          isoStartDay
        )
      };
    });
  }

  /**
   * A pure function to generate an array of boolean values
   * that specify if one of the days of the week is selected.
   *
   * This array is to be used as a prop on the WeekView component.
   *
   * @param {moment} weekStartDate
   *  The start date of the week this is to be generated
   * @param {moment} startOfSelectedWeek
   *  The start of the week that contains the selectedDate
   * @param {moment} selectedDate
   *  The date that is selected
   * @param {number} isoStartDay
   *  The ISOWeekday number that starts the week being displayed (1 - 7)
   *
   * @returns {Array<boolean>}
   *
   * @memberof DateSlider
   */
  _generateArrSelected(
    weekStartDate,
    startOfSelectedWeek,
    selectedDate,
    isoStartDay
  ) {
    let result = [false, false, false, false, false, false, false];
    if (weekStartDate && weekStartDate.isSame(startOfSelectedWeek, "day")) {
      result[DateFns.getSelectedDayOfWeek(selectedDate, isoStartDay)] = true;
    }
    return result;
  }

  /**
   * Captures the FlatList instance ref, so that
   * the scrollTo method can be called on it.
   * This is populated from the DateSliderComponent.
   *
   * @param {FlatList} flatlist
   *   The FlatList instance
   *
   * @memberof DateSlider
   */
  _captureFlatListRef = flatlist => (this.flatlist = flatlist);

  /**
   * ReactNative method called by the FlatList
   * This needs to be here as it uses viewWidth held in state
   *
   * @param {Object} data
   * @param {number} index
   *
   * @memberof DateSlider
   */
  _getItemLayout = (data, index) => {
    return {
      length: this.state.viewWidth,
      offset: this.state.viewWidth * index,
      index
    };
  };

  /**
   * ReactNative method called by the view containing the FlatList
   * This needs to be here as handles setting the viewWidth
   * based on what the containing view reports.
   *
   * @param {Object} event
   *  The event generated from the RN onLayout call
   *
   * @memberof DateSlider
   */
  _onLayout = event => {
    const nativeEvent = event.nativeEvent;
    this.setState({ viewWidth: nativeEvent.layout.width });
    if (this.viewableIndex === -1) {
      this._scrollTo(this.selectedIndex);
    } else {
      this._scrollTo(this.viewableIndex);
    }
  };

  /**
   * Scrolls the flatlist to the specified index.
   * This wraps the call in a setTimeout(_,0) Promise
   * to stop issues when the index to be scrolled to has not yet been
   * rendered.
   *
   * @param {number} index
   *  The index to scroll the flatlist to
   *
   * @memberof DateSlider
   */
  _scrollTo = index => {
    const scrollToPromise = new Promise(resolve => setTimeout(resolve, 0));
    scrollToPromise.then(() => {
      this.flatlist.scrollToIndex({
        viewPosition: 0,
        index: index,
        animated: false
      });
    });
  };

  _onEndReached = () => {
    this.isExpanding = true;
    const lastDate = this.state.arrDates[this.state.arrDates.length - 1].date;
    const appendWeeksFrom = lastDate.clone().add(1, "weeks");
    const arrNewDates = this._generateDates(
      appendWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate,
      this.props.isoStartDay
    );
    this.setState({ arrDates: [...this.state.arrDates, ...arrNewDates] });
    this.isExpanding = false;
  };

  _onStartReached = () => {
    const firstDate = this.state.arrDates[0].date;
    const prependWeeksFrom = firstDate
      .clone()
      .subtract(EXTEND_WEEKS_BY, "weeks");
    const arrNewDates = this._generateDates(
      prependWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate,
      this.props.isoStartDay
    );
    this.setState({ arrDates: [...arrNewDates, ...this.state.arrDates] });
    this.selectedIndex = this.selectedIndex + EXTEND_WEEKS_BY;
    this._scrollTo(END_THRESHOLD - 1 + EXTEND_WEEKS_BY);
    this.isExpanding = false;
  };

  _onViewableItemsChanged = ({ viewableItems, changed }) => {
    if (viewableItems.length === 1) {
      const viewableIndex = viewableItems[0].index;
      const changedIndex = changed[0].index;
      this.viewableIndex = viewableIndex;

      if (changedIndex === viewableIndex - 1) {
        // Week forward
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (changedIndex === viewableIndex + 1) {
        // week back
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (changedIndex <= END_THRESHOLD && viewableIndex === changedIndex - 1) {
        if (!this.isExpanding) {
          this.isExpanding = true;
          this._onStartReached();
        }
      }
    }
  };

  _onDateSelected = date => {
    this._setSelectedDate(date);
    this.props.onDateSelected(date);
  };

  _setSelectedDate = date => {
    const startOfSelectedWeek = DateFns.getStartOfWeek(
      date,
      this.props.isoStartDay
    );

    let arrDates = [...this.state.arrDates];

    const prevSelectedIndex = this.selectedIndex;

    const selectedIndex = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    if (selectedIndex > 0) {
      arrDates[prevSelectedIndex].arrSelected = this._generateArrSelected();
      arrDates[selectedIndex].arrSelected = this._generateArrSelected(
        arrDates[selectedIndex].date,
        startOfSelectedWeek,
        date,
        this.props.isoStartDay
      );

      this.selectedIndex = selectedIndex;

      this.setState({
        arrDates: arrDates,
        selectedDate: date
      });
    }
  };
}

export default DateSlider;
<|endoftext|>


<commit_before>var Stream = require('stream')
// through
//
// a stream that does nothing but re-emit the input.
// useful for aggregating a series of changing but not ending streams into one stream)

exports = module.exports = through
through.through = through

exports.from = from

//create a readable writable stream.

function through (write, end) {
  write = write || function (data) { this.emit('data', data) }
  end = (
    'sync'== end || !end
  //use sync end. (default)
  ? function () { this.emit('end') }
  : 'async' == end || end === true
  //use async end.
  //must eventually call drain if paused.
  //else will not end.
  ? function () {
      if(!this.paused)
        return this.emit('end')
     var self = this
     this.once('drain', function () {
        self.emit('end')
      })
    }
  //use custom end function
  : end
  )
  var ended = false, destroyed = false
  var stream = new Stream()
  stream.readable = stream.writable = true
  stream.paused = false
  stream.write = function (data) {
    write.call(this, data)
    return !stream.paused
  }
  //this will be registered as the first 'end' listener
  //must call destroy next tick, to make sure we're after any
  //stream piped from here.
  stream.on('end', function () {
    stream.readable = false
    if(!stream.writable)
      process.nextTick(function () {
        stream.destroy()
      })
  })

  stream.end = function (data) {
    if(ended) throw new Error('cannot call end twice')
    ended = true
    if(arguments.length) stream.write(data)
    this.writable = false
    end.call(this)
    if(!this.readable)
      this.destroy()
  }
  stream.destroy = function () {
    if(destroyed) return
    destroyed = true
    ended = true
    stream.writable = stream.readable = false
    stream.emit('close')
  }
  stream.pause = function () {
    stream.paused = true
  }
  stream.resume = function () {
    if(stream.paused) {
      stream.paused = false
      stream.emit('drain')
    }
  }
  return stream
}

//create a readable stream.

function from (source) {
  if(Array.isArray(source))
    return from (function (i) {
      if(source.length)
        this.emit('data', source.shift())
      else
        this.emit('end')
      return true
    })

  var s = new Stream(), i = 0, ended = false, started = false
  s.readable = true
  s.writable = false
  s.paused = false
  s.pause = function () {
    started = true
    s.paused = true
  }
  function next () {
    var n = 0, r = false
    if(ended) return
    while(!ended && !s.paused && source.call(s, i, function () {
      if(!n++ && !s.ended && !s.paused)
          next()
    }))
      ;
  }
  s.resume = function () {
    started = true
    s.paused = false
    next()
  }
  s.on('end', function () {
    ended = true
    s.readable = false
    process.nextTick(s.destroy)
  })
  s.destroy = function () {
    ended = true
    s.emit('close')
  }
  /*
    by default, the stream will start emitting at nextTick
    if you want, you can pause it, after pipeing.
    you can also resume before next tick, and that will also
    work.
  */
  process.nextTick(function () {
    if(!started) s.resume()
  })
  return s
}

//create a writable stream.
function to () {

}
<commit_msg>remove from<commit_after>var Stream = require('stream')

// through
//
// a stream that does nothing but re-emit the input.
// useful for aggregating a series of changing but not ending streams into one stream)

exports = module.exports = through
through.through = through

//create a readable writable stream.

function through (write, end) {
  write = write || function (data) { this.emit('data', data) }
  end = (
    'sync'== end || !end
  //use sync end. (default)
  ? function () { this.emit('end') }
  : 'async' == end || end === true
  //use async end.
  //must eventually call drain if paused.
  //else will not end.
  ? function () {
      if(!this.paused)
        return this.emit('end')
     var self = this
     this.once('drain', function () {
        self.emit('end')
      })
    }
  //use custom end function
  : end
  )
  var ended = false, destroyed = false
  var stream = new Stream()
  stream.readable = stream.writable = true
  stream.paused = false
  stream.write = function (data) {
    write.call(this, data)
    return !stream.paused
  }
  //this will be registered as the first 'end' listener
  //must call destroy next tick, to make sure we're after any
  //stream piped from here.
  stream.on('end', function () {
    stream.readable = false
    if(!stream.writable)
      process.nextTick(function () {
        stream.destroy()
      })
  })

  stream.end = function (data) {
    if(ended) throw new Error('cannot call end twice')
    ended = true
    if(arguments.length) stream.write(data)
    this.writable = false
    end.call(this)
    if(!this.readable)
      this.destroy()
  }
  stream.destroy = function () {
    if(destroyed) return
    destroyed = true
    ended = true
    stream.writable = stream.readable = false
    stream.emit('close')
  }
  stream.pause = function () {
    stream.paused = true
  }
  stream.resume = function () {
    if(stream.paused) {
      stream.paused = false
      stream.emit('drain')
    }
  }
  return stream
}

<|endoftext|>


<commit_before>'use strict';
var util = require('util');

var oneDay = 86400000;

module.exports = function(connect) {

	/**
	* Connect's Store.
	*/
	var Store = (connect.session) ? connect.session.Store : connect.Store;

	/*
	* Return datastore appropriate string of the current time
	* @api private
	* @return {String | date}
	*/
	function dateAsISO(knex, aDate) {
		var date;
		if (aDate != null) {
			date = new Date(aDate);
		} else {
			date = new Date();
		}
		if(isOracle(knex)){
			return date;
		}
		return isMySQL(knex) || isMSSQL(knex) ? date.toISOString().slice(0, 19).replace('T', ' ') : date.toISOString();
	}

	/*
	* Return dialect-aware type name for timestamp
	* @return {String} type name for timestamp
	* @api private
	*/
	function timestampTypeName(knex) {
		return isMySQL(knex) || isMSSQL(knex) ? 'DATETIME' : isPostgres(knex) ? 'timestamp with time zone' : 'timestamp';
	}

	/*
	 * Return condition for filtering by expiration
	 * @return {String} expired sql condition string
	 * @api private
	 */
	function expiredCondition(knex) {
		var condition ='CAST(? as '+timestampTypeName(knex)+') <= expired';
		if(isSqlite3(knex)) { 	// sqlite3 date condition is a special case.
			condition = 'datetime(?) <= datetime(expired)';
		} else if (isOracle(knex)) {
			condition = 'CAST(? as '+timestampTypeName(knex)+') <= "expired"';
		}
		return condition;
	}

	/*
	* Returns true if the specified knex instance is using sqlite3.
	* @return {bool}
	* @api private
	*/
	function isSqlite3(knex) {
		return knex.client.dialect === 'sqlite3';
	}

	/*
	* Returns true if the specified knex instance is using mysql.
	* @return {bool}
	* @api private
	*/
	function isMySQL(knex) {
		return ['mysql', 'mariasql', 'mariadb'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using mssql.
	* @return {bool}
	* @api private
	*/
	function isMSSQL(knex) {
		return ['mssql'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using postgresql.
	* @return {bool}
	* @api private
	*/
	function isPostgres(knex) {
		return ['postgresql'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using oracle.
	* @return {bool}
	* @api private
	*/
	function isOracle(knex) {
		return ['oracle', 'oracledb'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Remove expired sessions from database.
	* @param {Object} store
	* @param {number} interval
	* @api private
	*/
	function dbCleanup(store, interval) {
		return store.ready.then(function () {
			var condition = 'expired < CAST(? as ' + timestampTypeName(store.knex) + ')';
			if(isSqlite3(store.knex)) { 	// sqlite3 date condition is a special case.
				condition = 'datetime(expired) < datetime(?)';
			} else if (isOracle(store.knex)) {
				condition = '"expired" < CAST(? as ' + timestampTypeName(store.knex) + ')';
			}
			return store.knex(store.tablename).del()
			.whereRaw(condition, dateAsISO(store.knex));
		}).finally(function() {
			setTimeout(dbCleanup, interval, store, interval).unref()
		});
	}

	/*
	* Initialize KnexStore with the given options.
	*
	* @param {Object} options
	* @api public
	*/
	function KnexStore(options) {
		var self = this;

		options = options || {};
		Store.call(self, options);

		if (!options.clearInterval) {
			// Time to run clear expired function.
			options.clearInterval =  60000;
		}

		self.createtable = options.hasOwnProperty('createtable') ? options.createtable :  true;
		self.tablename = options.tablename || 'sessions';
		self.sidfieldname = options.sidfieldname || 'sid';
		self.knex = options.knex || require('knex')({
			client: 'sqlite3',
			// debug: true,
			connection: {
				filename: "connect-session-knex.sqlite"
			}
		});

		self.ready = self.knex.schema.hasTable(self.tablename)
		.then(function (exists) {
			if (!exists && self.createtable) {
				return self.knex.schema.createTable(self.tablename, function (table) {
					table.string(self.sidfieldname).primary();
					if (isMSSQL(self.knex)) {
						table.text('sess').notNullable();
					} else {
						table.json('sess').notNullable();
					}
					if (isMySQL(self.knex) || isMSSQL(self.knex)) {
						table.dateTime('expired').notNullable().index();
					} else {
						table.timestamp('expired').notNullable().index();
					}
				});
			}
			return exists;
		})
		.then(function (exists) {
			if (exists) {
				dbCleanup(self, options.clearInterval);
			}
			return null;
		});
	}

	// KnexStore.prototype.__proto__ = Store.prototype;
	util.inherits(KnexStore, Store);

	/*
	* Attempt to fetch session by the given sid.
	*
	* @param {String} sid
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.get = function(sid, fn) {
		var self = this;
		return self.ready.then(function () {
			var condition = expiredCondition(self.knex);
			return self.knex
			.select('sess')
			.from(self.tablename)
			.where(self.sidfieldname, '=', sid)
			.andWhereRaw(condition, dateAsISO(self.knex))
			.then(function (response) {
				var ret;
				if (response[0]) {
					ret = response[0].sess;
					if (typeof ret === "string") {
						ret = JSON.parse(ret);
					}
				}
				return ret;
			})
			.asCallback(fn)
		});
	};

	/*
	* Commit the given `sess` object associated with the given `sid`.
	*
	* @param {String} sid
	* @param {Session} sess
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.set = function(sid, sess, fn) {
		var self = this;
		var maxAge = sess.cookie.maxAge;
		var now = new Date().getTime();
		var expired = maxAge ? now + maxAge : now + oneDay;
		sess = JSON.stringify(sess);
		var postgresfastq = 'with new_values (' + self.sidfieldname + ', expired, sess) as (' +
		'  values (?, ?::timestamp with time zone, ?::json)' +
		'), ' +
		'upsert as ' +
		'( ' +
		'  update ' + self.tablename + ' cs set ' +
		'    ' + self.sidfieldname + ' = nv.' + self.sidfieldname + ', ' +
		'    expired = nv.expired, ' +
		'    sess = nv.sess ' +
		'  from new_values nv ' +
		'  where cs.' + self.sidfieldname + ' = nv.' + self.sidfieldname + ' ' +
		'  returning cs.* ' +
		')' +
		'insert into ' + self.tablename + ' (' + self.sidfieldname + ', expired, sess) ' +
		'select ' + self.sidfieldname + ', expired, sess ' +
		'from new_values ' +
		'where not exists (select 1 from upsert up where up.' + self.sidfieldname + ' = new_values.' + self.sidfieldname + ')';

		var sqlitefastq = 'insert or replace into ' + self.tablename + ' (' + self.sidfieldname + ', expired, sess) values (?, ?, ?);';

		var mysqlfastq = 'insert into ' + self.tablename + ' (' + self.sidfieldname + ', expired, sess) values (?, ?, ?) on duplicate key update expired=values(expired), sess=values(sess);';

		var mssqlfastq = 'merge ' + self.tablename + ' as T ' +
		  'using (values (?, ?, ?)) as S (' + self.sidfieldname + ', expired, sess) ' +
		  'on (T.' + self.sidfieldname + ' = S.' + self.sidfieldname + ') ' +
		  'when matched then ' +
		    'update set expired = S.expired, sess = S.sess ' +
			'when not matched by target then ' +
		  'insert (' + self.sidfieldname + ', expired, sess) values (S.' + self.sidfieldname + ', S.expired, S.sess) ' +
		  'output inserted.*;';

		var dbDate = dateAsISO(self.knex, expired);

		if (isSqlite3(self.knex)) {
			// sqlite optimized query
			return self.ready.then(function () {
				return self.knex.raw(sqlitefastq, [sid, dbDate, sess ])
				.then(function (result) {
					return [1];
				})
				.asCallback(fn);
			});
		} else if (isPostgres(self.knex) && parseFloat(self.knex.client.version) >= 9.2) {
			// postgresql optimized query
			return self.ready.then(function () {
				return self.knex.raw(postgresfastq, [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else if (isMySQL(self.knex)) {
			// mysql/mariaDB optimized query
			return self.ready.then(function () {
				return self.knex.raw(mysqlfastq, [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else if (isMSSQL(self.knex)) {
			// mysql/mariaDB optimized query
			return self.ready.then(function () {
				return self.knex.raw(mssqlfastq, [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else {
			return self.ready.then(function () {
				return self.knex.transaction(function (trx) {
					return trx.select('*')
					.forUpdate()
					.from(self.tablename)
					.where(self.sidfieldname, '=', sid)
					.then(function (foundKeys) {
						if (foundKeys.length === 0) {
							return trx.from(self.tablename)
							.insert({
								[self.sidfieldname]: sid,
								expired: dbDate,
								sess: sess
							});
						} else {
							return trx(self.tablename)
							.where(self.sidfieldname, '=', sid)
							.update({
								expired: dbDate,
								sess: sess
							});
						}
					});
				})
				.asCallback(fn)
			});
		}
	};

	/**
	 * Touch the given session object associated with the given session ID.
	 *
	 * @param {String} sid
	 * @param {Session} sess
	 * @param {Function} fn
	 * @public
	 */
	KnexStore.prototype.touch = function(sid, sess, fn) {
		if (sess && sess.cookie && sess.cookie.expires) {
			var condition = expiredCondition(this.knex);

			return this.knex(this.tablename)
				.where(this.sidfieldname, '=', sid)
				.andWhereRaw(condition, dateAsISO(this.knex))
				.update({
					expired: dateAsISO(this.knex, sess.cookie.expires)
				})
				.asCallback(fn);
		}

		fn();
	};

	/*
	* Destroy the session associated with the given `sid`.
	*
	* @param {String} sid
	* @api public
	*/
	KnexStore.prototype.destroy = function(sid, fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.del()
			.from(self.tablename)
			.where(self.sidfieldname, '=', sid)
			.asCallback(fn)
		});
	};

	/*
	* Fetch number of sessions.
	*
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.length = function(fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.count(self.sidfieldname + ' as count')
			.from(self.tablename)
			.then(function (response) {
				return response[0].count | 0;
			})
			.asCallback(fn)
		})
	};

	/*
	* Clear all sessions.
	*
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.clear = function(fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.del()
			.from(self.tablename)
			.asCallback(fn)
		});
	};

	return KnexStore;

};
<commit_msg>Moved the upsert queries into their own functions (minor performance gain).<commit_after>'use strict';
var util = require('util');

var oneDay = 86400000;

module.exports = function(connect) {

	/**
	* Connect's Store.
	*/
	var Store = (connect.session) ? connect.session.Store : connect.Store;

	/*
	* Return datastore appropriate string of the current time
	* @api private
	* @return {String | date}
	*/
	function dateAsISO(knex, aDate) {
		var date;
		if (aDate != null) {
			date = new Date(aDate);
		} else {
			date = new Date();
		}
		if(isOracle(knex)){
			return date;
		}
		return isMySQL(knex) || isMSSQL(knex) ? date.toISOString().slice(0, 19).replace('T', ' ') : date.toISOString();
	}

	/*
	* Return dialect-aware type name for timestamp
	* @return {String} type name for timestamp
	* @api private
	*/
	function timestampTypeName(knex) {
		return isMySQL(knex) || isMSSQL(knex) ? 'DATETIME' : isPostgres(knex) ? 'timestamp with time zone' : 'timestamp';
	}

	/*
	 * Return condition for filtering by expiration
	 * @return {String} expired sql condition string
	 * @api private
	 */
	function expiredCondition(knex) {
		var condition ='CAST(? as '+timestampTypeName(knex)+') <= expired';
		if(isSqlite3(knex)) { 	// sqlite3 date condition is a special case.
			condition = 'datetime(?) <= datetime(expired)';
		} else if (isOracle(knex)) {
			condition = 'CAST(? as '+timestampTypeName(knex)+') <= "expired"';
		}
		return condition;
	}

	/*
	* Returns true if the specified knex instance is using sqlite3.
	* @return {bool}
	* @api private
	*/
	function isSqlite3(knex) {
		return knex.client.dialect === 'sqlite3';
	}

	/*
	* Returns true if the specified knex instance is using mysql.
	* @return {bool}
	* @api private
	*/
	function isMySQL(knex) {
		return ['mysql', 'mariasql', 'mariadb'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using mssql.
	* @return {bool}
	* @api private
	*/
	function isMSSQL(knex) {
		return ['mssql'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using postgresql.
	* @return {bool}
	* @api private
	*/
	function isPostgres(knex) {
		return ['postgresql'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns true if the specified knex instance is using oracle.
	* @return {bool}
	* @api private
	*/
	function isOracle(knex) {
		return ['oracle', 'oracledb'].indexOf(knex.client.dialect) > -1;
	}

	/*
	* Returns PostgreSQL fast upsert query.
	* @return {string}
	* @api private
	*/
	function getPostgresFastQuery(tablename, sidfieldname) {
		return 'with new_values (' + sidfieldname + ', expired, sess) as (' +
			'  values (?, ?::timestamp with time zone, ?::json)' +
			'), ' +
			'upsert as ' +
			'( ' +
			'  update ' + tablename + ' cs set ' +
			'    ' + sidfieldname + ' = nv.' + sidfieldname + ', ' +
			'    expired = nv.expired, ' +
			'    sess = nv.sess ' +
			'  from new_values nv ' +
			'  where cs.' + sidfieldname + ' = nv.' + sidfieldname + ' ' +
			'  returning cs.* ' +
			')' +
			'insert into ' + tablename + ' (' + sidfieldname + ', expired, sess) ' +
			'select ' + sidfieldname + ', expired, sess ' +
			'from new_values ' +
			'where not exists (select 1 from upsert up where up.' + sidfieldname + ' = new_values.' + sidfieldname + ')';
	}

	/*
	* Returns SQLite fast upsert query.
	* @return {string}
	* @api private
	*/
	function getSqliteFastQuery(tablename, sidfieldname) {
		return 'insert or replace into ' + tablename + ' (' + sidfieldname + ', expired, sess) values (?, ?, ?);';
	}

	/*
	* Returns MySQL fast upsert query.
	* @return {string}
	* @api private
	*/
	function getMysqlFastQuery(tablename, sidfieldname) {
		return 'insert into ' + tablename + ' (' + sidfieldname + ', expired, sess) values (?, ?, ?) on duplicate key update expired=values(expired), sess=values(sess);';
	}

	/*
	* Returns MSSQL fast upsert query.
	* @return {string}
	* @api private
	*/
	function getMssqlFastQuery(tablename, sidfieldname) {
		return 'merge ' + tablename + ' as T ' +
			'using (values (?, ?, ?)) as S (' + sidfieldname + ', expired, sess) ' +
			'on (T.' + sidfieldname + ' = S.' + sidfieldname + ') ' +
			'when matched then ' +
			'update set expired = S.expired, sess = S.sess ' +
			'when not matched by target then ' +
			'insert (' + sidfieldname + ', expired, sess) values (S.' + sidfieldname + ', S.expired, S.sess) ' +
			'output inserted.*;';
	}

	/*
	* Remove expired sessions from database.
	* @param {Object} store
	* @param {number} interval
	* @api private
	*/
	function dbCleanup(store, interval) {
		return store.ready.then(function () {
			var condition = 'expired < CAST(? as ' + timestampTypeName(store.knex) + ')';
			if(isSqlite3(store.knex)) { 	// sqlite3 date condition is a special case.
				condition = 'datetime(expired) < datetime(?)';
			} else if (isOracle(store.knex)) {
				condition = '"expired" < CAST(? as ' + timestampTypeName(store.knex) + ')';
			}
			return store.knex(store.tablename).del()
			.whereRaw(condition, dateAsISO(store.knex));
		}).finally(function() {
			setTimeout(dbCleanup, interval, store, interval).unref()
		});
	}

	/*
	* Initialize KnexStore with the given options.
	*
	* @param {Object} options
	* @api public
	*/
	function KnexStore(options) {
		var self = this;

		options = options || {};
		Store.call(self, options);

		if (!options.clearInterval) {
			// Time to run clear expired function.
			options.clearInterval =  60000;
		}

		self.createtable = options.hasOwnProperty('createtable') ? options.createtable :  true;
		self.tablename = options.tablename || 'sessions';
		self.sidfieldname = options.sidfieldname || 'sid';
		self.knex = options.knex || require('knex')({
			client: 'sqlite3',
			// debug: true,
			connection: {
				filename: "connect-session-knex.sqlite"
			}
		});

		self.ready = self.knex.schema.hasTable(self.tablename)
		.then(function (exists) {
			if (!exists && self.createtable) {
				return self.knex.schema.createTable(self.tablename, function (table) {
					table.string(self.sidfieldname).primary();
					if (isMSSQL(self.knex)) {
						table.text('sess').notNullable();
					} else {
						table.json('sess').notNullable();
					}
					if (isMySQL(self.knex) || isMSSQL(self.knex)) {
						table.dateTime('expired').notNullable().index();
					} else {
						table.timestamp('expired').notNullable().index();
					}
				});
			}
			return exists;
		})
		.then(function (exists) {
			if (exists) {
				dbCleanup(self, options.clearInterval);
			}
			return null;
		});
	}

	// KnexStore.prototype.__proto__ = Store.prototype;
	util.inherits(KnexStore, Store);

	/*
	* Attempt to fetch session by the given sid.
	*
	* @param {String} sid
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.get = function(sid, fn) {
		var self = this;
		return self.ready.then(function () {
			var condition = expiredCondition(self.knex);
			return self.knex
			.select('sess')
			.from(self.tablename)
			.where(self.sidfieldname, '=', sid)
			.andWhereRaw(condition, dateAsISO(self.knex))
			.then(function (response) {
				var ret;
				if (response[0]) {
					ret = response[0].sess;
					if (typeof ret === "string") {
						ret = JSON.parse(ret);
					}
				}
				return ret;
			})
			.asCallback(fn)
		});
	};

	/*
	* Commit the given `sess` object associated with the given `sid`.
	*
	* @param {String} sid
	* @param {Session} sess
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.set = function(sid, sess, fn) {
		var self = this;
		var maxAge = sess.cookie.maxAge;
		var now = new Date().getTime();
		var expired = maxAge ? now + maxAge : now + oneDay;
		sess = JSON.stringify(sess);

		var dbDate = dateAsISO(self.knex, expired);

		if (isSqlite3(self.knex)) {
			// sqlite optimized query
			return self.ready.then(function () {
				return self.knex.raw(getSqliteFastQuery(self.tablename, self.sidfieldname), [sid, dbDate, sess ])
				.then(function (result) {
					return [1];
				})
				.asCallback(fn);
			});
		} else if (isPostgres(self.knex) && parseFloat(self.knex.client.version) >= 9.2) {
			// postgresql optimized query
			return self.ready.then(function () {
				return self.knex.raw(getPostgresFastQuery(self.tablename, self.sidfieldname), [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else if (isMySQL(self.knex)) {
			// mysql/mariaDB optimized query
			return self.ready.then(function () {
				return self.knex.raw(getMysqlFastQuery(self.tablename, self.sidfieldname), [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else if (isMSSQL(self.knex)) {
			// mysql/mariaDB optimized query
			return self.ready.then(function () {
				return self.knex.raw(getMssqlFastQuery(self.tablename, self.sidfieldname), [sid, dbDate, sess ])
				.asCallback(fn);
			});
		} else {
			return self.ready.then(function () {
				return self.knex.transaction(function (trx) {
					return trx.select('*')
					.forUpdate()
					.from(self.tablename)
					.where(self.sidfieldname, '=', sid)
					.then(function (foundKeys) {
						if (foundKeys.length === 0) {
							return trx.from(self.tablename)
							.insert({
								[self.sidfieldname]: sid,
								expired: dbDate,
								sess: sess
							});
						} else {
							return trx(self.tablename)
							.where(self.sidfieldname, '=', sid)
							.update({
								expired: dbDate,
								sess: sess
							});
						}
					});
				})
				.asCallback(fn)
			});
		}
	};

	/**
	 * Touch the given session object associated with the given session ID.
	 *
	 * @param {String} sid
	 * @param {Session} sess
	 * @param {Function} fn
	 * @public
	 */
	KnexStore.prototype.touch = function(sid, sess, fn) {
		if (sess && sess.cookie && sess.cookie.expires) {
			var condition = expiredCondition(this.knex);

			return this.knex(this.tablename)
				.where(this.sidfieldname, '=', sid)
				.andWhereRaw(condition, dateAsISO(this.knex))
				.update({
					expired: dateAsISO(this.knex, sess.cookie.expires)
				})
				.asCallback(fn);
		}

		fn();
	};

	/*
	* Destroy the session associated with the given `sid`.
	*
	* @param {String} sid
	* @api public
	*/
	KnexStore.prototype.destroy = function(sid, fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.del()
			.from(self.tablename)
			.where(self.sidfieldname, '=', sid)
			.asCallback(fn)
		});
	};

	/*
	* Fetch number of sessions.
	*
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.length = function(fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.count(self.sidfieldname + ' as count')
			.from(self.tablename)
			.then(function (response) {
				return response[0].count | 0;
			})
			.asCallback(fn)
		})
	};

	/*
	* Clear all sessions.
	*
	* @param {Function} fn
	* @api public
	*/
	KnexStore.prototype.clear = function(fn) {
		var self = this;
		return self.ready.then(function () {
			return self.knex.del()
			.from(self.tablename)
			.asCallback(fn)
		});
	};

	return KnexStore;

};
<|endoftext|>


<commit_before>var Yts = require('./lib/yts.js');
var chalk = require('chalk');
var path = require('path');
var tmpdir = require('os-tmpdir');
var isOutdated = require('is-outdated');
var Configstore = require('configstore');
var pkg = require('./package.json');
var parseTorrent = require('parse-torrent');
var open = require('open');
var validURL = require('valid-url');
var spawn = require('child_process').spawn;

var Conf = new Configstore(pkg.name, {
  lastUpdate: Date.now(),
  lastVersion: pkg.version
});

var showRecentMovies = function (options) {
  options = options || {};
  options.sort_by = options.sort_by || 'year';
  options.order_by = options.order_by || 'desc';

  Yts.listMovies(options, function (err, res) {
    if (err) { console.log(err); }

    res.data.movies.forEach(function (movie) {
      console.log(chalk.underline.yellow(movie.id) + ': ' + movie.title_long);
    });
  });
};

var searchMovie = function (str) {
  Yts.listMovies({ query_term: str }, function (err, res) {
    if (err) { console.log(err); }

    if (res.data.movies.length < 1) {
      console.log('No movies found');
    } else {
      res.data.movies.forEach(function (movie) {
        console.log(chalk.underline.yellow(movie.id) + ': ' + movie.title_long);
      });
    }
  });
};

var getInfo = function (movieID) {
  Yts.movieDetails({ movie_id: movieID }, function (err, res) {
    if (err) { return console.log(err); }

    if (res.status === 'error') {
      return console.log(chalk.red(res.status_message));
    }

    console.log('Opening browser with movie info...');
    if (validURL.isWebUri(res.data.url)) {
      open(res.data.url);
    } else {
      console.log(chalk.yellow('Movie has invalid URL'));
    }
  });
}

var getMovie = function (movieID, quality, subs) {
  quality = quality || '720p';
  subs = subs || 'english';
  var YSubs = require('./lib/ysubs.js');

  Yts.movieDetails({ movie_id: movieID }, function (err, res) {
    if (res.status === 'error') {
      return console.log(chalk.red(res.status_message));
    }

    if (!res.data.torrents || res.data.torrents.length === 0) {
      return console.log(chalk.red('Movie has no torrents available'));
    }

    if (res.data.torrents.length === 1) {
      quality = res.data.torrents[0].quality;
      console.log(chalk.yellow('Using ' + quality));
    }

    var subsSavePath = path.join(tmpdir(), res.data.slug + '.srt');

    var torrInfo = res.data.torrents.filter(function (torr) {
      return torr.quality === quality;
    }).pop();

    parseTorrent.remote(torrInfo.url, function (err, tinfo) {
      var magnetURI = parseTorrent.toMagnetURI(tinfo);

      var peerflixPath = path.join(__dirname, 'node_modules', '.bin', 'peerflix');
      var peerflix = spawn(peerflixPath, [
        '-t',
        subsSavePath,
        magnetURI,
        '--vlc'
      ]);

      peerflix.stdout.on('data', function (data) {
        process.stdout.write(data);
      });

      peerflix.stderr.on('data', function (data) {
        process.stdout.write(data);
      });
    });

    YSubs.getSubtitles(res.data.imdb_code, function (err, data) {
      if (!data || !data[subs]) {
        return console.log('No subtitles available in %s', subs);
      }
      var zipPath = data[subs].shift().url;
      YSubs.fetchSubtitle(zipPath, subsSavePath, function (err, data) {
        if (err) { console.log(err); }
      });
    });
  })
};

var showHelp = function () {
  console.log('movees [options]');
  console.log('Options:');
  console.log('--search <search term>  search for a movie');
  console.log('--info <movie id>  open web page with movie info');
  console.log('--watch <movie id> [--quality <720p|1080p|3d> [--subs <subtitle language>]   watch a movie');
  console.log('--latest [--page <page number>] [--limit <number of movies p/page>]  show latest movies available');
  console.log('--version  show version');
  console.log('--help     show usage help');
};

var showVersion = function () {
  console.log('Movees (version %s)', pkg.version);
};

var showUpdateMsg = function (version) {
  console.log('\n----------------------------------------');
  console.log(chalk.bold('** UPDATE AVAILABLE **'));
  console.log(chalk.underline('New version:') + ' ' + chalk.green(version));
  console.log(chalk.underline('Current version:') + ' ' + chalk.red(pkg.version));
  console.log('\nPlease update with: npm update -g movees');
  console.log('----------------------------------------\n');
};

var checkForUpdates = function () {
  var lastUpdate = Conf.get('lastUpdate');
  var newVersion = Conf.get('newVersion');
  if ((Date.now() - lastUpdate) >= 432e6) { // 5 days
    isOutdated('movees', pkg.version, function (err, res) {
      if (res) {
        Conf.set('newVersion', res.version);
        showUpdateMsg(res.version);
      } else {
        Conf.del('newVersion');
      }
    });
    Conf.set('lastUpdate', Date.now());
  } else {
    // check if we had an update
    if (pkg.version !== Conf.get('lastVersion')) {
      Conf.set('lastVersion', pkg.version);
      Conf.del('newVersion');
      return;
    }
    if (newVersion) {
      showUpdateMsg(newVersion);
    }
  }
};

/**
 * CLI arguments handling
 */

// avoid calling npm when testing
module.exports = function (argv) {
  if (!argv.test) {
    checkForUpdates();
  }

  if (argv.search) {
    searchMovie(argv.search);
  } else if (argv.watch) {
    getMovie(argv.watch, argv.quality, argv.subs);
  } else if (argv.info) {
    getInfo(argv.info);
  } else if (argv.latest) {
    var opts = {};
    opts.page = argv.page || 1;
    opts.limit = argv.limit || '20';
    showRecentMovies(opts);
  } else if (argv.version) {
    showVersion();
  } else {
    showHelp();
  }
};
<commit_msg>Update index.js<commit_after>var Yts = require('./lib/yts.js');
var chalk = require('chalk');
var path = require('path');
var tmpdir = require('os-tmpdir');
var isOutdated = require('is-outdated');
var Configstore = require('configstore');
var pkg = require('./package.json');
var parseTorrent = require('parse-torrent');
var open = require('open');
var validURL = require('valid-url');
var spawn = require('child_process').spawn;

var Conf = new Configstore(pkg.name, {
  lastUpdate: Date.now(),
  lastVersion: pkg.version
});

var showRecentMovies = function (options) {
  options = options || {};
  options.sort_by = options.sort_by || 'year';
  options.order_by = options.order_by || 'desc';

  Yts.listMovies(options, function (err, res) {
    if (err) { console.log(err); }

    res.data.movies.forEach(function (movie) {
      console.log(chalk.underline.yellow(movie.id) + ' : ' + movie.title_long);
    });
  });
};

var searchMovie = function (str) {
  Yts.listMovies({ query_term: str }, function (err, res) {
    if (err) { console.log(err); }

    if (res.data.movies.length < 1) {
      console.log('No movies found');
    } else {
      res.data.movies.forEach(function (movie) {
        console.log(chalk.underline.yellow(movie.id) + ': ' + movie.title_long);
      });
    }
  });
};

var getInfo = function (movieID) {
  Yts.movieDetails({ movie_id: movieID }, function (err, res) {
    if (err) { return console.log(err); }

    if (res.status === 'error') {
      return console.log(chalk.red(res.status_message));
    }

    console.log('Opening browser with movie info...');
    if (validURL.isWebUri(res.data.url)) {
      open(res.data.url);
    } else {
      console.log(chalk.yellow('Movie has invalid URL'));
    }
  });
}

var getMovie = function (movieID, quality, subs) {
  quality = quality || '720p';
  subs = subs || 'english';
  var YSubs = require('./lib/ysubs.js');

  Yts.movieDetails({ movie_id: movieID }, function (err, res) {
    if (res.status === 'error') {
      return console.log(chalk.red(res.status_message));
    }

    if (!res.data.torrents || res.data.torrents.length === 0) {
      return console.log(chalk.red('Movie has no torrents available'));
    }

    if (res.data.torrents.length === 1) {
      quality = res.data.torrents[0].quality;
      console.log(chalk.yellow('Using ' + quality));
    }

    var subsSavePath = path.join(tmpdir(), res.data.slug + '.srt');

    var torrInfo = res.data.torrents.filter(function (torr) {
      return torr.quality === quality;
    }).pop();

    parseTorrent.remote(torrInfo.url, function (err, tinfo) {
      var magnetURI = parseTorrent.toMagnetURI(tinfo);

      var peerflixPath = path.join(__dirname, 'node_modules', '.bin', 'peerflix');
      var peerflix = spawn(peerflixPath, [
        '-t',
        subsSavePath,
        magnetURI,
        '--vlc'
      ]);

      peerflix.stdout.on('data', function (data) {
        process.stdout.write(data);
      });

      peerflix.stderr.on('data', function (data) {
        process.stdout.write(data);
      });
    });

    YSubs.getSubtitles(res.data.imdb_code, function (err, data) {
      if (!data || !data[subs]) {
        return console.log('No subtitles available in %s', subs);
      }
      var zipPath = data[subs].shift().url;
      YSubs.fetchSubtitle(zipPath, subsSavePath, function (err, data) {
        if (err) { console.log(err); }
      });
    });
  })
};

var showHelp = function () {
  console.log('movees [options]');
  console.log('Options:');
  console.log('--search <search term>  search for a movie');
  console.log('--info <movie id>  open web page with movie info');
  console.log('--watch <movie id> [--quality <720p|1080p|3d> [--subs <subtitle language>]   watch a movie');
  console.log('--latest [--page <page number>] [--limit <number of movies p/page>]  show latest movies available');
  console.log('--version  show version');
  console.log('--help     show usage help');
};

var showVersion = function () {
  console.log('Movees (version %s)', pkg.version);
};

var showUpdateMsg = function (version) {
  console.log('\n----------------------------------------');
  console.log(chalk.bold('** UPDATE AVAILABLE **'));
  console.log(chalk.underline('New version:') + ' ' + chalk.green(version));
  console.log(chalk.underline('Current version:') + ' ' + chalk.red(pkg.version));
  console.log('\nPlease update with: npm update -g movees');
  console.log('----------------------------------------\n');
};

var checkForUpdates = function () {
  var lastUpdate = Conf.get('lastUpdate');
  var newVersion = Conf.get('newVersion');
  if ((Date.now() - lastUpdate) >= 432e6) { // 5 days
    isOutdated('movees', pkg.version, function (err, res) {
      if (res) {
        Conf.set('newVersion', res.version);
        showUpdateMsg(res.version);
      } else {
        Conf.del('newVersion');
      }
    });
    Conf.set('lastUpdate', Date.now());
  } else {
    // check if we had an update
    if (pkg.version !== Conf.get('lastVersion')) {
      Conf.set('lastVersion', pkg.version);
      Conf.del('newVersion');
      return;
    }
    if (newVersion) {
      showUpdateMsg(newVersion);
    }
  }
};

/**
 * CLI arguments handling
 */

// avoid calling npm when testing
module.exports = function (argv) {
  if (!argv.test) {
    checkForUpdates();
  }

  if (argv.search) {
    searchMovie(argv.search);
  } else if (argv.watch) {
    getMovie(argv.watch, argv.quality, argv.subs);
  } else if (argv.info) {
    getInfo(argv.info);
  } else if (argv.latest) {
    var opts = {};
    opts.page = argv.page || 1;
    opts.limit = argv.limit || '20';
    showRecentMovies(opts);
  } else if (argv.version) {
    showVersion();
  } else {
    showHelp();
  }
};
<|endoftext|>


<commit_before>const Discord       = require('discord.js')
const request       = require('request-promise')
const config        = require('./data/client.json')
const DiscordServer = require('./DiscordServer')

module.exports =

class DiscordBot {
    constructor() {
        this.initialize();
        this.servers = {};
    }

    initialize() {
        this.bot = new Discord.Client();

        this.bot.on("ready", this.ready.bind(this));
        this.bot.on("message", this.message.bind(this));
        this.bot.on("guildMemberAdd", this.guildMemberAdd.bind(this));

        this.bot.login(config.token);
    }

    ready() {
        console.log("RoVer is ready.");
        this.bot.user.setGame("http://eryn.io/RoVer");
    }

    async message(msg) {
        if (msg.content.toLowerCase() === "!verify" && msg.guild) {
            DiscordServer.clearMemberCache(msg.author.id);

            let action = await this.getServer(msg.guild.id).verifyMember(msg.author.id);

            if (!action.status) {
                msg.reply(action.error);
            } else {
                msg.reply(`Welcome to the server, ${action.robloxUsername}!`);
            }
        }else if (msg.guild && msg.member.hasPermission('ADMINISTRATOR')) {
            let command = msg.cleanContent.toLowerCase().split(' ')[0];

            let args = msg.cleanContent.split(' ');
            args.shift();
            let argument = args.join(' ');

            let server = this.getServer(msg.guild.id);

            if (command === "!rover") {
                msg.reply("**RoVer Admin Commands:**\n\n`!RoVerVerifiedRole <exact role name>`\n`!RoVerNickname <true|false>`\n`!RoVerAnnounceChannel <exact channel name>`");
            } else if (command === "!roververifiedrole") {
                if (argument.length > 0) {
                    let role = msg.guild.roles.find('name', argument);
                    if (role) {
                        server.setSetting('verifiedRole', role.id);
                        msg.reply(`Set verified role to ${argument}`);
                    } else {
                        msg.reply(`Couldn't find role \`${argument}\`. Make sure you type it exactly (including capitalization).`);
                    }
                } else {
                    server.setSetting('verifiedRole', null);
                    msg.reply("Cleared verified role, users will no longer receive a role when they verify.");
                }
            } else if (command === "!roverannouncechannel") {
                if (argument.length > 0) {
                    let channel = msg.guild.channels.find('name', argument);
                    if (channel) {
                        server.setSetting('announceChannel', channel.id);
                        msg.reply(`Set verify announcement channel to ${argument}`);
                    } else {
                        msg.reply(`Couldn't find channel \`${argument}\`. Make sure you type it exactly (including capitalization).`);
                    }
                } else {
                    server.setSetting('announceChannel', null);
                    msg.reply("Verified users will no longer be announced.");
                }
            } else if (command === "!rovernickname") {
                if (argument.length > 0) {
                    server.setSetting('nicknameUsers', argument === 'true');
                    if (argument === 'true') {
                        msg.reply("The bot will now nickname users to their Roblox username.");
                    } else {
                        msg.reply("The bot will no longer nickname users to their Roblox name.");
                    }
                } else {
                    msg.reply("Requires argument (true|false)");
                }
            }
        }
    }

    async guildMemberAdd(member) {
        let action = await this.getServer(member.guild.id).verifyMember(member.id);

        if (action.status) {
            member.send(`Welcome to the server, ${action.robloxUsername}!`);
        } else {
            member.send("Welcome to the server! Visit the following link to verify your Roblox account: https://verify.eryn.io");
        }
    }

    getServer(id) {
        if (!this.servers[id]) {
            this.servers[id] = new DiscordServer(this, id);
        }
        return this.servers[id];
    }

    async globallyUpdateMember(id) {
        DiscordServer.clearMemberCache(id);

        for (let guild of this.bot.guilds.array()) {
            let server = this.getServer(guild.id);

            let action = await server.verifyMember(id);

            if (!action.status && !action.nonFatal) {
                break;
            }
        }
    }
}<commit_msg>Make sure member exists before checking permissions<commit_after>const Discord       = require('discord.js')
const request       = require('request-promise')
const config        = require('./data/client.json')
const DiscordServer = require('./DiscordServer')

module.exports =

class DiscordBot {
    constructor() {
        this.initialize();
        this.servers = {};
    }

    initialize() {
        this.bot = new Discord.Client();

        this.bot.on("ready", this.ready.bind(this));
        this.bot.on("message", this.message.bind(this));
        this.bot.on("guildMemberAdd", this.guildMemberAdd.bind(this));

        this.bot.login(config.token);
    }

    ready() {
        console.log("RoVer is ready.");
        this.bot.user.setGame("http://eryn.io/RoVer");
    }

    async message(msg) {
        if (msg.content.toLowerCase() === "!verify" && msg.guild) {
            DiscordServer.clearMemberCache(msg.author.id);

            let action = await this.getServer(msg.guild.id).verifyMember(msg.author.id);

            if (!action.status) {
                msg.reply(action.error);
            } else {
                msg.reply(`Welcome to the server, ${action.robloxUsername}!`);
            }
        }else if (msg.guild && msg.member && msg.member.hasPermission('ADMINISTRATOR')) {
            let command = msg.cleanContent.toLowerCase().split(' ')[0];

            let args = msg.cleanContent.split(' ');
            args.shift();
            let argument = args.join(' ');

            let server = this.getServer(msg.guild.id);

            if (command === "!rover") {
                msg.reply("**RoVer Admin Commands:**\n\n`!RoVerVerifiedRole <exact role name>`\n`!RoVerNickname <true|false>`\n`!RoVerAnnounceChannel <exact channel name>`");
            } else if (command === "!roververifiedrole") {
                if (argument.length > 0) {
                    let role = msg.guild.roles.find('name', argument);
                    if (role) {
                        server.setSetting('verifiedRole', role.id);
                        msg.reply(`Set verified role to ${argument}`);
                    } else {
                        msg.reply(`Couldn't find role \`${argument}\`. Make sure you type it exactly (including capitalization).`);
                    }
                } else {
                    server.setSetting('verifiedRole', null);
                    msg.reply("Cleared verified role, users will no longer receive a role when they verify.");
                }
            } else if (command === "!roverannouncechannel") {
                if (argument.length > 0) {
                    let channel = msg.guild.channels.find('name', argument);
                    if (channel) {
                        server.setSetting('announceChannel', channel.id);
                        msg.reply(`Set verify announcement channel to ${argument}`);
                    } else {
                        msg.reply(`Couldn't find channel \`${argument}\`. Make sure you type it exactly (including capitalization).`);
                    }
                } else {
                    server.setSetting('announceChannel', null);
                    msg.reply("Verified users will no longer be announced.");
                }
            } else if (command === "!rovernickname") {
                if (argument.length > 0) {
                    server.setSetting('nicknameUsers', argument === 'true');
                    if (argument === 'true') {
                        msg.reply("The bot will now nickname users to their Roblox username.");
                    } else {
                        msg.reply("The bot will no longer nickname users to their Roblox name.");
                    }
                } else {
                    msg.reply("Requires argument (true|false)");
                }
            }
        }
    }

    async guildMemberAdd(member) {
        let action = await this.getServer(member.guild.id).verifyMember(member.id);

        if (action.status) {
            member.send(`Welcome to the server, ${action.robloxUsername}!`);
        } else {
            member.send("Welcome to the server! Visit the following link to verify your Roblox account: https://verify.eryn.io");
        }
    }

    getServer(id) {
        if (!this.servers[id]) {
            this.servers[id] = new DiscordServer(this, id);
        }
        return this.servers[id];
    }

    async globallyUpdateMember(id) {
        DiscordServer.clearMemberCache(id);

        for (let guild of this.bot.guilds.array()) {
            let server = this.getServer(guild.id);

            let action = await server.verifyMember(id);

            if (!action.status && !action.nonFatal) {
                break;
            }
        }
    }
}<|endoftext|>


<commit_before>// ========================================================================
// SproutCore -- JavaScript Application Framework
// Copyright ©2006-2008, Sprout Systems, Inc. and contributors.
// Portions copyright ©2008 Apple Inc.  All rights reserved.
// ========================================================================

sc_require('controllers/object');
sc_require('mixins/selection_support');
sc_require('private/tree_item_observer');

/**
  @class

  A TreeController manages a tree of model objects that you might want to
  display in the UI using a collection view.  For the most part, you should
  work with a TreeController much like you would an ObjectController, except
  that the TreeController will also provide an arrangedObjects property that
  can be used as the content of a CollectionView.

  TODO: Document More

  @extends SC.ObjectController
  @extends SC.SelectionSupport
  @since SproutCore 1.0
*/
SC.TreeController = SC.ObjectController.extend(SC.SelectionSupport,
/** @scope SC.TreeController.prototype */ {

  // ..........................................................
  // PROPERTIES
  //

  /**
    Set to YES if you want the top-level items in the tree to be displayed as
    group items in the collection view.

    @property {Boolean}
  */
  treeItemIsGrouped: NO,

  /**
    If your content support expanding and collapsing of content, then set this
    property to the name of the key on your model that should be used to
    determine the expansion state of the item.  The default is
    "treeItemIsExpanded"

    @property {String}
  */
  treeItemIsExpandedKey: "treeItemIsExpanded",

  /**
    Set to the name of the property on your content object that holds the
    children array for each tree node.  The default is "treeItemChildren".

    @property {String}
  */
  treeItemChildrenKey: "treeItemChildren",

  /**
    Returns an SC.Array object that actually will represent the tree as a
    flat array suitable for use by a CollectionView.  Other than binding this
    property as the content of a CollectionView, you generally should not
    use this property directly.  Instead, work on the tree content using the
    TreeController like you would any other ObjectController.

    @property {SC.Array}
  */
  arrangedObjects: function() {
    var ret, content = this.get('content');
    if (content) {
      ret = SC.TreeItemObserver.create({ item: content, delegate: this });
    } else ret = null; // empty!
    this._sctc_arrangedObjects = ret ;

    return ret ;
  }.property().cacheable(),

  // ..........................................................
  // PRIVATE
  //

  /**
    @private

    Manually invalidate the arrangedObjects cache so that we can teardown
    any existing value.  We do it via an observer so that this will fire
    immediately instead of waiting on some other component to get
    arrangedObjects again.
  */
  _sctc_invalidateArrangedObjects: function() {
    this.propertyWillChange('arrangedObjects');

    var ret = this._sctc_arrangedObjects;
    if (ret) ret.destroy();
    this._sctc_arrangedObjects = null;

    this.propertyDidChange('arrangedObjects');
  }.observes('content', 'treeItemIsExpandedKey', 'treeItemChildrenKey', 'treeItemIsGrouped'),

  _sctc_arrangedObjectsContentDidChange: function() {
    this.updateSelectionAfterContentChange();
  }.observes('*arrangedObjects.[]'),

  /**
    @private

    Returns the first item in arrangeObjects that is not a group.  This uses
    a brute force approach right now; we assume you probably don't have a lot
    of groups up front.
  */
  firstSelectableObject: function() {
    var objects = this.get('arrangedObjects'),
        indexes = objects.contentGroupIndexes(null, objects),
        len     = objects.get('length'),
        idx     = 0;
    while(indexes.contains(idx) && (idx<len)) idx++;
    return idx>=len ? null : objects.objectAt(idx);
  }.property()

});

<commit_msg>Make TreeController's firstSelectableObject method work if arrangedObjects is null.<commit_after>// ========================================================================
// SproutCore -- JavaScript Application Framework
// Copyright ©2006-2008, Sprout Systems, Inc. and contributors.
// Portions copyright ©2008 Apple Inc.  All rights reserved.
// ========================================================================

sc_require('controllers/object');
sc_require('mixins/selection_support');
sc_require('private/tree_item_observer');

/**
  @class

  A TreeController manages a tree of model objects that you might want to
  display in the UI using a collection view.  For the most part, you should
  work with a TreeController much like you would an ObjectController, except
  that the TreeController will also provide an arrangedObjects property that
  can be used as the content of a CollectionView.

  TODO: Document More

  @extends SC.ObjectController
  @extends SC.SelectionSupport
  @since SproutCore 1.0
*/
SC.TreeController = SC.ObjectController.extend(SC.SelectionSupport,
/** @scope SC.TreeController.prototype */ {

  // ..........................................................
  // PROPERTIES
  //

  /**
    Set to YES if you want the top-level items in the tree to be displayed as
    group items in the collection view.

    @property {Boolean}
  */
  treeItemIsGrouped: NO,

  /**
    If your content support expanding and collapsing of content, then set this
    property to the name of the key on your model that should be used to
    determine the expansion state of the item.  The default is
    "treeItemIsExpanded"

    @property {String}
  */
  treeItemIsExpandedKey: "treeItemIsExpanded",

  /**
    Set to the name of the property on your content object that holds the
    children array for each tree node.  The default is "treeItemChildren".

    @property {String}
  */
  treeItemChildrenKey: "treeItemChildren",

  /**
    Returns an SC.Array object that actually will represent the tree as a
    flat array suitable for use by a CollectionView.  Other than binding this
    property as the content of a CollectionView, you generally should not
    use this property directly.  Instead, work on the tree content using the
    TreeController like you would any other ObjectController.

    @property {SC.Array}
  */
  arrangedObjects: function() {
    var ret, content = this.get('content');
    if (content) {
      ret = SC.TreeItemObserver.create({ item: content, delegate: this });
    } else ret = null; // empty!
    this._sctc_arrangedObjects = ret ;

    return ret ;
  }.property().cacheable(),

  // ..........................................................
  // PRIVATE
  //

  /**
    @private

    Manually invalidate the arrangedObjects cache so that we can teardown
    any existing value.  We do it via an observer so that this will fire
    immediately instead of waiting on some other component to get
    arrangedObjects again.
  */
  _sctc_invalidateArrangedObjects: function() {
    this.propertyWillChange('arrangedObjects');

    var ret = this._sctc_arrangedObjects;
    if (ret) ret.destroy();
    this._sctc_arrangedObjects = null;

    this.propertyDidChange('arrangedObjects');
  }.observes('content', 'treeItemIsExpandedKey', 'treeItemChildrenKey', 'treeItemIsGrouped'),

  _sctc_arrangedObjectsContentDidChange: function() {
    this.updateSelectionAfterContentChange();
  }.observes('*arrangedObjects.[]'),

  /**
    @private

    Returns the first item in arrangeObjects that is not a group.  This uses
    a brute force approach right now; we assume you probably don't have a lot
    of groups up front.
  */
  firstSelectableObject: function() {
    var objects = this.get('arrangedObjects'),
        indexes, len, idx     = 0;

    if (!objects) return null; // fast track

    indexes = objects.contentGroupIndexes(null, objects);
    len = objects.get('length');
    while(indexes.contains(idx) && (idx<len)) idx++;
    return idx>=len ? null : objects.objectAt(idx);
  }.property()

});

<|endoftext|>


<commit_before>'use strict';

const assign        = require('object-assign');
const webworkify    = require('webworkify');

const MathLib       = require('./lib/mathlib');
const Pool          = require('./lib/pool');
const utils         = require('./lib/utils');
const worker        = require('./lib/worker');
const createRegions = require('./lib/tiler');

let NEED_SAFARI_FIX = false;
try {
  if (typeof navigator !== 'undefined' && navigator.userAgent) {
    NEED_SAFARI_FIX = navigator.userAgent.indexOf('Safari') >= 0;
  }
} catch (e) {}

let concurrency = 1;
if (typeof navigator !== 'undefined') {
  concurrency = Math.min(navigator.hardwareConcurrency || 1, 4);
}

const DEFAULT_PICA_OPTS = {
  tile: 1024,
  concurrency,
  features: [ 'all' ],
  idle: 2000
};

const DEFAULT_RESIZE_OPTS = {
  quality:          3,
  alpha:            false,
  unsharpAmount:    0,
  unsharpRadius:    0.0,
  unsharpThreshold: 0
};

function workerFablic() {
  return {
    value: webworkify(worker),
    destroy: function () {
      this.value.terminate();

      if (typeof window !== 'undefined') {
        let url = window.URL || window.webkitURL || window.mozURL || window.msURL;
        if (url && url.revokeObjectURL && this.value.objectURL) {
          url.revokeObjectURL(this.value.objectURL);
        }
      }
    }
  };
}

////////////////////////////////////////////////////////////////////////////////
// API methods

function Pica(options) {
  if (!(this instanceof Pica)) return new Pica(options);

  this.options = assign(DEFAULT_PICA_OPTS, options || {});

  this.__limit = utils.limiter(this.options.concurrency);

  // List of supported features, according to options & browser/node.js
  this.features = {
    js:   false, // pure JS implementation, can be disabled for testing
    wasm: false, // webassembly implementation for heavy functions
    cib:  false, // resize via createImageBitmap (only FF at this moment)
    ww:   false  // webworkers
  };

  this.__workersPool = null;

  // Store requested features for webworkers
  this.__requested_features = [];

  this.__mathlib = null;
}

Pica.prototype.init = function () {
  if (this.__initPromise) return this.__initPromise;

  let features = this.options.features.slice();

  if (features.indexOf('all') >= 0) {
    features = [ 'cib', 'wasm', 'js', 'ww' ];
  }

  this.__requested_features = features;

  this.__mathlib = new MathLib(features);

  // Check WebWorker support if requested
  if (features.indexOf('ww') >= 0) {
    if ((typeof window !== 'undefined') && ('Worker' in window)) {
      // IE <= 11 don't allow to create webworkers from string. We should check it.
      // https://connect.microsoft.com/IE/feedback/details/801810/web-workers-from-blob-urls-in-ie-10-and-11
      try {
        let wkr = require('webworkify')(function () {});
        wkr.terminate();
        this.features.ww   = true;
        this.__workersPool = new Pool(workerFablic, this.options.idle);
      } catch (__) {}
    }
  }

  let initMath = this.__mathlib.init().then(mathlib => {
    // Copy detected features
    assign(this.features, mathlib.features);
  });

  let checkCib = utils.cib_support().then(status => {
    this.features.cib = status;
  });

  // Init math lib. That's async because can load some
  this.__initPromise = Promise.all([ initMath, checkCib ]).then(() => this);

  return this.__initPromise;
};

Pica.prototype.resize = function (from, to, options) {
  let opts = DEFAULT_RESIZE_OPTS;

  if (!isNaN(options)) {
    opts = assign(opts, { quality: options });
  } else if (options) {
    opts = assign(opts, options);
  }

  opts.toWidth  = to.width;
  opts.toHeigth = to.height;
  opts.width    = from.naturalWidth || from.width;
  opts.height   = from.naturalHeight || from.height;

  let toCtx = to.getContext('2d', { alpha: Boolean(opts.alpha) });

  return this.init().then(() => {
    // if createImageBitmap supports resize, just do it and return
    if (this.feature_cib) {
      return createImageBitmap(from, {
        resizeWidth:   opts.toWidth,
        resizeHeight:  opts.toHeigth,
        resizeQuality: utils.cib_quality_name(opts.quality)
      })
      .then(imageBitmap => {
        // if no unsharp - draw directly to output canvas
        if (!opts.unsharpAmount) {
          toCtx.drawImage(imageBitmap, 0, 0);
          imageBitmap.close();
          toCtx = null;
          return to;
        }

        let tmpCanvas = document.createElement('canvas');

        tmpCanvas.width  = opts.toWidth;
        tmpCanvas.height = opts.toHeigth;

        let tmpCtx = tmpCanvas.getContext('2d', { alpha: Boolean(opts.alpha) });

        tmpCtx.drawImage(imageBitmap, 0, 0);
        imageBitmap.close();

        let iData = tmpCtx.getImageData(0, 0, opts.toWidth, opts.toHeigth);

        this.__mathlib.unsharp(
          iData.data,
          opts.toWidth,
          opts.toHeigth,
          opts.unsharpAmount,
          opts.unsharpRadius,
          opts.unsharpThreshold
        );

        toCtx.putImageData(iData, 0, 0);
        iData = tmpCtx = tmpCanvas = toCtx = null;
        return to;
      });
    }

    //
    // No easy way, let's resize manually via arrays
    //

    let srcCtx;
    let srcImageBitmap;

    // Share cache between calls:
    //
    // - wasm instance
    // - wasm memory object
    //
    let cache = {};

    // Call resizer in webworker or locally, depending on config
    const invokeResize = opts => {
      return Promise.resolve().then(() => {
        if (!this.features.ww) return this.__mathlib.resizeAndUnsharp(opts, cache);

        return new Promise((resolve, reject) => {
          let w = this.__workersPool.acquire();

          w.value.onmessage = ev => {
            w.release();

            if (ev.data.err) reject(ev.data.err);
            else resolve(ev.data.result);
          };

          w.value.postMessage({
            opts,
            features: this.__requested_features,
            preload: {
              wasm_nodule: this.__mathlib.__
            }
          }, [ opts.src.buffer ]);
        });
      });
    };

    const processTile = (tile => this.__limit(() => {
      let srcImageData;

      // Extract tile RGBA buffer, depending on input type
      if (utils.isCanvas(from)) {
        // If input is Canvas - extract region data directly
        srcImageData = srcCtx.getImageData(tile.x, tile.y, tile.width, tile.height);
      } else {
        // If input is Image or decoded to ImageBitmap,
        // draw region to temporary canvas and extract data from it
        //
        // Note! Attempt to reuse this canvas causes significant slowdown in chrome
        //
        let tmpCanvas = document.createElement('canvas');
        tmpCanvas.width  = tile.width;
        tmpCanvas.height = tile.height;

        let tmpCtx = tmpCanvas.getContext('2d', { alpha: Boolean(opts.alpha) });
        tmpCtx.globalCompositeOperation = 'copy';
        tmpCtx.drawImage(srcImageBitmap || from,
          tile.x, tile.y, tile.width, tile.height,
          0, 0, tile.width, tile.height);

        srcImageData = tmpCtx.getImageData(0, 0, tile.width, tile.height);
        tmpCtx = tmpCanvas = null;
      }

      let o = {
        src:              srcImageData.data,
        width:            tile.width,
        height:           tile.height,
        toWidth:          tile.toWidth,
        toHeight:         tile.toHeight,
        scaleX:           tile.scaleX,
        scaleY:           tile.scaleY,
        offsetX:          tile.offsetX,
        offsetY:          tile.offsetY,
        quality:          opts.quality,
        alpha:            opts.alpha,
        unsharpAmount:    opts.unsharpAmount,
        unsharpRadius:    opts.unsharpRadius,
        unsharpThreshold: opts.unsharpThreshold
      };

      return Promise.resolve()
        .then(() => invokeResize(o))
        .then(result => {
          srcImageData = null;

          let toImageData;

          if (typeof ImageData !== 'undefined') {
            // this branch is for browsers
            toImageData = new ImageData(new Uint8ClampedArray(result), tile.toWidth, tile.toHeight);
          } else {
            // fallback for node-canvas
            toImageData = toCtx.createImageData(tile.toWidth, tile.toHeight);
            toImageData.data.set(result);
          }

          if (NEED_SAFARI_FIX) {
            // Safari draws thin white stripes between tiles without this fix
            toCtx.putImageData(toImageData, tile.toX, tile.toY,
              tile.toInnerX - tile.toX, tile.toInnerY - tile.toY,
              tile.toInnerWidth + 1e-5, tile.toInnerHeight + 1e-5);
          } else {
            toCtx.putImageData(toImageData, tile.toX, tile.toY,
              tile.toInnerX - tile.toX, tile.toInnerY - tile.toY,
              tile.toInnerWidth, tile.toInnerHeight);
          }
        });
    }));

    // Need normalize data source first. It can be canvas or image.
    // If image - try to decode in background if possible
    return Promise.resolve().then(() => {
      if (utils.isCanvas(from)) {
        srcCtx = from.getContext('2d', { alpha: Boolean(opts.alpha) });
        return null;
      }

      if (utils.isImage(from)) {
        // try do decode image in background for faster next operations
        if (typeof createImageBitmap === 'undefined') return null;

        return createImageBitmap(from)
          .then(imageBitmap => {
            srcImageBitmap = imageBitmap;
          });
      }

      throw new Error('".from" should be image or canvas');
    })
    .then(() => {
      //
      // Here we are with "normalized" source,
      // follow to tiling
      //

      let DEST_TILE_BORDER = 3; // Max possible filter window size

      let regions = createRegions({
        width:        opts.width,
        height:       opts.height,
        srcTileSize:  this.options.tile,
        toWidth:      opts.toWidth,
        toHeight:     opts.toHeigth,
        destTileBorder: Math.ceil(Math.max(DEST_TILE_BORDER, 2.5 * opts.unsharpRadius|0))
      });

      let jobs = regions.map(tile => processTile(tile));

      return Promise.all(jobs).then(() => {
        if (srcImageBitmap) {
          srcImageBitmap.close();
          srcImageBitmap = null;
        }

        return to;
      });
    });
  });
};

// RGBA buffer resize
//
Pica.prototype.resizeBuffer = function (options) {
  const opts = assign(DEFAULT_RESIZE_OPTS, options);

  return this.init()
    .then(() => this.__mathlib.resizeAndUnsharp(opts));
};

Pica.prototype.toBlob = function (canvas, mimeType, quality) {
  mimeType = mimeType || 'image/png';

  return new Promise(resolve => {
    if (canvas.toBlob) {
      canvas.toBlob(blob => resolve(blob), mimeType, quality);
      return;
    }

    // Fallback for old browsers
    const asString = atob(canvas.toDataURL(mimeType, quality).split(',')[1]);
    const len      = asString.length;
    const asBuffer = new Uint8Array(len);

    for (let i = 0; i < len; i++) {
      asBuffer[i] = asString.charCodeAt(i);
    }

    resolve(new Blob([ asBuffer ], { type: mimeType }));
  });
};

Pica.prototype.debug = function () {};

module.exports = Pica;
<commit_msg>Add resize cancel support<commit_after>'use strict';

const assign        = require('object-assign');
const webworkify    = require('webworkify');

const MathLib       = require('./lib/mathlib');
const Pool          = require('./lib/pool');
const utils         = require('./lib/utils');
const worker        = require('./lib/worker');
const createRegions = require('./lib/tiler');

let NEED_SAFARI_FIX = false;
try {
  if (typeof navigator !== 'undefined' && navigator.userAgent) {
    NEED_SAFARI_FIX = navigator.userAgent.indexOf('Safari') >= 0;
  }
} catch (e) {}

let concurrency = 1;
if (typeof navigator !== 'undefined') {
  concurrency = Math.min(navigator.hardwareConcurrency || 1, 4);
}

const DEFAULT_PICA_OPTS = {
  tile: 1024,
  concurrency,
  features: [ 'all' ],
  idle: 2000
};

const DEFAULT_RESIZE_OPTS = {
  quality:          3,
  alpha:            false,
  unsharpAmount:    0,
  unsharpRadius:    0.0,
  unsharpThreshold: 0
};

function workerFablic() {
  return {
    value: webworkify(worker),
    destroy: function () {
      this.value.terminate();

      if (typeof window !== 'undefined') {
        let url = window.URL || window.webkitURL || window.mozURL || window.msURL;
        if (url && url.revokeObjectURL && this.value.objectURL) {
          url.revokeObjectURL(this.value.objectURL);
        }
      }
    }
  };
}

////////////////////////////////////////////////////////////////////////////////
// API methods

function Pica(options) {
  if (!(this instanceof Pica)) return new Pica(options);

  this.options = assign(DEFAULT_PICA_OPTS, options || {});

  this.__limit = utils.limiter(this.options.concurrency);

  // List of supported features, according to options & browser/node.js
  this.features = {
    js:   false, // pure JS implementation, can be disabled for testing
    wasm: false, // webassembly implementation for heavy functions
    cib:  false, // resize via createImageBitmap (only FF at this moment)
    ww:   false  // webworkers
  };

  this.__workersPool = null;

  // Store requested features for webworkers
  this.__requested_features = [];

  this.__mathlib = null;
}

Pica.prototype.init = function () {
  if (this.__initPromise) return this.__initPromise;

  let features = this.options.features.slice();

  if (features.indexOf('all') >= 0) {
    features = [ 'cib', 'wasm', 'js', 'ww' ];
  }

  this.__requested_features = features;

  this.__mathlib = new MathLib(features);

  // Check WebWorker support if requested
  if (features.indexOf('ww') >= 0) {
    if ((typeof window !== 'undefined') && ('Worker' in window)) {
      // IE <= 11 don't allow to create webworkers from string. We should check it.
      // https://connect.microsoft.com/IE/feedback/details/801810/web-workers-from-blob-urls-in-ie-10-and-11
      try {
        let wkr = require('webworkify')(function () {});
        wkr.terminate();
        this.features.ww   = true;
        this.__workersPool = new Pool(workerFablic, this.options.idle);
      } catch (__) {}
    }
  }

  let initMath = this.__mathlib.init().then(mathlib => {
    // Copy detected features
    assign(this.features, mathlib.features);
  });

  let checkCib = utils.cib_support().then(status => {
    this.features.cib = status;
  });

  // Init math lib. That's async because can load some
  this.__initPromise = Promise.all([ initMath, checkCib ]).then(() => this);

  return this.__initPromise;
};

Pica.prototype.resize = function (from, to, options) {
  let opts = DEFAULT_RESIZE_OPTS;

  if (!isNaN(options)) {
    opts = assign(opts, { quality: options });
  } else if (options) {
    opts = assign(opts, options);
  }

  opts.toWidth  = to.width;
  opts.toHeigth = to.height;
  opts.width    = from.naturalWidth || from.width;
  opts.height   = from.naturalHeight || from.height;

  let canceled    = false;
  let cancelToken = null;

  if (opts.cancelToken) {
    // Wrap cancelToken to avoid successive resolve & set flag
    cancelToken = opts.cancelToken.then(
      data => { canceled = true; throw data; },
      err  => { canceled = true; throw err; }
    );
  }

  let toCtx = to.getContext('2d', { alpha: Boolean(opts.alpha) });

  return this.init().then(() => {
    if (canceled) return cancelToken;

    // if createImageBitmap supports resize, just do it and return
    if (this.feature_cib) {
      return createImageBitmap(from, {
        resizeWidth:   opts.toWidth,
        resizeHeight:  opts.toHeigth,
        resizeQuality: utils.cib_quality_name(opts.quality)
      })
      .then(imageBitmap => {
        if (canceled) return cancelToken;

        // if no unsharp - draw directly to output canvas
        if (!opts.unsharpAmount) {
          toCtx.drawImage(imageBitmap, 0, 0);
          imageBitmap.close();
          toCtx = null;
          return to;
        }

        let tmpCanvas = document.createElement('canvas');

        tmpCanvas.width  = opts.toWidth;
        tmpCanvas.height = opts.toHeigth;

        let tmpCtx = tmpCanvas.getContext('2d', { alpha: Boolean(opts.alpha) });

        tmpCtx.drawImage(imageBitmap, 0, 0);
        imageBitmap.close();

        let iData = tmpCtx.getImageData(0, 0, opts.toWidth, opts.toHeigth);

        this.__mathlib.unsharp(
          iData.data,
          opts.toWidth,
          opts.toHeigth,
          opts.unsharpAmount,
          opts.unsharpRadius,
          opts.unsharpThreshold
        );

        toCtx.putImageData(iData, 0, 0);
        iData = tmpCtx = tmpCanvas = toCtx = null;
        return to;
      });
    }

    //
    // No easy way, let's resize manually via arrays
    //

    let srcCtx;
    let srcImageBitmap;

    // Share cache between calls:
    //
    // - wasm instance
    // - wasm memory object
    //
    let cache = {};

    // Call resizer in webworker or locally, depending on config
    const invokeResize = opts => {
      return Promise.resolve().then(() => {
        if (!this.features.ww) return this.__mathlib.resizeAndUnsharp(opts, cache);

        return new Promise((resolve, reject) => {
          let w = this.__workersPool.acquire();

          if (cancelToken) cancelToken.catch(err => reject(err));

          w.value.onmessage = ev => {
            w.release();

            if (ev.data.err) reject(ev.data.err);
            else resolve(ev.data.result);
          };

          w.value.postMessage({
            opts,
            features: this.__requested_features,
            preload: {
              wasm_nodule: this.__mathlib.__
            }
          }, [ opts.src.buffer ]);
        });
      });
    };

    const processTile = (tile => this.__limit(() => {
      if (canceled) return cancelToken;

      let srcImageData;

      // Extract tile RGBA buffer, depending on input type
      if (utils.isCanvas(from)) {
        // If input is Canvas - extract region data directly
        srcImageData = srcCtx.getImageData(tile.x, tile.y, tile.width, tile.height);
      } else {
        // If input is Image or decoded to ImageBitmap,
        // draw region to temporary canvas and extract data from it
        //
        // Note! Attempt to reuse this canvas causes significant slowdown in chrome
        //
        let tmpCanvas = document.createElement('canvas');
        tmpCanvas.width  = tile.width;
        tmpCanvas.height = tile.height;

        let tmpCtx = tmpCanvas.getContext('2d', { alpha: Boolean(opts.alpha) });
        tmpCtx.globalCompositeOperation = 'copy';
        tmpCtx.drawImage(srcImageBitmap || from,
          tile.x, tile.y, tile.width, tile.height,
          0, 0, tile.width, tile.height);

        srcImageData = tmpCtx.getImageData(0, 0, tile.width, tile.height);
        tmpCtx = tmpCanvas = null;
      }

      let o = {
        src:              srcImageData.data,
        width:            tile.width,
        height:           tile.height,
        toWidth:          tile.toWidth,
        toHeight:         tile.toHeight,
        scaleX:           tile.scaleX,
        scaleY:           tile.scaleY,
        offsetX:          tile.offsetX,
        offsetY:          tile.offsetY,
        quality:          opts.quality,
        alpha:            opts.alpha,
        unsharpAmount:    opts.unsharpAmount,
        unsharpRadius:    opts.unsharpRadius,
        unsharpThreshold: opts.unsharpThreshold
      };

      return Promise.resolve()
        .then(() => invokeResize(o))
        .then(result => {
          if (canceled) return cancelToken;

          srcImageData = null;

          let toImageData;

          if (typeof ImageData !== 'undefined') {
            // this branch is for browsers
            toImageData = new ImageData(new Uint8ClampedArray(result), tile.toWidth, tile.toHeight);
          } else {
            // fallback for node-canvas
            toImageData = toCtx.createImageData(tile.toWidth, tile.toHeight);
            toImageData.data.set(result);
          }

          if (NEED_SAFARI_FIX) {
            // Safari draws thin white stripes between tiles without this fix
            toCtx.putImageData(toImageData, tile.toX, tile.toY,
              tile.toInnerX - tile.toX, tile.toInnerY - tile.toY,
              tile.toInnerWidth + 1e-5, tile.toInnerHeight + 1e-5);
          } else {
            toCtx.putImageData(toImageData, tile.toX, tile.toY,
              tile.toInnerX - tile.toX, tile.toInnerY - tile.toY,
              tile.toInnerWidth, tile.toInnerHeight);
          }

          return null;
        });
    }));

    // Need normalize data source first. It can be canvas or image.
    // If image - try to decode in background if possible
    return Promise.resolve().then(() => {
      if (utils.isCanvas(from)) {
        srcCtx = from.getContext('2d', { alpha: Boolean(opts.alpha) });
        return null;
      }

      if (utils.isImage(from)) {
        // try do decode image in background for faster next operations
        if (typeof createImageBitmap === 'undefined') return null;

        return createImageBitmap(from)
          .then(imageBitmap => {
            srcImageBitmap = imageBitmap;
          });
      }

      throw new Error('".from" should be image or canvas');
    })
    .then(() => {
      if (canceled) return cancelToken;

      //
      // Here we are with "normalized" source,
      // follow to tiling
      //

      let DEST_TILE_BORDER = 3; // Max possible filter window size

      let regions = createRegions({
        width:        opts.width,
        height:       opts.height,
        srcTileSize:  this.options.tile,
        toWidth:      opts.toWidth,
        toHeight:     opts.toHeigth,
        destTileBorder: Math.ceil(Math.max(DEST_TILE_BORDER, 2.5 * opts.unsharpRadius|0))
      });

      let jobs = regions.map(tile => processTile(tile));

      function cleanup() {
        if (srcImageBitmap) {
          srcImageBitmap.close();
          srcImageBitmap = null;
        }
      }

      return Promise.all(jobs).then(
        () =>  { cleanup(); return to; },
        err => { cleanup(); throw err; }
      );
    });
  });
};

// RGBA buffer resize
//
Pica.prototype.resizeBuffer = function (options) {
  const opts = assign(DEFAULT_RESIZE_OPTS, options);

  return this.init()
    .then(() => this.__mathlib.resizeAndUnsharp(opts));
};

Pica.prototype.toBlob = function (canvas, mimeType, quality) {
  mimeType = mimeType || 'image/png';

  return new Promise(resolve => {
    if (canvas.toBlob) {
      canvas.toBlob(blob => resolve(blob), mimeType, quality);
      return;
    }

    // Fallback for old browsers
    const asString = atob(canvas.toDataURL(mimeType, quality).split(',')[1]);
    const len      = asString.length;
    const asBuffer = new Uint8Array(len);

    for (let i = 0; i < len; i++) {
      asBuffer[i] = asString.charCodeAt(i);
    }

    resolve(new Blob([ asBuffer ], { type: mimeType }));
  });
};

Pica.prototype.debug = function () {};

module.exports = Pica;
<|endoftext|>