Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve livechat parser & add remaining action nodes #285

Merged
merged 4 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions examples/livechat/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
## Live Chat

The library's Live Chat parser and poller were heavily based on YouTube's original compiled code, this makes it behave in a similar if not identical way to YouTube's Live Chat. Here you can do all sorts of funny things, ex; track messages, donations, polls, and much more.
Represents a livestream chat.

## Usage

Before fetching a Live Chat, you have to retrieve the target livestream's info:
Before fetching a live chat, you have to retrieve the target livestream's info:

```js
const info = await yt.getInfo('video_id');
```

Then you may request a Live Chat instance:
Then you may request a live chat instance:
```js
const livechat = await info.getLiveChat();
```
Expand All @@ -21,6 +21,7 @@ const livechat = await info.getLiveChat();
* [.ev](#ev) ⇒ `EventEmitter`
* [.start](#start) ⇒ `function`
* [.stop](#stop) ⇒ `function`
* [.applyFilter](#applyfilter) ⇒ `function`
* [.getItemMenu](#getitemmenu) ⇒ `function`
* [.sendMessage](#sendmessage) ⇒ `function`

Expand All @@ -31,25 +32,44 @@ Live Chat's EventEmitter.
**Events:**

- `start`

Fired when the live chat is started.

Arguments:
| Type | Description |
| --- | --- |
| `LiveChatContinuation` | Initial chat data, actions, info, etc. |

- `chat-update`


Fired when a new chat action is received.

Arguments:
| Type | Description |
| --- | --- |
| `ChatAction` | Chat Action |
| `ChatAction` | Chat action |

- `metadata-update`

Fired when the livestream's metadata is updated.

Arguments:
| Type | Description |
| --- | --- |
| `LiveMetadata` | LiveStream Metadata |
| `LiveMetadata` | Livestream metadata |

- `error`

Fired when an error occurs.

Arguments:
| Type | Description |
| --- | --- |
| `Error` | Details about the error |

- `end`

Fired when the livestream ends.

<a name="start"></a>
### start()
Expand All @@ -59,6 +79,15 @@ Starts the Live Chat.
### stop()
Stops the Live Chat.

<a name="applyfilter"></a>
### applyFilter(filter)

Applies given filter to the live chat.

| Param | Type | Description |
| --- | --- | --- |
| filter | `string` | Can be `TOP_CHAT` or `LIVE_CHAT` |

<a name="getitemmenu"></a>
### getItemMenu(item)
Retrieves given chat item's menu.
Expand Down
47 changes: 39 additions & 8 deletions examples/livechat/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';

import { LiveChatContinuation } from 'youtubei.js/dist/src/parser';
import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/LiveChat';

(async () => {
const yt = await Innertube.create({ cache: new UniversalCache() });
const yt = await Innertube.create({ cache: new UniversalCache(), generate_session_locally: true });


const search = await yt.search('Lofi girl live');
const search = await yt.search('lofi hip hop radio - beats to relax/study to');
const info = await yt.getInfo(search.videos[0].as(YTNodes.Video).id);

const livechat = info.getLiveChat();

livechat.on('start', (initial_data: LiveChatContinuation) => {
/**
* Initial info is what you see when you first open a Live Chat — this is; inital actions (pinned messages, top donations..), account's info and so on.
* Initial info is what you see when you first open a a live chat — this is; initial actions (pinned messages, top donations..), account's info and so forth.
*/
console.info(`Hey ${initial_data.viewer_name || 'Guest'}, welcome to Live Chat!`);

const pinned_action = initial_data.actions.firstOfType(YTNodes.AddBannerToLiveChatCommand);

console.info(`Hey ${initial_data.viewer_name || 'N/A'}, welcome to Live Chat!`);
if (pinned_action) {
if (pinned_action.banner?.contents?.is(YTNodes.LiveChatTextMessage)) {
console.info(
'\n', 'Pinned message:\n',
pinned_action.banner.contents.author?.name.toString(), '-', pinned_action?.banner.contents.message.toString(),
'\n'
);
}
}
});

livechat.on('error', (error: Error) => console.info('Live chat error:', error));

livechat.on('end', () => console.info('This live stream has ended.'));

livechat.on('chat-update', (action: ChatAction) => {
/**
* An action represents what is being added to
Expand All @@ -43,24 +56,42 @@ import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/Li
switch (item.type) {
case 'LiveChatTextMessage':
console.info(
`${item.as(YTNodes.LiveChatTextMessage).author?.is_moderator ? '[MOD]' : ''}`,
`${hours} - ${item.as(YTNodes.LiveChatTextMessage).author?.name.toString()}:\n` +
`${item.as(YTNodes.LiveChatTextMessage).message.toString()}\n`
);
break;
case 'LiveChatPaidMessage':
console.info(
`${item.as(YTNodes.LiveChatPaidMessage).author?.is_moderator ? '[MOD]' : ''}`,
`${hours} - ${item.as(YTNodes.LiveChatPaidMessage).author.name.toString()}:\n` +
`${item.as(YTNodes.LiveChatPaidMessage).message.toString()}\n`,
`${item.as(YTNodes.LiveChatPaidMessage).purchase_amount}\n`
);
break;
case 'LiveChatPaidSticker':
console.info(
`${item.as(YTNodes.LiveChatPaidSticker).author?.is_moderator ? '[MOD]' : ''}`,
`${hours} - ${item.as(YTNodes.LiveChatPaidSticker).author.name.toString()}:\n` +
`${item.as(YTNodes.LiveChatPaidSticker).purchase_amount}\n`
);
break;
default:
console.debug(action);
break;
}
}

if (action.is(YTNodes.MarkChatItemAsDeletedAction)) {
console.warn(`Message ${action.target_item_id} just got deleted and should be replaced with ${action.deleted_state_message.toString()}!`, '\n');
if (action.is(YTNodes.AddBannerToLiveChatCommand)) {
console.info('Message pinned:', action.banner?.contents);
}

if (action.is(YTNodes.RemoveBannerForLiveChatCommand)) {
console.info(`Message with action id ${action.target_action_id} was unpinned.`);
}

if (action.is(YTNodes.RemoveChatItemAction)) {
console.warn(`Message with action id ${action.target_item_id} just got deleted!`, '\n');
}
});

Expand Down
15 changes: 9 additions & 6 deletions src/parser/classes/LiveChatHeader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import Parser from '../index';
import type Menu from './menus/Menu';
import type Button from './Button';
import type SortFilterSubMenu from './SortFilterSubMenu';
import { YTNode } from '../helpers';

class LiveChatHeader extends YTNode {
static type = 'LiveChatHeader';

overflow_menu;
collapse_button;
view_selector;
overflow_menu: Menu | null;
collapse_button: Button | null;
view_selector: SortFilterSubMenu | null;

constructor(data: any) {
super();
this.overflow_menu = Parser.parse(data.overflowMenu);
this.collapse_button = Parser.parse(data.collapseButton);
this.view_selector = Parser.parse(data.viewSelector);
this.overflow_menu = Parser.parseItem<Menu>(data.overflowMenu);
this.collapse_button = Parser.parseItem<Button>(data.collapseButton);
this.view_selector = Parser.parseItem<SortFilterSubMenu>(data.viewSelector);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/parser/classes/LiveChatItemList.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import Parser from '../index';
import { YTNode } from '../helpers';
import type Button from './Button';

class LiveChatItemList extends YTNode {
static type = 'LiveChatItemList';

max_items_to_display: string;
more_comments_below_button;
more_comments_below_button: Button | null;

constructor(data: any) {
super();
this.max_items_to_display = data.maxItemsToDisplay;
this.more_comments_below_button = Parser.parse(data.moreCommentsBelowButton);
this.more_comments_below_button = Parser.parseItem<Button>(data.moreCommentsBelowButton);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/parser/classes/LiveChatMessageInput.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import Text from './misc/Text';
import Parser from '../index';
import Thumbnail from './misc/Thumbnail';
import type Button from './Button';
import { YTNode } from '../helpers';

class LiveChatMessageInput extends YTNode {
static type = 'LiveChatMessageInput';

author_name: Text;
author_photo: Thumbnail[];
send_button;
send_button: Button | null;
target_id: string;

constructor(data: any) {
super();
this.author_name = new Text(data.authorName);
this.author_photo = Thumbnail.fromResponse(data.authorPhoto);
this.send_button = Parser.parse(data.sendButton);
this.send_button = Parser.parseItem<Button>(data.sendButton);
this.target_id = data.targetId;
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/parser/classes/LiveChatParticipantsList.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import Parser from '../index';
import Text from './misc/Text';
import { YTNode } from '../helpers';
import { ObservedArray, YTNode } from '../helpers';
import type LiveChatParticipant from './LiveChatParticipant';

class LiveChatParticipantsList extends YTNode {
static type = 'LiveChatParticipantsList';

title: Text;
participants;
participants: ObservedArray<LiveChatParticipant>;

constructor(data: any) {
super();
this.title = new Text(data.title);
this.participants = Parser.parse(data.participants);
this.participants = Parser.parseArray<LiveChatParticipant>(data.participants);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/parser/classes/SortFilterSubMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SortFilterSubMenu extends YTNode {
sub_menu_items?: {
title: string;
selected: boolean;
continuation: string | null;
continuation: string;
endpoint: NavigationEndpoint;
subtitle: string | null;
}[];
Expand Down Expand Up @@ -40,7 +40,7 @@ class SortFilterSubMenu extends YTNode {
this.sub_menu_items = data.subMenuItems.map((item: any) => ({
title: item.title,
selected: item.selected,
continuation: item.continuation?.reloadContinuationData?.continuation || null,
continuation: item.continuation?.reloadContinuationData?.continuation,
endpoint: new NavigationEndpoint(item.serviceEndpoint),
subtitle: item.subtitle || null
}));
Expand Down
25 changes: 25 additions & 0 deletions src/parser/classes/UpsellDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Parser from '..';
import { YTNode } from '../helpers';
import type Button from './Button';
import Text from './misc/Text';

class UpsellDialog extends YTNode {
static type = 'UpsellDialog';

message_title: Text;
message_text: Text;
action_button: Button | null;
dismiss_button: Button | null;
is_visible: boolean;

constructor(data: any) {
super();
this.message_title = new Text(data.dialogMessageTitle);
this.message_text = new Text(data.dialogMessageText);
this.action_button = Parser.parseItem<Button>(data.actionButton);
this.dismiss_button = Parser.parseItem<Button>(data.dismissButton);
this.is_visible = data.isVisible;
}
}

export default UpsellDialog;
5 changes: 3 additions & 2 deletions src/parser/classes/livechat/AddBannerToLiveChatCommand.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Parser from '../../index';
import { YTNode } from '../../helpers';
import type LiveChatBanner from './items/LiveChatBanner';

class AddBannerToLiveChatCommand extends YTNode {
static type = 'AddBannerToLiveChatCommand';

banner;
banner: LiveChatBanner | null;

constructor(data: any) {
super();
this.banner = Parser.parse(data.bannerRenderer);
this.banner = Parser.parseItem<LiveChatBanner>(data.bannerRenderer);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/parser/classes/livechat/AddLiveChatTickerItemAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class AddLiveChatTickerItemAction extends YTNode {
static type = 'AddLiveChatTickerItemAction';

item;
duration_sec;
duration_sec: string; // TODO: check this assumption

constructor(data: any) {
super();
Expand Down
14 changes: 14 additions & 0 deletions src/parser/classes/livechat/DimChatItemAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { YTNode } from '../../helpers';

class DimChatItemAction extends YTNode {
static type = 'DimChatItemAction';

client_assigned_id: string;

constructor(data: any) {
super();
this.client_assigned_id = data.clientAssignedId;
}
}

export default DimChatItemAction;
2 changes: 1 addition & 1 deletion src/parser/classes/livechat/ReplaceChatItemAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ReplaceChatItemAction extends YTNode {
constructor(data: any) {
super();
this.target_item_id = data.targetItemId;
this.replacement_item = Parser.parse(data.replacementItem);
this.replacement_item = Parser.parseItem(data.replacementItem);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/parser/classes/livechat/ReplayChatItemAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class ReplayChatItemAction extends YTNode {

constructor(data: any) {
super();
this.actions = Parser.parse(data.actions?.map((action: any) => {
this.actions = Parser.parseArray(data.actions?.map((action: any) => {
delete action.clickTrackingParams;
return action;
})) || [];
}));
this.video_offset_time_msec = data.videoOffsetTimeMsec;
}
}
Expand Down
Loading