@distube/ytsr
Version:
A ytsr fork. Made for DisTube.js.org.
188 lines (169 loc) • 5.38 kB
JavaScript
const PARSE_ITEM = require('./parseItem.js');
const { request } = require('undici');
const UTIL = require('./util.js');
const BASE_SEARCH_URL = 'https://www.youtube.com/results';
const BASE_API_URL = 'https://www.youtube.com/youtubei/v1/search';
const CACHE = new Map([
// ['apiKey', 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'],
['clientVersion', '2.20240606.06.00'],
['playlistParams', 'EgIQAw%3D%3D'],
]);
const saveCache = (parsed, opts) => {
// if (parsed.apiKey) CACHE.set('apiKey', parsed.apiKey);
// else if (CACHE.has('apiKey')) parsed.apiKey = CACHE.get('apiKey');
if (parsed.context && parsed.context.client && parsed.context.client.clientVersion) {
CACHE.set('clientVersion', parsed.context.client.clientVersion);
} else if (CACHE.has('clientVersion')) {
parsed.context = UTIL.buildPostContext(CACHE.get('clientVersion'), opts);
}
const plParams =
UTIL.getPlaylistParams(parsed) ||
(parsed.body ? UTIL.betweenFromRight(parsed.body, `"params":"`, '"}},"tooltip":"Search for Playlist"') : null);
if (plParams) CACHE.set('playlistParams', plParams);
};
// eslint-disable-next-line complexity
const main = (module.exports = async (searchString, options, rt = 3) => {
UTIL.checkForUpdates();
if (rt === 2) {
// CACHE.delete('apiKey');
CACHE.delete('clientVersion');
CACHE.delete('playlistParams');
}
if (rt === 0) throw new Error('Unable to find JSON!');
// Set default values
const opts = UTIL.checkArgs(searchString, options);
let parsed = {};
if (
!opts.safeSearch ||
!CACHE.has('clientVersion') ||
!CACHE.has('playlistParams')
// || !CACHE.has('apiKey')
) {
const res = await request(BASE_SEARCH_URL, Object.assign({}, opts.requestOptions, { query: opts.query }));
const body = await res.body.text();
parsed = UTIL.parseBody(body, opts);
}
saveCache(parsed, opts);
if (opts.type === 'playlist') {
const params = CACHE.get('playlistParams');
parsed.json = await UTIL.doPost(
BASE_API_URL,
Object.assign({}, opts.requestOptions, {
query: {
// key: parsed.apiKey,
prettyPrint: false,
},
}),
{
context: parsed.context,
params,
query: searchString,
},
);
if (!parsed.json) throw new Error('Cannot searching for Playlist!');
} else if (opts.safeSearch || !parsed.json) {
try {
parsed.json = await UTIL.doPost(
BASE_API_URL,
Object.assign({}, opts.requestOptions, {
query: {
// key: parsed.apiKey,
prettyPrint: false,
},
}),
{
context: parsed.context,
query: searchString,
},
);
} catch (e) {
if (rt === 1) throw e;
}
}
if (!parsed.json) return main(searchString, options, rt - 1);
const resp = { query: opts.search };
try {
// Parse items
const { rawItems, continuation } = UTIL.parseWrapper(
parsed.json.contents.twoColumnSearchResultsRenderer.primaryContents,
);
// Parse items
resp.items = rawItems
.map(a => PARSE_ITEM(a, resp))
.filter(r => r && r.type === opts.type)
.filter((_, index) => index < opts.limit);
// Adjust tracker
opts.limit -= resp.items.length;
// Get amount of results
resp.results = Number(parsed.json.estimatedResults) || 0;
let token = null;
if (continuation) token = continuation.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
// We're already on last page or hit the limit
if (!token || opts.limit < 1) return resp;
// Recursively fetch more items
const nestedResp = await parsePage2(
// parsed.apiKey,
token,
parsed.context,
opts,
);
// Merge the responses
resp.items.push(...nestedResp);
return resp;
} catch (e) {
parsed.query = searchString;
parsed.requestOptions = opts.requestOptions;
parsed.error = UTIL.errorToObject(e);
UTIL.logger(parsed);
throw e;
}
});
const parsePage2 = async (
// apiKey,
token,
context,
opts,
) => {
const json = await UTIL.doPost(
BASE_API_URL,
Object.assign({}, opts.requestOptions, {
query: {
// key: apiKey,
prettyPrint: false,
},
}),
{ context, continuation: token },
);
if (!Array.isArray(json.onResponseReceivedCommands)) {
// No more content
return [];
}
try {
const { rawItems, continuation } = UTIL.parsePage2Wrapper(
json.onResponseReceivedCommands[0].appendContinuationItemsAction.continuationItems,
);
const parsedItems = rawItems
.map(PARSE_ITEM)
.filter(r => r && r.type === opts.type)
.filter((_, index) => index < opts.limit);
// Adjust tracker
opts.limit -= parsedItems.length;
let nextToken = null;
if (continuation) nextToken = continuation.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
// We're already on last page or hit the limit
if (!nextToken || opts.limit < 1) return parsedItems;
// Recursively fetch more items
const nestedResp = await parsePage2(
// apiKey,
nextToken,
context,
opts,
);
parsedItems.push(...nestedResp);
return parsedItems;
} catch (e) {
json.error = UTIL.errorToObject(e);
UTIL.logger(json);
return [];
}
};