Open Benjamin-Loison opened 7 months ago
import requests
import json
import blackboxprotobuf
import base64
import hashlib
import time
# Both kept timestamp from actual request and current timestamp work.
currentTime = 1711466739#int(time.time())
SAPISID = 'CENSORED'
__Secure_1PSIDTS = 'sidts-CENSORED'
__Secure_1PSID = 'CENSORED'
__Secure_1PAPISID = 'CENSORED'
SAPISIDHASH = f'{currentTime}_' + hashlib.sha1(f'{currentTime} {SAPISID} https://www.youtube.com'.encode('ascii')).digest().hex()
def getBase64Protobuf(message, typedef):
data = blackboxprotobuf.encode_message(message, typedef)
return base64.b64encode(data).decode('ascii')
message = {
'2': 'community',
'25': {
'22': 'UgwSoAm2bGLaJM44UTZ4AaABCQ'
},
}
typedef = {
'2': {
'type': 'string'
},
'25': {
'type': 'message',
'message_typedef': {
'22': {
'type': 'string'
}
},
'field_order': [
'22'
]
},
}
params = getBase64Protobuf(message, typedef)
url = 'https://www.youtube.com/youtubei/v1/browse'
headers = {
'Origin': 'https://www.youtube.com',
'Authorization': f'SAPISIDHASH {SAPISIDHASH}',
}
cookies = {
'__Secure-1PSIDTS': __Secure_1PSIDTS,
'__Secure-1PSID': __Secure_1PSID,
'__Secure-1PAPISID': __Secure_1PAPISID,
}
data = {
'context': {
'client': {
'clientName': 'WEB',
'clientVersion': '2.20240325.01.00',
}
},
'browseId': 'UCQxJsAlqmBPAbR_0syDi9mg',
'params': params,
}
data = requests.post(url, headers = headers, cookies = cookies, json = data).json()
print('voteRatioIfSelected' in str(data))
To add this feature it requires a different method than I usually used. I currently have a quite promising approach but to make that it will work for a long period of time, I will wait one day to make sure that my prototype still works in case there is no temporary authorization process that I am not aware of. The currently working script with both hardcoded and current timestamps work in my VirtualBox Linux Mint (trust)
virtual machine in /home/benjamin/Desktop/260324_YouTube-operational-API_issues_258.py
.
With both hardcoded and current timestamps it does not work anymore. It seems that only changing SAPISID
, __Secure_1PSIDTS
, __Secure_1PSID
and __Secure_1PAPISID
work. Should algorithmitically properly verify that, I restored initial script state.
https://discord.com/channels/933841502155706418/933841503103627316/1216147048219414559
https://discord.com/channels/933841502155706418/933841503103627316/1217157740787663019
I indeed remember having used my personal YouTube account not in an incognito window, so let us try doing so this time and pay attention not keeping the web-browser open too long to have the cookie rotation. Pay attention to actual requests to make sure such one is not performed in this short time frame.
For security and possibly ease the process, I use a not 2FA account:
-----BEGIN PGP MESSAGE-----
hF4DTQa9Wom5MBgSAQdAR4Xg4n8T5rVeQ+dt10iv+FSJ2wTz+3VIVYH7Gszug3Yw
kYrEHPbY/I0zuHKWyWdxhQuAYKNPfWOAhFLH0hh12LLVMi+kPXdXRVt+BQkU84o2
0lgB9jaamfo8DAEf0QQHy3lzA2NJMW3phApIzBZdWdvbvkVMlg7Lj+HseSTZ7rUA
YjXGBW9bN6fr9CtjnO1igdhJqGU4XjtpY6+MwJnS5E1v3UA9+RbD28JZ
=oIZQ
-----END PGP MESSAGE-----
https://blog.lepine.pro/protobuf-standard-pour-echanger-des-donnes-php-go
Both following work:
import requests
import json
import blackboxprotobuf
import base64
import hashlib
import time
# Both kept timestamp from actual request and current timestamp work.
currentTime = 1711824127#int(time.time())
SAPISID = 'CENSORED'
__Secure_3PSID = 'CENSORED'
__Secure_3PAPISID = 'CENSORED'
SAPISIDHASH = f'{currentTime}_' + hashlib.sha1(f'{currentTime} {SAPISID} https://www.youtube.com'.encode('ascii')).digest().hex()
def getBase64Protobuf(message, typedef):
data = blackboxprotobuf.encode_message(message, typedef)
return base64.b64encode(data).decode('ascii')
message = {
'2': 'community',
'25': {
'22': 'UgwSoAm2bGLaJM44UTZ4AaABCQ'
},
}
typedef = {
'2': {
'type': 'string'
},
'25': {
'type': 'message',
'message_typedef': {
'22': {
'type': 'string'
}
},
'field_order': [
'22'
]
},
}
params = getBase64Protobuf(message, typedef)
url = 'https://www.youtube.com/youtubei/v1/browse'
headers = {
'Origin': 'https://www.youtube.com',
'Authorization': f'SAPISIDHASH {SAPISIDHASH}',
}
cookies = {
'__Secure-3PSID': __Secure_3PSID,
'__Secure-3PAPISID': __Secure_3PAPISID,
}
data = {
'context': {
'client': {
'clientName': 'WEB',
'clientVersion': '2.20240325.01.00',
}
},
'browseId': 'UCQxJsAlqmBPAbR_0syDi9mg',
'params': params,
}
data = requests.post(url, headers = headers, cookies = cookies, json = data).json()
print('voteRatioIfSelected' in str(data))
Let us wait an additional 24 hours.
Still working after 24 hours, so I am implementing a PHP equivalent to integrate to the API:
<?php
$myArray = [
'__Secure-3PSID' => '__Secure-3PSID_VALUE',
'__Secure-3PAPISID' => '__Secure-3PAPISID_VALUE',
];
//$result = array_map(function($k, $v) { return "$k=$v"; }, array_keys($myArray), array_values($myArray));
$result = array_map(fn($k, $v) => "$k=$v", array_keys($myArray), array_values($myArray));
print_r($result);
Related to #190.
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once __DIR__ . '/vendor/autoload.php';
include_once 'proto/php/Browse.php';
include_once 'proto/php/GPBMetadata/Browse.php';
include_once 'proto/php/SubBrowse.php';
include_once 'proto/php/GPBMetadata/SubBrowse.php';
$channelId = 'UCQxJsAlqmBPAbR_0syDi9mg';
$postId = 'UgwSoAm2bGLaJM44UTZ4AaABCQ';
$currentTime = 1711824127;//time()
$SAPISID = 'CENSORED';
$__Secure_3PSID = 'CENSORED';
$__Secure_3PAPISID = 'CENSORED';
$ORIGIN = 'https://www.youtube.com';
$SAPISIDHASH = "${currentTime}_" . sha1("$currentTime $SAPISID $ORIGIN");
$url = 'https://www.youtube.com/youtubei/v1/browse';
$subBrowse = new \SubBrowse();
$subBrowse->setPostId($postId);
$browse = new \Browse();
$browse->setEndpoint('community');
$browse->setSubBrowse($subBrowse);
$params = base64_encode($browse->serializeToString());
define('MUSIC_VERSION', '2.9999099');
$rawData = [
'context' => [
'client' => [
'clientName' => 'WEB',
'clientVersion' => MUSIC_VERSION
]
],
'browseId' => $channelId,
'params' => $params,
];
function implodeArray($anArray, $separator)
{
return array_map(fn($k, $v) => "${k}${separator}${v}", array_keys($anArray), array_values($anArray));
}
$opts = [
'http' => [
'method' => 'POST',
'header' => implodeArray([
'Content-Type' => 'application/json',
'Origin' => $ORIGIN,
'Authorization' => "SAPISIDHASH $SAPISIDHASH",
'Cookie' => implode('; ', implodeArray([
'__Secure-3PSID' => $__Secure_3PSID,
'__Secure-3PAPISID' => $__Secure_3PAPISID,
], '=')),
], ': '),
'content' => json_encode($rawData),
]
];
$context = stream_context_create($opts);
$result = json_decode(file_get_contents($url, false, $context), true);
// TODO: pay attention to tab
$result = $result['contents']['twoColumnBrowseResultsRenderer']['tabs'][5]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['backstagePostThreadRenderer']['post'];
//die('isIn: ' . str_contains($result, 'voteRatioIfSelected'));
die(json_encode($result));
There is a design choice here, to propose a version without Protobuf and a version with Protobuf, or one of both. I choose only a version with Protobuf to make clearer what is going on in the code, despite making hosting our own instance more complex.
Also note that not linking a YouTube account version is wanted.
An interesting aspect is that if provide incorrect credentials, for instance the CENSORED
ones, then only voteRatio
is not retrievable but otherwise things work as expected.
However the downside is that have to precise the channel id which is potentially unknown to the end-user.
curl https://www.youtube.com/youtubei/v1/browse -H 'Content-Type: application/json' --data-raw '{"context": {"client": {"clientName": "WEB", "clientVersion": "2.20240327.00.00"}}, "browseId": "UCQxJsAlqmBPAbR_0syDi9mg", "params": "Egljb21tdW5pdHnKAR2yARpVZ3dTb0FtMmJHTGFKTTQ0VVRaNEFhQUJDUeoCBBABGAE%3D"}'
I just realized that SAPISID
is identical to __Secure-3PAPISID
.
Would solve the Stack Overflow question 78221750, also asked on Discord.
Let us first ensure that without being authenticated it is not doable. I confirm that without being authenticated from YouTube UI point of view there does not seem to be any button etc and the retrieve data as JSON do not contain the results. Note that it seems possible to withdraw a vote.
Then can assign an account to the official instance leveraging it to see votes and if necessary vote to see them.
Maybe as a first step can provide the feature only to
community
endpoint and not inchannels?part=community
to ease the implementation while providing the feature. Related to #69.Related to #251.