From 0081e11ebcff8c719902d93edf8760d3e8702e00 Mon Sep 17 00:00:00 2001 From: Luan Date: Sat, 26 Oct 2024 17:45:09 -0300 Subject: [PATCH] refactor!: Deprecate `account#getAnalytics`, `account#getTimeWatched` and `account#getAnalytics` Due to recent changes by YouTube, these actions can no longer be executed using web based OAuth tokens nor cookies. --- .../endpoints/account/AccountListEndpoint.ts | 3 ++- src/core/managers/AccountManager.ts | 24 +++++++++++++------ src/parser/classes/AccountItemSection.ts | 9 +++---- src/parser/classes/AccountSectionList.ts | 11 +++++---- src/parser/classes/CopyLink.ts | 6 ++--- .../classes/actions/GetMultiPageMenuAction.ts | 15 ++++++++++++ src/parser/classes/comments/Comment.ts | 7 ++---- src/parser/nodes.ts | 1 + src/parser/youtube/AccountInfo.ts | 5 +--- src/types/Endpoints.ts | 3 ++- 10 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 src/parser/classes/actions/GetMultiPageMenuAction.ts diff --git a/src/core/endpoints/account/AccountListEndpoint.ts b/src/core/endpoints/account/AccountListEndpoint.ts index 692c9fce1..d464235c5 100644 --- a/src/core/endpoints/account/AccountListEndpoint.ts +++ b/src/core/endpoints/account/AccountListEndpoint.ts @@ -8,6 +8,7 @@ export const PATH = '/account/accounts_list'; */ export function build(): IAccountListRequest { return { - client: 'ANDROID' + client: 'TV', + callCircumstance: 2 }; } \ No newline at end of file diff --git a/src/core/managers/AccountManager.ts b/src/core/managers/AccountManager.ts index ff18632f0..e740190d4 100644 --- a/src/core/managers/AccountManager.ts +++ b/src/core/managers/AccountManager.ts @@ -4,6 +4,7 @@ import AccountInfo from '../../parser/youtube/AccountInfo.js'; import Analytics from '../../parser/youtube/Analytics.js'; import Settings from '../../parser/youtube/Settings.js'; import TimeWatched from '../../parser/youtube/TimeWatched.js'; +import CopyLink from '../../parser/classes/CopyLink.js'; import { InnertubeError, u8ToBase64 } from '../../utils/Utils.js'; import { Account, BrowseEndpoint, Channel } from '../endpoints/index.js'; @@ -26,6 +27,7 @@ export default class AccountManager { /** * Edits channel name. * @param new_name - The new channel name. + * @deprecated This method is deprecated and will be removed in a future release. */ editName: (new_name: string) => { if (!this.#actions.session.logged_in) @@ -41,6 +43,7 @@ export default class AccountManager { /** * Edits channel description. * @param new_description - The new description. + * @deprecated This method is deprecated and will be removed in a future release. */ editDescription: (new_description: string) => { if (!this.#actions.session.logged_in) @@ -55,6 +58,7 @@ export default class AccountManager { }, /** * Retrieves basic channel analytics. + * @deprecated This method is deprecated and will be removed in a future release. */ getBasicAnalytics: () => this.getAnalytics() }; @@ -77,6 +81,7 @@ export default class AccountManager { /** * Retrieves time watched statistics. + * @deprecated This method is deprecated and will be removed in a future release. */ async getTimeWatched(): Promise { const response = await this.#actions.execute( @@ -103,22 +108,27 @@ export default class AccountManager { /** * Retrieves basic channel analytics. + * @deprecated This method is deprecated and will be removed in a future release. */ async getAnalytics(): Promise { - const info = await this.getInfo(); + const advanced_settings = await this.#actions.execute( + BrowseEndpoint.PATH, { browseId: 'SPaccount_advanced', parse: true } + ); + + const copy_link_button = advanced_settings.contents_memo?.getType(CopyLink).find((node) => node.short_url.startsWith('UC')); - const writer = ChannelAnalytics.encode({ + if (!copy_link_button || !copy_link_button.short_url) + throw new InnertubeError('Channel ID not found'); + + const params = encodeURIComponent(u8ToBase64(ChannelAnalytics.encode({ params: { - channelId: info.footers?.endpoint.payload.browseId + channelId: copy_link_button.short_url } - }); - - const params = encodeURIComponent(u8ToBase64(writer.finish())); + }).finish())); const response = await this.#actions.execute( BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEanalytics_screen', - client: 'ANDROID', params }) ); diff --git a/src/parser/classes/AccountItemSection.ts b/src/parser/classes/AccountItemSection.ts index 6c0fe7bf1..e4930299c 100644 --- a/src/parser/classes/AccountItemSection.ts +++ b/src/parser/classes/AccountItemSection.ts @@ -1,18 +1,19 @@ import { Parser } from '../index.js'; import AccountItem from './AccountItem.js'; import AccountItemSectionHeader from './AccountItemSectionHeader.js'; -import { YTNode, observe, type ObservedArray } from '../helpers.js'; +import { YTNode, type ObservedArray } from '../helpers.js'; import type { RawNode } from '../index.js'; +import CompactLink from './CompactLink.js'; export default class AccountItemSection extends YTNode { static type = 'AccountItemSection'; - contents: ObservedArray; - header: AccountItemSectionHeader | null; + public contents: ObservedArray; + public header: AccountItemSectionHeader | null; constructor(data: RawNode) { super(); - this.contents = observe(data.contents.map((ac: RawNode) => new AccountItem(ac.accountItem))); + this.contents = Parser.parseArray(data.contents, [ AccountItem, CompactLink ]); this.header = Parser.parseItem(data.header, AccountItemSectionHeader); } } \ No newline at end of file diff --git a/src/parser/classes/AccountSectionList.ts b/src/parser/classes/AccountSectionList.ts index 28faa9985..46a3f9a80 100644 --- a/src/parser/classes/AccountSectionList.ts +++ b/src/parser/classes/AccountSectionList.ts @@ -2,18 +2,19 @@ import { Parser } from '../index.js'; import AccountChannel from './AccountChannel.js'; import AccountItemSection from './AccountItemSection.js'; -import { YTNode } from '../helpers.js'; import type { RawNode } from '../index.js'; +import type { ObservedArray } from '../helpers.js'; +import { YTNode } from '../helpers.js'; export default class AccountSectionList extends YTNode { static type = 'AccountSectionList'; - contents; - footers; + public contents: ObservedArray; + public footers: ObservedArray; constructor(data: RawNode) { super(); - this.contents = Parser.parseItem(data.contents[0], AccountItemSection); - this.footers = Parser.parseItem(data.footers[0], AccountChannel); + this.contents = Parser.parseArray(data.contents, AccountItemSection); + this.footers = Parser.parseArray(data.footers, AccountChannel); } } \ No newline at end of file diff --git a/src/parser/classes/CopyLink.ts b/src/parser/classes/CopyLink.ts index f09c471cf..b447c294e 100644 --- a/src/parser/classes/CopyLink.ts +++ b/src/parser/classes/CopyLink.ts @@ -5,9 +5,9 @@ import Button from './Button.js'; export default class CopyLink extends YTNode { static type = 'CopyLink'; - copy_button: Button | null; - short_url: string; - style: string; + public copy_button: Button | null; + public short_url: string; + public style: string; constructor(data: RawNode) { super(); diff --git a/src/parser/classes/actions/GetMultiPageMenuAction.ts b/src/parser/classes/actions/GetMultiPageMenuAction.ts new file mode 100644 index 000000000..15af055cb --- /dev/null +++ b/src/parser/classes/actions/GetMultiPageMenuAction.ts @@ -0,0 +1,15 @@ +import { Parser } from '../../index.js'; +import { YTNode } from '../../helpers.js'; +import type { RawNode } from '../../index.js'; +import MultiPageMenu from '../menus/MultiPageMenu.js'; + +export default class GetMultiPageMenuAction extends YTNode { + static type = 'GetMultiPageMenuAction'; + + public menu: MultiPageMenu | null; + + constructor(data: RawNode) { + super(); + this.menu = Parser.parseItem(data.menu, MultiPageMenu); + } +} \ No newline at end of file diff --git a/src/parser/classes/comments/Comment.ts b/src/parser/classes/comments/Comment.ts index eb738d0a7..5be67e906 100644 --- a/src/parser/classes/comments/Comment.ts +++ b/src/parser/classes/comments/Comment.ts @@ -148,11 +148,8 @@ export default class Comment extends YTNode { * Translates the comment to a given language. * @param target_language - Ex; en, ja */ - async translate(target_language: string): Promise<{ + async translate(target_language: string): Promise { if (!this.#actions) throw new InnertubeError('An active caller must be provide to perform this operation.'); @@ -167,7 +164,7 @@ export default class Comment extends YTNode { }; const action = ProtoUtils.encodeCommentActionParams(22, payload); - const response = await this.#actions.execute('comment/perform_comment_action', { action, client: 'ANDROID' }); + const response = await this.#actions.execute('comment/perform_comment_action', { action }); // XXX: Should move this to Parser#parseResponse const mutations = response.data.frameworkUpdates?.entityBatchUpdate?.mutations; diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index b4c6bf3e9..a6dc9d53e 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -9,6 +9,7 @@ export { default as AccountItemSection } from './classes/AccountItemSection.js'; export { default as AccountItemSectionHeader } from './classes/AccountItemSectionHeader.js'; export { default as AccountSectionList } from './classes/AccountSectionList.js'; export { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js'; +export { default as GetMultiPageMenuAction } from './classes/actions/GetMultiPageMenuAction.js'; export { default as OpenPopupAction } from './classes/actions/OpenPopupAction.js'; export { default as UpdateEngagementPanelAction } from './classes/actions/UpdateEngagementPanelAction.js'; export { default as Alert } from './classes/Alert.js'; diff --git a/src/parser/youtube/AccountInfo.ts b/src/parser/youtube/AccountInfo.ts index eae052c8d..3d8ccefb9 100644 --- a/src/parser/youtube/AccountInfo.ts +++ b/src/parser/youtube/AccountInfo.ts @@ -5,13 +5,11 @@ import AccountSectionList from '../classes/AccountSectionList.js'; import type { ApiResponse } from '../../core/index.js'; import type { IParsedResponse } from '../types/index.js'; import type AccountItemSection from '../classes/AccountItemSection.js'; -import type AccountChannel from '../classes/AccountChannel.js'; export default class AccountInfo { #page: IParsedResponse; contents: AccountItemSection | null; - footers: AccountChannel | null; constructor(response: ApiResponse) { this.#page = Parser.parseResponse(response.data); @@ -24,8 +22,7 @@ export default class AccountInfo { if (!account_section_list) throw new InnertubeError('Account section list not found'); - this.contents = account_section_list.contents; - this.footers = account_section_list.footers; + this.contents = account_section_list.contents.first(); } get page(): IParsedResponse { diff --git a/src/types/Endpoints.ts b/src/types/Endpoints.ts index c07407b00..106720b9e 100644 --- a/src/types/Endpoints.ts +++ b/src/types/Endpoints.ts @@ -184,7 +184,8 @@ export interface IChannelEditDescriptionRequest extends ObjectSnakeToCamel