mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-23 23:09:28 +00:00
feat: add parsers for TimeWatched
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
src/parser/classes/SettingBoolean.ts
Normal file
38
src/parser/classes/SettingBoolean.ts
Normal file
@@ -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;
|
||||
18
src/parser/classes/SimpleTextSection.ts
Normal file
18
src/parser/classes/SimpleTextSection.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
src/parser/classes/analytics/StatRow.ts
Normal file
18
src/parser/classes/analytics/StatRow.ts
Normal file
@@ -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;
|
||||
18
src/parser/classes/misc/ChildElement.ts
Normal file
18
src/parser/classes/misc/ChildElement.ts
Normal file
@@ -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;
|
||||
@@ -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<string, YTNodeConstructor> = {
|
||||
AnalyticsVodCarouselCard,
|
||||
CtaGoToCreatorStudio,
|
||||
DataModelSection,
|
||||
StatRow,
|
||||
AutomixPreviewVideo,
|
||||
BackstageImage,
|
||||
BackstagePost,
|
||||
@@ -433,10 +437,12 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
SearchSuggestionsSection,
|
||||
SecondarySearchContainer,
|
||||
SectionList,
|
||||
SettingBoolean,
|
||||
Shelf,
|
||||
ShowingResultsFor,
|
||||
SimpleCardContent,
|
||||
SimpleCardTeaser,
|
||||
SimpleTextSection,
|
||||
SingleActionEmergencySupport,
|
||||
SingleColumnBrowseResults,
|
||||
SingleColumnMusicWatchNextResults,
|
||||
|
||||
30
src/parser/youtube/TimeWatched.ts
Normal file
30
src/parser/youtube/TimeWatched.ts
Normal file
@@ -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;
|
||||
@@ -205,19 +205,6 @@ export function hasKeys<T extends object, R extends (keyof T)[]>(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();
|
||||
|
||||
Reference in New Issue
Block a user