Compare commits

...

11 Commits

Author SHA1 Message Date
github-actions[bot]
1b00e2c6ce chore(main): release 9.4.0 (#644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-29 09:14:43 -03:00
LuanRT
ea82beaa10 feat(Parser): Add MusicResponsiveHeader node 2024-04-29 08:24:13 -03:00
absidue
0ba8c54257 feat(Format): Add spatial_audio_type (#647) 2024-04-29 08:10:08 -03:00
Brahim Hadriche
7315fca1b4 Add getPlaylists function (#650) 2024-04-29 08:09:35 -03:00
Brahim Hadriche
0602dd2c3d Lint fix (#651) 2024-04-29 08:07:24 -03:00
LuanRT
13321888e8 chore(PlayerEndpoint): Remove outdated code 2024-04-29 08:05:59 -03:00
absidue
d48b9d0946 chore(HTTPClient): Add X-Youtube-Client-Name and remove X-Origin headers (#645) 2024-04-25 18:04:10 -03:00
LuanRT
592ddac30f chore: Fix tests
Oops :)
2024-04-19 16:37:38 -03:00
LuanRT
1ec2ea85e2 refactor(Music#getRelated): Return page contents directy 2024-04-19 16:22:21 -03:00
absidue
064436cef3 feat(Format): Add projection_type and stereo_layout (#643)
5930ebda46
2024-04-19 16:08:12 -03:00
ChunkyProgrammer
4022d7aa89 Remove test code (#636) 2024-04-11 23:29:46 -03:00
16 changed files with 139 additions and 20 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## [9.4.0](https://github.com/LuanRT/YouTube.js/compare/v9.3.0...v9.4.0) (2024-04-29)
### Features
* **Format:** Add `projection_type` and `stereo_layout` ([#643](https://github.com/LuanRT/YouTube.js/issues/643)) ([064436c](https://github.com/LuanRT/YouTube.js/commit/064436cef30e892d8f569d4f7b146557fd72b09f))
* **Format:** Add `spatial_audio_type` ([#647](https://github.com/LuanRT/YouTube.js/issues/647)) ([0ba8c54](https://github.com/LuanRT/YouTube.js/commit/0ba8c54257b068d7e4518c982396acb42f1dd41d))
* **Parser:** Add `MusicResponsiveHeader` node ([ea82bea](https://github.com/LuanRT/YouTube.js/commit/ea82beaa10f6c877d6dd3102e10f6ae382560e0f))
## [9.3.0](https://github.com/LuanRT/YouTube.js/compare/v9.2.1...v9.3.0) (2024-04-11)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "youtubei.js",
"version": "9.3.0",
"version": "9.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "9.3.0",
"version": "9.4.0",
"funding": [
"https://github.com/sponsors/LuanRT"
],

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "9.3.0",
"version": "9.4.0",
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",

View File

@@ -325,6 +325,18 @@ export default class Innertube {
return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0;
}
/**
* Retrieves playlists.
*/
async getPlaylists() {
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEplaylist_aggregation' }), parse: true }
);
const feed = new Feed(this.actions, response);
return feed.playlists;
}
/**
* Retrieves playlist contents.
* @param id - Playlist id

View File

@@ -168,6 +168,7 @@ export default class Actions {
'FEhistory',
'FEsubscriptions',
'FEchannels',
'FEplaylist_aggregation',
'FEmusic_listening_review',
'FEmusic_library_landing',
'SPaccount_overview',

View File

@@ -9,7 +9,6 @@ import {
import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.js';
import Message from '../../parser/classes/Message.js';
import MusicCarouselShelf from '../../parser/classes/MusicCarouselShelf.js';
import MusicDescriptionShelf from '../../parser/classes/MusicDescriptionShelf.js';
import MusicQueue from '../../parser/classes/MusicQueue.js';
import MusicTwoRowItem from '../../parser/classes/MusicTwoRowItem.js';
@@ -278,7 +277,7 @@ export default class Music {
* Retrieves related content.
* @param video_id - The video id.
*/
async getRelated(video_id: string): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
async getRelated(video_id: string): Promise<SectionList | Message> {
throwIfMissing({ video_id });
const response = await this.#actions.execute(
@@ -297,9 +296,9 @@ export default class Music {
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
const contents = page.contents.item().as(SectionList, Message);
return shelves;
return contents;
}
/**

View File

@@ -1,4 +1,3 @@
import { encodeShortsParam } from '../../proto/index.js';
import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.js';
export const PATH = '/player';
@@ -9,11 +8,6 @@ export const PATH = '/player';
* @returns The payload.
*/
export function build(opts: PlayerEndpointOptions): IPlayerRequest {
const is_android =
opts.client === 'ANDROID' ||
opts.client === 'YTMUSIC_ANDROID' ||
opts.client === 'YTSTUDIO_ANDROID';
return {
playbackContext: {
contentPlaybackContext: {
@@ -43,8 +37,7 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
...{
client: opts.client,
playlistId: opts.playlist_id,
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
params: is_android ? encodeShortsParam() : opts.params
params: opts.params
}
};
}

View File

@@ -0,0 +1,14 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import ChipView from './ChipView.js';
export default class ChipBarView extends YTNode {
static type = 'ChipBarView';
chips: ObservedArray<ChipView> | null;
constructor(data: RawNode) {
super();
this.chips = Parser.parseArray(data.chips, ChipView);
}
}

View File

@@ -0,0 +1,20 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class ChipView extends YTNode {
static type = 'ChipView';
text: string;
display_type: string;
endpoint: NavigationEndpoint;
chip_entity_key: string;
constructor(data: RawNode) {
super();
this.text = data.text;
this.display_type = data.displayType;
this.endpoint = new NavigationEndpoint(data.tapCommand);
this.chip_entity_key = data.chipEntityKey;
}
}

View File

@@ -0,0 +1,43 @@
import { Parser, type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
import MusicThumbnail from './MusicThumbnail.js';
import MusicDescriptionShelf from './MusicDescriptionShelf.js';
import MusicInlineBadge from './MusicInlineBadge.js';
import MusicPlayButton from './MusicPlayButton.js';
import ToggleButton from './ToggleButton.js';
import Menu from './menus/Menu.js';
import type { ObservedArray } from '../helpers.js';
export default class MusicResponsiveHeader extends YTNode {
static type = 'MusicResponsiveHeader';
thumbnail: MusicThumbnail | null;
buttons: ObservedArray<ToggleButton | MusicPlayButton | Menu> | null;
title: Text;
subtitle: Text;
strapline_text_one: Text;
strapline_thumbnail: MusicThumbnail | null;
second_subtitle: Text;
subtitle_badge?: ObservedArray<MusicInlineBadge> | null;
description?: MusicDescriptionShelf | null;
constructor(data: RawNode) {
super();
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
this.buttons = Parser.parseArray(data.buttons, [ ToggleButton, MusicPlayButton, Menu ]);
this.title = new Text(data.title);
this.subtitle = new Text(data.subtitle);
this.strapline_text_one = new Text(data.straplineTextOne);
this.strapline_thumbnail = Parser.parseItem(data.straplineThumbnail, MusicThumbnail);
this.second_subtitle = new Text(data.secondSubtitle);
if (Reflect.has(data, 'subtitleBadge')) {
this.subtitle_badge = Parser.parseArray(data.subtitleBadge, MusicInlineBadge);
}
if (Reflect.has(data, 'description')) {
this.description = Parser.parseItem(data.description, MusicDescriptionShelf);
}
}
}

View File

@@ -43,8 +43,6 @@ export default class CommentView extends YTNode {
};
author?: Author;
test: any;
is_liked?: boolean;
is_disliked?: boolean;
is_hearted?: boolean;

View File

@@ -12,6 +12,8 @@ export default class Format {
average_bitrate?: number;
width: number;
height: number;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
init_range?: {
start: number;
@@ -41,6 +43,7 @@ export default class Format {
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
max_dvr_duration_sec?: number;
target_duration_dec?: number;
has_audio: boolean;
@@ -77,6 +80,8 @@ export default class Format {
this.average_bitrate = data.averageBitrate;
this.width = data.width;
this.height = data.height;
this.projection_type = data.projectionType;
this.stereo_layout = data.stereoLayout?.replace('STEREO_LAYOUT_', '');
this.init_range = data.initRange ? {
start: parseInt(data.initRange.start),
@@ -101,6 +106,7 @@ export default class Format {
this.audio_sample_rate = parseInt(data.audioSampleRate);
this.audio_channels = data.audioChannels;
this.loudness_db = data.loudnessDb;
this.spatial_audio_type = data.spatialAudioType?.replace('SPATIAL_AUDIO_TYPE_', '');
this.max_dvr_duration_sec = data.maxDvrDurationSec;
this.target_duration_dec = data.targetDurationSec;
this.has_audio = !!data.audioBitrate || !!data.audioQuality;

View File

@@ -55,8 +55,10 @@ export { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailW
export { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer.js';
export { default as Chapter } from './classes/Chapter.js';
export { default as ChildVideo } from './classes/ChildVideo.js';
export { default as ChipBarView } from './classes/ChipBarView.js';
export { default as ChipCloud } from './classes/ChipCloud.js';
export { default as ChipCloudChip } from './classes/ChipCloudChip.js';
export { default as ChipView } from './classes/ChipView.js';
export { default as ClipAdState } from './classes/ClipAdState.js';
export { default as ClipCreation } from './classes/ClipCreation.js';
export { default as ClipCreationScrubber } from './classes/ClipCreationScrubber.js';
@@ -261,6 +263,7 @@ export { default as MusicNavigationButton } from './classes/MusicNavigationButto
export { default as MusicPlayButton } from './classes/MusicPlayButton.js';
export { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.js';
export { default as MusicQueue } from './classes/MusicQueue.js';
export { default as MusicResponsiveHeader } from './classes/MusicResponsiveHeader.js';
export { default as MusicResponsiveListItem } from './classes/MusicResponsiveListItem.js';
export { default as MusicResponsiveListItemFixedColumn } from './classes/MusicResponsiveListItemFixedColumn.js';
export { default as MusicResponsiveListItemFlexColumn } from './classes/MusicResponsiveListItemFlexColumn.js';

View File

@@ -34,12 +34,14 @@ export const OAUTH = Object.freeze({
});
export const CLIENTS = Object.freeze({
iOS: {
NAME_ID: '5',
NAME: 'iOS',
VERSION: '18.06.35',
USER_AGENT: 'com.google.ios.youtube/18.06.35 (iPhone; CPU iPhone OS 14_4 like Mac OS X; en_US)',
DEVICE_MODEL: 'iPhone10,6'
},
WEB: {
NAME_ID: '1',
NAME: 'WEB',
VERSION: '2.20240111.09.00',
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
@@ -47,28 +49,34 @@ export const CLIENTS = Object.freeze({
STATIC_VISITOR_ID: '6zpwvWUNAco'
},
WEB_KIDS: {
NAME_ID: '76',
NAME: 'WEB_KIDS',
VERSION: '2.20230111.00.00'
},
YTMUSIC: {
NAME_ID: '67',
NAME: 'WEB_REMIX',
VERSION: '1.20211213.00.00'
},
ANDROID: {
NAME_ID: '3',
NAME: 'ANDROID',
VERSION: '18.48.37',
SDK_VERSION: 33,
USER_AGENT: 'com.google.android.youtube/18.48.37(Linux; U; Android 13; en_US; sdk_gphone64_x86_64 Build/UPB4.230623.005) gzip'
},
YTSTUDIO_ANDROID: {
NAME_ID: '14',
NAME: 'ANDROID_CREATOR',
VERSION: '22.43.101'
},
YTMUSIC_ANDROID: {
NAME_ID: '21',
NAME: 'ANDROID_MUSIC',
VERSION: '5.34.51'
},
TV_EMBEDDED: {
NAME_ID: '85',
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
VERSION: '2.0'
}

View File

@@ -57,9 +57,16 @@ export default class HTTPClient {
request_headers.set('Accept', '*/*');
request_headers.set('Accept-Language', '*');
request_headers.set('X-Goog-Visitor-Id', this.#session.context.client.visitorData || '');
request_headers.set('X-Origin', request_url.origin);
request_headers.set('X-Youtube-Client-Version', this.#session.context.client.clientVersion || '');
const client_constant = Object.values(Constants.CLIENTS).find((client) => {
return client.NAME === this.#session.context.client.clientName;
});
if (client_constant) {
request_headers.set('X-Youtube-Client-Name', client_constant.NAME_ID);
}
if (Platform.shim.server) {
request_headers.set('User-Agent', getRandomUserAgent('desktop'));
request_headers.set('origin', request_url.origin);
@@ -90,6 +97,14 @@ export default class HTTPClient {
this.#adjustContext(n_body.context, n_body.client);
request_headers.set('x-youtube-client-version', n_body.context.client.clientVersion);
const client_constant = Object.values(Constants.CLIENTS).find((client) => {
return client.NAME === n_body.context.client.clientName;
});
if (client_constant) {
request_headers.set('X-Youtube-Client-Name', client_constant.NAME_ID);
}
delete n_body.client;
if (Platform.shim.server) {
@@ -109,7 +124,6 @@ export default class HTTPClient {
request_headers.set('User-Agent', Constants.CLIENTS.ANDROID.USER_AGENT);
request_headers.set('X-GOOG-API-FORMAT-VERSION', '2');
request_headers.delete('X-Youtube-Client-Version');
request_headers.delete('X-Origin');
}
}

View File

@@ -402,7 +402,6 @@ describe('YouTube.js Tests', () => {
test('Innertube#music.getRelated', async () => {
const related = await innertube.music.getRelated('eaJHysi5tYg');
expect(related).toBeDefined();
expect(related?.length).toBeGreaterThan(0);
});
test('Innertube#music.getSearchSuggestions', async () => {