UNPKG

spotify-lens

Version:

A enhanced wrapper for Spotify-api. It provides a simple api to the data otherwise have to be computed. It also provides a cli playback control and data export

200 lines (185 loc) 5.44 kB
const SpotifyWebApi = require('spotify-web-api-node') const _ = require('lodash') const { distinctReduceBy, pruneTrack, errorHandler, pruneArtist, } = require('./utils') const chalk = require('chalk') const info = chalk.blue const DEFAULT_LIMIT = 50 const FEATURE_KEYS = [ 'danceability', 'energy', 'key', 'loudness', 'mode', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo', ] module.exports = class extends SpotifyWebApi { constructor(config) { super(config) this.getAccessToken = this.getAccessToken.bind(this) } pageConcater(getter) { return async function(id) { const limit = DEFAULT_LIMIT let total if (id) { total = (await getter(id, { offset: 0, limit })).body.total } else { total = (await getter({ offset: 0, limit })).body.total } const batch = Math.ceil(total / limit) console.log( info(`Total: ${total} objects found, getting in ${batch} batches`), ) return _.range(0, batch).map(async i => id ? getter(id, { offset: i * limit, limit, }) : getter({ offset: i * limit, limit, }), ) } } timeRangeConcater(getter) { return function(opt) { return async function(pruneFn) { const TIME_RANGES = ['long_term', 'medium_term', 'short_term'] const requests = TIME_RANGES.map(tr => getter({ time_range: tr, limit: (opt && opt.limit) || DEFAULT_LIMIT, offset: (opt && opt.offset) || 0, }), ) const responses = await Promise.all(requests) const items = responses.map(r => r.body.items) if (pruneFn) { items.forEach(arr => arr.forEach(pruneFn)) } return _.zipObject(TIME_RANGES, items) } } } async getAllTracks(playlistId) { const getter = playlistId ? this.getPlaylistTracks.bind(this) : this.getMySavedTracks.bind(this) const tasks = await this.pageConcater(getter)(playlistId) const responses = await Promise.all(tasks) const tracks = _.flatMap(responses, el => el.body.items) tracks.forEach(t => { const { track } = t pruneTrack(track) }) return tracks } async getAllArtists(playlistId) { const trackList = await this.getAllTracks(playlistId) const dict = _.flatMap(trackList, el => { el.track.artists.forEach(el => { el.count = 1 }) return el.track.artists }).reduce(distinctReduceBy('id'), {}) const finalList = _.flatMap(Object.values(dict)) console.log(info(`Total: ${finalList.length} artists found`)) return _.orderBy(finalList, ['count', 'name'], ['desc', 'asc']) } async addCurrent() { const { body: { item: { id, name }, }, } = await this.getMyCurrentPlayingTrack() console.log(info(`Now playing ${name}`)) try { await this.addToMySavedTracks([id]) } catch (error) { errorHandler(error)('Failed to add to the library') } } async getAllPlaylists() { const tasks = await this.pageConcater(this.getUserPlaylists.bind(this))() const data = await Promise.all(tasks) const items = _.flatMap(data, el => el.body.items) return items } getTopArtists(options) { return this.timeRangeConcater(this.getMyTopArtists.bind(this))(options)( pruneArtist, ) } getTopTracks(options) { return this.timeRangeConcater(this.getMyTopTracks.bind(this))(options)( pruneTrack, ) } async analyzeGenre(playlistId) { const artistIdList = (await this.getAllArtists(playlistId)).map(el => el.id) const tasks = _.chunk(artistIdList, DEFAULT_LIMIT).map(el => this.getArtists.bind(this)(el), ) const data = await Promise.all(tasks) const genreList = _.flatMapDeep(data, ({ body }) => body.artists.map(a => a.genres.map(g => ({ name: g, count: 1, })), ), ) const genreDict = genreList.reduce(distinctReduceBy('name'), {}) const ordered = Object.values(genreDict) console.log(info(`Total: ${ordered.length} genres found`)) return _.orderBy(ordered, ['count', 'name'], ['desc', 'asc']) } async analyzeGenreTokenized(playlistId) { const genreList = await this.analyzeGenre(playlistId) const tokenDict = _.flatMapDeep(genreList, el => { const tokens = el.name.split(/\s+/) return tokens.map(t => ({ name: t, count: el.count, })) }).reduce(distinctReduceBy('name'), {}) return _.orderBy( Object.values(tokenDict), ['count', 'name'], ['desc', 'asc'], ) } async analyzeAudioFeatures(playlistId) { const trackIdArr = (await this.getAllTracks(playlistId)).map( el => el.track.id, ) const chunks = _.chunk(trackIdArr, DEFAULT_LIMIT) const data = await Promise.all( chunks.map(ch => this.getAudioFeaturesForTracks.bind(this)(ch)), ) const audioFeatures = _.flatMap(data, d => d.body.audio_features) const average = audioFeatures.reduce((acc, cur) => { const obj = {} FEATURE_KEYS.forEach(key => { obj[key] = (acc[key] + cur[key]) / 2 }) return obj }) return _.toPairs(average).map(el => ({ name: el[0], value: el[1], })) } }