feat: add <info>#addToWatchHistory() (#169)

* dev: add `Actions#stats()`

* dev(parser): parse playback tracking urls

* dev: fix a small bug (unrelated)

* feat: add `<info>#addToWatchHistory()`

* docs: update API ref
This commit is contained in:
LuanRT
2022-09-06 02:40:22 -03:00
committed by GitHub
parent 85fc468cc9
commit 28a651ea3a
8 changed files with 102 additions and 5 deletions

View File

@@ -330,6 +330,9 @@ Retrieves video info, including playback data and even layout elements such as m
- `<info>#getWatchNextContinuation()`
- Retrieves the next batch of items for the watch next feed.
- `<info>#addToWatchHistory()`
- Adds the video to the watch history.
- `<info>#page`
- Returns original InnerTube response (sanitized).

View File

@@ -681,6 +681,26 @@ class Actions {
return this.#wrap(response);
}
/**
* Makes calls to the playback tracking API.
*/
async stats(url: string, client: { client_name: string; client_version: string }, params: { [key: string]: any }) {
const s_url = new URL(url);
s_url.searchParams.set('ver', '2');
s_url.searchParams.set('c', client.client_name.toLowerCase());
s_url.searchParams.set('cbrver', client.client_version);
s_url.searchParams.set('cver', client.client_version);
for (const key of Object.keys(params)) {
s_url.searchParams.set(key, params[key]);
}
const response = await this.#session.http.fetch(s_url);
return response;
}
/**
* Executes an API call.
* @param action - endpoint

View File

@@ -46,7 +46,7 @@ class Music {
const continuation = this.#actions.execute('/next', { client: 'YTMUSIC', videoId: video_id });
const response = await Promise.all([ initial_info, continuation ]);
return new TrackInfo(response, this.#actions);
return new TrackInfo(response, this.#actions, cpn);
}
/**

View File

@@ -252,6 +252,10 @@ export default class Parser {
refinements: data.refinements || null,
estimated_results: data.estimatedResults ? parseInt(data.estimatedResults) : null,
player_overlays: Parser.parse(data.playerOverlays),
playback_tracking: data.playbackTracking ? {
videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl,
videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl
} : null,
playability_status: data.playabilityStatus ? {
status: data.playabilityStatus.status as string,
error_screen: Parser.parse(data.playabilityStatus.errorScreen),

View File

@@ -75,6 +75,9 @@ class VideoInfo {
endscreen;
captions;
cards;
#playback_tracking;
primary_info;
secondary_info;
merchandise;
@@ -130,6 +133,8 @@ class VideoInfo {
this.captions = info.captions;
this.cards = info.cards;
this.#playback_tracking = info.playback_tracking;
const two_col = next?.contents.item().as(TwoColumnWatchNextResults);
const results = two_col?.results;
@@ -177,6 +182,28 @@ class VideoInfo {
return this;
}
/**
* Adds the video to the watch history.
*/
async addToWatchHistory() {
if (!this.#playback_tracking)
throw new InnertubeError('Playback tracking not available');
const url_params = {
cpn: this.#cpn,
fmt: 251,
rtn: 0,
rt: 0
};
const response = await this.#actions.stats(this.#playback_tracking.videostats_playback_url, {
client_name: Constants.CLIENTS.WEB.NAME,
client_version: Constants.CLIENTS.WEB.VERSION
}, url_params);
return response;
}
/**
* Retrieves watch next feed continuation.
*/
@@ -258,6 +285,10 @@ class VideoInfo {
return this.#actions;
}
get cpn() {
return this.#cpn;
}
get page() {
return this.#page;
}

View File

@@ -2,6 +2,7 @@ import Parser, { ParsedResponse } from '../index';
import Actions, { AxioslikeResponse } from '../../core/Actions';
import Playlist from './Playlist';
import MusicHeader from '../classes/MusicHeader';
import MusicCarouselShelf from '../classes/MusicCarouselShelf';
import MusicElementHeader from '../classes/MusicElementHeader';
import HighlightsCarousel from '../classes/HighlightsCarousel';
@@ -10,6 +11,7 @@ import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults';
import Tab from '../classes/Tab';
import ItemSection from '../classes/ItemSection';
import SectionList from '../classes/SectionList';
import Message from '../classes/Message';
import { InnertubeError } from '../../utils/Utils';
@@ -24,14 +26,18 @@ class Recap {
this.#page = Parser.parseResponse(response.data);
this.#actions = actions;
this.header = this.#page.header.item().as(MusicElementHeader).element?.model?.item().as(HighlightsCarousel);
const header = this.#page.header.item();
this.header = header.is(MusicElementHeader) ?
this.#page.header.item().as(MusicElementHeader).element?.model?.item().as(HighlightsCarousel) :
this.#page.header.item().as(MusicHeader);
const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.firstOfType(Tab);
if (!tab)
throw new InnertubeError('Target tab not found');
this.sections = tab.content?.as(SectionList).contents.array().as(ItemSection, MusicCarouselShelf);
this.sections = tab.content?.as(SectionList).contents.array().as(ItemSection, MusicCarouselShelf, Message);
}
/**
@@ -41,6 +47,9 @@ class Recap {
if (!this.header)
throw new InnertubeError('Header not found');
if (!this.header.is(HighlightsCarousel))
throw new InnertubeError('Recap playlist not available, check back later.');
const endpoint = this.header.panels[0].text_on_tap_endpoint;
const response = await endpoint.callTest(this.#actions, { client: 'YTMUSIC' });

View File

@@ -1,5 +1,6 @@
import Parser, { ParsedResponse } from '..';
import Actions, { AxioslikeResponse } from '../../core/Actions';
import Constants from '../../utils/Constants';
import { InnertubeError } from '../../utils/Utils';
import Tab from '../classes/Tab';
@@ -13,6 +14,7 @@ import PlayerOverlay from '../classes/PlayerOverlay';
class TrackInfo {
#page: [ ParsedResponse, ParsedResponse? ];
#actions: Actions;
#cpn;
basic_info;
streaming_data;
@@ -20,17 +22,20 @@ class TrackInfo {
storyboards;
endscreen;
#playback_tracking;
tabs;
current_video_endpoint;
player_overlays;
constructor(data: [AxioslikeResponse, AxioslikeResponse?], actions: Actions) {
constructor(data: [AxioslikeResponse, AxioslikeResponse?], actions: Actions, cpn: string) {
this.#actions = actions;
const info = Parser.parseResponse(data[0].data);
const next = data?.[1]?.data ? Parser.parseResponse(data[1].data) : undefined;
this.#page = [ info, next ];
this.#cpn = cpn;
if (info.playability_status?.status === 'ERROR')
throw new InnertubeError('This video is unavailable', info.playability_status);
@@ -54,6 +59,8 @@ class TrackInfo {
this.storyboards = info.storyboards;
this.endscreen = info.endscreen;
this.#playback_tracking = info.playback_tracking;
if (next) {
const single_col = next.contents.item().as(SingleColumnMusicWatchNextResults);
const tabbed_results = single_col.contents.item().as(Tabbed).contents.item().as(WatchNextTabbedResults);
@@ -66,6 +73,28 @@ class TrackInfo {
}
}
/**
* Adds the song to the watch history.
*/
async addToWatchHistory() {
if (!this.#playback_tracking)
throw new InnertubeError('Playback tracking not available');
const url_params = {
cpn: this.#cpn,
fmt: 251,
rtn: 0,
rt: 0
};
const response = await this.#actions.stats(this.#playback_tracking.videostats_playback_url, {
client_name: Constants.CLIENTS.YTMUSIC.NAME,
client_version: Constants.CLIENTS.YTMUSIC.VERSION
}, url_params);
return response;
}
get page() {
return this.#page;
}

View File

@@ -33,7 +33,8 @@ export const OAUTH = Object.freeze({
});
export const CLIENTS = Object.freeze({
WEB: {
NAME: 'WEB'
NAME: 'WEB',
VERSION: '2.20220902.01.00'
},
YTMUSIC: {
NAME: 'WEB_REMIX',