Files
YouTube.js/src/parser/ytmusic/Library.ts

192 lines
6.3 KiB
TypeScript

import Parser, { GridContinuation, MusicShelfContinuation, SectionListContinuation } from '../index.js';
import type Actions from '../../core/Actions.js';
import type { ApiResponse } from '../../core/Actions.js';
import Grid from '../classes/Grid.js';
import MusicShelf from '../classes/MusicShelf.js';
import MusicSideAlignedItem from '../classes/MusicSideAlignedItem.js';
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
import SectionList from '../classes/SectionList.js';
import ChipCloud from '../classes/ChipCloud.js';
import ChipCloudChip from '../classes/ChipCloudChip.js';
import MusicMultiSelectMenuItem from '../classes/menus/MusicMultiSelectMenuItem.js';
import MusicSortFilterButton from '../classes/MusicSortFilterButton.js';
import type MusicMenuItemDivider from '../classes/menus/MusicMenuItemDivider.js';
import { InnertubeError } from '../../utils/Utils.js';
import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/ParsedResponse.js';
class Library {
#page: IBrowseResponse;
#actions: Actions;
#continuation?: string | null;
header?: MusicSideAlignedItem;
contents?: ObservedArray<Grid | MusicShelf>;
constructor(response: ApiResponse, actions: Actions) {
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
this.#actions = actions;
const section_list = this.#page.contents_memo?.getType(SectionList).first();
this.header = section_list?.header?.as(MusicSideAlignedItem);
this.contents = section_list?.contents?.as(Grid, MusicShelf);
this.#continuation = this.contents?.find((list: Grid | MusicShelf) => list.continuation)?.continuation;
}
/**
* Applies given sort option to the library items.
*/
async applySort(sort_by: string | MusicMultiSelectMenuItem): Promise<Library> {
let target_item: MusicMultiSelectMenuItem | undefined;
if (typeof sort_by === 'string') {
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
const options = button?.menu?.options
.filter(
(item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem
) as MusicMultiSelectMenuItem[];
target_item = options?.find((item) => item.title === sort_by);
if (!target_item)
throw new InnertubeError(`Sort option "${sort_by}" not found`, { available_filters: options.map((item) => item.title) });
} else if (sort_by instanceof MusicMultiSelectMenuItem) {
target_item = sort_by;
}
if (!target_item)
throw new InnertubeError('Invalid sort option');
if (target_item.selected)
return this;
const cmd = target_item.endpoint?.payload?.commands?.find((cmd: any) => cmd.browseSectionListReloadEndpoint)?.browseSectionListReloadEndpoint;
if (!cmd)
throw new InnertubeError('Failed to find sort option command');
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
continuation: cmd.continuation.reloadContinuationData.continuation,
parse: true
});
const previously_selected_item = this.#page.contents_memo?.getType(MusicMultiSelectMenuItem)?.find((item) => item.selected);
if (previously_selected_item)
previously_selected_item.selected = false;
target_item.selected = true;
this.contents = response.continuation_contents?.as(SectionListContinuation).contents?.as(Grid, MusicShelf);
return this;
}
/**
* Applies given filter to the library.
*/
async applyFilter(filter: string | ChipCloudChip): Promise<Library> {
let target_chip: ChipCloudChip | undefined;
const chip_cloud = this.#page.contents_memo?.getType(ChipCloud).first();
if (typeof filter === 'string') {
target_chip = chip_cloud?.chips.get({ text: filter });
if (!target_chip)
throw new InnertubeError(`Filter "${filter}" not found`, { available_filters: this.filters });
} else if (filter instanceof ChipCloudChip) {
target_chip = filter;
}
if (!target_chip)
throw new InnertubeError('Invalid filter', filter);
const target_cmd = new NavigationEndpoint(target_chip.endpoint?.payload?.commands?.[0]);
const response = await target_cmd.call(this.#actions, { client: 'YTMUSIC' });
return new Library(response, this.#actions);
}
/**
* Retrieves continuation of the library items.
*/
async getContinuation(): Promise<LibraryContinuation> {
if (!this.#continuation)
throw new InnertubeError('No continuation available');
const page = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
continuation: this.#continuation
});
return new LibraryContinuation(page, this.#actions);
}
get has_continuation(): boolean {
return !!this.#continuation;
}
get sort_options(): string[] {
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
const options = button?.menu?.options.filter((item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem) as MusicMultiSelectMenuItem[];
return options.map((item) => item.title);
}
get filters(): string[] {
return this.#page.contents_memo?.getType(ChipCloud)?.first().chips.map((chip: ChipCloudChip) => chip.text) || [];
}
get page(): IBrowseResponse {
return this.#page;
}
}
class LibraryContinuation {
#page;
#actions;
#continuation;
contents: GridContinuation | MusicShelfContinuation;
constructor(response: ApiResponse, actions: Actions) {
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
this.#actions = actions;
if (!this.#page.continuation_contents)
throw new InnertubeError('No continuation contents found');
this.contents = this.#page.continuation_contents.as(MusicShelfContinuation, GridContinuation);
this.#continuation = this.contents.continuation || null;
}
async getContinuation(): Promise<LibraryContinuation> {
if (!this.#continuation)
throw new InnertubeError('No continuation available');
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
continuation: this.#continuation
});
return new LibraryContinuation(response, this.#actions);
}
get has_continuation(): boolean {
return !!this.#continuation;
}
get page(): IBrowseResponse {
return this.#page;
}
}
export { LibraryContinuation };
export default Library;