diff --git a/src/core/AccountManager.ts b/src/core/AccountManager.ts index d385e34e..8f3abcc0 100644 --- a/src/core/AccountManager.ts +++ b/src/core/AccountManager.ts @@ -1,8 +1,10 @@ -import { throwIfMissing, findNode } from '../utils/Utils'; -import Constants from '../utils/Constants'; -import Analytics from '../parser/youtube/Analytics'; import Proto from '../proto/index'; import Actions from './Actions'; +import Constants from '../utils/Constants'; +import { throwIfMissing, findNode } from '../utils/Utils'; + +import Analytics from '../parser/youtube/Analytics'; +import TimeWatched from '../parser/youtube/TimeWatched'; class AccountManager { #actions; @@ -129,20 +131,12 @@ class AccountManager { * Retrieves time watched statistics. */ async getTimeWatched() { - const response = await this.#actions.browse('SPtime_watched', { client: 'ANDROID' }); - const rows: any[] = findNode(response.data, 'contents', 'statRowRenderer', 11, false); + const response = await this.#actions.execute('/browse', { + browseId: 'SPtime_watched', + client: 'ANDROID' + }); - const stats = rows.map((row: any) => { - const renderer = row.statRowRenderer; - if (renderer) { - return { - title: renderer.title.runs.map((run: any) => run.text).join(''), - time: renderer.contents.runs.map((run: any) => run.text).join('') - }; - } - }).filter((stat: any) => stat); - - return stats; + return new TimeWatched(response); } /** diff --git a/src/core/Actions.ts b/src/core/Actions.ts index b1375f26..7e6fd448 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -689,6 +689,11 @@ class Actions { if (!args.protobuf) { data = { ...args }; + if (Reflect.has(data, 'browseId')) { + if (this.#needsLogin(data.browseId) && !this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + } + if (Reflect.has(data, 'parse')) delete data.parse; diff --git a/src/parser/classes/Element.ts b/src/parser/classes/Element.ts index 66efada1..f1c74c02 100644 --- a/src/parser/classes/Element.ts +++ b/src/parser/classes/Element.ts @@ -1,15 +1,22 @@ import Parser from '../index'; +import ChildElement from './misc/ChildElement'; + import { YTNode } from '../helpers'; class Element extends YTNode { static type = 'Element'; model; + child_elements?: ChildElement[]; constructor(data: any) { super(); const type = data.newElement.type.componentType; - this.model = Parser.parse(type.model); + this.model = Parser.parse(type?.model); + + if (data.newElement?.childElements) { + this.child_elements = data.newElement?.childElements?.map((el: any) => new ChildElement(el)) || null; + } } } diff --git a/src/parser/classes/SettingBoolean.ts b/src/parser/classes/SettingBoolean.ts new file mode 100644 index 00000000..c9bc6bba --- /dev/null +++ b/src/parser/classes/SettingBoolean.ts @@ -0,0 +1,38 @@ +import Text from './misc/Text'; +import NavigationEndpoint from './NavigationEndpoint'; + +import { YTNode } from '../helpers'; + +class SettingBoolean extends YTNode { + static type = 'SettingBoolean'; + + title?: Text; + summary?: Text; + enable_endpoint?: NavigationEndpoint; + disable_endpoint?: NavigationEndpoint; + item_id: string; + + constructor(data: any) { + super(); + + if (data.title) { + this.title = new Text(data.title); + } + + if (data.summary) { + this.summary = new Text(data.summary); + } + + if (data.enableServiceEndpoint) { + this.enable_endpoint = new NavigationEndpoint(data.enableServiceEndpoint); + } + + if (data.disableServiceEndpoint) { + this.disable_endpoint = new NavigationEndpoint(data.disableServiceEndpoint); + } + + this.item_id = data.itemId; + } +} + +export default SettingBoolean; \ No newline at end of file diff --git a/src/parser/classes/SimpleTextSection.ts b/src/parser/classes/SimpleTextSection.ts new file mode 100644 index 00000000..b4c5bd5d --- /dev/null +++ b/src/parser/classes/SimpleTextSection.ts @@ -0,0 +1,18 @@ +import Text from './misc/Text'; + +import { YTNode } from '../helpers'; + +class SimpleTextSection extends YTNode { + static type = 'SimpleTextSection'; + + lines: Text[]; + style: string; + + constructor(data: any) { + super(); + this.lines = data.lines.map((line: any) => new Text(line)); + this.style = data.layoutStyle; + } +} + +export default SimpleTextSection; \ No newline at end of file diff --git a/src/parser/classes/analytics/AnalyticsVodCarouselCard.ts b/src/parser/classes/analytics/AnalyticsVodCarouselCard.ts index 4ebb6d2b..05192c2d 100644 --- a/src/parser/classes/analytics/AnalyticsVodCarouselCard.ts +++ b/src/parser/classes/analytics/AnalyticsVodCarouselCard.ts @@ -5,12 +5,18 @@ class AnalyticsVodCarouselCard extends YTNode { static type = 'AnalyticsVodCarouselCard'; title: string; - videos: Video[]; + videos: Video[] | null; + no_data_message?: string; constructor(data: any) { super(); this.title = data.title; - this.videos = data.videoCarouselData.videos.map((video: any) => new Video(video)); + + if (data.noDataMessage) { + this.no_data_message = data.noDataMessage; + } + + this.videos = data.videoCarouselData?.videos.map((video: any) => new Video(video)) || null; } } diff --git a/src/parser/classes/analytics/StatRow.ts b/src/parser/classes/analytics/StatRow.ts new file mode 100644 index 00000000..b60c48c0 --- /dev/null +++ b/src/parser/classes/analytics/StatRow.ts @@ -0,0 +1,18 @@ +import Text from '../misc/Text'; + +import { YTNode } from '../../helpers'; + +class StatRow extends YTNode { + static type = 'StatRow'; + + title: Text; + contents: Text; + + constructor(data: any) { + super(); + this.title = new Text(data.title); + this.contents = new Text(data.contents); + } +} + +export default StatRow; \ No newline at end of file diff --git a/src/parser/classes/misc/ChildElement.ts b/src/parser/classes/misc/ChildElement.ts new file mode 100644 index 00000000..3359325b --- /dev/null +++ b/src/parser/classes/misc/ChildElement.ts @@ -0,0 +1,18 @@ +class ChildElement { + static type = 'ChildElement'; + + text: string | null; + properties; + child_elements?: ChildElement[]; + + constructor(data: any) { + this.text = data.type.textType?.text?.content || null; + this.properties = data.properties; + + if (data.childElements) { + this.child_elements = data.childElements.map((el: any) => new ChildElement(el)); + } + } +} + +export default ChildElement; \ No newline at end of file diff --git a/src/parser/map.ts b/src/parser/map.ts index 106d4914..d86ec297 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -11,6 +11,7 @@ import { default as AnalyticsVideo } from './classes/analytics/AnalyticsVideo'; import { default as AnalyticsVodCarouselCard } from './classes/analytics/AnalyticsVodCarouselCard'; import { default as CtaGoToCreatorStudio } from './classes/analytics/CtaGoToCreatorStudio'; import { default as DataModelSection } from './classes/analytics/DataModelSection'; +import { default as StatRow } from './classes/analytics/StatRow'; import { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo'; import { default as BackstageImage } from './classes/BackstageImage'; import { default as BackstagePost } from './classes/BackstagePost'; @@ -192,10 +193,12 @@ import { default as SearchSuggestion } from './classes/SearchSuggestion'; import { default as SearchSuggestionsSection } from './classes/SearchSuggestionsSection'; import { default as SecondarySearchContainer } from './classes/SecondarySearchContainer'; import { default as SectionList } from './classes/SectionList'; +import { default as SettingBoolean } from './classes/SettingBoolean'; import { default as Shelf } from './classes/Shelf'; import { default as ShowingResultsFor } from './classes/ShowingResultsFor'; import { default as SimpleCardContent } from './classes/SimpleCardContent'; import { default as SimpleCardTeaser } from './classes/SimpleCardTeaser'; +import { default as SimpleTextSection } from './classes/SimpleTextSection'; import { default as SingleActionEmergencySupport } from './classes/SingleActionEmergencySupport'; import { default as SingleColumnBrowseResults } from './classes/SingleColumnBrowseResults'; import { default as SingleColumnMusicWatchNextResults } from './classes/SingleColumnMusicWatchNextResults'; @@ -252,6 +255,7 @@ const map: Record = { AnalyticsVodCarouselCard, CtaGoToCreatorStudio, DataModelSection, + StatRow, AutomixPreviewVideo, BackstageImage, BackstagePost, @@ -433,10 +437,12 @@ const map: Record = { SearchSuggestionsSection, SecondarySearchContainer, SectionList, + SettingBoolean, Shelf, ShowingResultsFor, SimpleCardContent, SimpleCardTeaser, + SimpleTextSection, SingleActionEmergencySupport, SingleColumnBrowseResults, SingleColumnMusicWatchNextResults, diff --git a/src/parser/youtube/TimeWatched.ts b/src/parser/youtube/TimeWatched.ts new file mode 100644 index 00000000..547fa616 --- /dev/null +++ b/src/parser/youtube/TimeWatched.ts @@ -0,0 +1,30 @@ +import Parser, { ParsedResponse } from '..'; +import { AxioslikeResponse } from '../../core/Actions'; + +import ItemSection from '../classes/ItemSection'; +import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults'; +import SectionList from '../classes/SectionList'; + +import { InnertubeError } from '../../utils/Utils'; + +class TimeWatched { + #page; + contents; + + constructor(response: AxioslikeResponse) { + this.#page = Parser.parseResponse(response.data); + + const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.get({ selected: true }); + + if (!tab) + throw new InnertubeError('Could not find target tab.'); + + this.contents = tab.content?.as(SectionList).contents.array().as(ItemSection); + } + + get page(): ParsedResponse { + return this.#page; + } +} + +export default TimeWatched; \ No newline at end of file diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 512e36cf..71a9dec2 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -205,19 +205,6 @@ export function hasKeys(params: T, ...k return true; } -/** - * Turns the ntoken transform data into a valid json array - */ -export function refineNTokenData(data: string) { - return data - .replace(/function\(d,e\)/g, '"function(d,e)').replace(/function\(d\)/g, '"function(d)') - .replace(/function\(\)/g, '"function()').replace(/function\(d,e,f\)/g, '"function(d,e,f)') - .replace(/\[function\(d,e,f\)/g, '["function(d,e,f)').replace(/,b,/g, ',"b",') - .replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]') - .replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",') - .replace(/""/g, '').replace(/length]\)}"/g, 'length])}'); -} - export function uuidv4() { if (getRuntime() === 'node') { return Reflect.get(module, 'require')('crypto').webcrypto.randomUUID();