Compare commits

..

5 Commits

Author SHA1 Message Date
LuanRT
c76f5f478d 2.5.1 2022-11-30 19:11:40 -03:00
LuanRT
49d1432b5a chore: fix a few inconsistencies 2022-11-30 19:02:49 -03:00
LuanRT
be157ef016 fix: signature decipher extraction failing (#249) 2022-11-30 18:39:37 -03:00
LuanRT
9f703203b6 chore(docs): update readme 2022-11-29 05:49:15 -03:00
LuanRT
516eeeff45 refactor: improve Search parser (#247)
* refactor: improve Search parser

* chore: lint
2022-11-29 03:50:17 -03:00
28 changed files with 119 additions and 115 deletions

View File

@@ -1,5 +1,3 @@
<!-- Hi there, fellow coder :) -->
<!-- BADGE LINKS -->
[npm]: https://www.npmjs.com/package/youtubei.js
[versions]: https://www.npmjs.com/package/youtubei.js?activeTab=versions
@@ -10,37 +8,23 @@
<!-- OTHER LINKS -->
[project]: https://github.com/LuanRT/YouTube.js
[twitter]: https://twitter.com/lrt_nooneknows
[twitter]: https://twitter.com/thesciencephile
[discord]: https://discord.gg/syDu7Yks54
[nodejs]: https://nodejs.org
<!-- INTRODUCTION -->
<h1 align=center>
YouTube.js
</h1>
<h1 align=center>YouTube.js</h1>
<p align=center>
<i>
A full-featured wrapper around the InnerTube API, which is what YouTube itself uses.
</i>
</p>
<p align="center">
<a href="https://github.com/LuanRT/YouTube.js/issues">
Report Bug
</a>
·
<a href="https://github.com/LuanRT/YouTube.js/issues">
Request Feature
</a>
</p>
<p align=center>A full-featured wrapper around the InnerTube API, which is what YouTube itself uses.</p>
<!-- BADGES -->
<div align="center">
[![Tests](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg)][actions]
[![Latest version](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)][versions]
[![NPM Version](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)][versions]
[![Codefactor](https://www.codefactor.io/repository/github/luanrt/youtube.js/badge)][codefactor]
[![Downloads](https://img.shields.io/npm/dt/youtubei.js)][npm]
[![Discord](https://img.shields.io/badge/discord-online-brightgreen.svg)][discord]
[![Say thanks](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)][say-thanks]
<br>
[![Donate](https://img.shields.io/badge/donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)][github-sponsors]
@@ -57,7 +41,7 @@
<body>
<tr>
<td align="center">
<a href="https://serpapi.com/">
<a href="https://serpapi.com/" target="_blank">
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
<br>
<b>
@@ -105,7 +89,7 @@ ___
</details>
<!-- ABOUT THE PROJECT -->
## About
## Description
InnerTube is an API used across all YouTube clients, it was created to simplify[^1] the internal structure of the platform in a way that updates, tweaks, and experiments can be easily made. This library handles all the low-level communication with InnerTube, providing a simple, fast, and efficient way to interact with YouTube programmatically.
@@ -245,7 +229,7 @@ const yt = await Innertube.create({
* `Innertube`
<details>
<summary>objects</summary>
<summary>Objects</summary>
<p>
* [.session](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/session.md)
@@ -260,7 +244,7 @@ const yt = await Innertube.create({
<details>
<summary>methods</summary>
<summary>Methods</summary>
<p>
* [.getInfo(video_id, client?)](#getinfo)
@@ -690,7 +674,7 @@ Feel free to check the [issues page](https://github.com/LuanRT/YouTube.js/issues
<!-- CONTACT -->
## Contact
LuanRT - [@lrt_nooneknows][twitter] - luan.lrt4@gmail.com
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
Project Link: [https://github.com/LuanRT/YouTube.js][project]

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "2.5.0",
"version": "2.5.1",
"description": "Full-featured wrapper around YouTube's private API. Supports YouTube, YouTube Music and YouTube Studio (WIP).",
"main": "./dist/index.js",
"browser": "./bundle/browser.js",

View File

@@ -167,13 +167,13 @@ export default class Player {
static extractSigSourceCode(data: string) {
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
const obj_name = calls?.split('.')?.[0]?.replace(';', '');
const functions = getStringBetweenStrings(data, `var ${obj_name}=`, '};');
const obj_name = calls?.split(/\.|\[/)?.[0]?.replace(';', '')?.trim();
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
if (!functions || !calls)
console.warn(new PlayerError('Failed to extract signature decipher algorithm'));
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}=${functions}}${calls} return a.join("") } descramble_sig(sig);`;
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
}
static extractNSigSourceCode(data: string) {

View File

@@ -1,7 +1,7 @@
import Parser from '..';
import { YTNode } from '../helpers';
export default class CarouselHeader extends YTNode {
class CarouselHeader extends YTNode {
static type = 'CarouselHeader';
contents: YTNode[];
@@ -10,4 +10,6 @@ export default class CarouselHeader extends YTNode {
super();
this.contents = Parser.parseArray(data.contents);
}
}
}
export default CarouselHeader;

View File

@@ -3,7 +3,7 @@ import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class CarouselItem extends YTNode {
class CarouselItem extends YTNode {
static type = 'CarouselItem';
items: YTNode[];
@@ -20,4 +20,6 @@ export default class CarouselItem extends YTNode {
this.pagination_thumbnails = Thumbnail.fromResponse(data.paginationThumbnails);
this.paginator_alignment = data.paginatorAlignment;
}
}
}
export default CarouselItem;

View File

@@ -4,7 +4,7 @@ import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
export default class CompactStation extends YTNode {
class CompactStation extends YTNode {
static type = 'CompactStation';
title: Text;
@@ -22,4 +22,6 @@ export default class CompactStation extends YTNode {
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
}
}
}
export default CompactStation;

View File

@@ -4,7 +4,7 @@ import { YTNode } from '../helpers';
import Text from './misc/Text';
import NavigationEndpoint from './NavigationEndpoint';
export default class DefaultPromoPanel extends YTNode {
class DefaultPromoPanel extends YTNode {
static type = 'DefaultPromoPanel';
title: Text;
@@ -33,4 +33,6 @@ export default class DefaultPromoPanel extends YTNode {
this.metadata_order = data.metadataOrder;
this.panel_layout = data.panelLayout;
}
}
}
export default DefaultPromoPanel;

View File

@@ -2,7 +2,7 @@ import Parser from '../index';
import { YTNode } from '../helpers';
import ChipCloudChip from './ChipCloudChip';
export default class FeedFilterChipBar extends YTNode {
class FeedFilterChipBar extends YTNode {
static type = 'FeedFilterChipBar';
contents;
@@ -11,4 +11,6 @@ export default class FeedFilterChipBar extends YTNode {
super();
this.contents = Parser.parseArray<ChipCloudChip>(data.contents, ChipCloudChip);
}
}
}
export default FeedFilterChipBar;

View File

@@ -1,7 +1,7 @@
import Parser from '..';
import { YTNode } from '../helpers';
export default class GameCard extends YTNode {
class GameCard extends YTNode {
static type = 'GameCard';
game;
@@ -10,4 +10,6 @@ export default class GameCard extends YTNode {
super();
this.game = Parser.parseItem(data.game);
}
}
}
export default GameCard;

View File

@@ -4,7 +4,7 @@ import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
export default class GameDetails extends YTNode {
class GameDetails extends YTNode {
static type = 'GameDetails';
title: Text;
@@ -21,4 +21,6 @@ export default class GameDetails extends YTNode {
this.endpoint = new NavigationEndpoint(data.endpoint);
this.is_official_box_art = data.isOfficialBoxArt;
}
}
}
export default GameDetails;

View File

@@ -4,7 +4,7 @@ import { YTNode } from '../helpers';
class Panel {
static type = 'Panel';
thumbnail: {
thumbnail?: {
image: {
url: string;
width: number;
@@ -43,13 +43,15 @@ class Panel {
};
constructor(data: any) {
this.thumbnail = {
image: data.thumbnail.image.sources,
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
content_mode: data.thumbnail.contentMode,
crop_options: data.thumbnail.cropOptions
};
if (data.thumbnail) {
this.thumbnail = {
image: data.thumbnail.image.sources,
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
content_mode: data.thumbnail.contentMode,
crop_options: data.thumbnail.cropOptions
};
}
this.background_image = {
image: data.backgroundImage.image.sources,

View File

@@ -1,5 +1,7 @@
import Parser from '../index';
import { YTNode } from '../helpers';
import SearchRefinementCard from './SearchRefinementCard';
import Button from './Button';
class HorizontalCardList extends YTNode {
static type = 'HorizontalCardList';
@@ -11,10 +13,10 @@ class HorizontalCardList extends YTNode {
constructor(data: any) {
super();
this.cards = Parser.parse(data.cards);
this.header = Parser.parse(data.header);
this.previous_button = Parser.parse(data.previousButton);
this.next_button = Parser.parse(data.nextButton);
this.cards = Parser.parseArray<SearchRefinementCard>(data.cards, SearchRefinementCard);
this.header = Parser.parseItem(data.header);
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
}
}

View File

@@ -7,7 +7,7 @@ import SubscribeButton from './SubscribeButton';
import MetadataBadge from './MetadataBadge';
import Button from './Button';
export default class InteractiveTabbedHeader extends YTNode {
class InteractiveTabbedHeader extends YTNode {
static type = 'InteractiveTabbedHeader';
header_type: string;
@@ -33,4 +33,6 @@ export default class InteractiveTabbedHeader extends YTNode {
this.buttons = Parser.parseArray<SubscribeButton | Button>(data.buttons, [ SubscribeButton, Button ]);
this.auto_generated = new Text(data.autoGenerated);
}
}
}
export default InteractiveTabbedHeader;

View File

@@ -1,7 +1,7 @@
import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class PlaylistCustomThumbnail extends YTNode {
class PlaylistCustomThumbnail extends YTNode {
static type = 'PlaylistCustomThumbnail';
thumbnail: Thumbnail[];
@@ -10,4 +10,6 @@ export default class PlaylistCustomThumbnail extends YTNode {
super();
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
}
}
}
export default PlaylistCustomThumbnail;

View File

@@ -5,7 +5,7 @@ import Button from './Button';
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
export default class RecognitionShelf extends YTNode {
class RecognitionShelf extends YTNode {
static type = 'RecognitionShelf';
title: Text;
@@ -23,4 +23,6 @@ export default class RecognitionShelf extends YTNode {
this.button = Parser.parseItem<Button>(data.button, Button);
this.surface = data.surface;
}
}
}
export default RecognitionShelf;

View File

@@ -20,14 +20,14 @@ class Shelf extends YTNode {
this.endpoint = new NavigationEndpoint(data.endpoint);
}
this.content = Parser.parse(data.content) || null;
this.content = Parser.parseItem(data.content) || null;
if (data.icon?.iconType) {
this.icon_type = data.icon?.iconType;
}
if (data.menu) {
this.menu = Parser.parse(data.menu);
this.menu = Parser.parseItem(data.menu);
}
}
}

View File

@@ -1,7 +1,7 @@
import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class ThumbnailLandscapePortrait extends YTNode {
class ThumbnailLandscapePortrait extends YTNode {
static type = 'ThumbnailLandscapePortrait';
landscape: Thumbnail[];
@@ -12,4 +12,6 @@ export default class ThumbnailLandscapePortrait extends YTNode {
this.landscape = Thumbnail.fromResponse(data.landscape);
this.portrait = Thumbnail.fromResponse(data.portrait);
}
}
}
export default ThumbnailLandscapePortrait;

View File

@@ -6,7 +6,7 @@ import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import SubscribeButton from './SubscribeButton';
export default class TopicChannelDetails extends YTNode {
class TopicChannelDetails extends YTNode {
static type = 'TopicChannelDetails';
title: Text;
@@ -24,4 +24,6 @@ export default class TopicChannelDetails extends YTNode {
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton, SubscribeButton);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
}
}
export default TopicChannelDetails;

View File

@@ -1,4 +1,5 @@
import Parser from '../index';
import Text from './misc/Text';
import { YTNode } from '../helpers';
class UniversalWatchCard extends YTNode {
@@ -7,13 +8,17 @@ class UniversalWatchCard extends YTNode {
header;
call_to_action;
sections;
collapsed_label?: Text;
constructor(data: any) {
super();
// TODO: use parseItem / parseArray for these
this.header = Parser.parse(data.header);
this.call_to_action = Parser.parse(data.callToAction);
this.sections = Parser.parse(data.sections);
this.header = Parser.parseItem(data.header);
this.call_to_action = Parser.parseItem(data.callToAction);
this.sections = Parser.parseArray(data.sections);
if (data.collapsedLabel) {
this.collapsed_label = new Text(data.collapsedLabel);
}
}
}

View File

@@ -13,7 +13,7 @@ class VerticalWatchCardList extends YTNode {
constructor(data: any) {
super();
this.items = Parser.parse(data.items);
this.items = Parser.parseArray(data.items);
this.contents = this.items; // XXX: alias for consistency
this.view_all_text = new Text(data.viewAllText);
this.view_all_endpoint = new NavigationEndpoint(data.viewAllEndpoint);

View File

@@ -1,9 +1,10 @@
import Parser from '../index';
import Parser from '..';
import Text from './misc/Text';
import Author from './misc/Author';
import Menu from './menus/Menu';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import { timeToSeconds } from '../../utils/Utils';
import { YTNode } from '../helpers';
@@ -55,7 +56,7 @@ class Video extends YTNode {
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.rich_thumbnail = data.richThumbnail ? Parser.parse(data.richThumbnail) : null;
this.rich_thumbnail = data.richThumbnail ? Parser.parseItem(data.richThumbnail) : null;
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.published = new Text(data.publishedTimeText);

View File

@@ -1,9 +1,11 @@
import Video from './Video';
export default class VideoCard extends Video {
class VideoCard extends Video {
static type = 'VideoCard';
constructor(data: any) {
super(data);
}
}
}
export default VideoCard;

View File

@@ -13,9 +13,9 @@ class WatchCardHeroVideo extends YTNode {
constructor(data: any) {
super();
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.call_to_action_button = Parser.parse(data.callToActionButton);
this.hero_image = Parser.parse(data.heroImage);
this.label = data.lengthText.accessibility.accessibilityData.label;
this.call_to_action_button = Parser.parseItem(data.callToActionButton);
this.hero_image = Parser.parseItem(data.heroImage);
this.label = data.lengthText?.accessibility.accessibilityData.label || '';
}
}

View File

@@ -8,7 +8,7 @@ class WatchCardSectionSequence extends YTNode {
constructor(data: any) {
super();
this.lists = Parser.parse(data.lists);
this.lists = Parser.parseArray(data.lists);
}
}

View File

@@ -39,10 +39,10 @@ class Library {
}
async #getAll(shelf: Shelf): Promise<Playlist | History | Feed> {
if (!shelf.menu?.item().as(Menu).hasKey('top_level_buttons'))
if (!shelf.menu?.as(Menu).hasKey('top_level_buttons'))
throw new InnertubeError(`The ${shelf.title.text} shelf doesn't have more items`);
const button = shelf.menu.item().as(Menu).top_level_buttons.get({ text: 'See all' });
const button = shelf.menu.as(Menu).top_level_buttons.get({ text: 'See all' });
if (!button)
throw new InnertubeError('Did not find target button.');

View File

@@ -1,19 +1,15 @@
import Actions from '../../core/Actions';
import { observe, ObservedArray, YTNode } from '../helpers';
import { ObservedArray, YTNode } from '../helpers';
import { InnertubeError } from '../../utils/Utils';
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';
class Search extends Feed {
export default class Search extends Feed {
results: ObservedArray<YTNode> | null | undefined;
refinements;
estimated_results;
@@ -24,30 +20,16 @@ class Search extends Feed {
super(actions, data, already_parsed);
const contents =
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents?.item().as(SectionList).contents.array() ||
this.page.contents_memo.getType(SectionList)?.[0]?.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;
this.results = contents.firstOfType(ItemSection)?.contents;
const card_list = this.results?.get({ type: 'HorizontalCardList' }, true)?.as(HorizontalCardList);
const universal_watch_card = secondary_contents?.firstOfType(UniversalWatchCard);
this.refinements = this.page.refinements || [];
this.estimated_results = this.page.estimated_results;
this.watch_card = {
header: universal_watch_card?.header.item() || null,
call_to_action: universal_watch_card?.call_to_action?.item()?.as(WatchCardHeroVideo) || null,
sections: universal_watch_card?.sections?.array()?.filterType(WatchCardSectionSequence) || []
};
this.refinement_cards = {
header: card_list?.header.item().as(RichListHeader) || null,
cards: card_list?.cards.array().filterType(SearchRefinementCard) || observe([] as SearchRefinementCard[])
};
this.watch_card = this.page?.contents_memo.getType(UniversalWatchCard)?.[0];
this.refinement_cards = this.results?.get({ type: 'HorizontalCardList' }, true)?.as(HorizontalCardList);
}
/**
@@ -57,7 +39,8 @@ class Search extends Feed {
let target_card: SearchRefinementCard | undefined;
if (typeof card === 'string') {
target_card = this.refinement_cards.cards.get({ query: card });
if (!this.refinement_cards) throw new InnertubeError('No refinement cards found.');
target_card = this.refinement_cards?.cards.get({ query: card });
if (!target_card)
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
} else if (card.type === 'SearchRefinementCard') {
@@ -71,8 +54,11 @@ class Search extends Feed {
return new Search(this.actions, page, true);
}
/**
* Returns a list of refinement card queries.
*/
get refinement_card_queries() {
return this.refinement_cards.cards.map((card) => card.query);
return this.refinement_cards?.cards.map((card) => card.query);
}
/**
@@ -82,6 +68,4 @@ class Search extends Feed {
const continuation = await this.getContinuationData();
return new Search(this.actions, continuation, true);
}
}
export default Search;
}

View File

@@ -48,7 +48,7 @@ export const CLIENTS = Object.freeze({
},
YTMUSIC_ANDROID: {
NAME: 'ANDROID_MUSIC',
VERSION: '5.17.51'
VERSION: '5.34.51'
},
TV_EMBEDDED: {
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',