fast-speedtest-api
Version:
A speed test powered by fast.com
204 lines (180 loc) • 5.76 kB
JavaScript
const https = require('https');
const http = require('http');
const Timer = require('./Timer');
const ApiError = require('./ApiError');
const DEFAULT_SPEEDTEST_TIMEOUT = 5000; // ms
const DEFAULT_URL_COUNT = 5;
const DEFAULT_BUFFER_SIZE = 8;
const MAX_CHECK_INTERVAL = 200; // ms
class Api {
/**
* Create an Api object
*
* @param {object} options {token<string>, [verbose<boolean>, timeout<number>,
* https<boolean>, urlCount<number>, bufferSize<number>, unit<function>]}
*/
constructor(options) {
if (!options) {
throw new Error('You must define options in Api constructor');
}
if (!options.token) {
throw new Error('You must define app token');
}
if (options.unit && typeof options.unit !== 'function') {
throw new Error('Invalid unit');
}
this.token = options.token;
this.verbose = options.verbose || false;
this.timeout = options.timeout || DEFAULT_SPEEDTEST_TIMEOUT;
this.https = options.https == null ? true : Boolean(options.https);
this.urlCount = options.urlCount || DEFAULT_URL_COUNT;
this.bufferSize = options.bufferSize || DEFAULT_BUFFER_SIZE;
this.unit = options.unit || Api.UNITS.Bps;
}
/**
* Compute average from array of number
*
* @static
* @param {Array} arr array of number or null
* @return {number} The average
*/
static average(arr) {
// remove nulls from list
const arrWithoutNulls = arr.filter(e => e);
if (arrWithoutNulls.length === 0) {
return 0;
}
return arrWithoutNulls.reduce((a, b) => a + b) / arrWithoutNulls.length;
}
/**
* Get data from the specified URL
*
* @async
* @param {string} url The URL to download from
* @return {Promise} The request and response from the URL
*/
async get(url) {
return new Promise((resolve, reject) => {
const request = (this.https ? https : http).get(url, (response) => {
if (response.headers['content-type'].includes('json')) {
response.setEncoding('utf8');
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
const parsedData = JSON.parse(rawData);
response.data = parsedData;
resolve({
response,
request,
});
});
} else {
resolve({
response,
request,
});
}
}).on('error', (e) => {
reject(e);
});
});
}
/**
* Get videos to download url from Fast api
*
* @async
* @return {Array<string>} List of videos url
*/
async getTargets() {
try {
const targets = [];
while (targets.length < this.urlCount) {
/* eslint-disable no-await-in-loop */
const { response } = await this.get(`http${this.https ? 's' : ''}://api.fast.com/netflix/speedtest?https=${this.https ? 'true' : 'false'}&token=${this.token}&urlCount=${this.urlCount - targets.length}`);
/* eslint-enable no-await-in-loop */
if (response.statusCode !== 200) {
if (response.statusCode === 403) {
throw new ApiError({ code: ApiError.CODES.BAD_TOKEN });
}
throw new ApiError({ code: ApiError.CODES.UNKNOWN });
}
targets.push(...response.data);
}
return targets.map(target => target.url);
} catch (e) {
if (e.code === 'ENOTFOUND') {
if (this.https) {
throw new ApiError({ code: ApiError.CODES.UNREACHABLE_HTTPS_API });
} else {
throw new ApiError({ code: ApiError.CODES.UNREACHABLE_HTTP_API });
}
} else {
throw e;
}
}
}
/**
* Resolves when timeout or when the first video finished downloading
*
* @returns {Promise<number>} Speed in selected unit (Default: Bps)
*/
async getSpeed() {
let targets = null;
try {
targets = await this.getTargets();
} catch (e) {
throw e;
}
let bytes = 0;
const requestList = [];
const timer = new Timer(this.timeout, () => {
requestList.forEach(r => r.abort());
});
targets.forEach(async (target) => {
const {response, request} = await this.get(target);
requestList.push(request);
response.on('data', (data) => {
bytes += data.length;
});
response.on('end', () => {
// when first video is downloaded
timer.stop(); // stop timer and execute timer callback
});
});
return new Promise((resolve) => {
let i = 0;
const recents = new Array(this.bufferSize).fill(null); // list of most recent speeds
const interval = Math.min(
this.timeout / this.bufferSize,
MAX_CHECK_INTERVAL,
); // ms
const refreshIntervalId = setInterval(() => {
i = (i + 1) % recents.length; // loop through recents
recents[i] = bytes / (interval / 1000); // add most recent bytes/second
if (this.verbose) {
console.log(`Current speed: ${this.unit(this.constructor.average(recents))} ${this.unit.name}`);
}
bytes = 0;// reset bytes count
}, interval);
timer.addCallback(() => {
clearInterval(refreshIntervalId);
resolve(this.unit(this.constructor.average(recents)));
});
timer.start();
});
}
}
Api.UNITS = {
// rawSpeed is Bps
Bps: rawSpeed => rawSpeed,
KBps: rawSpeed => rawSpeed / 1000,
MBps: rawSpeed => rawSpeed / 1000000,
GBps: rawSpeed => rawSpeed / 1000000000,
bps: rawSpeed => rawSpeed * 8,
Kbps: rawSpeed => (rawSpeed * 8) / 1000,
Mbps: rawSpeed => (rawSpeed * 8) / 1000000,
Gbps: rawSpeed => (rawSpeed * 8) / 1000000000,
};
module.exports = Api;