mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-07-03 09:35:05 +00:00
refactor: rewrite search suggestions logic (#92)
* refactor: rewrite YouTube Music search suggestions The search suggestions method can be found under `Innertube#music.getSearchSuggestions(query)` * feat: allow `execute(..)` to return parsed data This simplifies how response data is handled and also makes it easier for end users to write custom functionality. * style: lint code * chore: change a few things * refactor: rewrite YouTube search suggestions * chore(package): build * chore: update type declarations * chore: fix tests
This commit is contained in:
@@ -23,6 +23,7 @@ const EventEmitter = require('events');
|
||||
const { PassThrough } = BROWSER ? require('stream-browserify') : require('stream');
|
||||
|
||||
const Request = require('./utils/Request');
|
||||
const Constants = require('./utils/Constants');
|
||||
|
||||
const {
|
||||
InnertubeError,
|
||||
@@ -195,22 +196,27 @@ class Innertube {
|
||||
|
||||
/**
|
||||
* Retrieves search suggestions for a given query.
|
||||
*
|
||||
* @param {string} query - the search query.
|
||||
* @param {object} [options] - search options.
|
||||
* @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`.
|
||||
* @returns {Promise.<{ query: string, results: string[] }>}
|
||||
*/
|
||||
async getSearchSuggestions(query, options = { client: 'YOUTUBE' }) {
|
||||
async getSearchSuggestions(query) {
|
||||
throwIfMissing({ query });
|
||||
|
||||
const response = await this.actions.getSearchSuggestions(options.client, query);
|
||||
if (options.client === 'YTMUSIC' && !response.data.contents) return [];
|
||||
const response = await this.#request({
|
||||
url: 'search',
|
||||
baseURL: Constants.URLS.YT_SUGGESTIONS,
|
||||
params: {
|
||||
q: query,
|
||||
ds: 'yt',
|
||||
client: 'youtube',
|
||||
xssi: 't',
|
||||
oe: 'UTF',
|
||||
gl: this.context.client.gl,
|
||||
hl: this.context.client.hl
|
||||
}
|
||||
});
|
||||
|
||||
const suggestions = new OldParser(this, response.data, {
|
||||
client: options.client,
|
||||
data_type: 'SEARCH_SUGGESTIONS'
|
||||
}).parse();
|
||||
const data = JSON.parse(response.data.replace(')]}\'', ''));
|
||||
const suggestions = data[1].map((suggestion) => suggestion[0]);
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const Uuid = require('uuid');
|
||||
const Proto = require('../proto');
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
const Parser = require('../parser/contents');
|
||||
|
||||
/** @namespace */
|
||||
class Actions {
|
||||
@@ -621,10 +622,14 @@ class Actions {
|
||||
* Executes an API call.
|
||||
* @param {string} action - endpoint
|
||||
* @param {object} args - call arguments
|
||||
* @param {boolean} [args.parse]
|
||||
*/
|
||||
async execute(action, args) {
|
||||
const data = { ...args };
|
||||
|
||||
if (Reflect.has(data, 'parse'))
|
||||
delete data.parse;
|
||||
|
||||
if (Reflect.has(data, 'request'))
|
||||
delete data.request;
|
||||
|
||||
@@ -641,7 +646,13 @@ class Actions {
|
||||
delete data.token;
|
||||
}
|
||||
|
||||
return this.#request.post(action, data);
|
||||
const response = await this.#request.post(action, data);
|
||||
|
||||
if (args.parse) {
|
||||
return Parser.parseResponse(response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
#needsLogin(id) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const Library = require('../parser/ytmusic/Library');
|
||||
const Artist = require('../parser/ytmusic/Artist');
|
||||
const Album = require('../parser/ytmusic/Album');
|
||||
|
||||
const { InnertubeError, observe } = require('../utils/Utils');
|
||||
const { InnertubeError, throwIfMissing, observe } = require('../utils/Utils');
|
||||
|
||||
/** @namespace */
|
||||
class Music {
|
||||
@@ -32,6 +32,7 @@ class Music {
|
||||
* @returns {Promise.<Search>}
|
||||
*/
|
||||
async search(query, filters) {
|
||||
throwIfMissing({ query });
|
||||
const response = await this.#actions.search({ query, filters, client: 'YTMUSIC' });
|
||||
return new Search(response, this.#actions, { is_filtered: filters?.hasOwnProperty('type') && filters.type !== 'all' });
|
||||
}
|
||||
@@ -73,8 +74,13 @@ class Music {
|
||||
* @returns {Promise.<Artist>}
|
||||
*/
|
||||
async getArtist(artist_id) {
|
||||
if (!artist_id.startsWith('UC')) throw new InnertubeError('Invalid artist id', artist_id);
|
||||
throwIfMissing({ artist_id });
|
||||
|
||||
if (!artist_id.startsWith('UC'))
|
||||
throw new InnertubeError('Invalid artist id', artist_id);
|
||||
|
||||
const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' });
|
||||
|
||||
return new Artist(response, this.#actions);
|
||||
}
|
||||
|
||||
@@ -85,8 +91,13 @@ class Music {
|
||||
* @returns {Promise.<Album>}
|
||||
*/
|
||||
async getAlbum(album_id) {
|
||||
if (!album_id.startsWith('MPR')) throw new InnertubeError('Invalid album id', album_id);
|
||||
throwIfMissing({ album_id });
|
||||
|
||||
if (!album_id.startsWith('MPR'))
|
||||
throw new InnertubeError('Invalid album id', album_id);
|
||||
|
||||
const response = await this.#actions.browse(album_id, { client: 'YTMUSIC' });
|
||||
|
||||
return new Album(response, this.#actions);
|
||||
}
|
||||
|
||||
@@ -96,6 +107,8 @@ class Music {
|
||||
* @param {string} video_id
|
||||
*/
|
||||
async getLyrics(video_id) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
@@ -123,6 +136,8 @@ class Music {
|
||||
* @param {string} video_id
|
||||
*/
|
||||
async getUpNext(video_id) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
@@ -149,6 +164,8 @@ class Music {
|
||||
* @param {string} video_id
|
||||
*/
|
||||
async getRelated(video_id) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
@@ -167,6 +184,24 @@ class Music {
|
||||
info: info?.description.toString() || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves search suggestions for the given query.
|
||||
* @param {string} query
|
||||
* @returns {Promise.<import('../parser/contents/classes/SearchSuggestion')[] | import('../parser/contents/classes/HistorySuggestion')[]>}
|
||||
*/
|
||||
async getSearchSuggestions(query) {
|
||||
const payload = {
|
||||
parse: true,
|
||||
input: query,
|
||||
client: 'YTMUSIC'
|
||||
};
|
||||
|
||||
const response = await this.#actions.execute('/music/get_search_suggestions', payload);
|
||||
const search_suggestions_section = response.contents_memo.get('SearchSuggestionsSection')?.[0];
|
||||
|
||||
return search_suggestions_section?.contents || [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Music;
|
||||
13
lib/parser/contents/classes/HistorySuggestion.js
Normal file
13
lib/parser/contents/classes/HistorySuggestion.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const SearchSuggestion = require('./SearchSuggestion');
|
||||
|
||||
class HistorySuggestion extends SearchSuggestion {
|
||||
type = 'HistorySuggestion';
|
||||
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HistorySuggestion;
|
||||
@@ -16,7 +16,7 @@ class MusicShelf {
|
||||
this.endpoint = new NavigationEndpoint(data.bottomEndpoint);
|
||||
}
|
||||
|
||||
if (this.continuation) {
|
||||
if (data.continuations) {
|
||||
this.continuation = data.continuations?.[0].nextContinuationData.continuation;
|
||||
}
|
||||
|
||||
|
||||
20
lib/parser/contents/classes/SearchSuggestion.js
Normal file
20
lib/parser/contents/classes/SearchSuggestion.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const Text = require('./Text');
|
||||
const NavigationEndpoint = require('./NavigationEndpoint');
|
||||
|
||||
class SearchSuggestion {
|
||||
type = 'SearchSuggestion';
|
||||
|
||||
constructor(data) {
|
||||
this.suggestion = new Text(data.suggestion);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.icon_type = data.icon.iconType;
|
||||
|
||||
if (data.serviceEndpoint) {
|
||||
this.service_endpoint = new NavigationEndpoint(data.serviceEndpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SearchSuggestion;
|
||||
13
lib/parser/contents/classes/SearchSuggestionsSection.js
Normal file
13
lib/parser/contents/classes/SearchSuggestionsSection.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const Parser = require('..');
|
||||
|
||||
class SearchSuggestionsSection {
|
||||
type = 'SearchSuggestionsSection';
|
||||
|
||||
constructor(data) {
|
||||
this.contents = Parser.parse(data.contents);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SearchSuggestionsSection;
|
||||
@@ -5,14 +5,8 @@ const Parser = require('..');
|
||||
class TabbedSearchResults {
|
||||
type = 'TabbedSearchResults';
|
||||
|
||||
#data;
|
||||
|
||||
constructor(data) {
|
||||
this.#data = data;
|
||||
}
|
||||
|
||||
get tabs() {
|
||||
return Parser.parse(this.#data.tabs);
|
||||
this.tabs = Parser.parse(data.tabs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -403,8 +403,9 @@ class VideoInfo {
|
||||
response.data.on('data', (chunk) => {
|
||||
downloaded_size += chunk.length;
|
||||
|
||||
const size = (format.content_length / 1024 / 1024).toFixed(2);
|
||||
const percentage = Math.floor((downloaded_size / format.content_length) * 100);
|
||||
// Note: format.content_length is NOT always available so we need to use what we get from the headers.
|
||||
const size = (response.headers['content-length'] / 1024 / 1024).toFixed(2);
|
||||
const percentage = Math.floor((downloaded_size / response.headers['content-length']) * 100);
|
||||
|
||||
stream.emit('progress', {
|
||||
size,
|
||||
@@ -469,8 +470,9 @@ class VideoInfo {
|
||||
response.data.on('data', (chunk) => {
|
||||
downloaded_size += chunk.length;
|
||||
|
||||
const size = (format.content_length / 1024 / 1024).toFixed(2);
|
||||
const percentage = Math.floor((downloaded_size / format.content_length) * 100);
|
||||
// Note: format.content_length is NOT always available so we need to use what we get from the headers.
|
||||
const size = (response.headers['content-length'] / 1024 / 1024).toFixed(2);
|
||||
const percentage = Math.floor((downloaded_size / response.headers['content-length']) * 100);
|
||||
|
||||
stream.emit('progress', {
|
||||
size,
|
||||
|
||||
@@ -33,6 +33,10 @@ class HomeFeed {
|
||||
const response = await this.#actions.browse(this.#continuation, { is_ctoken: true, client: 'YTMUSIC' });
|
||||
return new HomeFeed(response, this.#actions);
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomeFeed;
|
||||
Reference in New Issue
Block a user