├── .gitignore ├── logo.png ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── npm-publish.yml ├── package.json ├── license ├── README.md ├── billboard-top-100.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .vscode -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darthbatman/billboard-top-100/HEAD/logo.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "airbnb-base", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly", 11 | "describe": true, 12 | "it": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018 16 | }, 17 | "rules": { 18 | } 19 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '**' 9 | pull_request: 10 | branches: 11 | - '*' 12 | schedule: 13 | - cron: '0 20 * * *' 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | - run: npm install 26 | - run: npm run lint 27 | 28 | test: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: 'npm' 37 | - run: npm install 38 | - run: npm test || (sleep 600 && npm test) 39 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 16 16 | - run: npm install 17 | - run: npm run lint 18 | 19 | test: 20 | needs: lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: 16 27 | - run: npm install 28 | - run: npm test 29 | 30 | publish: 31 | needs: test 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v2 36 | with: 37 | node-version: 16 38 | registry-url: https://registry.npmjs.org/ 39 | - run: npm install 40 | - run: npm publish 41 | env: 42 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "billboard-top-100", 3 | "version": "2.6.5", 4 | "description": "Gets the top songs, albums, and artists from Billboard's charts", 5 | "main": "billboard-top-100.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "cheerio": "^1.0.0-rc.10", 11 | "moment": "^2.24.0", 12 | "request": "^2.88.0" 13 | }, 14 | "devDependencies": { 15 | "chai": "^4.2.0", 16 | "eslint": "^8.8.0", 17 | "eslint-config-airbnb": "^19.0.4", 18 | "mocha": "^9.2.0" 19 | }, 20 | "scripts": { 21 | "lint": "eslint billboard-top-100.js test/test.js", 22 | "test": "mocha" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/darthbatman/billboard-top-100.git" 27 | }, 28 | "keywords": [ 29 | "billboard", 30 | "charts", 31 | "songs", 32 | "music" 33 | ], 34 | "author": "Rishi Masand (https://github.com/darthbatman)", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/darthbatman/billboard-top-100/issues" 38 | }, 39 | "homepage": "https://github.com/darthbatman/billboard-top-100#readme" 40 | } 41 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Rishi Masand (https://github.com/darthbatman) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |

5 | 6 | Node.js API to retrieve top songs, albums, and artists from Billboard's charts 7 | 8 | ![](https://github.com/darthbatman/billboard-top-100/actions/workflows/build.yml/badge.svg) 9 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/darthbatman/billboard-top-100) 10 | [![](https://img.shields.io/badge/donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=WNNFJMBY2ETG4&no_recurring=0¤cy_code=USD) 11 | 12 | ## install 13 | 14 | ```bash 15 | npm install billboard-top-100 16 | ``` 17 | 18 | ## example 19 | 20 | ### getChart 21 | 22 | ```js 23 | const { getChart } = require('billboard-top-100'); 24 | 25 | // date format YYYY-MM-DD 26 | getChart('hot-100', '2016-08-27', (err, chart) => { 27 | if (err) console.log(err); 28 | // week of the chart in the date format YYYY-MM-DD 29 | console.log(chart.week); 30 | // URL of the previous week's chart 31 | console.log(chart.previousWeek.url); 32 | // date of the previous week's chart in the date format YYYY-MM-DD 33 | console.log(chart.previousWeek.date); 34 | // URL of the next week's chart 35 | console.log(chart.nextWeek.url); 36 | // date of the next week's chart in the date format YYYY-MM-DD 37 | console.log(chart.nextWeek.date); 38 | // array of top 100 songs for week of August 27, 2016 39 | console.log(chart.songs); 40 | // song with rank: 4 for week of August 27, 2016 41 | console.log(chart.songs[3]); 42 | // title of top song for week of August 27, 2016 43 | console.log(chart.songs[0].title); 44 | // artist of top songs for week of August 27, 2016 45 | console.log(chart.songs[0].artist); 46 | // rank of top song (1) for week of August 27, 2016 47 | console.log(chart.songs[0].rank); 48 | // URL for Billboard cover image of top song for week of August 27, 2016 49 | console.log(chart.songs[0].cover); 50 | // position info of top song 51 | console.log(chart.songs[0].position.positionLastWeek); 52 | console.log(chart.songs[0].position.peakPosition); 53 | console.log(chart.songs[0].position.weeksOnChart); 54 | }); 55 | 56 | // chartName defaults to hot-100 57 | // date defaults to Saturday of this week 58 | getChart((err, chart) => { 59 | if (err) console.log(err); 60 | console.log(chart); 61 | }); 62 | 63 | // date defaults to Saturday of this week 64 | getChart('rock-digital-song-sales', (err, chart) => { 65 | if (err) console.log(err); 66 | console.log(chart); 67 | }); 68 | ``` 69 | 70 | ### listCharts 71 | 72 | ```js 73 | // list all charts 74 | const { listCharts } = require('billboard-top-100'); 75 | 76 | listCharts((err, charts) => { 77 | if (err) console.log(err); 78 | // array of all charts 79 | console.log(charts); 80 | }); 81 | ``` 82 | 83 | ## license 84 | 85 | MIT © [Rishi Masand](https://github.com/darthbatman) 86 | 87 | ## donation 88 | If you find `billboard-top-100` useful and would like to support the developer, please consider donating. Thank you. 89 | 90 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate/?business=WNNFJMBY2ETG4&no_recurring=0¤cy_code=USD) 91 | -------------------------------------------------------------------------------- /billboard-top-100.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const cheerio = require('cheerio'); 3 | const moment = require('moment'); 4 | 5 | const BILLBOARD_BASE_URL = 'http://www.billboard.com'; 6 | const BILLBOARD_CHARTS_URL = `${BILLBOARD_BASE_URL}/charts/`; 7 | const BILLBOARD_CHART_CATEGORY_URL_PREFIX = `${BILLBOARD_BASE_URL}/pmc-ajax/charts-fetch-all-chart/selected_category-`; 8 | const BILLBOARD_CHART_CATEGORY_URL_SUFFIX = '/chart_type-weekly/'; 9 | 10 | function getChart(name, date, cb) { 11 | let chartName = name; 12 | let chartDate = date; 13 | let callback = cb; 14 | 15 | if (typeof name === 'function') { 16 | // if name not specified, default to hot-100 chart for current week, 17 | // and set callback method accordingly 18 | callback = name; 19 | chartName = 'hot-100'; 20 | chartDate = ''; 21 | } 22 | 23 | if (typeof date === 'function') { 24 | // if date not specified, default to specified chart for current week, 25 | // and set callback method accordingly 26 | callback = date; 27 | chartDate = ''; 28 | } 29 | 30 | const chart = {}; 31 | chart.songs = []; 32 | 33 | const requestURL = `${BILLBOARD_CHARTS_URL}${chartName}/${chartDate}`; 34 | request(requestURL, (error, response, html) => { 35 | if (error) { 36 | callback(error, null); 37 | return; 38 | } 39 | 40 | const $ = cheerio.load(html); 41 | 42 | let d = null; 43 | for (let i = 0; i < $('.c-heading').length; i += 1) { 44 | if ($('.c-heading')[i].children[0].data.includes('Week of ')) { 45 | d = moment(new Date($('.c-heading')[i].children[0].data.trim().slice('Week of '.length))); 46 | break; 47 | } 48 | } 49 | 50 | chart.week = d.format('YYYY-MM-DD'); 51 | 52 | const prevWeek = d.subtract(7, 'days').format('YYYY-MM-DD'); 53 | chart.previousWeek = { 54 | date: prevWeek, 55 | url: `${BILLBOARD_CHARTS_URL}${chartName}/${prevWeek}`, 56 | }; 57 | 58 | const nextWeek = d.add(14, 'days').format('YYYY-MM-DD'); 59 | chart.nextWeek = { 60 | date: nextWeek, 61 | url: `${BILLBOARD_CHARTS_URL}${chartName}/${nextWeek}`, 62 | }; 63 | 64 | const chartItems = $('.o-chart-results-list-row-container'); 65 | for (let i = 0; i < chartItems.length; i += 1) { 66 | const infoContainer = chartItems[i].children[1]; 67 | const titleAndArtistContainer = infoContainer.children[7].children[1].children[1]; 68 | const posInfo = infoContainer.children[7].children[1]; 69 | 70 | const rank = parseInt(infoContainer.children[1].children[1].children[0].data.trim(), 10); 71 | const title = titleAndArtistContainer.children[1].children[0].data.trim(); 72 | const artist = titleAndArtistContainer.children[3] 73 | ? titleAndArtistContainer.children[3].children[0].data.trim() : undefined; 74 | const cover = infoContainer.children[3].children[1].children[1].children[1].attribs['data-lazy-src']; 75 | const position = { 76 | positionLastWeek: parseInt(posInfo.children[7].children[1].children[0].data.trim(), 10), 77 | peakPosition: parseInt(posInfo.children[9].children[1].children[0].data.trim(), 10), 78 | weeksOnChart: parseInt(posInfo.children[11].children[1].children[0].data.trim(), 10), 79 | }; 80 | 81 | if (artist) { 82 | chart.songs.push({ 83 | rank, 84 | title, 85 | artist, 86 | cover, 87 | position, 88 | }); 89 | } else { 90 | chart.songs.push({ 91 | rank, 92 | artist: title, 93 | cover, 94 | position, 95 | }); 96 | } 97 | } 98 | 99 | if (chart.songs.length > 1) { 100 | callback(null, chart); 101 | } else { 102 | callback('Songs not found.', null); 103 | } 104 | }); 105 | } 106 | 107 | const getChartsFromCategories = async (categoryURLs, cb) => { 108 | const charts = []; 109 | 110 | const promises = categoryURLs.map((categoryURL) => new Promise(((res) => { 111 | request(categoryURL, (error, response, html) => { 112 | if (error) { 113 | res(); 114 | } 115 | const $ = cheerio.load(JSON.parse(html).html); 116 | 117 | const chartLinks = $('a.lrv-u-flex.lrv-u-flex-direction-column'); 118 | for (let i = 0; i < chartLinks.length; i += 1) { 119 | if (chartLinks[i].attribs.href.startsWith('/charts/')) { 120 | charts.push({ name: chartLinks[i].children[1].children[1].children[0].data.trim(), url: `${BILLBOARD_BASE_URL}${chartLinks[i].attribs.href}` }); 121 | } 122 | } 123 | res(); 124 | }); 125 | }))); 126 | 127 | Promise.all(promises).then(() => { 128 | cb(charts); 129 | }); 130 | }; 131 | 132 | function listCharts(cb) { 133 | if (typeof cb !== 'function') { 134 | cb('Specified callback is not a function.', null); 135 | return; 136 | } 137 | 138 | request(BILLBOARD_CHARTS_URL, (error, response, html) => { 139 | if (error) { 140 | cb(error, null); 141 | return; 142 | } 143 | 144 | const $ = cheerio.load(html); 145 | 146 | const categoryElements = $('.o-nav__list-item.lrv-u-color-grey-medium-dark'); 147 | const categoryURLs = []; 148 | for (let i = 0; i < categoryElements.length; i += 1) { 149 | if (categoryElements[i].children && categoryElements[i].children[1].attribs.href === '#') { 150 | const categoryName = encodeURIComponent(categoryElements[i].children[1].attribs.rel); 151 | categoryURLs.push(`${BILLBOARD_CHART_CATEGORY_URL_PREFIX}${categoryName}${BILLBOARD_CHART_CATEGORY_URL_SUFFIX}`); 152 | } 153 | } 154 | 155 | getChartsFromCategories(categoryURLs, (charts) => { 156 | if (charts.length > 0) { 157 | cb(null, charts); 158 | } else { 159 | cb('No charts found.', null); 160 | } 161 | }); 162 | }); 163 | } 164 | 165 | module.exports = { 166 | getChart, 167 | listCharts, 168 | }; 169 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const { getChart, listCharts } = require('../billboard-top-100'); 4 | 5 | describe('getChart()', () => { 6 | describe('get a past chart (hot-100: week of 2016-11-19)', () => { 7 | it('should callback with the hot-100 chart for the week of 2016-11-19', (done) => { 8 | getChart('hot-100', '2016-11-19', (err, chart) => { 9 | if (err) done(err); 10 | 11 | assert.equal(chart.week, '2016-11-19', 'chart week is `2016-11-19`'); 12 | assert.equal(chart.previousWeek.date, '2016-11-12', 'date of chart\'s previous week is `2016-11-12`'); 13 | assert.equal(chart.previousWeek.url, 'http://www.billboard.com/charts/hot-100/2016-11-12', 'url of chart\'s previous week is correct'); 14 | assert.equal(chart.nextWeek.date, '2016-11-26', 'date of chart\'s next week is `2016-11-26`'); 15 | assert.equal(chart.nextWeek.url, 'http://www.billboard.com/charts/hot-100/2016-11-26', 'url of chart\'s next week is correct'); 16 | 17 | assert.lengthOf(chart.songs, 100, 'chart has 100 songs'); 18 | 19 | assert.deepEqual(chart.songs[0], { 20 | rank: 1, 21 | title: 'Closer', 22 | artist: 'The Chainsmokers Featuring Halsey', 23 | cover: 'https://charts-static.billboard.com/img/2013/11/the-chainsmokers-39n-87x87.jpg', 24 | position: { positionLastWeek: 1, peakPosition: 1, weeksOnChart: 14 }, 25 | }, 'first song is correct'); 26 | 27 | assert.deepEqual(chart.songs[37], { 28 | rank: 38, 29 | title: 'Tiimmy Turner', 30 | artist: 'Desiigner', 31 | cover: 32 | 'https://charts-static.billboard.com/img/2016/08/desiigner-l9j-106x106.jpg', 33 | position: { positionLastWeek: 37, peakPosition: 34, weeksOnChart: 15 }, 34 | }, 'arbitrary (38th) song is correct'); 35 | 36 | assert.deepEqual(chart.songs[99], { 37 | rank: 100, 38 | title: 'Cool Girl', 39 | artist: 'Tove Lo', 40 | cover: 'https://charts-static.billboard.com/img/2013/10/tove-lo-8n0-180x180.jpg', 41 | position: { positionLastWeek: NaN, peakPosition: 84, weeksOnChart: 5 }, 42 | }, 'last song is correct'); 43 | 44 | done(); 45 | }); 46 | }).timeout(10000); 47 | }); 48 | 49 | describe('get a current chart (hot-100)', () => { 50 | it('should callback with the hot-100 chart for this week', (done) => { 51 | getChart('hot-100', (err, chart) => { 52 | if (err) done(err); 53 | 54 | assert.lengthOf(chart.songs, 100, 'chart has 100 songs'); 55 | 56 | const firstSong = chart.songs[0]; 57 | assert(firstSong, 'first song is non-null and defined'); 58 | assert(firstSong.rank, 'first song has rank'); 59 | assert(firstSong.title, 'first song has title'); 60 | assert(firstSong.artist, 'first song has artist'); 61 | assert(firstSong.position, 'first song has non-null and defined position'); 62 | assert(firstSong.position.peakPosition, 'first song has peak position'); 63 | assert(firstSong.position.weeksOnChart, 'first song has weeks on chart'); 64 | 65 | const arbitrarySong = chart.songs[38]; 66 | assert(arbitrarySong, 'arbitrary (38th) song is non-null and defined'); 67 | assert(arbitrarySong.rank, 'arbitrary (38th) song has rank'); 68 | assert(arbitrarySong.title, 'arbitrary (38th) song has title'); 69 | assert(arbitrarySong.artist, 'arbitrary (38th) song has artist'); 70 | assert(arbitrarySong.cover, 'arbitrary (38th) song has cover'); 71 | assert(arbitrarySong.position, 'arbitrary (38th) song has non-null and defined position'); 72 | assert(arbitrarySong.position.peakPosition, 'arbitrary (38th) song has peak position'); 73 | assert(arbitrarySong.position.weeksOnChart, 'arbitrary (38th) song has weeks on chart'); 74 | 75 | const lastSong = chart.songs[99]; 76 | assert(lastSong, 'last song is non-null and defined'); 77 | assert(lastSong.rank, 'last song has rank'); 78 | assert(lastSong.title, 'last song has title'); 79 | assert(lastSong.artist, 'last song has artist'); 80 | assert(lastSong.cover, 'last song has cover'); 81 | assert(lastSong.position, 'last song has non-null and defined position'); 82 | assert(lastSong.position.peakPosition, 'last song has peak position'); 83 | assert(lastSong.position.weeksOnChart, 'last song has weeks on chart'); 84 | 85 | done(); 86 | }); 87 | }).timeout(10000); 88 | }); 89 | 90 | describe('get a current chart (latin-songs)', () => { 91 | it('should callback with the latin-songs chart for this week', (done) => { 92 | getChart('latin-songs', (err, chart) => { 93 | if (err) done(err); 94 | 95 | assert.lengthOf(chart.songs, 50, 'chart has 50 songs'); 96 | 97 | const firstSong = chart.songs[0]; 98 | assert(firstSong, 'first song is non-null and defined'); 99 | assert(firstSong.rank, 'first song has rank'); 100 | assert(firstSong.title, 'first song has title'); 101 | assert(firstSong.artist, 'first song has artist'); 102 | assert(firstSong.position, 'first song has non-null and defined position'); 103 | assert(firstSong.position.peakPosition, 'first song has peak position'); 104 | assert(firstSong.position.weeksOnChart, 'first song has weeks on chart'); 105 | 106 | const arbitrarySong = chart.songs[38]; 107 | assert(arbitrarySong, 'arbitrary (38th) song is non-null and defined'); 108 | assert(arbitrarySong.rank, 'arbitrary (38th) song has rank'); 109 | assert(arbitrarySong.title, 'arbitrary (38th) song has title'); 110 | assert(arbitrarySong.artist, 'arbitrary (38th) song has artist'); 111 | assert(arbitrarySong.cover, 'arbitrary (38th) song has cover'); 112 | assert(arbitrarySong.position, 'arbitrary (38th) song has non-null and defined position'); 113 | assert(arbitrarySong.position.peakPosition, 'arbitrary (38th) song has peak position'); 114 | assert(arbitrarySong.position.weeksOnChart, 'arbitrary (38th) song has weeks on chart'); 115 | 116 | const lastSong = chart.songs[49]; 117 | assert(lastSong, 'last song is non-null and defined'); 118 | assert(lastSong.rank, 'last song has rank'); 119 | assert(lastSong.title, 'last song has title'); 120 | assert(lastSong.artist, 'last song has artist'); 121 | assert(lastSong.cover, 'last song has cover'); 122 | assert(lastSong.position, 'last song has non-null and defined position'); 123 | assert(lastSong.position.peakPosition, 'last song has peak position'); 124 | assert(lastSong.position.weeksOnChart, 'last song has weeks on chart'); 125 | 126 | done(); 127 | }); 128 | }).timeout(10000); 129 | }); 130 | 131 | describe('get the current artist chart (artist-100)', () => { 132 | it('should callback with the artist-100 chart for this week', (done) => { 133 | getChart('artist-100', (err, chart) => { 134 | if (err) done(err); 135 | 136 | assert.lengthOf(chart.songs, 100, 'chart has 100 artists'); 137 | 138 | const firstArtist = chart.songs[0]; 139 | assert(firstArtist, 'first artist is non-null and defined'); 140 | assert(firstArtist.rank, 'first artist has rank'); 141 | assert(firstArtist.artist, 'first artist has artist'); 142 | assert(firstArtist.position, 'first artist has non-null and defined position'); 143 | assert(firstArtist.position.peakPosition, 'first artist has peak position'); 144 | assert(firstArtist.position.weeksOnChart, 'first artist has weeks on chart'); 145 | 146 | const arbitraryArtist = chart.songs[38]; 147 | assert(arbitraryArtist, 'arbitrary (38th) artist is non-null and defined'); 148 | assert(arbitraryArtist.rank, 'arbitrary (38th) artist has rank'); 149 | assert(arbitraryArtist.artist, 'arbitrary (38th) artist has artist'); 150 | assert(arbitraryArtist.cover, 'arbitrary (38th) artist has cover'); 151 | assert(arbitraryArtist.position, 'arbitrary (38th) artist has non-null and defined position'); 152 | assert(arbitraryArtist.position.peakPosition, 'arbitrary (38th) artist has peak position'); 153 | assert(arbitraryArtist.position.weeksOnChart, 'arbitrary (38th) artist has weeks on chart'); 154 | 155 | const lastArtist = chart.songs[99]; 156 | assert(lastArtist, 'last artist is non-null and defined'); 157 | assert(lastArtist.rank, 'last artist has rank'); 158 | assert(lastArtist.artist, 'last artist has artist'); 159 | assert(lastArtist.cover, 'last artist has cover'); 160 | assert(lastArtist.position, 'last artist has non-null and defined position'); 161 | assert(lastArtist.position.peakPosition, 'last artist has peak position'); 162 | assert(lastArtist.position.weeksOnChart, 'last artist has weeks on chart'); 163 | 164 | done(); 165 | }); 166 | }).timeout(10000); 167 | }); 168 | }); 169 | 170 | describe('listCharts()', () => { 171 | describe('get all charts', () => { 172 | it('should callback with all available charts', (done) => { 173 | listCharts((err, charts) => { 174 | if (err) done(err); 175 | 176 | assert(charts, 'charts is non-null'); 177 | assert(charts.length, 'charts is non-empty'); 178 | 179 | for (let i = 0; i < charts.length; i += 1) { 180 | assert(charts[i], 'chart element is non-null and defined'); 181 | assert(charts[i].name, 'chart element has name'); 182 | assert(charts[i].url, 'chart element has url'); 183 | assert(charts[i].url.split('http://www.billboard.com/charts/')[1], 'chart has correct format'); 184 | } 185 | 186 | done(); 187 | }); 188 | }).timeout(10000); 189 | }); 190 | }); 191 | --------------------------------------------------------------------------------