Spotify.js

/*
 *  Simple Javascript wrapper for the Spotify API
 *
 *  Copyright (c) 2021 Puyodead1
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 */

const fetch = require("node-fetch");

const Albums = require("./apis/Albums");
const Artists = require("./apis/Artists");
const Browse = require("./apis/Browse");
const Playlists = require("./apis/Playlists");
const Tracks = require("./apis/Tracks");
const Users = require("./apis/Users");

const Album = require("./objects/Album");
const Artist = require("./objects/Artist");
const Playlist = require("./objects/Playlist");
const Track = require("./objects/Track");

/** Main client */
class Spotify {
  /**
   * Creates a new spotify instance
   * @param {String} clientID spotify client id
   * @param {String} clientSecret spotify client sercret
   */
  constructor(clientID, clientSecret) {
    this._clientID = clientID;
    this._clientSecret = clientSecret;

    this.albums = new Albums(this);
    this.artists = new Artists(this);
    this.browse = new Browse(this);
    this.playlists = new Playlists(this);
    this.tracks = new Tracks(this);
    this.users = new Users(this);

    this.authenticated = false;
    this._access_token = null;
    this._token_type = null;
    this._expires_in = null;

    this.nextRequest = null;

    this.API_BASE = "https://api.spotify.com/v1";
  }

  createTimer() {
    this.nextRequest = setTimeout(async () => {
      console.debug("Refreshing token...");
      await this.getToken();
      this.createTimer();
      console.debug(`Refreshing token in ${this._expires_in} seconds`);
    }, this._expires_in * 1000);
  }

  /**
   * Gets initial tokens and create a refresh timer
   * @returns void
   */
  async login() {
    if (this.nextRequest) return;

    try {
      await this.getToken();
      this.createTimer();
      console.debug(`Refreshing token in ${this._expires_in} seconds`);
    } catch (e) {
      throw e;
    }
  }

  /**
   * Attempts to log the client in and obtain a token
   * @async
   * @returns object promise with access token, token type and expires in
   */
  async getToken() {
    return new Promise((resolve, reject) => {
      fetch("https://accounts.spotify.com/api/token", {
        method: "post",
        headers: {
          Authorization: `Basic ${new Buffer.from(`${this._clientID}:${this._clientSecret}`).toString("base64")}`,
        },
        body: new URLSearchParams({ grant_type: "client_credentials" }),
      })
        .then((res) => res.json())
        .then((res) => {
          this._access_token = res.access_token;
          this._token_type = res.token_type;
          this._expires_in = res.expires_in;
          resolve();
        })
        .catch((error) => {
          reject(error);
        });
    });
  }

  /**
   * Makes a request to the api
   * @param {String} url
   * @returns Promise
   */
  makeRequest(url) {
    return new Promise((resolve, reject) => {
      fetch(this.API_BASE + url, {
        method: "get",
        headers: { Authorization: `${this._token_type} ${this._access_token}` },
      })
        .then((res) => res.json())
        .then((res) => {
          if (res.error) reject(res);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
  }

  /**
   * Searches Spotify API
   * @param {String} q - the search query
   * @param {Number} limit - max number of results to return; default: 20, min: 1, max: 50; *LIMIT IS APPLIED FOR EACH TYPE, NOT TOTAL RESPONSE*
   * @param {Array} type - list of types to search for
   * @returns Object of arrays for each type
   *
   * {@link https://developer.spotify.com/documentation/web-api/reference/search/search/}
   */
  search(q, limit, types) {
    return new Promise((resolve, reject) => {
      this.makeRequest(`/search?q=${q}&type=${types.join(",")}&limit=${limit}`)
        .then((res) => {
          const results = {};
          types.forEach((type) => {
            results[`${type}s`] = [];
            res[`${type}s`].items.forEach((item) => {
              switch (type) {
                case "album":
                  results[`${type}s`].push(new Album(item));
                  break;
                case "artist":
                  results[`${type}s`].push(new Artist(item));
                  break;
                case "playlist":
                  results[`${type}s`].push(new Playlist(item));
                  break;
                case "track":
                  results[`${type}s`].push(new Track(item));
                  break;
              }
            });
          });
          resolve(results);
        })
        .catch((error) => reject(error));
    });
  }
}

module.exports = Spotify;