mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-23 23:09:28 +00:00
refactor: improve live chat polling (#220)
* dev: add RemoveChatItemByAuthorAction renderer parser * dev: improve live chat polling
This commit is contained in:
14
src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts
Normal file
14
src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../../helpers';
|
||||
|
||||
class RemoveChatItemByAuthorAction extends YTNode {
|
||||
static type = 'RemoveChatItemByAuthorAction';
|
||||
|
||||
external_channel_id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.external_channel_id = data.externalChannelId;
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveChatItemByAuthorAction;
|
||||
@@ -112,6 +112,7 @@ import { default as MarkChatItemAsDeletedAction } from './classes/livechat/MarkC
|
||||
import { default as MarkChatItemsByAuthorAsDeletedAction } from './classes/livechat/MarkChatItemsByAuthorAsDeletedAction';
|
||||
import { default as RemoveBannerForLiveChatCommand } from './classes/livechat/RemoveBannerForLiveChatCommand';
|
||||
import { default as RemoveChatItemAction } from './classes/livechat/RemoveChatItemAction';
|
||||
import { default as RemoveChatItemByAuthorAction } from './classes/livechat/RemoveChatItemByAuthorAction';
|
||||
import { default as ReplaceChatItemAction } from './classes/livechat/ReplaceChatItemAction';
|
||||
import { default as ReplayChatItemAction } from './classes/livechat/ReplayChatItemAction';
|
||||
import { default as ShowLiveChatActionPanelAction } from './classes/livechat/ShowLiveChatActionPanelAction';
|
||||
@@ -389,6 +390,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MarkChatItemsByAuthorAsDeletedAction,
|
||||
RemoveBannerForLiveChatCommand,
|
||||
RemoveChatItemAction,
|
||||
RemoveChatItemByAuthorAction,
|
||||
ReplaceChatItemAction,
|
||||
ReplayChatItemAction,
|
||||
ShowLiveChatActionPanelAction,
|
||||
|
||||
@@ -20,8 +20,10 @@ import AddBannerToLiveChatCommand from '../classes/livechat/AddBannerToLiveChatC
|
||||
import RemoveBannerForLiveChatCommand from '../classes/livechat/RemoveBannerForLiveChatCommand';
|
||||
import ShowLiveChatTooltipCommand from '../classes/livechat/ShowLiveChatTooltipCommand';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import Proto from '../../proto/index';
|
||||
import { InnertubeError, uuidv4 } from '../../utils/Utils';
|
||||
import { ObservedArray, YTNode } from '../helpers';
|
||||
|
||||
import LiveChatTextMessage from '../classes/livechat/items/LiveChatTextMessage';
|
||||
import LiveChatPaidMessage from '../classes/livechat/items/LiveChatPaidMessage';
|
||||
import LiveChatPaidSticker from '../classes/livechat/items/LiveChatPaidSticker';
|
||||
@@ -36,7 +38,7 @@ export type ChatAction =
|
||||
MarkChatItemAsDeletedAction | MarkChatItemsByAuthorAsDeletedAction | RemoveBannerForLiveChatCommand |
|
||||
ReplaceChatItemAction | ReplayChatItemAction | ShowLiveChatActionPanelAction | ShowLiveChatTooltipCommand;
|
||||
|
||||
export type ChatItemHasMenuEndpoint = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
|
||||
export type ChatItemWithMenu = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
|
||||
|
||||
export interface LiveMetadata {
|
||||
title: UpdateTitleAction | undefined;
|
||||
@@ -52,9 +54,6 @@ class LiveChat extends EventEmitter {
|
||||
#continuation;
|
||||
#mcontinuation?: string;
|
||||
|
||||
#lc_polling_interval_ms = 1000;
|
||||
#md_polling_interval_ms = 5000;
|
||||
|
||||
initial_info?: LiveChatContinuation;
|
||||
metadata?: LiveMetadata;
|
||||
|
||||
@@ -83,117 +82,113 @@ class LiveChat extends EventEmitter {
|
||||
}
|
||||
|
||||
#pollLivechat() {
|
||||
const lc_poller = setTimeout(() => {
|
||||
(async () => {
|
||||
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
|
||||
const response = await this.#actions.livechat(endpoint, { ctoken: this.#continuation });
|
||||
(async () => {
|
||||
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
|
||||
const response = await this.#actions.execute(endpoint, { continuation: this.#continuation });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const contents = data.continuation_contents;
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const contents = data.continuation_contents;
|
||||
|
||||
if (!(contents instanceof LiveChatContinuation))
|
||||
throw new InnertubeError('Continuation is not a LiveChatContinuation');
|
||||
if (!(contents instanceof LiveChatContinuation))
|
||||
throw new InnertubeError('Continuation is not a LiveChatContinuation');
|
||||
|
||||
this.#continuation = contents.continuation.token;
|
||||
this.#lc_polling_interval_ms = contents.continuation.timeout_ms;
|
||||
this.#continuation = contents.continuation.token;
|
||||
|
||||
// Header only exists in the first request
|
||||
if (contents.header) {
|
||||
this.initial_info = contents;
|
||||
this.emit('start', contents);
|
||||
} else {
|
||||
await this.#emitSmoothedActions(contents.actions);
|
||||
}
|
||||
// Header only exists in the first request
|
||||
if (contents.header) {
|
||||
this.initial_info = contents;
|
||||
this.emit('start', contents);
|
||||
} else {
|
||||
await this.#emitSmoothedActions(contents.actions);
|
||||
}
|
||||
|
||||
clearTimeout(lc_poller);
|
||||
// If there are no actions then we wait 1000 milliseconds, otherwise
|
||||
// The amount of items on the action queue will determine the polling interval.
|
||||
if (!contents.actions.length && !contents.header)
|
||||
await this.#wait(1000);
|
||||
|
||||
this.running && this.#pollLivechat();
|
||||
})().catch((err) => Promise.reject(err));
|
||||
}, this.#lc_polling_interval_ms);
|
||||
if (this.running)
|
||||
this.#pollLivechat();
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures actions are emitted at the right speed.
|
||||
* This was adapted from YouTube's compiled code (Android).
|
||||
* This was adapted from YouTube's compiled code (Android & Web).
|
||||
*/
|
||||
async #emitSmoothedActions(actions: ObservedArray<YTNode>) {
|
||||
async #emitSmoothedActions(action_queue: YTNode[]) {
|
||||
const base = 1E4;
|
||||
|
||||
let delay = actions.length < base / 80 ? 1 : 0;
|
||||
let delay = action_queue.length < base / 80 ? 1 : Math.ceil(action_queue.length / (base / 80));
|
||||
|
||||
const emit_delay_ms =
|
||||
delay == 1 ? (
|
||||
delay = base / actions.length,
|
||||
delay = base / action_queue.length,
|
||||
delay *= Math.random() + 0.5,
|
||||
delay = Math.min(1E3, delay),
|
||||
delay = Math.max(80, delay)
|
||||
) : delay = 80;
|
||||
|
||||
for (const action of actions) {
|
||||
for (const action of action_queue) {
|
||||
await this.#wait(emit_delay_ms);
|
||||
this.emit('chat-update', action);
|
||||
}
|
||||
}
|
||||
|
||||
#pollMetadata() {
|
||||
const md_poller = setTimeout(() => {
|
||||
(async () => {
|
||||
const payload = {
|
||||
video_id: this.#video_info.basic_info.id,
|
||||
ctoken: undefined as string | undefined
|
||||
};
|
||||
(async () => {
|
||||
const payload: {
|
||||
videoId: string | undefined;
|
||||
continuation?: string;
|
||||
} = { videoId: this.#video_info.basic_info.id };
|
||||
|
||||
if (this.#mcontinuation) {
|
||||
payload.ctoken = this.#mcontinuation;
|
||||
}
|
||||
if (this.#mcontinuation) {
|
||||
payload.continuation = this.#mcontinuation;
|
||||
}
|
||||
|
||||
const response = await this.#actions.livechat('updated_metadata', payload);
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const response = await this.#actions.execute('/updated_metadata', payload);
|
||||
const data = Parser.parseResponse(response.data);
|
||||
|
||||
this.#mcontinuation = data.continuation?.token;
|
||||
this.#md_polling_interval_ms = data.continuation?.timeout_ms || this.#md_polling_interval_ms;
|
||||
this.#mcontinuation = data.continuation?.token;
|
||||
|
||||
this.metadata = {
|
||||
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
|
||||
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
|
||||
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
|
||||
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
|
||||
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
|
||||
};
|
||||
this.metadata = {
|
||||
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
|
||||
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
|
||||
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
|
||||
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
|
||||
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
|
||||
};
|
||||
|
||||
this.emit('metadata-update', this.metadata);
|
||||
this.emit('metadata-update', this.metadata);
|
||||
|
||||
clearTimeout(md_poller);
|
||||
await this.#wait(5000);
|
||||
|
||||
this.running && this.#pollMetadata();
|
||||
})().catch((err) => Promise.reject(err));
|
||||
}, this.#md_polling_interval_ms);
|
||||
if (this.running)
|
||||
this.#pollMetadata();
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message.
|
||||
*/
|
||||
async sendMessage(text: string): Promise<ObservedArray<AddChatItemAction>> {
|
||||
const response = await this.#actions.livechat('live_chat/send_message', {
|
||||
text,
|
||||
...{
|
||||
video_id: this.#video_info.basic_info.id,
|
||||
channel_id: this.#video_info.basic_info.channel_id
|
||||
}
|
||||
const response = await this.#actions.execute('/live_chat/send_message', {
|
||||
params: Proto.encodeMessageParams(this.#video_info.basic_info.channel_id as string, this.#video_info.basic_info.id as string),
|
||||
richMessage: { textSegments: [ { text } ] },
|
||||
clientMessageId: uuidv4(),
|
||||
parse: true
|
||||
});
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
|
||||
if (!data.actions)
|
||||
if (!response.actions)
|
||||
throw new InnertubeError('Response did not have an "actions" property. The call may have failed.');
|
||||
|
||||
return data.actions.array().as(AddChatItemAction);
|
||||
return response.actions.array().as(AddChatItemAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves given chat item's menu.
|
||||
*/
|
||||
async getItemMenu(item: ChatItemHasMenuEndpoint): Promise<ItemMenu> {
|
||||
async getItemMenu(item: ChatItemWithMenu): Promise<ItemMenu> {
|
||||
if (!item.menu_endpoint)
|
||||
throw new InnertubeError('This item does not have a menu.', item);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user