mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-30 18:06:15 +00:00
feat(Search): Add support for refinement chips (#1167)
Also, this removes the old code used for refinement cards. Doesn't look like YouTube uses them anymore.
This commit is contained in:
@@ -141,7 +141,7 @@ export interface IStreamingData {
|
||||
export type IPlayerResponse = Pick<IParsedResponse, 'captions' | 'cards' | 'endscreen' | 'microformat' | 'annotations' | 'playability_status' | 'streaming_data' | 'player_config' | 'playback_tracking' | 'storyboards' | 'video_details'>;
|
||||
export type INextResponse = Pick<IParsedResponse, 'contents' | 'contents_memo' | 'continuation_contents' | 'continuation_contents_memo' | 'current_video_endpoint' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'player_overlays' | 'engagement_panels'>;
|
||||
export type IBrowseResponse = Pick<IParsedResponse, 'background' | 'continuation_contents' | 'continuation_contents_memo' | 'on_response_received_actions' | 'on_response_received_actions_memo' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'contents' | 'contents_memo' | 'header' | 'header_memo' | 'metadata' | 'microformat' | 'alerts' | 'sidebar' | 'sidebar_memo'>;
|
||||
export type ISearchResponse = Pick<IParsedResponse, 'header' | 'header_memo' | 'contents' | 'contents_memo' | 'on_response_received_commands' | 'continuation_contents' | 'continuation_contents_memo' | 'refinements' | 'estimated_results'>;
|
||||
export type ISearchResponse = Pick<IParsedResponse, 'header' | 'header_memo' | 'contents' | 'contents_memo' | 'on_response_received_commands' | 'on_response_received_commands_memo' | 'continuation_contents' | 'continuation_contents_memo' | 'refinements' | 'estimated_results'>;
|
||||
export type IResolveURLResponse = Pick<IParsedResponse, 'endpoint'>;
|
||||
export type IGetTranscriptResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>;
|
||||
export type IGetNotificationsMenuResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import Feed from '../../core/mixins/Feed.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import HorizontalCardList from '../classes/HorizontalCardList.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import SearchHeader from '../classes/SearchHeader.js';
|
||||
import SearchRefinementCard from '../classes/SearchRefinementCard.js';
|
||||
import SearchSubMenu from '../classes/SearchSubMenu.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import UniversalWatchCard from '../classes/UniversalWatchCard.js';
|
||||
import AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import { ReloadContinuationItemsCommand } from '../continuations.js';
|
||||
|
||||
import { observe } from '../helpers.js';
|
||||
|
||||
import type { ApiResponse, Actions } from '../../core/index.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { ISearchResponse } from '../types/index.js';
|
||||
import { ReloadContinuationItemsCommand } from '../index.js';
|
||||
import AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js';
|
||||
|
||||
export default class Search extends Feed<ISearchResponse> {
|
||||
public header?: SearchHeader;
|
||||
@@ -23,23 +22,28 @@ export default class Search extends Feed<ISearchResponse> {
|
||||
public estimated_results: number;
|
||||
public sub_menu?: SearchSubMenu;
|
||||
public watch_card?: UniversalWatchCard;
|
||||
public refinement_cards?: HorizontalCardList | null;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | ISearchResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents_memo?.getType(SectionList)[0].contents ||
|
||||
this.page.on_response_received_commands_memo?.getType(SectionList)[0]?.contents ||
|
||||
this.page.on_response_received_commands?.[0].as(AppendContinuationItemsAction, ReloadContinuationItemsCommand).contents;
|
||||
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
|
||||
if (this.page.header)
|
||||
this.header = this.page.header.item().as(SearchHeader);
|
||||
if (this.page.on_response_received_commands && !this.page.header) {
|
||||
const headerSlot = this.page.on_response_received_commands.as(ReloadContinuationItemsCommand).find(
|
||||
(command) => command.is(ReloadContinuationItemsCommand) && command.slot === 'RELOAD_CONTINUATION_SLOT_HEADER'
|
||||
);
|
||||
this.header = headerSlot?.contents?.firstOfType(SearchHeader);
|
||||
} else {
|
||||
this.header = this.page.header?.item().as(SearchHeader);
|
||||
}
|
||||
|
||||
this.results = observe(contents.filterType(ItemSection).flatMap((section) => section.contents));
|
||||
|
||||
this.refinements = this.page.refinements || [];
|
||||
this.estimated_results = this.page.estimated_results || 0;
|
||||
|
||||
@@ -47,39 +51,54 @@ export default class Search extends Feed<ISearchResponse> {
|
||||
this.sub_menu = this.page.contents_memo.getType(SearchSubMenu)[0];
|
||||
this.watch_card = this.page.contents_memo.getType(UniversalWatchCard)[0];
|
||||
}
|
||||
|
||||
this.refinement_cards = this.results?.firstOfType(HorizontalCardList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given refinement card and returns a new {@link Search} object. Use {@link refinement_card_queries} to get a list of available refinement cards.
|
||||
* Applies a refinement filter to the search results.
|
||||
*
|
||||
* Use {@link Search.refinement_filters} to get a list of available refinements.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const results = await yt.search('PilotRedSun');
|
||||
* // Narrow down to only YouTube Shorts
|
||||
* const shortsOnly = await results.applyRefinement('Shorts');
|
||||
* ```
|
||||
* @param refinementFilter - The text label of the chip or the {@link ChipCloudChip} node itself.
|
||||
*/
|
||||
async selectRefinementCard(card: SearchRefinementCard | string): Promise<Search> {
|
||||
let target_card: SearchRefinementCard | undefined;
|
||||
async applyRefinement(refinementFilter: string | ChipCloudChip): Promise<Search> {
|
||||
let endpoint: NavigationEndpoint | undefined;
|
||||
|
||||
if (typeof card === 'string') {
|
||||
if (!this.refinement_cards) throw new InnertubeError('No refinement cards found.');
|
||||
target_card = this.refinement_cards?.cards.find((refinement_card): refinement_card is SearchRefinementCard => {
|
||||
return refinement_card.is(SearchRefinementCard) && refinement_card.query === card;
|
||||
});
|
||||
if (!target_card)
|
||||
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
|
||||
} else if (card.type === 'SearchRefinementCard') {
|
||||
target_card = card;
|
||||
if (typeof refinementFilter === 'string') {
|
||||
const chipBar = this.header?.chip_bar;
|
||||
if (!chipBar) throw new InnertubeError('No chip bar found in search header');
|
||||
|
||||
const targetChip = chipBar.chips.find((chip) => chip.text === refinementFilter);
|
||||
if (!targetChip) throw new InnertubeError(`Refinement filter "${refinementFilter}" not found`, { available_filters: this.refinement_filters });
|
||||
|
||||
endpoint = targetChip.endpoint;
|
||||
|
||||
if (!endpoint && targetChip.is_selected) return this; // The 'All' filter doesn't have an endpoint when it's selected.
|
||||
} else if (refinementFilter.is(ChipCloudChip)) {
|
||||
if (!refinementFilter.endpoint && refinementFilter.is_selected) return this;
|
||||
endpoint = refinementFilter.endpoint;
|
||||
} else {
|
||||
throw new InnertubeError('Invalid refinement card!');
|
||||
throw new InnertubeError('Invalid filter type');
|
||||
}
|
||||
|
||||
const page = await target_card.endpoint.call<ISearchResponse>(this.actions, { parse: true });
|
||||
if (!endpoint)
|
||||
throw new InnertubeError('Could not find endpoint for the specified filter');
|
||||
|
||||
const page = await endpoint.call<ISearchResponse>(this.actions, { parse: true });
|
||||
|
||||
return new Search(this.actions, page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of refinement card queries.
|
||||
* Returns a list of available refinement filters. Use {@link Search.applyRefinement} to apply a filter.
|
||||
*/
|
||||
get refinement_card_queries(): string[] {
|
||||
return this.refinement_cards?.cards.as(SearchRefinementCard).map((card) => card.query) || [];
|
||||
get refinement_filters(): string[] {
|
||||
return this.header?.chip_bar?.chips.map((chip) => chip.text) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user