Source: server/SimpleFreesound.js

import http from 'http';
import https from 'https';
import path from 'path';
import fs from 'fs';

import FreesoundQuery from '../common/FreesoundQuery';

const cwd = process.cwd();

const defaults = {
  destination: '.',
  publicPath: 'public',
  storeSoundsInfo: false,
};

/**
 * @memberof module:server
 *
 * @class <b><h5>server.SimpleFreesound</h5></b>
 *
 * Server side class for use in <code>Node.js</code>, allowing to query detailed
 * info on sounds and download them from
 * <a href="http://freesound.org" target="_blank">freesound</a>.
 *
 * - members
 *     - [soundsInfo]{@link module:server.SimpleFreesound#soundsInfo}
 *     - [currentSoundsInfo]{@link module:server.SimpleFreesound#currentSoundsInfo}
 * - methods
 *     - [query]{@link module:server.SimpleFreesound#query}
 *     - [queryFromIds]{@link module:server.SimpleFreesound#queryFromIds}
 *     - [download]{@link module:server.SimpleFreesound#download}
 *     - [queryAndDownload]{@link module:server.SimpleFreesound#queryAndDownload}
 *     - [clear]{@link module:server.SimpleFreesound#clear}
 *     - [readFromFile]{@link module:server.SimpleFreesound#readFromFile}
 *     - [writeToFile]{@link module:server.SimpleFreesound#writeToFile}
 *
 * Powered by
 * <a href="http://freesound.org/docs/api/" target="_blank">freesound api</a>.
 *
 * @param {String} apiKey - Your api key, as generated from your freesound
 * developer account when creating a new application.
 * @param {Object} options - Configuration options.
 * @param {String} [options.publicPath='public'] - The public path (relative to node's cwd) of the folder where clients can access files.
 * @param {String} [options.destination='.'] - The path (relative to the public path) of the folder where to download files.
 * @param {Boolean} [options.storeSoundsInfo=false] - Store all sounds detailed informations,
 * including preview urls, to optimize the number of queries to the API (can be memory consuming).
 *
 * @example
 * import SimpleFreesound from 'simple-freesound';
 *
 * const fs = new SimpleFreesound('your_freesound_api_key_goes_here', {
 *   destination: './downloads'
 * });
 * fs.query({
 *   search: [ 'space', 'insect' ],
 *   duration: [ 1, 20 ],
 * })
 * .then(() => fs.download())
 * .then(() => {
 *   console.log(fs.currentSoundsInfo);
 * });
 */
class SimpleFreesound extends FreesoundQuery {
  /** @constructor */
  constructor(apiKey, options = {}) {
    const opts = Object.assign({}, defaults, options);
    super(apiKey, opts.storeSoundsInfo);
    this.destination = opts.destination;
    this.publicPath = opts.publicPath;
  }

  /**
   * An object containing every detailed information obtained since
   * instantiation or last call to
   * [<code>clear()</code>]{@link module:server.SimpleFreesound#clear}.
   *
   * @property {Object} soundsInfo
   */
  get soundsInfo() {
    return this._mapToObject(this._soundsInfo);
  }

  set soundsInfo(si) {
    this._soundsInfo = this._objectToMap(si);
  }

  /**
   * An object containing the detailed information obtained from the last call to
   * [<code>query()</code>]{@link module:server.SimpleFreesound#query},
   * [<code>queryFromIds()</code>]{@link module:server.SimpleFreesound#queryFromIds},
   * [<code>download()</code>]{@link module:server.SimpleFreesound#download} or
   * [<code>queryAndDownload()</code>]{@link module:server.SimpleFreesound#queryAndDownload}.
   *
   * @property {Object} currentSoundsInfo
   */
  get currentSoundsInfo() {
    return this._mapToObject(this._currentSoundsInfo);
  }

  set currentSoundsInfo(csi) {
    this._currentSoundsInfo = this._objectToMap(csi);
  }

  /**
   * Get a list of sound ids with detailed information, that correspond to a set of query parameters.
   *
   * @param {Object} queryParams - The parameters used to build the query.
   * @param {Array.String} [queryParams.search] - The search terms that will be used to build the query.
   * @param {Array.String} [queryParams.username] - A list of usernames to search files from.
   * @param {Array} [queryParams.duration] - An array of size 2 : [ minDuration, maxDuration ] (in seconds).
   * If maxDuration is not a number, it will be interpreted as "*" (no maximum duration).
   *
   * @returns {Promise} A Promise object that resolves with the list of new sound ids if the query goes well.
   *
   * @throws {Error} An error if a problem occurs during the query.
   */
  query(queryParams) {
    return super.query(queryParams);
  }

  /**
   * Get detailed information of sounds from their ids.
   *
   * @param {Array.Number} ids - The ids of the sounds we want to get the detailed info of.
   *
   * @returns {Promise} A promise that will resolve with an array of the sound ids
   * the detailed info of which needed to be queried.
   *
   * @throws {Error} An error if a problem occurs during the query.
   */
  queryFromIds(ids) {
    return super.queryFromIds(ids);
  }

  /**
   * Download hq mp3 previews from their sound ids.
   *
   * @param {Array.Number} [ids=null] - The ids of the sounds to download.
   * If <code>null</code>, the ids from
   * [<code>currentSoundsInfo</code>]{@link module:server.SimpleFreesound#currentSoundsInfo}
   * will be used.
   *
   * @returns {Promise} A promise that will resolve if the downloads go well.
   *
   * @throws {Error} An error if a problem occurs during the downloads.
   */
  download(ids = null) {
    if (ids === null) {
      ids = Array.from(this._currentSoundsInfo.keys());
    }

    return this._downloadFilesFromUrls(ids);
  }

  /**
   * Download hq mp3 previews from queried sound information.
   *
   * @param {Object} queryParams - The parameters used to build the query.
   * @param {Array.String} [queryParams.search] - The search terms that will be used to build the query.
   * @param {Array.String} [queryParams.username] - A list of usernames to search files from.
   * @param {Array} [queryParams.duration] - An array of size 2 : [ minDuration, maxDuration ] (in seconds).
   * If maxDuration is not a number, it will be interpreted as "*" (no maximum duration).
   *
   * @param {String} [destination='.'] - The folder in which to save the downloaded files.
   *
   * @returns {Promise} A Promise object that resolves if the downloads go well.
   *
   * @throws {Error} An error if a problem occurs during the downloads.
   */
  queryAndDownload(queryParams) {
    return new Promise((resolve, reject) => {
      super.query(queryParams)
        .then(updatedIds => this._downloadFilesFromUrls(updatedIds))
        .then(updatedIds => resolve(updatedIds))
    });
  }

  /***
   * Cancel all unresolved yet promises (queries and downloads).
   */
  abort() {
    // TODO (no native way to cancel unresolved yet promises)
    // maybe using Promise.race() with a cancellable promise and
    // the result of Promise.all in a same Array / iterable ... ?
  }

  /** @private */
  _downloadFilesFromUrls(ids) {
    const promises = [];

    for (let i = 0; i < ids.length; i++) {
      promises.push(this._downloadFileFromUrl(ids[i]));
    }

    return Promise.all(promises);
  }

  /** @private */
  _downloadFileFromUrl(id) {
    return new Promise((resolve, reject) => {
      const dst = path.join(cwd, this.publicPath, this.destination, `${id}.mp3`)
      const file = fs.createWriteStream(dst);
      let url = this._soundsInfo.get(id).previews['preview-hq-mp3'];
      url = url.split(':');
      url[0] = 'https';
      url = url.join(':');

      const request = https.get(
        url,
        response => {
          response.pipe(file);

          file.on('finish', () => {
            const url = path.join(this.destination, `${id}.mp3`);
            this._soundsInfo.get(id).localUrl = url;
            this._currentSoundsInfo.get(id).localUrl = url;
            file.close();
            resolve();
          });

          file.on('error', err => {
            console.error(err);
            fs.unlink(dst);
            throw new Error(`Error downloading file ${id} : ${err}`);
          });
        }
      );
    });
  }

  /**
   * Clear the internal sound information lists.
   */
  clear() {
    this._soundsInfo = new Map();
    this._currentSoundsInfo = new Map();
  }

  /**
   * Retrieve sound information list from a JSON file.
   *
   * @param {String} filename - Url of the file to read.
   */
  readFromFile(filename) {
    var si = JSON.parse(fs.readFileSync(filename, 'utf-8'));

    if (si) {
      this._soundsInfo = new Map();

      for (let i in si) {
        this._soundsInfo.set(si[i]['id'], si[i]);
      }
    }
  }

  /**
   * Dump sound information list to a JSON file.
   *
   * @param {String} filename - Url of the file to dump the list of file information to.
   */
  writeToFile(filename) {
    fs.writeFileSync(
      filename,
      JSON.stringify(this._mapToObject(this._soundsInfo), null, 2),
      'utf-8'
    );
  }
};

export default SimpleFreesound;