YouTube-operational-API icon indicating copy to clipboard operation
YouTube-operational-API copied to clipboard

Retrieve votes of a community post poll

Open Benjamin-Loison opened this issue 1 year ago • 15 comments

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 in channels?part=community to ease the implementation while providing the feature. Related to #69.

minimizeCURL curl.sh 'voteRatioIfSelected'
curl https://www.youtube.com/youtubei/v1/browse -H 'Content-Type: application/json' -H 'Origin: https://www.youtube.com' -H 'Authorization: SAPISIDHASH CENSORED' -H 'Cookie: __Secure-1PSIDTS=sidts-CENSORED; __Secure-1PSID=CENSORED; __Secure-1PAPISID=CENSORED' --data-raw '{"context": {"client": {"clientName": "WEB", "clientVersion": "2.20240325.01.00"}}, "browseId": "UCQxJsAlqmBPAbR_0syDi9mg", "params": "CENSORED"}'
Python script:
import requests
import json

SAPISIDHASH = 'CENSORED'
__Secure_1PSIDTS = 'sidts-CENSORED'
__Secure_1PSID = 'CENSORED'
__Secure_1PAPISID = 'CENSORED'

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': 'CENSORED'
}

data = requests.post(url, headers = headers, cookies = cookies, json = data).json()
#print(json.dumps(data, indent = 4))
print('voteRatioIfSelected' in str(data))
Python script:
import requests
import json
import blackboxprotobuf
import base64

SAPISIDHASH = 'CENSORED'
__Secure_1PSIDTS = 'sidts-CENSORED'
__Secure_1PSID = 'CENSORED'
__Secure_1PAPISID = 'CENSORED'

def getBase64Protobuf(message, typedef):
    data = blackboxprotobuf.encode_message(message, typedef)
    return base64.b64encode(data).decode('ascii')

message = {
    '2': 'community',
    '25': {
        '22': 'UgwSoAm2bGLaJM44UTZ4AaABCQ'
    },
    '45': {
        '2': 1,
        '3': 1
    }
}

typedef = {
    '2': {
        'type': 'string'
    },
    '25': {
        'type': 'message',
        'message_typedef': {
            '22': {
                'type': 'string'
            }
        },
        'field_order': [
            '22'
        ]
    },
    '45': {
        'type': 'message',
        'message_typedef': {
            '2': {
                'type': 'int'
            },
            '3': {
                'type': 'int'
            }
        },
        'field_order': [
            '2',
            '3'
        ]
    }
}

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))
Python script:
import requests
import json
import blackboxprotobuf
import base64

SAPISIDHASH = 'CENSORED'
__Secure_1PSIDTS = 'sidts-CENSORED'
__Secure_1PSID = 'CENSORED'
__Secure_1PAPISID = 'CENSORED'

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))

Related to #251.

Benjamin-Loison avatar Mar 26 '24 14:03 Benjamin-Loison

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))

Benjamin-Loison avatar Mar 26 '24 15:03 Benjamin-Loison

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.

Benjamin-Loison avatar Mar 26 '24 15:03 Benjamin-Loison

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.

Benjamin-Loison avatar Mar 28 '24 13:03 Benjamin-Loison

https://www.youtube.com/channel/UCQxJsAlqmBPAbR_0syDi9mg/community?lb=UgwSoAm2bGLaJM44UTZ4AaABCQ

https://www.youtube.com/post/UgwSoAm2bGLaJM44UTZ4AaABCQ

Benjamin-Loison avatar Mar 28 '24 13:03 Benjamin-Loison

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-----

Benjamin-Loison avatar Mar 30 '24 18:03 Benjamin-Loison

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.

Benjamin-Loison avatar Mar 30 '24 18:03 Benjamin-Loison

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);

Benjamin-Loison avatar Mar 31 '24 18:03 Benjamin-Loison

Related to #190.

Benjamin-Loison avatar Mar 31 '24 19:03 Benjamin-Loison

Screenshot from 2024-03-31 21-34-54

Benjamin-Loison avatar Mar 31 '24 19:03 Benjamin-Loison

<?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.

Benjamin-Loison avatar Mar 31 '24 19:03 Benjamin-Loison

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"}'

Benjamin-Loison avatar Mar 31 '24 19:03 Benjamin-Loison

I just realized that SAPISID is identical to __Secure-3PAPISID.

Benjamin-Loison avatar Mar 31 '24 20:03 Benjamin-Loison