mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-26 16:18:51 +00:00
Compare commits
4 Commits
v17.2.0
...
feat/comme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
044ab16a0c | ||
|
|
28de687945 | ||
|
|
524db0dd7b | ||
|
|
bf6067b8b7 |
@@ -1,6 +1,9 @@
|
||||
import { Parser } from '../../index.js';
|
||||
import Button from '../Button.js';
|
||||
import Thumbnail from '../misc/Thumbnail.js';
|
||||
import CommentView from './CommentView.js';
|
||||
import CommentThread from './CommentThread.js';
|
||||
import ContinuationItem from '../ContinuationItem.js';
|
||||
|
||||
import { YTNode, type ObservedArray } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
@@ -8,15 +11,17 @@ import type { RawNode } from '../../index.js';
|
||||
export default class CommentReplies extends YTNode {
|
||||
static type = 'CommentReplies';
|
||||
|
||||
contents: ObservedArray<YTNode>;
|
||||
view_replies: Button | null;
|
||||
hide_replies: Button | null;
|
||||
view_replies_creator_thumbnail: Thumbnail[];
|
||||
has_channel_owner_replied: boolean;
|
||||
public contents: ObservedArray<CommentView | ContinuationItem>;
|
||||
public sub_threads: ObservedArray<CommentThread | ContinuationItem>;
|
||||
public view_replies: Button | null;
|
||||
public hide_replies: Button | null;
|
||||
public view_replies_creator_thumbnail: Thumbnail[];
|
||||
public has_channel_owner_replied: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.contents = Parser.parseArray(data.contents, [ CommentView, ContinuationItem ]);
|
||||
this.sub_threads = Parser.parseArray(data.subThreads, [ CommentThread, ContinuationItem ]);
|
||||
this.view_replies = Parser.parseItem(data.viewReplies, Button);
|
||||
this.hide_replies = Parser.parseItem(data.hideReplies, Button);
|
||||
this.view_replies_creator_thumbnail = Thumbnail.fromResponse(data.viewRepliesCreatorThumbnail);
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
|
||||
import { Parser } from '../../index.js';
|
||||
import Button from '../Button.js';
|
||||
import ContinuationItem from '../ContinuationItem.js';
|
||||
import CommentView from './CommentView.js';
|
||||
import CommentReplies from './CommentReplies.js';
|
||||
import CommentsContinuation from '../misc/CommentsContinuation.js';
|
||||
import AppendContinuationItemsAction from '../actions/AppendContinuationItemsAction.js';
|
||||
import { InnertubeError } from '../../../utils/Utils.js';
|
||||
import { observe, YTNode } from '../../helpers.js';
|
||||
|
||||
import type { RawNode } from '../../index.js';
|
||||
import type Actions from '../../../core/Actions.js';
|
||||
import type { Memo, ObservedArray } from '../../helpers.js';
|
||||
import type { ObservedArray } from '../../helpers.js';
|
||||
|
||||
export default class CommentThread extends YTNode {
|
||||
static type = 'CommentThread';
|
||||
|
||||
|
||||
public comment: CommentView | null;
|
||||
public replies?: ObservedArray<CommentView>;
|
||||
public replies?: ObservedArray<CommentThread>;
|
||||
public comment_replies_data: CommentReplies | null;
|
||||
public is_moderated_elq_comment: boolean;
|
||||
public has_replies: boolean;
|
||||
|
||||
public rendering_priority?:
|
||||
| 'RENDERING_PRIORITY_UNKNOWN'
|
||||
| 'RENDERING_PRIORITY_PINNED_COMMENT'
|
||||
| 'RENDERING_PRIORITY_LINKED_COMMENT'
|
||||
| 'RENDERING_PRIORITY_REALTIME_COMMENT'
|
||||
| 'RENDERING_PRIORITY_COMMUNITY_GUIDELINES_BELOW_HEADER'
|
||||
| 'RENDERING_PRIORITY_FAN_COMMUNITY_SETUP_CARD'
|
||||
| 'RENDERING_PRIORITY_COMMENT_HEADER';
|
||||
|
||||
#actions?: Actions;
|
||||
#continuation?: ContinuationItem;
|
||||
|
||||
@@ -28,37 +37,58 @@ export default class CommentThread extends YTNode {
|
||||
this.comment = Parser.parseItem(data.commentViewModel, CommentView);
|
||||
this.comment_replies_data = Parser.parseItem(data.replies, CommentReplies);
|
||||
this.is_moderated_elq_comment = data.isModeratedElqComment;
|
||||
this.rendering_priority = data.renderingPriority;
|
||||
this.has_replies = !!this.comment_replies_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this comment thread has more replies that can be fetched.
|
||||
*/
|
||||
get has_continuation(): boolean {
|
||||
if (!this.replies)
|
||||
throw new InnertubeError('Cannot determine if there is a continuation because this thread\'s replies have not been loaded.');
|
||||
throw new InnertubeError('Cannot determine if there is a continuation because this comment thread\'s replies have not been loaded');
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this comment thread has prepopulated reply data. If false, you will need to call {@link CommentThread.getReplies} to fetch the initial batch of replies.
|
||||
*/
|
||||
get is_prepopulated(): boolean {
|
||||
return !!this.comment_replies_data && this.comment_replies_data.sub_threads[0]?.is(CommentThread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves replies to this comment thread.
|
||||
*/
|
||||
async getReplies(): Promise<CommentThread> {
|
||||
if (!this.#actions)
|
||||
throw new InnertubeError('Actions instance not set for this thread.');
|
||||
throw new InnertubeError('Actions instance not set for this comment thread');
|
||||
|
||||
if (!this.comment_replies_data)
|
||||
throw new InnertubeError('This comment has no replies.', this);
|
||||
throw new InnertubeError('This comment thread has no replies', this);
|
||||
|
||||
const continuation = this.comment_replies_data.contents?.firstOfType(ContinuationItem);
|
||||
if (!this.is_prepopulated) {
|
||||
const continuation = this.comment_replies_data.sub_threads.firstOfType(ContinuationItem);
|
||||
|
||||
if (!continuation)
|
||||
throw new InnertubeError('Replies continuation not found.');
|
||||
if (!continuation)
|
||||
throw new InnertubeError('Replies continuation item not found');
|
||||
|
||||
const response = await continuation.endpoint.call(this.#actions, { parse: true });
|
||||
let endpoint = continuation.endpoint;
|
||||
|
||||
if (!response.on_response_received_endpoints_memo)
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
if (continuation.button)
|
||||
endpoint = continuation.button.endpoint;
|
||||
|
||||
this.replies = this.#getPatchedReplies(response.on_response_received_endpoints_memo);
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem)[0];
|
||||
const response = await endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
if (!response.on_response_received_endpoints)
|
||||
throw new InnertubeError('Unexpected response', response);
|
||||
|
||||
const appendContinuationItemsNode = response.on_response_received_endpoints.firstOfType(AppendContinuationItemsAction);
|
||||
|
||||
if (appendContinuationItemsNode) {
|
||||
this.#processList(appendContinuationItemsNode.contents.as(CommentThread, ContinuationItem));
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -66,40 +96,56 @@ export default class CommentThread extends YTNode {
|
||||
/**
|
||||
* Retrieves next batch of replies.
|
||||
*/
|
||||
async getContinuation(): Promise<CommentThread> {
|
||||
async getContinuation(): Promise<CommentsContinuation> {
|
||||
if (!this.replies)
|
||||
throw new InnertubeError('Cannot retrieve continuation because this thread\'s replies have not been loaded.');
|
||||
throw new InnertubeError('Cannot retrieve continuation because this comment thread\'s replies have not been loaded');
|
||||
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('Continuation not found.');
|
||||
throw new InnertubeError('No continuation item found');
|
||||
|
||||
if (!this.#actions)
|
||||
throw new InnertubeError('Actions instance not set for this thread.');
|
||||
throw new InnertubeError('Actions instance not set for this comment thread');
|
||||
|
||||
const load_more_button = this.#continuation.button?.as(Button);
|
||||
const loadMoreButton = this.#continuation.button?.as(Button);
|
||||
|
||||
if (!load_more_button)
|
||||
throw new InnertubeError('"Load more" button not found.');
|
||||
if (!loadMoreButton)
|
||||
throw new InnertubeError('Cannot retrieve continuation because the "Load more" button is missing');
|
||||
|
||||
const response = await load_more_button.endpoint.call(this.#actions, { parse: true });
|
||||
const response = await loadMoreButton.endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
if (!response.on_response_received_endpoints_memo)
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = this.#getPatchedReplies(response.on_response_received_endpoints_memo);
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem)[0];
|
||||
|
||||
return this;
|
||||
return new CommentsContinuation(this.#actions, response);
|
||||
}
|
||||
|
||||
setActions(actions: Actions) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public setActions(actions: Actions): void {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
#getPatchedReplies(data: Memo): ObservedArray<CommentView> {
|
||||
return observe(data.getType(CommentView).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
return comment;
|
||||
}));
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public processRepliesData(): void {
|
||||
if (this.is_prepopulated && !this.replies) {
|
||||
this.#processList(this.comment_replies_data!.sub_threads);
|
||||
}
|
||||
}
|
||||
|
||||
#processList(contents: ObservedArray<CommentThread | ContinuationItem>): void {
|
||||
if (!this.#actions)
|
||||
throw new InnertubeError('Actions instance not set for this comment thread');
|
||||
|
||||
this.replies = observe([]);
|
||||
|
||||
for (const item of contents) {
|
||||
if (item.is(CommentThread)) {
|
||||
item.setActions(this.#actions);
|
||||
item.processRepliesData();
|
||||
this.replies.push(item);
|
||||
} else if (item.is(ContinuationItem)) {
|
||||
this.#continuation = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/parser/classes/misc/CommentsContinuation.ts
Normal file
61
src/parser/classes/misc/CommentsContinuation.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { InnertubeError } from '../../../utils/Utils.js';
|
||||
import { type ObservedArray, observe } from '../../helpers.js';
|
||||
import AppendContinuationItemsAction from '../actions/AppendContinuationItemsAction.js';
|
||||
import CommentThread from '../comments/CommentThread.js';
|
||||
import ContinuationItem from '../ContinuationItem.js';
|
||||
|
||||
import type { Actions } from '../../../core/index.js';
|
||||
import type { INextResponse } from '../../types/index.js';
|
||||
|
||||
export default class CommentsContinuation {
|
||||
public replies: ObservedArray<CommentThread> = observe([]);
|
||||
|
||||
readonly #actions: Actions;
|
||||
readonly #nextContinuationItem?: ContinuationItem;
|
||||
|
||||
constructor(actions: Actions, data: INextResponse) {
|
||||
this.#actions = actions;
|
||||
|
||||
if (!data.on_response_received_endpoints || !data.on_response_received_endpoints_memo) {
|
||||
throw new InnertubeError('Invalid response received for comments continuation', data);
|
||||
}
|
||||
|
||||
const appendContinuationItemsNode = data.on_response_received_endpoints.firstOfType(AppendContinuationItemsAction);
|
||||
|
||||
if (appendContinuationItemsNode) {
|
||||
for (const item of appendContinuationItemsNode.contents) {
|
||||
if (item.is(CommentThread)) {
|
||||
item.setActions(this.#actions);
|
||||
item.processRepliesData();
|
||||
this.replies.push(item);
|
||||
} else if (item.is(ContinuationItem)) {
|
||||
this.#nextContinuationItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this comment thread has more replies that can be fetched.
|
||||
*/
|
||||
get has_continuation(): boolean {
|
||||
return !!this.#nextContinuationItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves next batch of replies.
|
||||
*/
|
||||
public async getContinuation(): Promise<CommentsContinuation> {
|
||||
if (!this.#nextContinuationItem)
|
||||
throw new InnertubeError('No continuation item found');
|
||||
|
||||
const loadMoreButton = this.#nextContinuationItem.button;
|
||||
|
||||
if (!loadMoreButton)
|
||||
throw new InnertubeError('"Load more" button not found in continuation item');
|
||||
|
||||
const response = await loadMoreButton.endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
return new CommentsContinuation(this.#actions, response);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export { default as AccessibilityData } from './classes/misc/AccessibilityData.j
|
||||
export { default as Author } from './classes/misc/Author.js';
|
||||
export { default as ChildElement } from './classes/misc/ChildElement.js';
|
||||
export { default as CommandContext } from './classes/misc/CommandContext.js';
|
||||
export { default as CommentsContinuation } from './classes/misc/CommentsContinuation.js';
|
||||
export { default as EmojiRun } from './classes/misc/EmojiRun.js';
|
||||
export { default as Format } from './classes/misc/Format.js';
|
||||
export { default as RendererContext } from './classes/misc/RendererContext.js';
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class Comments {
|
||||
const contents = this.#page.on_response_received_endpoints;
|
||||
|
||||
if (!contents)
|
||||
throw new InnertubeError('Comments page did not have any content.');
|
||||
throw new InnertubeError('The comments page did not have any content');
|
||||
|
||||
const header_node = contents.at(0)?.as(AppendContinuationItemsAction, ReloadContinuationItemsCommand);
|
||||
const body_node = contents.at(1)?.as(AppendContinuationItemsAction, ReloadContinuationItemsCommand);
|
||||
@@ -40,6 +40,7 @@ export default class Comments {
|
||||
if (thread.comment)
|
||||
thread.comment.setActions(this.#actions);
|
||||
thread.setActions(this.#actions);
|
||||
thread.processRepliesData();
|
||||
return thread;
|
||||
}));
|
||||
|
||||
@@ -52,7 +53,7 @@ export default class Comments {
|
||||
*/
|
||||
async applySort(sort: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
|
||||
if (!this.header)
|
||||
throw new InnertubeError('Page header is missing. Cannot apply sort option.');
|
||||
throw new InnertubeError('Could not apply sort because the comments header is missing');
|
||||
|
||||
let button;
|
||||
|
||||
@@ -63,7 +64,7 @@ export default class Comments {
|
||||
}
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Could not find target button.');
|
||||
throw new InnertubeError('Could not apply sort because the sort button is missing');
|
||||
|
||||
if (button.selected)
|
||||
return this;
|
||||
@@ -79,15 +80,15 @@ export default class Comments {
|
||||
*/
|
||||
async createComment(text: string): Promise<ApiResponse> {
|
||||
if (!this.header)
|
||||
throw new InnertubeError('Page header is missing. Cannot create comment.');
|
||||
throw new InnertubeError('Comment could not be created because the page header is missing');
|
||||
|
||||
const button = this.header.create_renderer?.as(CommentSimplebox).submit_button;
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Could not find target button. You are probably not logged in.');
|
||||
throw new InnertubeError('Comment could not be created because the comment button is missing');
|
||||
|
||||
if (!button.endpoint)
|
||||
throw new InnertubeError('Button does not have an endpoint.');
|
||||
throw new InnertubeError('Comment could not be created because the comment button does not have an endpoint');
|
||||
|
||||
return await button.endpoint.call(this.#actions, { commentText: text });
|
||||
}
|
||||
@@ -97,7 +98,7 @@ export default class Comments {
|
||||
*/
|
||||
async getContinuation(): Promise<Comments> {
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('Continuation not found');
|
||||
throw new InnertubeError('No continuation item found');
|
||||
|
||||
const data = await this.#continuation.endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
@@ -105,7 +106,7 @@ export default class Comments {
|
||||
const page = Object.assign({}, this.#page);
|
||||
|
||||
if (!page.on_response_received_endpoints || !data.on_response_received_endpoints)
|
||||
throw new InnertubeError('Invalid reponse format, missing on_response_received_endpoints.');
|
||||
throw new InnertubeError('Invalid reponse format, missing on_response_received_endpoints');
|
||||
|
||||
// Remove previous items and append the continuation.
|
||||
page.on_response_received_endpoints.pop();
|
||||
|
||||
Reference in New Issue
Block a user