2021-01-19 21:37:31 +01:00
|
|
|
const fetch = require('node-fetch');
|
|
|
|
const utils = require('./utils.js');
|
|
|
|
const parser = require('./parser.js');
|
2021-01-26 08:58:39 +01:00
|
|
|
const Cache = require('./cache.js');
|
2021-01-19 21:37:31 +01:00
|
|
|
|
2021-01-26 08:58:39 +01:00
|
|
|
const _cache = new Cache({ constant: 3600, page: 300 }, { page: 10 });
|
2021-01-19 21:37:31 +01:00
|
|
|
|
|
|
|
async function discover(params, options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat, 9),
|
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat, 21)
|
|
|
|
};
|
|
|
|
|
2021-01-23 14:49:34 +01:00
|
|
|
let resultParams;
|
|
|
|
return sanitizeDiscoverParams(params)
|
|
|
|
.then( sanitizedParams => {
|
2021-01-23 21:24:44 +01:00
|
|
|
resultParams = Object.assign({}, sanitizedParams);
|
|
|
|
// Passing an 'all' type subgenre (e.g. 'all-metal') in the discover url
|
|
|
|
// actually returns far fewer / zero results than without.
|
|
|
|
// The Bandcamp site also does not seem to include it in its discover requests...
|
|
|
|
if (sanitizedParams.time !== undefined) {
|
|
|
|
// If 'time' exists in sanitized params, then we have an 'all' type subgenre
|
|
|
|
// - refer to sanitizeDiscoverParams()
|
|
|
|
delete sanitizedParams.subgenre;
|
|
|
|
}
|
2021-01-23 14:49:34 +01:00
|
|
|
return utils.getDiscoverUrl(sanitizedParams);
|
|
|
|
})
|
2021-01-26 08:58:39 +01:00
|
|
|
.then( url => _fetchPage(url, true) )
|
2021-01-23 14:49:34 +01:00
|
|
|
.then( json => {
|
|
|
|
const result = parser.parseDiscoverResults(json, opts);
|
|
|
|
result.params = resultParams;
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function sanitizeDiscoverParams(params) {
|
|
|
|
return getDiscoverOptions().then( options => {
|
2021-01-24 14:04:07 +01:00
|
|
|
const getOptionValue = (optArr, value, defaultIndex = 0) => {
|
2021-01-23 14:49:34 +01:00
|
|
|
if (value !== undefined && optArr) {
|
2021-01-23 19:41:26 +01:00
|
|
|
const opt = optArr.find( o => o.value == value );
|
2021-01-23 14:49:34 +01:00
|
|
|
if (opt) {
|
|
|
|
return opt.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (optArr) {
|
2021-01-24 14:04:07 +01:00
|
|
|
return optArr[defaultIndex].value;
|
2021-01-23 14:49:34 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const sanitized = {
|
|
|
|
genre: getOptionValue(options.genres, params.genre),
|
|
|
|
sortBy: getOptionValue(options.sortBys, params.sortBy),
|
|
|
|
page: params.page || 0
|
|
|
|
};
|
|
|
|
if (sanitized.sortBy !== 'rec') {
|
|
|
|
// following only valid when sortBy is not 'rec' (artist-recommend)
|
2021-01-23 19:10:28 +01:00
|
|
|
const subgenreOptions = options.subgenres[sanitized.genre];
|
|
|
|
if (subgenreOptions) { // false if genre is 'all'
|
|
|
|
sanitized.subgenre = getOptionValue(subgenreOptions, params.subgenre);
|
2021-01-23 14:49:34 +01:00
|
|
|
}
|
2021-01-23 21:24:44 +01:00
|
|
|
// 'Time' option only available when there is effectively no subgenre (e.g. genre is 'all'
|
|
|
|
// or subgenre is 'all-metal')
|
2021-01-23 19:41:26 +01:00
|
|
|
const timeAllowed = sanitized.subgenre === undefined || sanitized.subgenre == subgenreOptions[0].value;
|
2021-01-23 19:10:28 +01:00
|
|
|
if (timeAllowed) {
|
2021-01-24 14:04:07 +01:00
|
|
|
sanitized.time = getOptionValue(options.times, params.time, 1);
|
2021-01-23 14:49:34 +01:00
|
|
|
}
|
|
|
|
sanitized.location = getOptionValue(options.locations, params.location);
|
|
|
|
sanitized.format = getOptionValue(options.formats, params.format);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
sanitized.artistRecommendationType = getOptionValue(options.artistRecommendationTypes, params.artistRecommendationType);
|
|
|
|
}
|
|
|
|
|
|
|
|
return sanitized;
|
|
|
|
});
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getDiscoverOptions() {
|
2021-01-26 08:58:39 +01:00
|
|
|
return _cache.getOrSet('constant', 'discoverOptions', () => {
|
|
|
|
return _fetchPage(utils.getSiteUrl()).then( html => parser.parseDiscoverOptions(html) );
|
|
|
|
});
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getImageFormat(idOrName) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
let result = null;
|
|
|
|
imageConstants.formats.every( format => {
|
|
|
|
if ( (typeof idOrName === 'string' && format.name === idOrName) ||
|
|
|
|
(Number.isInteger(idOrName) && format.id === idOrName) ) {
|
|
|
|
result = format;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getImageFormats(filter = '') {
|
|
|
|
return _getImageConstants().then( constants => {
|
|
|
|
if (filter === 'album') {
|
|
|
|
return constants.formats.filter( c => c.name.startsWith('art_') );
|
|
|
|
}
|
|
|
|
else if (filter === 'artist') {
|
|
|
|
return constants.formats.filter( c => c.name.startsWith('bio_') );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return constants.formats;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _getImageConstants() {
|
2021-01-26 08:58:39 +01:00
|
|
|
return _cache.getOrSet('constant', 'imageConstants', () => {
|
|
|
|
return _fetchPage(utils.getSiteUrl()).then( html => parser.parseImageConstants(html) );
|
|
|
|
});
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function _parseImageFormatArg(arg, defaultId = null) {
|
|
|
|
let result;
|
|
|
|
if (typeof arg === 'string' || Number.isInteger(arg)) {
|
|
|
|
result = await getImageFormat(arg);
|
|
|
|
}
|
|
|
|
else if (typeof arg === 'object') {
|
|
|
|
result = arg;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
result = null;
|
|
|
|
}
|
|
|
|
if (result === null && defaultId !== null) {
|
|
|
|
result = await getImageFormat(defaultId);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
async function getAlbumInfo(albumUrl, options = {}) {
|
|
|
|
const opts = {
|
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat),
|
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat),
|
|
|
|
includeRawData: options.includeRawData ? true : false
|
|
|
|
};
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(albumUrl).then( html => parser.parseAlbumInfo(html, opts) );
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getTrackInfo(trackUrl, options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
2021-01-22 18:53:31 +01:00
|
|
|
trackUrl,
|
2021-01-19 21:37:31 +01:00
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
2021-01-20 13:53:12 +01:00
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat, 9),
|
2021-01-19 21:37:31 +01:00
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat),
|
|
|
|
includeRawData: options.includeRawData ? true : false
|
|
|
|
};
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(trackUrl).then( html => parser.parseTrackInfo(html, opts) );
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getDiscography(artistOrLabelUrl, options = {}) {
|
2021-01-21 17:02:02 +01:00
|
|
|
const imageConstants = await _getImageConstants();
|
2021-01-19 21:37:31 +01:00
|
|
|
const opts = {
|
2021-01-21 17:02:02 +01:00
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
2021-01-19 21:37:31 +01:00
|
|
|
artistOrLabelUrl,
|
2021-01-21 17:47:12 +01:00
|
|
|
imageFormat: await _parseImageFormatArg(options.imageFormat, 9)
|
2021-01-19 21:37:31 +01:00
|
|
|
};
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(utils.getUrl('music', artistOrLabelUrl))
|
2021-01-19 21:37:31 +01:00
|
|
|
.then( html => parser.parseDiscography(html, opts) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getArtistOrLabelInfo(artistOrLabelUrl, options = {}) {
|
|
|
|
const opts = {
|
|
|
|
artistOrLabelUrl,
|
|
|
|
imageFormat: await _parseImageFormatArg(options.imageFormat)
|
|
|
|
};
|
2021-01-25 16:44:54 +01:00
|
|
|
// The landing page of some artists and labels don't actually
|
|
|
|
// contain the 'bio' column, so we fetch from the
|
|
|
|
// 'music' page instead. For artists, if the 'music' page does not
|
|
|
|
// have the artist info, we shall try with an album or track page
|
|
|
|
// (this is inefficient...perhaps there is a better way?).
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(utils.getUrl('music', artistOrLabelUrl))
|
|
|
|
.then( html => parser.parseArtistOrLabelInfo(html, opts) )
|
|
|
|
.then( info => {
|
|
|
|
if (info.type === 'label' || info.name !== '') {
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return getDiscography(artistOrLabelUrl, options)
|
|
|
|
.then( discographyItems => {
|
|
|
|
const firstAlbumOrTrack = discographyItems[0];
|
|
|
|
if (firstAlbumOrTrack) {
|
|
|
|
return firstAlbumOrTrack.url;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// fallback
|
|
|
|
return artistOrLabelUrl;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then( url => _fetchPage(url) )
|
|
|
|
.then( html => parser.parseArtistOrLabelInfo(html, opts) );
|
|
|
|
}
|
|
|
|
});
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getLabelArtists(labelUrl, options = {}) {
|
|
|
|
const opts = {
|
|
|
|
labelUrl,
|
|
|
|
imageFormat: await _parseImageFormatArg(options.imageFormat)
|
|
|
|
};
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(utils.getUrl('artists', labelUrl))
|
|
|
|
.then( html => parser.parseLabelArtists(html, opts) );
|
2021-01-19 21:37:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function search(params, options = {}) {
|
|
|
|
const opts = {
|
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat),
|
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat)
|
|
|
|
};
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(utils.getSearchUrl(params))
|
2021-01-19 21:37:31 +01:00
|
|
|
.then( html => parser.parseSearchResults(html, opts) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAlbumHighlightsByTag(tagUrl, options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
|
|
|
imageFormat: await _parseImageFormatArg(options.imageFormat, 9)
|
|
|
|
};
|
|
|
|
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(tagUrl)
|
2021-01-19 21:37:31 +01:00
|
|
|
.then( html => parser.parseAlbumHighlightsByTag(html, opts) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getTags() {
|
2021-01-26 08:58:39 +01:00
|
|
|
return _fetchPage(utils.getUrl('tags'))
|
2021-01-19 21:37:31 +01:00
|
|
|
.then( html => parser.parseTags(html) );
|
|
|
|
}
|
|
|
|
|
2021-02-03 11:09:12 +01:00
|
|
|
async function getAllShows(options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
|
|
|
showImageFormat: await _parseImageFormatArg(options.showImageFormat, 25)
|
|
|
|
};
|
|
|
|
return _fetchPage(utils.getAllShowsUrl(), true)
|
|
|
|
.then( json => parser.parseAllShows(json, opts) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getShow(showUrl, options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
|
|
|
showUrl,
|
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat, 9),
|
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat, 21),
|
|
|
|
showImageFormat: await _parseImageFormatArg(options.showImageFormat, 25)
|
|
|
|
};
|
|
|
|
return _fetchPage(showUrl)
|
|
|
|
.then( html => parser.parseShow(html, opts) );
|
|
|
|
}
|
|
|
|
|
2021-02-07 12:44:57 +01:00
|
|
|
async function getArticleCategories() {
|
|
|
|
return _fetchPage(utils.getDailyUrl())
|
|
|
|
.then( html => parser.parseArticleCategories(html) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getArticleList(params = {}, options = {}) {
|
|
|
|
if (params.categoryUrl == undefined) {
|
|
|
|
params.categoryUrl = utils.getUrl('latest', utils.getDailyUrl());
|
|
|
|
}
|
|
|
|
const opts = {
|
|
|
|
imageFormat: await _parseImageFormatArg(options.imageFormat)
|
|
|
|
};
|
|
|
|
return _fetchPage(utils.getDailyUrl(params))
|
|
|
|
.then( html => parser.parseArticleList(html, opts) );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getArticle(articleUrl, options = {}) {
|
|
|
|
const imageConstants = await _getImageConstants();
|
|
|
|
const opts = {
|
|
|
|
imageBaseUrl: imageConstants.baseUrl,
|
|
|
|
albumImageFormat: await _parseImageFormatArg(options.albumImageFormat, 9),
|
|
|
|
artistImageFormat: await _parseImageFormatArg(options.artistImageFormat, 21),
|
|
|
|
includeRawData: options.includeRawData ? true : false
|
|
|
|
};
|
|
|
|
return _fetchPage(articleUrl)
|
|
|
|
.then( html => parser.parseArticle(html, opts) );
|
|
|
|
}
|
|
|
|
|
2021-01-26 08:58:39 +01:00
|
|
|
async function _fetchPage(url, json = false) {
|
|
|
|
return _cache.getOrSet('page', url + (json ? ':json' : ':html'), () => {
|
|
|
|
return fetch(url).then( res => json ? res.json() : res.text() );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache functions
|
|
|
|
const cache = {
|
|
|
|
setTTL: _cache.setTTL.bind(_cache),
|
|
|
|
setMaxPages: (maxPages) => {
|
|
|
|
_cache.setMaxEntries('page', maxPages);
|
|
|
|
},
|
|
|
|
clear: _cache.clear.bind(_cache)
|
|
|
|
};
|
|
|
|
|
2021-01-19 21:37:31 +01:00
|
|
|
module.exports = {
|
|
|
|
discover,
|
|
|
|
getDiscoverOptions,
|
2021-01-23 14:49:34 +01:00
|
|
|
sanitizeDiscoverParams,
|
2021-01-19 21:37:31 +01:00
|
|
|
getImageFormats,
|
|
|
|
getImageFormat,
|
|
|
|
getAlbumInfo,
|
|
|
|
getTrackInfo,
|
|
|
|
getDiscography,
|
|
|
|
getArtistOrLabelInfo,
|
|
|
|
getLabelArtists,
|
|
|
|
search,
|
|
|
|
getAlbumHighlightsByTag,
|
2021-01-26 08:58:39 +01:00
|
|
|
getTags,
|
2021-02-03 11:09:12 +01:00
|
|
|
cache,
|
|
|
|
getAllShows,
|
|
|
|
getShow,
|
2021-02-07 12:44:57 +01:00
|
|
|
getArticleCategories,
|
|
|
|
getArticleList,
|
|
|
|
getArticle
|
2021-01-19 21:37:31 +01:00
|
|
|
};
|