mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 07:42:23 +02:00
YT: support obtaining info from player API
This commit is contained in:
@@ -155,16 +155,16 @@ function _getStreamRepresentation(stream)
|
|||||||
repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`);
|
repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modURL = stream.url
|
const encodedURL = Misc.encodeHTML(stream.url)
|
||||||
.replace('?', '/')
|
.replace('?', '/')
|
||||||
.replace(/&/g, '/')
|
.replace(/&/g, '/')
|
||||||
.replace(/=/g, '/');
|
.replace(/=/g, '/');
|
||||||
|
|
||||||
const segRange = `${stream.indexRange.start}-${stream.indexRange.end}`;
|
const segRange = `${stream.indexRange.start}-${stream.indexRange.end}`;
|
||||||
const initRange = `${stream.initRange.start}-${stream.initRange.end}`;
|
const initRange = `${stream.initRange.start}-${stream.initRange.end}`;
|
||||||
|
|
||||||
repArr.push(
|
repArr.push(
|
||||||
` <BaseURL>${modURL}</BaseURL>`,
|
` <BaseURL>${encodedURL}</BaseURL>`,
|
||||||
` <SegmentBase indexRange="${segRange}">`,
|
` <SegmentBase indexRange="${segRange}">`,
|
||||||
` <Initialization range="${initRange}"/>`,
|
` <Initialization range="${initRange}"/>`,
|
||||||
` </SegmentBase>`,
|
` </SegmentBase>`,
|
||||||
|
152
src/youtube.js
152
src/youtube.js
@@ -35,6 +35,11 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
this.downloadingVideoId = null;
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
this.lastInfo = null;
|
this.lastInfo = null;
|
||||||
|
this.postInfo = {
|
||||||
|
clientVersion: null,
|
||||||
|
visitorData: "",
|
||||||
|
};
|
||||||
|
|
||||||
this.cachedSig = {
|
this.cachedSig = {
|
||||||
id: null,
|
id: null,
|
||||||
actions: null,
|
actions: null,
|
||||||
@@ -112,6 +117,16 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!result)
|
||||||
|
result = await this._getPlayerInfoPromise(videoId).catch(debug);
|
||||||
|
|
||||||
|
if(!result || !result.data) {
|
||||||
|
if(result && result.isAborted) {
|
||||||
|
debug(new Error('download aborted'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!result)
|
if(!result)
|
||||||
result = await this._getInfoPromise(videoId).catch(debug);
|
result = await this._getInfoPromise(videoId).catch(debug);
|
||||||
|
|
||||||
@@ -285,34 +300,31 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadDataPromise(url)
|
_downloadDataPromise(url, method, reqData)
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const message = Soup.Message.new('GET', url);
|
const message = Soup.Message.new(method || 'GET', url);
|
||||||
const result = {
|
const result = {
|
||||||
data: '',
|
data: null,
|
||||||
isAborted: false,
|
isAborted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const chunkSignal = message.connect('got-chunk', (msg, chunk) => {
|
if(reqData) {
|
||||||
debug(`got chunk of data, length: ${chunk.length}`);
|
message.set_request(
|
||||||
|
"application/json",
|
||||||
const chunkData = chunk.get_data();
|
Soup.MemoryUse.COPY,
|
||||||
if(!chunkData) return;
|
reqData
|
||||||
|
);
|
||||||
result.data += (chunkData instanceof Uint8Array)
|
}
|
||||||
? ByteArray.toString(chunkData)
|
|
||||||
: chunkData;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue_message(message, (session, msg) => {
|
this.queue_message(message, (session, msg) => {
|
||||||
msg.disconnect(chunkSignal);
|
|
||||||
|
|
||||||
debug('got message response');
|
debug('got message response');
|
||||||
const statusCode = msg.status_code;
|
const statusCode = msg.status_code;
|
||||||
|
|
||||||
if(statusCode === 200)
|
if(statusCode === 200) {
|
||||||
|
result.data = msg.response_body.data;
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
debug(new Error(`response code: ${statusCode}`));
|
debug(new Error(`response code: ${statusCode}`));
|
||||||
|
|
||||||
@@ -348,6 +360,50 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getPlayerInfoPromise(videoId)
|
||||||
|
{
|
||||||
|
const data = this._getPlayerPostData(videoId);
|
||||||
|
const apiKey = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||||
|
const url = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if(!data) {
|
||||||
|
debug('not using player info due to missing data');
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
debug('downloading info from player');
|
||||||
|
|
||||||
|
this._downloadDataPromise(url, 'POST', data).then(result => {
|
||||||
|
if(result.isAborted)
|
||||||
|
return resolve(result);
|
||||||
|
|
||||||
|
debug('parsing player info JSON');
|
||||||
|
|
||||||
|
let info = null;
|
||||||
|
|
||||||
|
try { info = JSON.parse(result.data); }
|
||||||
|
catch(err) { debug(err.message); }
|
||||||
|
|
||||||
|
if(!info)
|
||||||
|
return reject(new Error('could not parse video info JSON'));
|
||||||
|
|
||||||
|
debug('successfully parsed video info JSON');
|
||||||
|
|
||||||
|
/* Update post info values from response */
|
||||||
|
if(info && info.responseContext && info.responseContext.visitorData) {
|
||||||
|
const visData = info.responseContext.visitorData;
|
||||||
|
|
||||||
|
this.postInfo.visitorData = visData;
|
||||||
|
debug(`new visitor ID: ${visData}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data = info;
|
||||||
|
resolve(result);
|
||||||
|
})
|
||||||
|
.catch(err => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_getInfoPromise(videoId)
|
_getInfoPromise(videoId)
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -358,6 +414,8 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
].join('&');
|
].join('&');
|
||||||
const url = `https://www.youtube.com/get_video_info?${query}`;
|
const url = `https://www.youtube.com/get_video_info?${query}`;
|
||||||
|
|
||||||
|
debug('downloading info from video');
|
||||||
|
|
||||||
this._downloadDataPromise(url).then(result => {
|
this._downloadDataPromise(url).then(result => {
|
||||||
if(result.isAborted)
|
if(result.isAborted)
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
@@ -370,6 +428,12 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
return reject(new Error('could not convert query to URI'));
|
return reject(new Error('could not convert query to URI'));
|
||||||
|
|
||||||
const playerResponse = gstUri.get_query_value('player_response');
|
const playerResponse = gstUri.get_query_value('player_response');
|
||||||
|
const cliVer = gstUri.get_query_value('cver');
|
||||||
|
|
||||||
|
if(cliVer && cliVer !== this.postInfo.clientVersion) {
|
||||||
|
this.postInfo.clientVersion = cliVer;
|
||||||
|
debug(`updated client version: ${cliVer}`);
|
||||||
|
}
|
||||||
|
|
||||||
if(!playerResponse)
|
if(!playerResponse)
|
||||||
return reject(new Error('no player response in query'));
|
return reject(new Error('no player response in query'));
|
||||||
@@ -504,7 +568,7 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
|
|
||||||
debug('stream deciphered');
|
debug('stream deciphered');
|
||||||
|
|
||||||
return `${url}&${sig}=${key}`;
|
return `${url}&${sig}=${encodeURIComponent(key)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createSubdirFileAsync(place, folderName, fileName, data)
|
async _createSubdirFileAsync(place, folderName, fileName, data)
|
||||||
@@ -563,6 +627,60 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
.catch(err => reject(err));
|
.catch(err => reject(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getPlayerPostData(videoId)
|
||||||
|
{
|
||||||
|
const cliVer = this.postInfo.clientVersion;
|
||||||
|
if(!cliVer) return null;
|
||||||
|
|
||||||
|
const visitor = this.postInfo.visitorData;
|
||||||
|
const ua = this.user_agent;
|
||||||
|
const browserVer = ua.substring(ua.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
if(!visitor)
|
||||||
|
debug('visitor ID is unknown');
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
videoId: videoId,
|
||||||
|
context: {
|
||||||
|
client: {
|
||||||
|
visitorData: visitor,
|
||||||
|
userAgent: `${ua},gzip(gfe)`,
|
||||||
|
clientName: "WEB",
|
||||||
|
clientVersion: cliVer,
|
||||||
|
osName: "X11",
|
||||||
|
osVersion: "",
|
||||||
|
originalUrl: `https://www.youtube.com/watch?v=${videoId}`,
|
||||||
|
browserName: "Firefox",
|
||||||
|
browserVersion: browserVer,
|
||||||
|
playerType: "UNIPLAYER"
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
lockedSafetyMode: false
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
useSsl: true,
|
||||||
|
internalExperimentFlags: [],
|
||||||
|
consistencyTokenJars: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playbackContext: {
|
||||||
|
contentPlaybackContext: {
|
||||||
|
html5Preference: "HTML5_PREF_WANTS",
|
||||||
|
lactMilliseconds: "1069",
|
||||||
|
referer: `https://www.youtube.com/watch?v=${videoId}`,
|
||||||
|
signatureTimestamp: 18702,
|
||||||
|
autoCaptionsDefaultOn: false,
|
||||||
|
liveContext: {
|
||||||
|
startWalltime: "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
captionParams: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function checkYouTubeUri(uri)
|
function checkYouTubeUri(uri)
|
||||||
|
Reference in New Issue
Block a user