mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9856a8359 | ||
|
|
4b29ad74de | ||
|
|
60730a5531 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "Full-featured wrapper around YouTube's private API.",
|
||||
"main": "./dist/index.js",
|
||||
"browser": "./bundle/browser.js",
|
||||
@@ -69,17 +69,18 @@
|
||||
"youtubedl",
|
||||
"youtube-dl",
|
||||
"youtube-downloader",
|
||||
"innertube",
|
||||
"youtube-music",
|
||||
"innertubeapi",
|
||||
"innertube",
|
||||
"unofficial",
|
||||
"downloader",
|
||||
"livechat",
|
||||
"studio",
|
||||
"upload",
|
||||
"ytmusic",
|
||||
"dislike",
|
||||
"search",
|
||||
"comment",
|
||||
"music",
|
||||
"like",
|
||||
"api"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class Music {
|
||||
async getArtist(artist_id: string) {
|
||||
throwIfMissing({ artist_id });
|
||||
|
||||
if (!artist_id.startsWith('UC'))
|
||||
if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist'))
|
||||
throw new InnertubeError('Invalid artist id', artist_id);
|
||||
|
||||
const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' });
|
||||
|
||||
@@ -225,7 +225,7 @@ const videos = response.contents_memo.getType(Video);
|
||||
|
||||
If you decompile a YouTube client and analize it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc.
|
||||
|
||||
These classes are used to parse objects from the response (which consists of protobuf messages) and also build requests. The website works in a similar way, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
|
||||
These classes are used to parse objects from the response, map them into models and generate the UI. The website works in a similar way, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
|
||||
|
||||
Here we're taking a similar approach, the parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, and on top of that it also parses navigation endpoints which allows us to make an API call with all required parameters in one line and even emulate client actions (eg; clicking a button).
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class DropdownItem extends YTNode {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
value?: number | string;
|
||||
iconType?: string;
|
||||
icon_type?: string;
|
||||
description?: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
|
||||
@@ -29,7 +29,7 @@ class DropdownItem extends YTNode {
|
||||
}
|
||||
|
||||
if (data.icon?.iconType) {
|
||||
this.iconType = data.icon?.iconType;
|
||||
this.icon_type = data.icon?.iconType;
|
||||
}
|
||||
|
||||
if (data.descriptionText) {
|
||||
|
||||
15
src/parser/classes/TitleAndButtonListHeader.ts
Normal file
15
src/parser/classes/TitleAndButtonListHeader.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class TitleAndButtonListHeader extends YTNode {
|
||||
static type = 'TitleAndButtonListHeader';
|
||||
|
||||
title: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
}
|
||||
|
||||
export default TitleAndButtonListHeader;
|
||||
@@ -244,6 +244,7 @@ import { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOv
|
||||
import { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel';
|
||||
import { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus';
|
||||
import { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOverlayToggleButton';
|
||||
import { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader';
|
||||
import { default as ToggleButton } from './classes/ToggleButton';
|
||||
import { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem';
|
||||
import { default as Tooltip } from './classes/Tooltip';
|
||||
@@ -508,6 +509,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
ThumbnailOverlaySidePanel,
|
||||
ThumbnailOverlayTimeStatus,
|
||||
ThumbnailOverlayToggleButton,
|
||||
TitleAndButtonListHeader,
|
||||
ToggleButton,
|
||||
ToggleMenuServiceItem,
|
||||
Tooltip,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import Actions from '../../core/Actions';
|
||||
import Feed from '../../core/Feed';
|
||||
import { observe, ObservedArray, YTNode } from '../helpers';
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import HorizontalCardList from '../classes/HorizontalCardList';
|
||||
|
||||
import Feed from '../../core/Feed';
|
||||
import SectionList from '../classes/SectionList';
|
||||
import ItemSection from '../classes/ItemSection';
|
||||
import HorizontalCardList from '../classes/HorizontalCardList';
|
||||
import RichListHeader from '../classes/RichListHeader';
|
||||
import SearchRefinementCard from '../classes/SearchRefinementCard';
|
||||
import TwoColumnSearchResults from '../classes/TwoColumnSearchResults';
|
||||
import UniversalWatchCard from '../classes/UniversalWatchCard';
|
||||
import WatchCardHeroVideo from '../classes/WatchCardHeroVideo';
|
||||
import WatchCardSectionSequence from '../classes/WatchCardSectionSequence';
|
||||
import { observe, ObservedArray, YTNode } from '../helpers';
|
||||
|
||||
class Search extends Feed {
|
||||
results: ObservedArray<YTNode> | null | undefined;
|
||||
@@ -22,11 +24,11 @@ class Search extends Feed {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents.item().as(TwoColumnSearchResults).primary_contents.item().key('contents').parsed().array() ||
|
||||
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents.item().as(SectionList).contents.array() ||
|
||||
this.page.on_response_received_commands?.[0].contents;
|
||||
|
||||
const secondary_contents_maybe = this.page.contents.item().key('secondary_contents');
|
||||
const secondary_contents = secondary_contents_maybe.isParsed() ? secondary_contents_maybe.parsed().item().key('contents').parsed().array() : undefined;
|
||||
const secondary_contents_maybe = this.page.contents?.item().key('secondary_contents');
|
||||
const secondary_contents = secondary_contents_maybe?.isParsed() ? secondary_contents_maybe.parsed().item().key('contents').parsed().array() : undefined;
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
|
||||
@@ -64,7 +66,7 @@ class Search extends Feed {
|
||||
throw new InnertubeError('Invalid refinement card!');
|
||||
}
|
||||
|
||||
const page = await target_card.endpoint.call(this.actions);
|
||||
const page = await target_card.endpoint.call(this.actions, undefined, true);
|
||||
|
||||
return new Search(this.actions, page, true);
|
||||
}
|
||||
@@ -81,4 +83,5 @@ class Search extends Feed {
|
||||
return new Search(this.actions, continuation, true);
|
||||
}
|
||||
}
|
||||
export default Search;
|
||||
|
||||
export default Search;
|
||||
@@ -7,6 +7,7 @@ import MusicCarouselShelf from '../classes/MusicCarouselShelf';
|
||||
import MusicPlaylistShelf from '../classes/MusicPlaylistShelf';
|
||||
import MusicImmersiveHeader from '../classes/MusicImmersiveHeader';
|
||||
import MusicVisualHeader from '../classes/MusicVisualHeader';
|
||||
import MusicHeader from '../classes/MusicHeader';
|
||||
|
||||
class Artist {
|
||||
#page;
|
||||
@@ -19,7 +20,7 @@ class Artist {
|
||||
this.#page = Parser.parseResponse((response as AxioslikeResponse).data);
|
||||
this.#actions = actions;
|
||||
|
||||
this.header = this.page.header.item().as(MusicImmersiveHeader, MusicVisualHeader);
|
||||
this.header = this.page.header.item().as(MusicImmersiveHeader, MusicVisualHeader, MusicHeader);
|
||||
|
||||
const music_shelf = this.#page.contents_memo.get('MusicShelf') as MusicShelf[] || [];
|
||||
const music_carousel_shelf = this.#page.contents_memo.get('MusicCarouselShelf') as MusicCarouselShelf[] || [];
|
||||
|
||||
@@ -20,6 +20,16 @@ describe('YouTube.js Tests', () => {
|
||||
expect(search.has_continuation).toBe(true);
|
||||
});
|
||||
|
||||
it('should retrieve YouTube search continuation', async () => {
|
||||
const search = await yt.search(VIDEOS[0].QUERY);
|
||||
const next = await search.getContinuation()
|
||||
expect(next.results?.length).toBeLessThanOrEqual(35);
|
||||
expect(next.videos.length).toBeLessThanOrEqual(35);
|
||||
expect(next.playlists.length).toBeLessThanOrEqual(35);
|
||||
expect(next.channels.length).toBeLessThanOrEqual(35);
|
||||
expect(next.has_continuation).toBe(true);
|
||||
});
|
||||
|
||||
it('should search on YouTube Music', async () => {
|
||||
const search = await yt.music.search(VIDEOS[1].QUERY);
|
||||
expect(search.songs?.contents.length).toBeLessThanOrEqual(3);
|
||||
|
||||
Reference in New Issue
Block a user