mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-30 09:55:18 +00:00
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:
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user