1

Тема: Алгоритм подписи youtube

Спрашивали - отвечаю.

При загрузке страницы с youtube, если видео доступно для воспроизведения, в коде html содержится json данные, под названием ytplayer.config. Там содержится вся информация о видео, его параметры и, самое главное, ссылки на видео и аудио потоки (файлы).

Форматы этих файлов разнообразны, отличаются кодеком, качеством и проч.

Ссылки на эти файлы содержат миллиарды кучу параметров. В том числе такой параметр как sig (от слова signature - подпись).
В тех видео, в которых есть какие-либо материалы так называемых партнёров (стоит какой-нибудь копирайт, например на музыку), там этот параметр sig отсутствует. Точнее даже отсутствует прямо указанный url. Но вместо него в json данных о формате медиа присутствует поле "cipher", в котором в формате urlEncode (через &) перечислены три параметра:

  • url - ссылка на медиа-поток, но без параметра sig.
  • s - заготовка для подписи (какбы значение подписи, но не валидная). Вот её то и нужно дешифровать.
  • sp - имя параметра подписи. Обычно всегда "sig".

Т.е. ссылка на поток будет равна полученной url плюс разделитель параметров "&", плюс имя параметра указанного в "sp", плюс знак равно, плюс дешифрованное значение "s".

Осталось дешифровать этот s.

Сами функции дешифровки находятся в js-скрипте, путь до которого можно взять из html кода или из ytplayer.config.assets.js. Выглядит обычно как /yts/jsbin/player_ias-vfl2ChXMk/ru_RU/base.js, где vfl2ChXMk - некий идентификатор плеера, который постоянно меняется со временем и может зависеть от региона и проч. Вместе с ним меняется и алгоритм дешифровки подписи.
Скриптик такой не хилый, на 1,2 метра.
Обычно, на сегодняшний день, функция дешифровки (её имя меняется от версии к версии) всё равно имеет примерно один и тот же вид. Но меняются местами вызовы функций и их параметры.

В js-скрипте плеера я ищу эту функцию по ключевым словам: a=a.split(""), она уже много лет так начинается. Например, в вышеприведённом js-скрипте эта функция такая:

Os=function(a){a=a.split("");Ns.yf(a,1);Ns.Wb(a,36);Ns.hi(a,64);return a.join("")};

Ищут её по регуляркам. Например, в скрипте на php она сейчас такая:

$fns  = preg_match('/\b\w{2}=function\(a\)\{a=a\.split\(""\);(.*?)return/s', $data, $m) ? $m[1] : ''; // Шаблон поиска функции дешифровки

Можно заметить, что в самой функции вызываются по очереди ещё функции, с переданными параметрами: Ns.yf(a,1);, Ns.Wb(a,36), Ns.hi(a,64);. Параметр a - это и есть значение подписи, которое дешифруется. Эти функции также нужно найти в этом же скрипте и посмотреть, что они из себя представляют.
Их также можно найти по регуляркам. Т.е. нужно найти, где они в js-скрипте объявляются. Например, объявление объекта Ns выглядит следующим образом:

var Ns={Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},hi:function(a){a.reverse()},yf:function(a,b){a.splice(0,b)}};

Немножко отформатируем:

var Ns={
    Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},
    hi:function(a){a.reverse()},
    yf:function(a,b){a.splice(0,b)}
};

У объекта Ns первая функция Wb:

Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}

Если приглядеться, смысл этой функции сводится к тому, что меняются местами два символа, первый и по указанному индексу в параметрах.
Функция Ns.hi - переворачивает строку.
Функция Ns.yf - разбивает (по сути обрезает) строку с указанного символа.
Народ заметил, что названия этих функций и объекта их содержащий постоянно меняются, также меняются числовые значения параметров от версии к версии js-скрипта. Но суть всегда остаётся (уже много лет): в разной последовательности, в разном количестве и с разными параметрами, но функции преобразования всегда три типа. Уж не знаю кто первый додумался, но остальные подсмотрели и подхватили - все три функции можно описать буквами, а значения параметров можно указать рядом с буквой:
r - revers, s - slice, w - swap
И функцию дешифровки Os=function(a){a=a.split("");Ns.yf(a,1);Ns.Wb(a,36);Ns.hi(a,64);return a.join("")}; можно записать как s1 w36 r и по этому алгоритму расшифровать. А ещё лучше, именно для такого идентификатора js-скрипта кэшировать эти значения, дабы не качать постоянно этот тяжелый js.
Т.е. для дешифровщика эта строка s1 w36 r будет означать, что нужно со строкой s (заготовкой из поля "cipher", про которую говорилось в начале) сделать следующее:

  1. s1 - взять строку с символа 1 (индекс начинается с 0)
  2. w36 - поменять местами символ 0 и 36
  3. r - перевернуть строку задом наперёд

Самое сложное и в то же время ненадёжное во всём этом - это поиск по регуляркам. Потому как гугл, нет нет, да что-то меняет. Придумывает как сменить вид этих функций. Поэтому приходится иногда подправлять эти регулярки.

Именно этим - получением алгоритма - занимается функция GetAlgorithm на php в файле getalgo.php:

///////////////////////////////////////////////////////////////////////////////
// Функция поиска алгоритма дешифровки подписи по ссылке на js-скрипт
function GetAlgorithm($jsUrl) {
	if      (substr($jsUrl, 0, 2)=="//") $jsUrl = "https:".$jsUrl;
	else if (substr($jsUrl, 0, 1)=="/" ) $jsUrl = "https://www.youtube.com".$jsUrl;
	$algo = "";
	$data = file_get_contents($jsUrl);
	$fns  = preg_match('/\b\w{2}=function\(a\)\{a=a\.split\(""\);(.*?)return/s', $data, $m) ? $m[1] : ''; // Шаблон поиска функции дешифровки
	$arr  = explode(';', $fns); // Получаем массив вызываемых команд в полученной функции дешифровки 
	// Перебираем все вызовы в полученной функции дешифровки из js-скрипта
	foreach ($arr as $func) {
		$textFunc = $func; // Текст вызываемой команды или функции
		// Если вызывается конкретная функция объекта - ищем объявление этого объекта и его функции в js-скрипте
		if (preg_match('/([\$\w]+)\.(\w+)\(/s', $textFunc, $m)) {
			$obj = $m[1]; // Имя объекта
			$fun = $m[2]; // Имя его вызываемой функции
			// Попытка найти объявление объекта и его функции по шаблону
			if (($obj!='a') && preg_match('/var '.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\()(.*?})/s', $data, $m))
				$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
			else if (($obj!='a') && preg_match('/var \\'.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\()(.*?})/s', $data, $m))
				$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
		}
		// Если вызывается именованная функция - поиск текста этой функции в js-скрипте
		if (preg_match('/a=(\w+)\(/s', $textFunc, $m)) {
			$fun = $m[1]; // Имя вызываемой функции
			// Попытки найти объявление этой функции по полученному имени в тексте js-скрипта
			if (preg_match('/var '.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\())(.*?})/s', $data, $m))
				$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
			else if (preg_match('/var \\'.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\())(.*?})/s', $data, $m))
				$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
		}
		// Получаем значение параметра в вызываемой команде или функции
		$numb = preg_match('/\(.*?(\d+)/s', $func, $m) ? $m[1] : '';
		// Определяем тип вызываемой функции в данной итерации
		$type = 'w'; // По-умолчанию w = Swap - поменять местами первый символ с символом по указанному индексу
		if     (preg_match('/revers/'        , $textFunc, $m)) $type = 'r'; // 'r' = Revers - перевернуть строку задом наперёд
		elseif (preg_match('/(splice|slice)/', $textFunc, $m)) $type = 's'; // 's' = Slice - обрезать строку на указанную длину
		if (($type!='r') && ($numb==='')) continue; // Если нет параметра у функции и это не Revers, то пропускаем, это не команда дешифровки
		$algo .= ($type=='r') ? $type.' ' : $type.$numb.' '; // Формируем алгоритм, указывая тип и значение параметра в виде "w4 r s29" 
	}
	return trim($algo);
}

Эта функция возвращает строку типа w23 s2 r w8. А вот такая функция дешифрует подпись указанным алгоритмом (из файла g.php):

///////////////////////////////////////////////////////////////////////////////
// Функция дешифровки заготовки подписи по указанному алгоритму в виде строки "w12 s34 r w9"
function YoutubeDecrypt($sig, $algorithm) {
	$method = explode(" ", $algorithm); // Получаем массив команд дешифровки
	if (!$sig) return "";               // Если нет заготовки подписи, то и дешифровать нечего
	foreach($method as $m)
	{	// Первая буква команды - тип: r - revers,  s - slice,  w - swap
		// Вторая буква команды - значение параметра вызываемой команды
		if           ($m     =='r') $sig = strrev($sig);
		elseif(substr($m,0,1)=='s') $sig = substr($sig, (int)substr($m, 1));
		elseif(substr($m,0,1)=='w') $sig =   swap($sig, (int)substr($m, 1));
	}
	return $sig;
}

///////////////////////////////////////////////////////////////////////////////
// Поменять местами первый символ в строке с символом по указанному индексу
function swap($str, $b) {
	$c = $str[0]; $str[0] = $str[$b]; $str[$b] = $c;
	return $str;
}

Вот такой вот принцип подписи видео на youtube. Надеюсь был полезен и теперь эти скрипты можно самим дорабатывать, если они перестают в какой-то момент работать.

Я обновил скрипт g.php на гитхабе и некоторые могли заметить, что я с каких то времён убрал со своего сервера этот опубликованный скрипт. Это сделал потому, что лично мне этот скрипт не нужен, а запросов к нему было столько, что youtube заблокировал получение данных с IP моего сервера и отвечал тем, что уж слишком много от меня идёт к нему запросов. В общем, блочит.
Поэтому, выкладывайте этот скрипт каждый у себя, а публиковать для всеобщего пользования на своём серваке я пока не буду.

Хотя, для проверки работоспособности и отладки, я всё-таки написал страничку с js-кодом, который этот скрипт проверяет и выводит информацию о доступных видео потоках. Что-то вроде savefrom.net только для одного youtub-а.
Youtube get links

Кстати да, с прошлого раза g.php теперь поменял формат отдачи, точнее структуру json ответа и ограничил количество параметров. Теперь он просто всегда отдаёт объект, содержащий о всех доступных ссылках видео. В массиве formats - ссылки на видео со звуком (их обычно одна или две), в массиве adaptiveFormats - ссылки на видео без звука и аудио, всё как отдаёт сам youtube. Единственное, скрипт на лету их, если нужно, декодирует.

Sony Bravia KDL-32CX523
Спасибо сказали: Spell, lidars, smsbox33

2 (2020.02.07 14:01:50 отредактировано smsbox3)

Re: Алгоритм подписи youtube

Ключик &link_only=1 не планируется добавить?

3

Re: Алгоритм подписи youtube

smsbox3 пишет:

Ключик &link_only=1 не планируется добавить?

Нет. Незачем. Этот скрипт больше для проверки работоспособности, показательный.
Но его мы можете легко доработать до любого состояния, в том числе и добавить параметр link_only. Там сделать, я думаю, это не очень трудно.По крайней мере для видео с не adaptive ссылками.

Sony Bravia KDL-32CX523

4

Re: Алгоритм подписи youtube

WendyH пишет:

скрипт больше для проверки работоспособности, показательный.

Спасибо, все верно,разобрать json нет проблем.Пробовал на разных серверах - кое-где (на зарубежных серверах) youtube не отдает запрос (по стране ограничения), а так работает превосходно.
Интересно на какой срок блокирует youtube за большое количество запросов? Неужели на всегда?

5

Re: Алгоритм подписи youtube

smsbox3 пишет:

Интересно на какой срок блокирует youtube за большое количество запросов? Неужели на всегда?

Не навсегда, а пока не введёшь капчу. Но отобразиться она должна именно в браузере с того сервера. Прокси не канает. Что-то нужно придумывать, но я забил.

Sony Bravia KDL-32CX523