Compare commits

...

24 Commits

Author SHA1 Message Date
github-actions[bot]
5e3846259f chore(main): release 5.2.1 (#429)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-03 22:30:29 -03:00
LuanRT
222dfce6bb fix: incorrect node parser implementations (#428)
These were causing some issues in v5.2.0.
2023-07-03 21:58:00 -03:00
github-actions[bot]
83cbfd631b chore(main): release 5.2.0 (#406)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-28 17:13:36 -03:00
RikThePixel
4f9427d752 fix(Playlist): Add thumbnail_renderer on Playlist when response includes it (#424) 2023-06-28 17:12:06 -03:00
Patrick Kan
07c1b3e0e5 fix(OAuth): client identity matching (#421) 2023-06-28 16:26:33 -03:00
absidue
89548ad48a chore(constants): Update web client version (#420) 2023-06-28 06:56:57 -03:00
absidue
519be72445 fix(PlayerEndpoint): Use different player params (#419)
* fix(PlayerEndpoint): Use different player params

* fix(PlayerEndpoint): Use new throttling bypass player params
2023-06-28 06:56:21 -03:00
Emma
e434bb2632 fix(VideoInfo.ts): reimplement get music_tracks (#409)
* fix(VideoInfo.ts): reimplement `get music_tracks`

- Add parser classes to parse needed data
  - Add `CarouselLockup`
  - Add `EngagementPanelSectionList`
  - Add `InfoRow`
  - Add `StructuredDescriptionContent`
  - Add `VideoDescriptionMusicSection`
  - Add `VideoDescriptionHeader`
  - Add `Factoid`
  - Add `ExpandableVideoDescriptionBody`
  - Add `AdsEngagementPanelContent`
- Add `engagement_panels` to raw and parsed next responses
- Add `engagement_panels` parsing code to `parser.ts`

* Check for song inside of video_lockup first before checking info_rows

* Add support for pulling artist ids out of music_tracks

- Add support for WRITERS InfoRow
- Check for video id inside of naviagation endpoint on info_row metadata

* Add `AdsEngagementPanelContent` to ignore list

* Switch `map => parseItem` to `parseArray`

* Use `Text` && `NavigationEndpoint`

* Replace `String` with `Text` in `ExpandableVideoDescriptionBody`
2023-06-28 06:54:55 -03:00
absidue
a11e5962c6 feat(VideoDetails): Add is_post_live_dvr property (#411) 2023-05-31 19:12:19 -03:00
Jake Reid Browning
77b39c79ee feat(ytmusic): Add support for YouTube Music mood filters (#404)
* add filters property to ytmusic HomeFeed

* remove implied type

* add applyFilter to HomeFeed

* add test

* remove section_list var
2023-05-23 15:00:05 -03:00
absidue
7c530d30ee chore(parser): Remove extra Array.from call (#407) 2023-05-20 18:00:15 -03:00
absidue
1e07a184ff perf(Search): Speed up results parsing (#408) 2023-05-20 17:57:48 -03:00
absidue
5de7b24dc5 perf(toDash): Speed up format filtering (#405) 2023-05-18 14:41:21 -03:00
github-actions[bot]
01fd1ee72a chore(main): release 5.1.0 (#403)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 01:12:16 -03:00
absidue
84b4f1efd1 feat(toDash): Add audio track labels to the manifest when available (#402) 2023-05-14 01:11:02 -03:00
absidue
046103a4d8 feat(ReelItem): Add accessibility label (#401) 2023-05-14 01:09:51 -03:00
github-actions[bot]
beb4733e84 chore(main): release 5.0.4 (#399)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-10 15:35:11 -03:00
absidue
66b026bf49 fix(Utils): Circular dependency introduced in 38a83c3c2a (#400) 2023-05-10 15:32:00 -03:00
absidue
26734194ab fix(bundles): Use ESM tslib build for the browser bundles (#397) 2023-05-06 15:54:18 -03:00
absidue
38a83c3c2a fix(Utils): Use instanceof in deepCompare instead of the constructor name (#398) 2023-05-06 15:47:51 -03:00
absidue
b1f19f16ac refactor(constants): Use namespace import internally (#396) 2023-05-06 15:46:20 -03:00
github-actions[bot]
891d889408 chore(main): release 5.0.3 (#395)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-03 19:47:01 -03:00
LuanRT
d4adb9eb6b chore(PlayerEndpoint.ts): add back attestationRequest field
This was accidentally removed in a recent PR. It tells InnerTube to omit the botguard program data.
2023-05-03 19:31:59 -03:00
LuanRT
3b0498b68b fix(Video): typo causing node parsing to fail 2023-05-02 03:21:02 -03:00
40 changed files with 451 additions and 95 deletions

View File

@@ -1,5 +1,58 @@
# Changelog
## [5.2.1](https://github.com/LuanRT/YouTube.js/compare/v5.2.0...v5.2.1) (2023-07-04)
### Bug Fixes
* incorrect node parser implementations ([#428](https://github.com/LuanRT/YouTube.js/issues/428)) ([222dfce](https://github.com/LuanRT/YouTube.js/commit/222dfce6bbd13b2cd80ae11540cbc0edd9053fc5))
## [5.2.0](https://github.com/LuanRT/YouTube.js/compare/v5.1.0...v5.2.0) (2023-06-28)
### Features
* **VideoDetails:** Add is_post_live_dvr property ([#411](https://github.com/LuanRT/YouTube.js/issues/411)) ([a11e596](https://github.com/LuanRT/YouTube.js/commit/a11e5962c6eb73b14623a9de1e6c8c2534146b1e))
* **ytmusic:** Add support for YouTube Music mood filters ([#404](https://github.com/LuanRT/YouTube.js/issues/404)) ([77b39c7](https://github.com/LuanRT/YouTube.js/commit/77b39c79ee0768eb203b7d47ea81286d470c21f2))
### Bug Fixes
* **OAuth:** client identity matching ([#421](https://github.com/LuanRT/YouTube.js/issues/421)) ([07c1b3e](https://github.com/LuanRT/YouTube.js/commit/07c1b3e0e57cb1fa42e4772775bfd1437bbc731f))
* **PlayerEndpoint:** Use different player params ([#419](https://github.com/LuanRT/YouTube.js/issues/419)) ([519be72](https://github.com/LuanRT/YouTube.js/commit/519be72445b7ff392b396e16bcb1dc05c7df8976))
* **Playlist:** Add thumbnail_renderer on Playlist when response includes it ([#424](https://github.com/LuanRT/YouTube.js/issues/424)) ([4f9427d](https://github.com/LuanRT/YouTube.js/commit/4f9427d752e89faec8dd1c4fd7a9607dca998c7a))
* **VideoInfo.ts:** reimplement `get music_tracks` ([#409](https://github.com/LuanRT/YouTube.js/issues/409)) ([e434bb2](https://github.com/LuanRT/YouTube.js/commit/e434bb2632fe2b20aab6f1e707a93ca76f9d5c91))
### Performance Improvements
* **Search:** Speed up results parsing ([#408](https://github.com/LuanRT/YouTube.js/issues/408)) ([1e07a18](https://github.com/LuanRT/YouTube.js/commit/1e07a184ffaff508ad5ba869cb5e7dc9f095f744))
* **toDash:** Speed up format filtering ([#405](https://github.com/LuanRT/YouTube.js/issues/405)) ([5de7b24](https://github.com/LuanRT/YouTube.js/commit/5de7b24dc55fca3eb8fccc6fa30d3c2cd60b8184))
## [5.1.0](https://github.com/LuanRT/YouTube.js/compare/v5.0.4...v5.1.0) (2023-05-14)
### Features
* **ReelItem:** Add accessibility label ([#401](https://github.com/LuanRT/YouTube.js/issues/401)) ([046103a](https://github.com/LuanRT/YouTube.js/commit/046103a4d8af09fafefab6e9f971184eeca75c2e))
* **toDash:** Add audio track labels to the manifest when available ([#402](https://github.com/LuanRT/YouTube.js/issues/402)) ([84b4f1e](https://github.com/LuanRT/YouTube.js/commit/84b4f1efd111321e4f3e5a87844790c4ec9b0b52))
## [5.0.4](https://github.com/LuanRT/YouTube.js/compare/v5.0.3...v5.0.4) (2023-05-10)
### Bug Fixes
* **bundles:** Use ESM tslib build for the browser bundles ([#397](https://github.com/LuanRT/YouTube.js/issues/397)) ([2673419](https://github.com/LuanRT/YouTube.js/commit/26734194ab0bc5a9f57e1c509d7646ce8903d0c6))
* **Utils:** Circular dependency introduced in 38a83c3c2aa814150d1d9b8ed99fca915c1d67fe ([#400](https://github.com/LuanRT/YouTube.js/issues/400)) ([66b026b](https://github.com/LuanRT/YouTube.js/commit/66b026bf493d71a39e12825938fe54dc63aefd16))
* **Utils:** Use instanceof in deepCompare instead of the constructor name ([#398](https://github.com/LuanRT/YouTube.js/issues/398)) ([38a83c3](https://github.com/LuanRT/YouTube.js/commit/38a83c3c2aa814150d1d9b8ed99fca915c1d67fe))
## [5.0.3](https://github.com/LuanRT/YouTube.js/compare/v5.0.2...v5.0.3) (2023-05-03)
### Bug Fixes
* **Video:** typo causing node parsing to fail ([3b0498b](https://github.com/LuanRT/YouTube.js/commit/3b0498b68b5378e63283e792bd45571c0b919e0b))
## [5.0.2](https://github.com/LuanRT/YouTube.js/compare/v5.0.1...v5.0.2) (2023-04-30)

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "5.0.2",
"version": "5.2.1",
"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",
@@ -73,7 +73,7 @@
"build:esm": "npx tsc",
"build:deno": "npx cpy ./src ./deno && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'linkedom';\" \"'https://esm.sh/linkedom';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./deno -r && npx replace \"new Jinter.default\" \"new Jinter\" ./deno -r",
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser",
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/browser.js --platform=browser",
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
"prepare": "npm run build",
"watch": "npx tsc --watch"

View File

@@ -20,7 +20,7 @@ import { AccountManager, InteractionManager, PlaylistManager } from './core/mana
import { Feed, TabbedFeed } from './core/mixins/index.js';
import Proto from './proto/index.js';
import Constants from './utils/Constants.js';
import * as Constants from './utils/Constants.js';
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js';
import {

View File

@@ -1,4 +1,4 @@
import Constants from '../utils/Constants.js';
import * as Constants from '../utils/Constants.js';
import { OAuthError, Platform } from '../utils/Utils.js';
import type Session from './Session.js';
@@ -96,7 +96,7 @@ export default class OAuth {
client_id: this.#identity.client_id,
scope: Constants.OAUTH.SCOPE,
device_id: Platform.shim.uuidv4(),
model_name: Constants.OAUTH.MODEL_NAME
device_model: Constants.OAUTH.MODEL_NAME
};
const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), {

View File

@@ -1,6 +1,6 @@
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
import Constants from '../utils/Constants.js';
import * as Constants from '../utils/Constants.js';
import type { ICache } from '../types/Cache.js';
import type { FetchFunction } from '../types/PlatformShim.js';

View File

@@ -1,4 +1,4 @@
import Constants, { CLIENTS } from '../utils/Constants.js';
import * as Constants from '../utils/Constants.js';
import EventEmitterLike from '../utils/EventEmitterLike.js';
import Actions from './Actions.js';
import Player from './Player.js';
@@ -325,7 +325,7 @@ export default class Session extends EventEmitterLike {
screenWidthPoints: 1920,
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
clientName: options.client_name,
clientVersion: CLIENTS.WEB.VERSION,
clientVersion: Constants.CLIENTS.WEB.VERSION,
osName: 'Windows',
osVersion: '10.0',
platform: options.device_category.toUpperCase(),
@@ -343,7 +343,7 @@ export default class Session extends EventEmitterLike {
}
};
return { context, api_key: CLIENTS.WEB.API_KEY, api_version: CLIENTS.WEB.API_VERSION };
return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION };
}
async signIn(credentials?: Credentials): Promise<void> {

View File

@@ -1,5 +1,5 @@
import Proto from '../../proto/index.js';
import { Constants } from '../../utils/index.js';
import * as Constants from '../../utils/Constants.js';
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.js';
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.js';

View File

@@ -28,6 +28,9 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
}
}
},
attestationRequest: {
omitBotguardData: true
},
racyCheckOk: true,
contentCheckOk: true,
videoId: opts.video_id,
@@ -35,7 +38,7 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
client: opts.client,
playlistId: opts.playlist_id,
// Workaround streaming URLs returning 403 when using Android clients and throttling in web clients.
params: '8AEB'
params: '2AMBCgIQBg'
}
};
}

View File

@@ -1,6 +1,6 @@
import type { ApiResponse } from '../Actions.js';
import type Actions from '../Actions.js';
import Constants from '../../utils/Constants.js';
import * as Constants from '../../utils/Constants.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
import FormatUtils from '../../utils/FormatUtils.js';
import { InnertubeError } from '../../utils/Utils.js';

View File

@@ -0,0 +1,17 @@
import { type ObservedArray, YTNode } from '../helpers.js';
import InfoRow from './InfoRow.js';
import Parser, { type RawNode } from '../index.js';
import CompactVideo from './CompactVideo.js';
export default class CarouselLockup extends YTNode {
static type = 'CarouselLockup';
info_rows: ObservedArray<InfoRow>;
video_lockup?: CompactVideo | null;
constructor(data: RawNode) {
super();
this.info_rows = Parser.parseArray(data.infoRows, InfoRow);
this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
}
}

View File

@@ -0,0 +1,25 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import ContinuationItem from './ContinuationItem.js';
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js';
import SectionList from './SectionList.js';
import StructuredDescriptionContent from './StructuredDescriptionContent.js';
export default class EngagementPanelSectionList extends YTNode {
static type = 'EngagementPanelSectionList';
header: EngagementPanelTitleHeader | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | null;
target_id?: string;
panel_identifier?: string;
visibility?: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]);
this.panel_identifier = data.panelIdentifier;
this.target_id = data.targetId;
this.visibility = data.visibility;
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Text from './misc/Text.js';
import Button from './Button.js';
export default class EngagementPanelTitleHeader extends YTNode {
static type = 'EngagementPanelTitleHeader';
title: Text;
visibility_button: Button | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.visibility_button = Parser.parseItem(data.visibilityButton, Button);
}
}

View File

@@ -0,0 +1,22 @@
import { YTNode } from '../helpers.js';
import { Text } from '../misc.js';
import type { RawNode } from '../index.js';
export default class ExpandableVideoDescriptionBody extends YTNode {
static type = 'ExpandableVideoDescriptionBody';
show_more_text: Text;
show_less_text: Text;
attributed_description_body_text?: string;
constructor(data: RawNode) {
super();
this.show_more_text = new Text(data.showMoreText);
this.show_less_text = new Text(data.showLessText);
if (Reflect.has(data, 'attributedDescriptionBodyText')) {
this.attributed_description_body_text = data.attributedDescriptionBodyText?.content;
}
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import { Text } from '../misc.js';
export default class Factoid extends YTNode {
static type = 'Factoid';
label: Text;
value: Text;
accessibility_text: String;
constructor(data: RawNode) {
super();
this.label = new Text(data.label);
this.value = new Text(data.value);
this.accessibility_text = data.accessibilityText;
}
}

View File

@@ -0,0 +1,29 @@
import { YTNode } from '../helpers.js';
import { Text } from '../misc.js';
import type { RawNode } from '../index.js';
export default class InfoRow extends YTNode {
static type = 'InfoRow';
title: Text;
default_metadata?: Text;
expanded_metadata?: Text;
info_row_expand_status_key?: String;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
if (Reflect.has(data, 'defaultMetadata')) {
this.default_metadata = new Text(data.defaultMetadata);
}
if (Reflect.has(data, 'expandedMetadata')) {
this.expanded_metadata = new Text(data.expandedMetadata);
}
if (Reflect.has(data, 'infoRowExpandStatusKey')) {
this.info_row_expand_status_key = data.infoRowExpandStatusKey;
}
}
}

View File

@@ -1,6 +1,7 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import PlaylistVideoThumbnail from './PlaylistVideoThumbnail.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
@@ -12,6 +13,7 @@ export default class Playlist extends YTNode {
title: Text;
author: Text | Author;
thumbnails: Thumbnail[];
thumbnail_renderer?: PlaylistVideoThumbnail;
video_count: Text;
video_count_short: Text;
first_videos: ObservedArray<YTNode>;
@@ -41,6 +43,10 @@ export default class Playlist extends YTNode {
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
if (Reflect.has(data, 'thumbnailRenderer')) {
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, PlaylistVideoThumbnail) || undefined;
}
if (Reflect.has(data, 'viewPlaylistText')) {
this.view_playlist = new Text(data.viewPlaylistText);
}

View File

@@ -12,6 +12,7 @@ export default class ReelItem extends YTNode {
thumbnails: Thumbnail[];
views: Text;
endpoint: NavigationEndpoint;
accessibility_label?: string;
constructor(data: RawNode) {
super();
@@ -20,5 +21,6 @@ export default class ReelItem extends YTNode {
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.views = new Text(data.viewCountText);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.accessibility_label = data.accessibility.accessibilityData.label;
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js';
import VideoDescriptionHeader from './VideoDescriptionHeader.js';
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js';
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
export default class StructuredDescriptionContent extends YTNode {
static type = 'StructuredDescriptionContent';
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection>;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection ]);
}
}

View File

@@ -53,7 +53,7 @@ export default class Video extends YTNode {
this.description_snippet = new Text(data.descriptionSnippet);
}
if (Reflect.has(data, 'detaildMetadataSnippets')) {
if (Reflect.has(data, 'detailedMetadataSnippets')) {
this.snippets = data.detailedMetadataSnippets.map((snippet: RawNode) => ({
text: new Text(snippet.snippetText),
hover_text: new Text(snippet.snippetHoverText)
@@ -127,4 +127,4 @@ export default class Video extends YTNode {
get best_thumbnail(): Thumbnail | undefined {
return this.thumbnails[0];
}
}
}

View File

@@ -0,0 +1,28 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { Text, Thumbnail } from '../misc.js';
import Factoid from './Factoid.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class VideoDescriptionHeader extends YTNode {
static type = 'VideoDescriptionHeader';
channel: Text;
channel_navigation_endpoint?: NavigationEndpoint;
channel_thumbnail: Thumbnail[];
factoids: ObservedArray<Factoid>;
publish_date: Text;
title: Text;
views: Text;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.channel = new Text(data.channel);
this.channel_navigation_endpoint = new NavigationEndpoint(data.channelNavigationEndpoint);
this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail);
this.publish_date = new Text(data.publishDate);
this.views = new Text(data.views);
this.factoids = Parser.parseArray(data.factoid, Factoid);
}
}

View File

@@ -0,0 +1,28 @@
import Parser, { type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class VideoDescriptionInfocardsSection extends YTNode {
static type = 'VideoDescriptionInfocardsSection';
section_title: Text;
creator_videos_button: Button | null;
creator_about_button: Button | null;
section_subtitle: Text;
channel_avatar: Thumbnail[];
channel_endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.section_title = new Text(data.sectionTitle);
this.creator_videos_button = Parser.parseItem(data.creatorVideosButton, Button);
this.creator_about_button = Parser.parseItem(data.creatorAboutButton, Button);
this.section_subtitle = new Text(data.sectionSubtitle);
this.channel_avatar = Thumbnail.fromResponse(data.channelAvatar);
this.channel_endpoint = new NavigationEndpoint(data.channelEndpoint);
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { Text } from '../misc.js';
import CarouselLockup from './CarouselLockup.js';
export default class VideoDescriptionMusicSection extends YTNode {
static type = 'VideoDescriptionMusicSection';
carousel_lockups: ObservedArray<CarouselLockup>;
section_title: Text;
constructor(data: RawNode) {
super();
this.carousel_lockups = Parser.parseArray(data.carouselLockups, CarouselLockup);
this.section_title = new Text(data.sectionTitle);
}
}

View File

@@ -1,4 +1,4 @@
import Constants from '../../../utils/Constants.js';
import * as Constants from '../../../utils/Constants.js';
import type { YTNode} from '../../helpers.js';
import { observe, type ObservedArray } from '../../helpers.js';
import Parser, { type RawNode } from '../../index.js';

View File

@@ -18,6 +18,7 @@ export default class VideoDetails {
is_live_content: boolean;
is_upcoming: boolean;
is_crawlable: boolean;
is_post_live_dvr: boolean;
constructor(data: RawNode) {
this.id = data.videoId;
@@ -35,6 +36,7 @@ export default class VideoDetails {
this.is_live = !!data.isLive;
this.is_live_content = !!data.isLiveContent;
this.is_upcoming = !!data.isUpcoming;
this.is_post_live_dvr = !!data.isPostLiveDvr;
this.is_crawlable = !!data.isCrawlable;
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-cond-assign */
import { Platform } from '../utils/Utils.js';
import { InnertubeError, Platform } from '../utils/Utils.js';
import Author from './classes/misc/Author.js';
import Text from './classes/misc/Text.js';
import Thumbnail from './classes/misc/Thumbnail.js';
@@ -62,7 +62,9 @@ export class YTNodeGenerator {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
static #logNewClass(classname: string, key_info: KeyInfo) {
console.warn(`${classname} not found!\nThis is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`);
console.warn(
new InnertubeError(`${classname} not found!\nThis is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`)
);
}
static #logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) {
console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.#camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`);
@@ -460,15 +462,15 @@ export class YTNodeGenerator {
* @returns The parsed value
*/
static parse(key: string, inference_type: InferenceType, data: any, key_path: string[] = [ 'data' ]) {
const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, key ]);
const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({ data }, [ ...key_path, key ]);
switch (inference_type.type) {
case 'renderer':
{
return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
}
case 'renderer_list':
{
return should_optional ? Parser.parse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
return should_optional ? Parser.parse(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
}
case 'object':
{
@@ -482,25 +484,25 @@ export class YTNodeGenerator {
case 'misc':
switch (inference_type.misc_type) {
case 'NavigationEndpoint':
return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
case 'Text':
return should_optional ? new Text(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
return should_optional ? new Text(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
case 'Thumbnail':
return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
case 'Author':
{
const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]);
const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({ data }, [ ...key_path, inference_type.params[0] ]);
return author_should_optional ? new Author(
this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]),
this.#accessDataFromKeyPath({ data }, [ ...key_path, inference_type.params[0] ]),
inference_type.params[1] ?
this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[1] ]) : undefined
this.#accessDataFromKeyPath({ data }, [ ...key_path, inference_type.params[1] ]) : undefined
) : undefined;
}
}
throw new Error('Unreachable code reached! Switch missing case!');
case 'primative':
case 'unknown':
return this.#accessDataFromKeyPath({data}, [ ...key_path, key ]);
return this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]);
}
}
static #passOne(classdata: any) {

View File

@@ -30,6 +30,7 @@ export { default as Card } from './classes/Card.js';
export { default as CardCollection } from './classes/CardCollection.js';
export { default as CarouselHeader } from './classes/CarouselHeader.js';
export { default as CarouselItem } from './classes/CarouselItem.js';
export { default as CarouselLockup } from './classes/CarouselLockup.js';
export { default as Channel } from './classes/Channel.js';
export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.js';
export { default as ChannelAgeGate } from './classes/ChannelAgeGate.js';
@@ -88,9 +89,13 @@ export { default as Endscreen } from './classes/Endscreen.js';
export { default as EndscreenElement } from './classes/EndscreenElement.js';
export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js';
export { default as EndScreenVideo } from './classes/EndScreenVideo.js';
export { default as EngagementPanelSectionList } from './classes/EngagementPanelSectionList.js';
export { default as EngagementPanelTitleHeader } from './classes/EngagementPanelTitleHeader.js';
export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js';
export { default as ExpandableTab } from './classes/ExpandableTab.js';
export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js';
export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js';
export { default as Factoid } from './classes/Factoid.js';
export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js';
export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js';
export { default as GameCard } from './classes/GameCard.js';
@@ -121,6 +126,7 @@ export { default as HorizontalMovieList } from './classes/HorizontalMovieList.js
export { default as IconLink } from './classes/IconLink.js';
export { default as InfoPanelContainer } from './classes/InfoPanelContainer.js';
export { default as InfoPanelContent } from './classes/InfoPanelContent.js';
export { default as InfoRow } from './classes/InfoRow.js';
export { default as InteractiveTabbedHeader } from './classes/InteractiveTabbedHeader.js';
export { default as ItemSection } from './classes/ItemSection.js';
export { default as ItemSectionHeader } from './classes/ItemSectionHeader.js';
@@ -299,6 +305,7 @@ export { default as SingleHeroImage } from './classes/SingleHeroImage.js';
export { default as SlimOwner } from './classes/SlimOwner.js';
export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js';
export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js';
export { default as StructuredDescriptionContent } from './classes/StructuredDescriptionContent.js';
export { default as SubFeedOption } from './classes/SubFeedOption.js';
export { default as SubFeedSelector } from './classes/SubFeedSelector.js';
export { default as SubscribeButton } from './classes/SubscribeButton.js';
@@ -335,6 +342,9 @@ export { default as VerticalList } from './classes/VerticalList.js';
export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js';
export { default as Video } from './classes/Video.js';
export { default as VideoCard } from './classes/VideoCard.js';
export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.js';
export { default as VideoDescriptionInfocardsSection } from './classes/VideoDescriptionInfocardsSection.js';
export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js';
export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js';
export { default as VideoOwner } from './classes/VideoOwner.js';
export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js';

View File

@@ -264,6 +264,13 @@ export default class Parser {
parsed_data.cards = cards;
}
const engagement_panels = data.engagementPanels?.map((e) => {
const item = this.parseItem(e, YTNodes.EngagementPanelSectionList) as YTNodes.EngagementPanelSectionList;
return item;
});
if (engagement_panels) {
parsed_data.engagement_panels = engagement_panels;
}
this.#createMemo();
const items = this.parse(data.items);
if (items) {
@@ -502,14 +509,15 @@ export default class Parser {
'BrandVideoShelf',
'BrandVideoSingleton',
'StatementBanner',
'GuideSigninPromo'
'GuideSigninPromo',
'AdsEngagementPanelContent'
]);
static shouldIgnore(classname: string) {
return this.ignore_list.has(classname);
}
static #rt_nodes = new Map<string, YTNodeConstructor>(Array.from(Object.entries(YTNodes)));
static #rt_nodes = new Map<string, YTNodeConstructor>(Object.entries(YTNodes));
static #dynamic_nodes = new Map<string, YTNodeConstructor>();
static getParserByName(classname: string) {
@@ -739,4 +747,4 @@ export class LiveChatContinuation extends YTNode {
this.viewer_name = data.viewerName;
}
}
}

View File

@@ -17,7 +17,7 @@ import type VideoDetails from '../classes/misc/VideoDetails.js';
import type Alert from '../classes/Alert.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.js';
export interface IParsedResponse {
actions?: SuperParsedResult<YTNode>;
actions_memo?: Memo;
@@ -73,6 +73,7 @@ export interface IParsedResponse {
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
cards?: CardCollection;
engagement_panels?: EngagementPanelSectionList[];
items?: SuperParsedResult<YTNode>;
}
@@ -111,6 +112,7 @@ export interface INextResponse {
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
on_response_received_endpoints_memo?: Memo;
player_overlays?: SuperParsedResult<YTNode>;
engagement_panels?: EngagementPanelSectionList[];
}
export interface IBrowseResponse {
@@ -163,4 +165,4 @@ export interface IUpdatedMetadataResponse {
export interface IGuideResponse {
items: SuperParsedResult<YTNode>;
items_memo: Memo;
}
}

View File

@@ -53,4 +53,5 @@ export interface IRawResponse {
cards?: RawNode;
items?: RawNode[];
frameworkUpdates?: any;
}
engagementPanels: RawNode[];
}

View File

@@ -30,7 +30,7 @@ class Search extends Feed<ISearchResponse> {
if (!contents)
throw new InnertubeError('No contents found in search response');
this.results = contents.filterType(ItemSection).find((section) => section.contents && section.contents.length > 0)?.contents;
this.results = contents.find((content) => content.is(ItemSection) && content.contents && content.contents.length > 0)?.as(ItemSection).contents;
this.refinements = this.page.refinements || [];
this.estimated_results = this.page.estimated_results;

View File

@@ -33,6 +33,8 @@ import type { ObservedArray, YTNode } from '../helpers.js';
import { InnertubeError } from '../../utils/Utils.js';
import { MediaInfo } from '../../core/mixins/index.js';
import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.js';
import { VideoDescriptionMusicSection } from '../nodes.js';
class VideoInfo extends MediaInfo {
#watch_next_continuation?: ContinuationItem;
@@ -336,41 +338,55 @@ class VideoInfo extends MediaInfo {
/**
* Get songs used in the video.
*/
// TODO: this seems to be broken with the new UI, further investigation needed
get music_tracks() {
/*
Const metadata = this.secondary_info?.metadata;
if (!metadata)
return [];
const songs = [];
let current_song: Record<string, Text[]> = {};
let is_music_section = false;
for (let i = 0; i < metadata.rows.length; i++) {
const row = metadata.rows[i];
if (row.is(MetadataRowHeader)) {
if (row.content?.toString().toLowerCase().startsWith('music')) {
is_music_section = true;
i++; // Skip the learn more link
}
continue;
}
if (!is_music_section)
continue;
if (row.is(MetadataRow))
current_song[row.title?.toString().toLowerCase().replace(/ /g, '_')] = row.contents;
// TODO: this makes no sense, we continue above when
if (row.has_divider_line) {
songs.push(current_song);
current_song = {};
}
// @TODO: Refactor this.
const description_content = this.page[1]?.engagement_panels?.filter((panel) => panel.content?.is(StructuredDescriptionContent));
if (description_content !== undefined && description_content.length > 0) {
const music_section = description_content[0].content?.as(StructuredDescriptionContent)?.items?.filterType(VideoDescriptionMusicSection);
if (music_section !== undefined && music_section.length > 0) {
return music_section[0].carousel_lockups?.map((lookup) => {
let song: string | undefined;
let artist: string | undefined;
let album: string | undefined;
let license: string | undefined;
let videoId: string | undefined;
let channelId: string | undefined;
}
if (is_music_section)
songs.push(current_song);
return songs;
*/
// If the song isn't in the video_lockup, it should be in the info_rows
song = lookup.video_lockup?.title?.toString();
// If the video id isn't in the video_lockup, it should be in the info_rows
videoId = lookup.video_lockup?.endpoint.payload.videoId;
for (let i = 0; i < lookup.info_rows.length; i++) {
const info_row = lookup.info_rows[i];
if (info_row.info_row_expand_status_key === undefined) {
if (song === undefined) {
song = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
if (videoId === undefined) {
const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint;
videoId = endpoint?.payload?.videoId;
}
} else {
album = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
}
} else {
if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-artists-row-state-id') !== -1) {
artist = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
if (channelId === undefined) {
const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint;
channelId = endpoint?.payload?.browseId;
}
}
if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-licenses-row-state-id') !== -1) {
license = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
}
}
}
return { song, artist, album, license, videoId, channelId };
});
}
}
return [];
}
}
export default VideoInfo;
export default VideoInfo;

View File

@@ -9,6 +9,8 @@ import type { ApiResponse } from '../../core/Actions.js';
import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/ParsedResponse.js';
import { InnertubeError } from '../../utils/Utils.js';
import ChipCloud from '../classes/ChipCloud.js';
import ChipCloudChip from '../classes/ChipCloudChip.js';
class HomeFeed {
#page: IBrowseResponse;
@@ -16,6 +18,7 @@ class HomeFeed {
#continuation?: string;
sections?: ObservedArray<MusicCarouselShelf | MusicTastebuilderShelf>;
header?: ChipCloud;
constructor(response: ApiResponse, actions: Actions) {
this.#actions = actions;
@@ -36,6 +39,7 @@ class HomeFeed {
return;
}
this.header = tab.content?.as(SectionList).header?.as(ChipCloud);
this.#continuation = tab.content?.as(SectionList).continuation;
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf);
}
@@ -55,6 +59,33 @@ class HomeFeed {
return new HomeFeed(response, this.#actions);
}
async applyFilter(target_filter: string | ChipCloudChip): Promise<HomeFeed> {
let cloud_chip: ChipCloudChip | undefined;
if (typeof target_filter === 'string') {
cloud_chip = this.header?.chips?.as(ChipCloudChip).get({ text: target_filter });
if (!cloud_chip)
throw new InnertubeError('Could not find filter with given name.', { available_filters: this.filters });
} else if (target_filter?.is(ChipCloudChip)) {
cloud_chip = target_filter;
}
if (!cloud_chip)
throw new InnertubeError('Invalid filter', { available_filters: this.filters });
if (cloud_chip?.is_selected) return this;
if (!cloud_chip.endpoint)
throw new InnertubeError('Selected filter does not have an endpoint.');
const response = await cloud_chip.endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new HomeFeed(response, this.#actions);
}
get filters(): string[] {
return this.header?.chips?.as(ChipCloudChip).map((chip) => chip.text) || [];
}
get has_continuation(): boolean {
return !!this.#continuation;
}

View File

@@ -1,7 +1,7 @@
import type Actions from '../../core/Actions.js';
import type { ApiResponse } from '../../core/Actions.js';
import Constants from '../../utils/Constants.js';
import * as Constants from '../../utils/Constants.js';
import { InnertubeError } from '../../utils/Utils.js';
import AutomixPreviewVideo from '../classes/AutomixPreviewVideo.js';

View File

@@ -20,6 +20,9 @@ export interface IPlayerRequest {
lactMilliseconds: string;
}
},
attestationRequest: {
omitBotguardData: boolean;
},
videoId: string;
racyCheckOk: boolean;
contentCheckOk: boolean;

View File

@@ -29,13 +29,13 @@ export const OAUTH = Object.freeze({
}),
REGEX: Object.freeze({
AUTH_SCRIPT: /<script id="base-js" src="(.*?)" nonce=".*?"><\/script>/,
CLIENT_IDENTITY: /.+?={};var .+?={clientId:"(?<client_id>.+?)",.+?:"(?<client_secret>.+?)"},/
CLIENT_IDENTITY: /var .+?={clientId:"(?<client_id>.+?)",.+?:"(?<client_secret>.+?)".+?}/
})
});
export const CLIENTS = Object.freeze({
WEB: {
NAME: 'WEB',
VERSION: '2.20230104.01.00',
VERSION: '2.20230622.06.00',
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
API_VERSION: 'v1',
STATIC_VISITOR_ID: '6zpwvWUNAco'
@@ -78,11 +78,3 @@ export const INNERTUBE_HEADERS_BASE = Object.freeze({
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json'
});
export default {
URLS,
OAUTH,
CLIENTS,
STREAM_HEADERS,
INNERTUBE_HEADERS_BASE
};

View File

@@ -5,7 +5,7 @@ import type Format from '../parser/classes/misc/Format.js';
import type AudioOnlyPlayability from '../parser/classes/AudioOnlyPlayability.js';
import type { YTNode } from '../parser/helpers.js';
import { Constants } from './index.js';
import * as Constants from './Constants.js';
import { getStringBetweenStrings, InnertubeError, Platform, streamToIterable } from './Utils.js';
export type URLTransformer = (url: URL) => URL;
@@ -249,22 +249,14 @@ class FormatUtils {
if (!streaming_data)
throw new InnertubeError('Streaming data not available');
let filtered_streaming_data;
let adaptive_formats;
if (format_filter) {
filtered_streaming_data = {
formats: streaming_data.formats.filter((fmt: Format) => !(format_filter(fmt))),
adaptive_formats: streaming_data.adaptive_formats.filter((fmt: Format) => !(format_filter(fmt))),
expires: streaming_data.expires,
dash_manifest_url: streaming_data.dash_manifest_url,
hls_manifest_url: streaming_data.hls_manifest_url
};
adaptive_formats = streaming_data.adaptive_formats.filter((fmt: Format) => !(format_filter(fmt)));
} else {
filtered_streaming_data = streaming_data;
adaptive_formats = streaming_data.adaptive_formats;
}
const { adaptive_formats } = filtered_streaming_data;
if (!adaptive_formats.length)
throw new InnertubeError('No adaptive formats found');
@@ -363,7 +355,12 @@ class FormatUtils {
this.#el(document, 'Role', {
schemeIdUri: 'urn:mpeg:dash:role:2011',
value: role
})
}),
this.#el(document, 'Label', {
id: set_id.toString()
}, [
document.createTextNode(first_format.audio_track?.display_name as string)
])
);
const set = this.#el(document, 'AdaptationSet', {
@@ -371,7 +368,9 @@ class FormatUtils {
mimeType: mime_types[i].split(';')[0],
startWithSAP: '1',
subsegmentAlignment: 'true',
lang: first_format.language as string
lang: first_format.language as string,
// Non-standard attribute used by shaka instead of the standard Label element
label: first_format.audio_track?.display_name as string
}, children);
period.appendChild(set);

View File

@@ -1,7 +1,7 @@
import type { Context } from '../core/Session.js';
import type Session from '../core/Session.js';
import type { FetchFunction } from '../types/PlatformShim.js';
import Constants from './Constants.js';
import * as Constants from './Constants.js';
import {
Platform,
generateSidAuth,

View File

@@ -1,5 +1,6 @@
import { Memo } from '../parser/helpers.js';
import type { EmojiRun, TextRun } from '../parser/misc.js';
import { Text } from '../parser/misc.js';
import type { FetchFunction } from '../types/PlatformShim.js';
import type PlatformShim from '../types/PlatformShim.js';
import userAgents from './user-agents.js';
@@ -47,7 +48,7 @@ export class ChannelError extends Error { }
export function deepCompare(obj1: any, obj2: any): boolean {
const keys = Reflect.ownKeys(obj1);
return keys.some((key) => {
const is_text = obj2[key]?.constructor.name === 'Text';
const is_text = obj2[key] instanceof Text;
if (!is_text && typeof obj2[key] === 'object') {
return JSON.stringify(obj1[key]) === JSON.stringify(obj2[key]);
}

View File

@@ -296,6 +296,13 @@ describe('YouTube.js Tests', () => {
expect(incremental_continuation.sections).toBeDefined();
expect(incremental_continuation.sections?.length).toBeGreaterThan(0);
});
test('HomeFeed#applyFilter', async () => {
home = await home.applyFilter(home.filters[1]);
expect(home).toBeDefined();
expect(home.sections).toBeDefined();
expect(home.sections?.length).toBeGreaterThan(0);
});
});
test('Innertube#music.getExplore', async () => {