Compare commits

...

2 Commits

Author SHA1 Message Date
LuanRT
28e766779e chore: v9.3.0 release 2024-04-11 21:20:11 +00:00
LuanRT
a482556187 chore: v9.2.1 release 2024-04-09 20:43:19 +00:00
5 changed files with 167 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "9.2.0",
"version": "9.3.0",
"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",

View File

@@ -90,7 +90,7 @@ export default class CommentThread extends YTNode {
if (!response.on_response_received_endpoints_memo)
throw new InnertubeError('Unexpected response.', response);
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment, CommentView).map((comment) => {
comment.setActions(this.#actions);
return comment;
}));

View File

@@ -1,15 +1,26 @@
import { YTNode } from '../../helpers.ts';
import type { RawNode } from '../../index.ts';
import type Actions from '../../../core/Actions.ts';
import NavigationEndpoint from '../NavigationEndpoint.ts';
import Author from '../misc/Author.ts';
import Text from '../misc/Text.ts';
import CommentReplyDialog from './CommentReplyDialog.ts';
import { InnertubeError } from '../../../utils/Utils.ts';
import * as Proto from '../../../proto/index.ts';
import type Actions from '../../../core/Actions.ts';
import type { ApiResponse } from '../../../core/Actions.ts';
import type { RawNode } from '../../index.ts';
export default class CommentView extends YTNode {
static type = 'CommentView';
#actions?: Actions;
like_command?: NavigationEndpoint;
dislike_command?: NavigationEndpoint;
unlike_command?: NavigationEndpoint;
undislike_command?: NavigationEndpoint;
reply_command?: NavigationEndpoint;
comment_id: string;
is_pinned: boolean;
keys: {
@@ -32,6 +43,8 @@ export default class CommentView extends YTNode {
};
author?: Author;
test: any;
is_liked?: boolean;
is_disliked?: boolean;
is_hearted?: boolean;
@@ -51,7 +64,7 @@ export default class CommentView extends YTNode {
};
}
applyMutations(comment?: RawNode, toolbar_state?: RawNode) {
applyMutations(comment?: RawNode, toolbar_state?: RawNode, toolbar_surface?: RawNode) {
if (comment) {
this.content = Text.fromAttributed(comment.properties.content);
this.published_time = comment.properties.publishedTime;
@@ -78,8 +91,148 @@ export default class CommentView extends YTNode {
if (toolbar_state) {
this.is_hearted = toolbar_state.heartState === 'TOOLBAR_HEART_STATE_HEARTED';
this.is_liked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_LIKED';
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_HEART_STATE_DISLIKED';
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_DISLIKED';
}
if (toolbar_surface && !Reflect.has(toolbar_surface, 'prepareAccountCommand')) {
this.like_command = new NavigationEndpoint(toolbar_surface.likeCommand);
this.dislike_command = new NavigationEndpoint(toolbar_surface.dislikeCommand);
this.unlike_command = new NavigationEndpoint(toolbar_surface.unlikeCommand);
this.undislike_command = new NavigationEndpoint(toolbar_surface.undislikeCommand);
this.reply_command = new NavigationEndpoint(toolbar_surface.replyCommand);
}
}
/**
* Likes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the like command is not found.
*/
async like(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.like_command)
throw new InnertubeError('Like command not found.');
if (this.is_liked)
throw new InnertubeError('This comment is already liked.', { comment_id: this.comment_id });
return this.like_command.call(this.#actions);
}
/**
* Dislikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the dislike command is not found.
*/
async dislike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.dislike_command)
throw new InnertubeError('Dislike command not found.');
if (this.is_disliked)
throw new InnertubeError('This comment is already disliked.', { comment_id: this.comment_id });
return this.dislike_command.call(this.#actions);
}
/**
* Unlikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the unlike command is not found.
*/
async unlike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.unlike_command)
throw new InnertubeError('Unlike command not found.');
if (!this.is_liked)
throw new InnertubeError('This comment is not liked.', { comment_id: this.comment_id });
return this.unlike_command.call(this.#actions);
}
/**
* Undislikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the undislike command is not found.
*/
async undislike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.undislike_command)
throw new InnertubeError('Undislike command not found.');
if (!this.is_disliked)
throw new InnertubeError('This comment is not disliked.', { comment_id: this.comment_id });
return this.undislike_command.call(this.#actions);
}
/**
* Replies to the comment.
* @param comment_text - The text of the reply.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the reply command is not found.
*/
async reply(comment_text: string): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.reply_command)
throw new InnertubeError('Reply command not found.');
const dialog = this.reply_command.dialog?.as(CommentReplyDialog);
if (!dialog)
throw new InnertubeError('Reply dialog not found.');
const reply_button = dialog.reply_button;
if (!reply_button)
throw new InnertubeError('Reply button not found in the dialog.');
if (!reply_button.endpoint)
throw new InnertubeError('Reply button endpoint not found.');
return reply_button.endpoint.call(this.#actions, { commentText: comment_text });
}
/**
* Translates the comment to the specified target language.
* @param target_language - The target language to translate the comment to, e.g. 'en', 'ja'.
* @returns A Promise that resolves to an ApiResponse object with the translated content, if available.
* @throws if the Actions instance is not set for this comment or if the comment content is not found.
*/
async translate(target_language: string): Promise<ApiResponse & { content?: string }> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');
if (!this.content)
throw new InnertubeError('Comment content not found.', { comment_id: this.comment_id });
// Emojis must be removed otherwise InnerTube throws a 400 status code at us.
const text = this.content.toString().replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu, '');
const payload = {
text,
target_language
};
const action = Proto.encodeCommentActionParams(22, payload);
const response = await this.#actions.execute('comment/perform_comment_action', { action, client: 'ANDROID' });
// XXX: Should move this to Parser#parseResponse
const mutations = response.data.frameworkUpdates?.entityBatchUpdate?.mutations;
const content = mutations?.[0]?.payload?.commentEntityPayload?.translatedContent?.content;
return { ...response, content };
}
setActions(actions: Actions | undefined) {

View File

@@ -710,7 +710,10 @@ export function applyCommentsMutations(memo: Memo, mutations: RawNode[]) {
.find((mutation) => mutation.payload?.engagementToolbarStateEntityPayload?.key === comment_view.keys.toolbar_state)
?.payload?.engagementToolbarStateEntityPayload;
comment_view.applyMutations(comment_mutation, toolbar_state_mutation);
const engagement_toolbar = mutations.find((mutation) => mutation.entityKey === comment_view.keys.toolbar_surface)
?.payload?.engagementToolbarSurfaceEntityPayload;
comment_view.applyMutations(comment_mutation, toolbar_state_mutation, engagement_toolbar);
}
}
}

View File

@@ -465,9 +465,6 @@ function getColorInfo(format: Format) {
if (color_info.transfer_characteristics) {
transfer_characteristics = COLOR_TRANSFER_CHARACTERISTICS[color_info.transfer_characteristics];
} else if (getStringBetweenStrings(format.mime_type, 'codecs="', '"')?.startsWith('avc1')) {
// YouTube's h264 streams always seem to be SDR, so this is a pretty safe bet.
transfer_characteristics = COLOR_TRANSFER_CHARACTERISTICS.BT709;
}
if (color_info.matrix_coefficients) {
@@ -486,6 +483,9 @@ function getColorInfo(format: Format) {
+ `InnerTube client: ${url.searchParams.get('c')}\nformat:`, anonymisedFormat);
}
}
} else if (getStringBetweenStrings(format.mime_type, 'codecs="', '"')?.startsWith('avc1')) {
// YouTube's h264 streams always seem to be SDR, so this is a pretty safe bet.
transfer_characteristics = COLOR_TRANSFER_CHARACTERISTICS.BT709;
}
const info: ColorInfo = {