Skip to content

Commit

Permalink
feat(info): cache ytdl.getInfo() calls to the same videos
Browse files Browse the repository at this point in the history
A 1 second cache, located in `ytdl.cache.info` has been introduced
for calls to `ytdl.getInfo()` to help with the case of
calling it, and then downloading the video with `ytdl()`,
which uses `ytdl.getInfo()` internally.

closes #331, closes #332
BREAKING CHANGES:
- ytdl.cache -> ytdl.cache.sig
  • Loading branch information
fent committed Aug 2, 2018
1 parent f6784ec commit dcb7281
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 41 deletions.
51 changes: 47 additions & 4 deletions lib/index.js
Expand Up @@ -15,7 +15,7 @@ const parseTime = require('m3u8stream/lib/parse-time');
*/
const ytdl = module.exports = function ytdl(link, options) {
const stream = createStream(options);
getInfo(link, options, (err, info) => {
ytdl.getInfo(link, options, (err, info) => {
if (err) {
stream.emit('error', err);
return;
Expand All @@ -27,8 +27,6 @@ const ytdl = module.exports = function ytdl(link, options) {
return stream;
};


ytdl.getInfo = getInfo;
ytdl.chooseFormat = util.chooseFormat;
ytdl.filterFormats = util.filterFormats;
ytdl.validateID = util.validateID;
Expand All @@ -37,7 +35,52 @@ ytdl.validateLink = deprecate(util.validateURL,
'ytdl.validateLink: Renamed to ytdl.validateURL');
ytdl.getURLVideoID = util.getURLVideoID;
ytdl.getVideoID = util.getVideoID;
ytdl.cache = sig.cache;
ytdl.cache = {
sig: sig.cache,
info: new Map(),
};


/**
* Cached version of `getInfo()`
* In case a user wants to get a video's info before downloading.
*
* @param {String} link
* @param {Object} options
* @param {Function(Error, Object)} callback
*/
ytdl.getInfo = (link, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}

if (!callback) {
return new Promise((resolve, reject) => {
ytdl.getInfo(link, options, (err, info) => {
if (err) return reject(err);
resolve(info);
});
});
}

const id = util.getVideoID(link);
if (id instanceof Error) return callback(id);

const key = [id, options.lang].join('-');
if (ytdl.cache.info.has(key)) {
callback(null, ytdl.cache.info.get(key));
} else {
getInfo(id, options, (err, info) => {
if (err) return callback(err);
ytdl.cache.info.set(key, info);
setTimeout(() => { ytdl.cache.info.delete(key); }, 1000);
callback(null, info);
});
}
};


function createStream(options) {
Expand Down
22 changes: 2 additions & 20 deletions lib/info.js
Expand Up @@ -23,29 +23,11 @@ const KEYS_TO_SPLIT = [
/**
* Gets info from a video.
*
* @param {String} link
* @param {String} id
* @param {Object} options
* @param {Function(Error, Object)} callback
*/
module.exports = function getInfo(link, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
if (!callback) {
return new Promise((resolve, reject) => {
getInfo(link, options, (err, info) => {
if (err) return reject(err);
resolve(info);
});
});
}

const id = util.getVideoID(link);
if (id instanceof Error) return callback(id);

module.exports = function getInfo(id, options, callback) {
// Try getting config from the video page first.
const params = 'hl=' + (options.lang || 'en');
let url = VIDEO_URL + id + '&' + params;
Expand Down
12 changes: 6 additions & 6 deletions test/download-test.js
Expand Up @@ -3,7 +3,7 @@ const path = require('path');
const fs = require('fs');
const url = require('url');
const streamEqual = require('stream-equal');
const spy = require('sinon').spy;
const sinon = require('sinon');
const nock = require('./nock');
const ytdl = require('..');

Expand All @@ -24,9 +24,9 @@ describe('Download video', () => {
});
});

beforeEach(() => {
ytdl.cache.clear();
});
let clock;
before(() => { clock = sinon.useFakeTimers({ toFake: ['setTimeout'] }); });
after(() => { clock.restore(); });

it('Should be pipeable and data equal to stored file', (done) => {
const scope = nock(id, {
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('Download video', () => {
stream.on('data', () => {
throw new Error('Should not emit `data`');
});
const abort = spy();
const abort = sinon.spy();
stream.on('abort', abort);
stream.on('error', (err) => {
scope.done();
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('Download video', () => {
});
});

const abort = spy();
const abort = sinon.spy();
stream.on('abort', abort);
stream.on('error', (err) => {
scope.done();
Expand Down
37 changes: 32 additions & 5 deletions test/info-test.js
Expand Up @@ -3,14 +3,14 @@ const path = require('path');
const fs = require('fs');
const assert = require('assert-diff');
const nock = require('./nock');
const spy = require('sinon').spy;
const sinon = require('sinon');
const muk = require('muk-prop');


describe('ytdl.getInfo()', () => {
beforeEach(() => {
ytdl.cache.clear();
});
let clock;
before(() => { clock = sinon.useFakeTimers({ toFake: ['setTimeout'] }); });
after(() => { clock.restore(); });

describe('From a regular video', () => {
const id = 'pJk0p-98Xzc';
Expand Down Expand Up @@ -112,6 +112,33 @@ describe('ytdl.getInfo()', () => {
});
});
});

describe('Called twice', () => {
const id = 'pJk0p-98Xzc';

it('Only makes requests once', (done) => {
const scope = nock(id, {
type: 'vevo',
dashmpd: true,
get_video_info: true,
player: 'player-en_US-vflV3n15C',
});

let myinfo = null;
ytdl.getInfo(id, (err, info) => {
assert.ifError(err);
myinfo = info;
assert.ok(info.description.length);
assert.equal(info.formats.length, expectedInfo.formats.length);
ytdl.getInfo(id, (err, info) => {
assert.ifError(err);
scope.done();
assert.equal(info, myinfo);
done();
});
});
});
});
});

describe('From a non-existant video', () => {
Expand Down Expand Up @@ -381,7 +408,7 @@ describe('ytdl.getInfo()', () => {

describe('When encountering a format not yet known with debug', () => {
it('Warns the console', (done) => {
const warn = spy();
const warn = sinon.spy();
muk(console, 'warn', warn);
after(muk.restore);

Expand Down
3 changes: 2 additions & 1 deletion test/irl-test.js
Expand Up @@ -19,7 +19,8 @@ describe('Try downloading videos without mocking', () => {
beforeEach(() => {
nock.cleanAll();
nock.enableNetConnect();
ytdl.cache.clear();
ytdl.cache.sig.clear();
ytdl.cache.info.clear();
});

Object.keys(videos).forEach((desc) => {
Expand Down
13 changes: 10 additions & 3 deletions test/nock.js
@@ -1,6 +1,7 @@
const path = require('path');
const url = require('url');
const nock = require('nock');
const ytdl = require('..');
const path = require('path');
const url = require('url');
const nock = require('nock');

const YT_HOST = 'https://www.youtube.com';
const VIDEO_PATH = '/watch?v=';
Expand All @@ -13,6 +14,12 @@ const INFO_PATH = '/get_video_info?';
before(() => { nock.disableNetConnect(); });
after(() => { nock.enableNetConnect(); });

beforeEach(() => {
ytdl.cache.sig.clear();
ytdl.cache.info.clear();
});


exports = module.exports = (id, opts) => {
opts = opts || {};
let scopes = [];
Expand Down
10 changes: 8 additions & 2 deletions test/sig-test.js
Expand Up @@ -26,10 +26,16 @@ describe('Get tokens', () => {

describe('Hit the same video twice', () => {
it('Gets html5player tokens from cache', (done) => {
sig.getTokens(url, {}, (err, tokens) => {
const scope = nock.url(url).replyWithFile(200, filepath);
sig.getTokens(url, true, (err, tokens) => {
assert.ifError(err);
scope.done();
assert.ok(tokens.length);
done();
sig.getTokens(url, {}, (err, tokens) => {
assert.ifError(err);
assert.ok(tokens.length);
done();
});
});
});
});
Expand Down

0 comments on commit dcb7281

Please sign in to comment.