Benjamin-Loison / YouTube-operational-API

YouTube operational API works when YouTube Data API v3 fails.
397 stars 50 forks source link

Retrieve votes of a community post poll #258

Open Benjamin-Loison opened 7 months ago

Benjamin-Loison commented 7 months ago

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"}'
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))
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))
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 commented 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))
Benjamin-Loison commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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

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

Benjamin-Loison commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

Related to #190.

Benjamin-Loison commented 7 months ago

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

Benjamin-Loison commented 7 months ago
<?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 commented 7 months ago
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 commented 7 months ago

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