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

6

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

Что-то перестал g.php работать, я так думаю, что поменять надо

if (!preg_match('/player.config\s*?=\s*?({.*?});/s', $page_html, $matches)) {

/player.config больше нет в коде. Требуется помощь клуба :)

7

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

smsbox3 пишет:

Что-то перестал g.php работать, я так думаю, что поменять надо

if (!preg_match('/player.config\s*?=\s*?({.*?});/s', $page_html, $matches)) {

/player.config больше нет в коде. Требуется помощь клуба :)

Ссылку можна в студию, где нет player.config

Отладка кода — это как охота. Только охота, на баги.

8 (2020.12.05 09:50:15 отредактировано smsbox3)

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

Spell пишет:

Ссылку можна в студию, где нет player.config

У меня таких ссылок много - вот например https://www.youtube.com/watch?v=Y5Fvsp02w0c
или https://www.youtube.com/watch?v=oxAaNzO3Q5g
или я ошибаюсь?

Вот здесь

https://www.youtube.com/watch?v=qm2FRrUkLnE

точно нет.

9

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

smsbox3 пишет:

или я ошибаюсь?

Если по-сути , то да.  :)

Отладка кода — это как охота. Только охота, на баги.

10 (2020.12.09 23:10:48 отредактировано smsbox3)

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

Побочный вопрос, какие itag у youtube видео со звуком, что на ТВ работает? Я нашел пока два 18 это mp4,audio/video разрешение 360p
и 22    это mp4,audio/video разрешение 720p.  Но разрешение может быть 480p или 240p - какие там коды?
Видел код 132-    hls-audio/video-    240p, но не знаю встречается реально или нет?

Вот здесь нашел описание кодовhttps://gist.github.com/sidneys/7095afe … b1034e01e2 133-138 без звука идет (дорожка отдельно). Форматы 3D сейчас не актуальны. Будет ли ТВ проигрывать напрямую webm? Впрочем я в тех видео, что хотел посмотреть эти кодеки не встречал.

11

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

Просмотрев исходник,я не наткнулся на player config возможно потому,что его там нет.А ссылки на видео он берет напрямую из http://www.youtube.com/watch?v=.Тому подтверждение,я выкладываю сам скрипт.Не буду спорить может я в чем то ошибся.Или вообще не по теме пищу...

Прикреплённые файлы сообщения

Скрипт на Yutube(без выбора качества).txt 6.91 kb, скачивалось 288 раз, начиная с 2020.12.16

Скрипт на Yutube.txt 6.92 kb, скачивалось 294 раза, начиная с 2020.12.15

"Хорошо написанная программа — это программа, написанная 2 раза" :-X
Спасибо сказали: smsbox3, pukhf2

12

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

михаил пишет:

Просмотрев исходник,я не наткнулся на player config возможно потому,что его там нет.А ссылки на видео он берет напрямую из http://www.youtube.com/watch?v=.Тому подтверждение,я выкладываю сам скрипт.Не буду спорить может я в чем то ошибся.Или вообще не по теме пищу...

Так и есть  player config  больше нет.  Скрипт хороший,его еще на php перевести :) На странице, что отдает youtube есть json, в котором содержатся нужная информация.  player config раньше указывал на начало этого  json, теперь расположен в другом месте и другое начал. Возможно можно написать универсальную функцию, что находит json и выделяет его на странице, тогда, как бы youtube не менял свой код, куда бы не перемещал json, мы будет выделять json и дальше уже получим рабочие ссылки.

13

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

Можно сделать привязку с

+ открыть спойлер

гитхабом и там все корректировать как у WendyH

при обновлении заменять скрипт рабочим....Если вы об этом.

"Хорошо написанная программа — это программа, написанная 2 раза" :-X

14 (2020.12.16 10:56:00 отредактировано pukhf)

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

михаил пишет:

Просмотрев исходник,я не наткнулся на player config возможно потому,что его там нет.А ссылки на видео он берет напрямую из http://www.youtube.com/watch?v=.Тому подтверждение,я выкладываю сам скрипт.Не буду спорить может я в чем то ошибся.Или вообще не по теме пищу...


Спасибо, Михаил, скрипт рабочий! НО:

1. Видео только 640 х 360 (adaptive, maxheight не помогают)

2. Не воспроизводит видео, закодированное WEBP или VP9 (Неопознанный кодек: VLC не может опознать видео- или аудиокодек)

P.S. Перемотка и длительность видео работает

15

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

pukhf пишет:

1. Видео только 640 х 360 (adaptive, maxheight не помогают)

это по тому, что по коду 18 (mp4,audio/video разрешение) 360p выводит, если оставить 22 ( mp4,audio/video разрешение 720p.) - то будет лучше.
Выбор вот здесь, можно просто удалить неработающие

if      (itag in ([13,17,160                  ])) height = 144;
        else if (itag in ([5,36,92,132,133,242        ])) height = 240;
        else if (itag in ([6                          ])) height = 270;
        else if (itag in ([18,34,43,82,100,93,134,243 ])) height = 360;
        else if (itag in ([35,44,83,101,94,135,244,43 ])) height = 480;
        else if (itag in ([22,45,84,102,95,136,298,247])) height = 720;
        else if (itag in ([37,46,85,96,137,248,299    ])) height = 1080;
        else if (itag in ([264,271                    ])) height = 1440;
        else if (itag in ([266,138                    ])) height = 2160;
        else if (itag in ([272                        ])) height = 2304;
        else if (itag in ([38                         ])) height = 3072;
        else continue;

Разрешение 1080 и выше, как я понимаю без звука?

Спасибо сказали: михаил1

16

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

Скрипт изменен.Выбор качества без ключа,прописано максимальное.Нужен тест,могу и ошибаться.

"Хорошо написанная программа — это программа, написанная 2 раза" :-X
Спасибо сказали: pukhf1

17

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

михаил пишет:

Скрипт изменен.Выбор качества без ключа,прописано максимальное.Нужен тест,могу и ошибаться.

Спасибо, Михаил, работает! Единственное, что я изменил, это поменял 248 на 22, т.к. не было звука, но в HD тоже смотреть можно.

Спасибо сказали: михаил1

18

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

Единственный недостаток,кто догадался,вот в этом месте(в самом скрипте GetLink_Youtube31)

 } else {
    sData = HmsJsonDecode(sData);
    HmsRegExMatch('"itag":248,"url"\\s*:\\s*"(.*?)"', sData, MediaResourceLink);
    return true;
  }

надо изменить itag на то значение,с каким качеством вы предпочитаете смотреть и которое есть на сайте.Выбрать можно из 15 поста,спасибо smsbox3.И спасибо за тест скрипта pukhf

"Хорошо написанная программа — это программа, написанная 2 раза" :-X

19

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

михаил пишет:

Единственный недостаток,кто догадался,вот в этом месте(в самом скрипте GetLink_Youtube31)

Есть еще один недостаток, те видео которые имеют подпись с параметром sig не будут воспроизводится.
для подкаста youtube 4.4 скрипт

+ GetLink_Youtube33
///////////////////////////////////////////////////////////////////////////////
// Получение ссылки на Youtube
bool GetLink_Youtube33(string sLink) {
  string sData, sVideoID='', sAudio='', sSubtitlesLanguage='ru',
  sSubtitlesUrl, sFile, sVal, sMsg, sConfig, sHeaders, hlsUrl, subsUrl, jsUrl, 
  streamMap, algorithm, sType, itag, sig, alg, s, sp; TStringList QLIST;
  int i, n, w, num, height, priority, minPriority = 90, selHeight, maxHeight = 1080, audioQual;
  TJsonObject JSON, PLAYER_RESPONSE, VIDEO; TJsonArray FORMATS, SUBS; TRegExpr RegEx;
  
  sHeaders = 'Referer: '+sLink+'\r\n'+
       'User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36\r\n'+
       'Origin: https://www.youtube.com\r\n';
  
  if (HmsRegExMatch('--maxheight=(\\d+)', mpPodcastParameters, sVal)) maxHeight = StrToInt(sVal);
  HmsRegExMatch('--sublanguage=(\\w{2})', mpPodcastParameters, sSubtitlesLanguage);
  bool bSubtitles = Pos('--subtitles' , mpPodcastParameters)>0;  
  bool bAdaptive  = Pos('--adaptive'  , mpPodcastParameters)>0;  
  bool bQualLog   = Pos('--qualitylog', mpPodcastParameters)>0;
  
  if (!HmsRegExMatch('[\\?&]v=([^&]+)'       , sLink, sVideoID))
    if (!HmsRegExMatch('youtu.be/([^&]+)'      , sLink, sVideoID))
    HmsRegExMatch('/(?:embed|v)/([^\\?]+)', sLink, sVideoID);
  
  if (sVideoID=='') return;
  
  sLink = gsUrlBase+'/watch?v='+sVideoID+'&hl=ru';
  sData = HmsDownloadURL(sLink, sHeaders, true);
  sData = HmsRemoveLineBreaks(sData);
  HmsRegExMatch('"jsUrl":"([^"]+)",', sData, jsUrl);
  
  if (!HmsRegExMatch('ytInitialPlayerResponse\\s*=\\s*({.*});', sData, sConfig, 1, PCRE_SINGLELINE)) {
    HmsLogMessage(2 , mpTitle+': No ytInitialPlayerResponse data in loaded page.'); 
    return; 
  }
  
  JSON = TJsonObject.Create();
  PLAYER_RESPONSE = TJsonObject.Create();
  QLIST = TStringList.Create();
  try {
    JSON.LoadFromString(sConfig);
    PLAYER_RESPONSE.LoadFromString(JSON.S['streamingData']);
    
    // Если есть субтитры и в дополнительных параметрах указано их показывать - загружаем 
    if (bSubtitles && JSON.B['captions\\playerCaptionsTracklistRenderer\\captionTracks']) {
      string sTime1, sTime2, engSubs; float nStart, nDur;
      SUBS = JSON.O['captions\\playerCaptionsTracklistRenderer\\captionTracks'].AsArray;
      for (i=0; i < SUBS.Length; i++) {
        if (SUBS[i].S['languageCode']==sSubtitlesLanguage) subsUrl = SUBS[i].S['baseUrl'];
        if (SUBS[i].S['languageCode']=='en'              ) engSubs = SUBS[i].S['baseUrl'];
      }
      if ((subsUrl=='') && (engSubs!='')) subsUrl = engSubs+'&tlang='+sSubtitlesLanguage;
      if (subsUrl!='') {
        sData = HmsDownloadURL(subsUrl+'&fmt=srv3', sHeaders, true);
        sMsg  = ''; i = 0;
        RegEx = TRegExpr.Create('(<(text|p).*?</(text|p)>)', PCRE_SINGLELINE); // Convert to srt format
        try {
          if (RegEx.Search(sData)) do {
            if      (HmsRegExMatch('start="([\\d\\.]+)', RegEx.Match, sVal)) nStart = StrToFloat(ReplaceStr(sVal, '.', ','))*1000;
            else if (HmsRegExMatch('t="(\\d+)'         , RegEx.Match, sVal)) nStart = StrToFloat(sVal);
            if      (HmsRegExMatch('dur="([\\d\\.]+)'  , RegEx.Match, sVal)) nDur   = StrToFloat(ReplaceStr(sVal, '.', ','))*1000;
            else if (HmsRegExMatch('d="(\\d+)'         , RegEx.Match, sVal)) nDur   = StrToFloat(sVal);
            sTime1 = HmsTimeFormat(Int(nStart/1000))+','+RightCopy(Str(nStart), 3);
            sTime2 = HmsTimeFormat(Int((nStart+nDur)/1000))+','+RightCopy(Str(nStart+nDur), 3);
            sMsg += Format("%d\n%s --> %s\n%s\n\n", [i, sTime1, sTime2, HmsHtmlToText(HmsHtmlToText(RegEx.Match(0), 65001))]);
            i++;
          } while (RegEx.SearchAgain());
        } finally { RegEx.Free(); }
        sFile = HmsSubtitlesDirectory+'\\Youtube\\'+PodcastItem.ItemID+'.'+sSubtitlesLanguage+'.srt';
        HmsStringToFile(sMsg, sFile);
        if (Trim(PodcastItem[mpiSubtitleLanguage])!='') bAdaptive = false;
        PodcastItem[mpiSubtitleLanguage] = sFile;
      }
    }
    
    hlsUrl = PLAYER_RESPONSE.S['hlsManifestUrl'];
    //jsUrl  = JSON.S['assets\\js'];
    
    if (hlsUrl!='') {
      MediaResourceLink = ' '+hlsUrl;
      bAdaptive = false;
      sData = HmsDownloadUrl(hlsUrl, sHeaders, true);
      RegEx = TRegExpr.Create('BANDWIDTH=(\\d+).*?RESOLUTION=(\\d+)x(\\d+).*?(http[^#]*)', PCRE_SINGLELINE);
      try {
        if (RegEx.Search(sData)) do {
          sLink = '' + RegEx.Match(4);
          height = StrToIntDef(RegEx.Match(3), 0);
          if (mpPodcastMediaFormats!='') {
            priority = HmsMediaFormatPriority(height, mpPodcastMediaFormats);
            if ((priority>=0) && (priority>minPriority)) {
              MediaResourceLink = sLink; minPriority = priority;
            }
          } else if ((height > selHeight) && (height <= maxHeight)) {
            MediaResourceLink = sLink; selHeight = height;
          }
          QLIST.Values[Format('%.4d', [height])] = sLink;
        } while (RegEx.SearchAgain());
      } finally { RegEx.Free(); }
      
    } else if (PLAYER_RESPONSE.B['formats']) {
      FORMATS = PLAYER_RESPONSE.O['formats'].AsArray;
      if (FORMATS[0].B['signatureCipher'])
      algorithm = HmsDownloadURL('https://hms.lostcut.net/yt/getalgo.php?jsurl='+HmsHttpEncode(jsUrl));
      for(i=0; i<FORMATS.Length; i++) {
        VIDEO = FORMATS[i];
        if (VIDEO.B['signatureCipher']) {
          sLink = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 'url', '', '&'));
          sig   = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 's'  , '', '&'));
          sp    = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 'sp' , '', '&'));
          if (sig!='') {
            for (w=1; w<=WordCount(algorithm, ' '); w++) {
              alg = ExtractWord(w, algorithm, ' ');
              if (Length(alg)<1) continue;
              if (Length(alg)>1) TryStrToInt(Copy(alg, 2, 4), num);
              if (alg[1]=='r') {s=''; for(n=Length(sig); n>0; n--) s+=sig[n]; sig = s;   } // Reverse
              if (alg[1]=='s') {sig = Copy(sig, num+1, Length(sig));                     } // Clone
              if (alg[1]=='w') {n = (num-Trunc(num/Length(sig)))+1; Swap(sig[1], sig[n]);} // Swap
            }
          }
          sLink += '&'+sp+'='+sig;
        } else {
          sLink = VIDEO.S['url'];
        }
        height = VIDEO.I['height'];
        if (height>0) QLIST.Values[Format('%.4d', [height])] = sLink;
        if (mpPodcastMediaFormats!='') {
          priority = HmsMediaFormatPriority(height, mpPodcastMediaFormats);
          if ((priority>=0) || (priority<minPriority)) {
            MediaResourceLink = sLink; minPriority = priority; selHeight = height;
          }
        } else if ((height>selHeight) && (height<= maxHeight)) {
          MediaResourceLink = sLink; selHeight = height;
          
        } else if ((height>=selHeight) && (height<= maxHeight) && (VIDEO.I['itag'] in ([18,22,37,38,82,83,84,85]))) {
          // Если высота такая же, но формат MP4 - то выбираем именно его (делаем приоритет MP4)
          MediaResourceLink = sLink; selHeight = height;
        }
      }
    } 
    if (bAdaptive || (MediaResourceLink=='') && PLAYER_RESPONSE.B['adaptiveFormats']) {
      FORMATS = PLAYER_RESPONSE.O['adaptiveFormats'].AsArray;
      if (FORMATS[0].B['signatureCipher'] && (algorithm==''))
       algorithm = HmsDownloadURL('https://hms.lostcut.net/yt/getalgo.php?jsurl='+HmsHttpEncode(jsUrl));
      for(i=0; i<FORMATS.Length; i++) {
        VIDEO = FORMATS[i];
        if (VIDEO.B['signatureCipher']) {
          sLink = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 'url', '', '&'));
          sig   = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 's'  , '', '&'));
          sp    = HmsHttpDecode(ExtractParam(VIDEO.S['signatureCipher'], 'sp' , '', '&'));
          if (sig!='') {
            for (w=1; w<=WordCount(algorithm, ' '); w++) {
              alg = ExtractWord(w, algorithm, ' ');
              if (Length(alg)<1) continue;
              if (Length(alg)>1) TryStrToInt(Copy(alg, 2, 4), num);
              if (alg[1]=='r') {s=''; for(n=Length(sig); n>0; n--) s+=sig[n]; sig = s;   } // Reverse
              if (alg[1]=='s') {sig = Copy(sig, num+1, Length(sig));                     } // Clone
              if (alg[1]=='w') {n = (num-Trunc(num/Length(sig)))+1; Swap(sig[1], sig[n]);} // Swap
            }
          }
          sLink += '&'+sp+'='+sig;
        } else {
          sLink = VIDEO.S['url'];
        }
        if (VIDEO.B['audioSampleRate'] && (audioQual < VIDEO.I['bitrate'])) {
          sAudio = sLink; audioQual = VIDEO.I['bitrate']; continue; 
        }
        height = VIDEO.I['height'];
        if (height>0) QLIST.Values[Format('%.4d', [height])] = sLink;
        if (mpPodcastMediaFormats!='') {
          priority = HmsMediaFormatPriority(height, mpPodcastMediaFormats);
          if ((priority>=0) || (priority<minPriority)) {
            MediaResourceLink = sLink; minPriority = priority; selHeight = height;
          }
        } else if ((height>selHeight) && (height<= maxHeight)) {
          MediaResourceLink = sLink; selHeight = height;
        }
      }
      sVal = ""; if (Trim(mpTimeStart)!="") sVal = " -ss "+mpTimeStart;
      if (bAdaptive && (sAudio!='')) {
        if (Pos('--downloadaudio', mpPodcastParameters)>0) {
          sFile = HmsTempDirectory+'\\'+PodcastItem.ItemID+'.webm';
          if (!FileExists(sFile))
            HmsDownloadURLToFile(sAudio, sFile, sHeaders, true);
        } else sFile = sAudio;
        MediaResourceLink = '-hide_banner -reconnect 1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2 -fflags +genpts -i "'+Trim(MediaResourceLink)+'"'+sVal+' -i "'+Trim(sFile)+'"';
      }
    }
    // Если еще не установлена реальная длительность видео - устанавливаем
    if ((Trim(mpTimeLength)=='') || (RightCopy(mpTimeLength, 6)=='00.000') && (hlsUrl=='')) {
      PodcastItem[mpiTimeLength] = HmsTimeFormat(JSON.I['videoDetails\\lengthSeconds']);
    }
    if (bQualLog) {
      QLIST.Sort();
      sMsg = 'Доступное качество: ';
      for (i = 0; i < QLIST.Count; i++) {
        if (i>0) sMsg += ', ';
        sMsg += IntToStr(StrToInt(QLIST.Names[i])); // Обрезаем лидирующие нули
      }
      sMsg += '. Выбрано: ' + Str(selHeight);
      HmsLogMessage(1, mpTitle+'. '+sMsg);
    }
  } finally { JSON.Free; PLAYER_RESPONSE.Free; QLIST.Free; }
}

///////
Отладка кода — это как охота. Только охота, на баги.
Спасибо сказали: pukhf, abvis2

20

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

У меня youtube стал капчу временами отваливать :( Вместо кода, с сервера получаю "Из вашей сети отправлено очень много запросов.
Чтобы вернуться на YouTube, введите указанный ниже код подтверждени"

21

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

Что-то Youtube опять глючит... Какие-то видео показывает, какие-то нет...

+ Ну, например:

Это видео показывает:)

Monaldin & Emma Peters - Femme Like U (Teenex Remix)

-----------------------------------------------------------------------------------------------------------------------------------

А это уже нет:(

Minelli - Rampampam (Robert Cristian Remix)

Обе ссылки из одного и того же плейлиста.

Ипользую подкаст с исправлениями Spell'a.

Может кто-нибудь подскажет: что исправить?

22

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

pukhf пишет:

Может кто-нибудь подскажет: что исправить?

Там сам youtube  изменил название функции дешифровки подписи, и скрипт не находит эти ключи и дешифровка подписи не проходит, ссылка получается не действительной для просмотра. В подкасте используется скрипт на серверe WendyH, и как то поправить нет возможности.

Отладка кода — это как охота. Только охота, на баги.
Спасибо сказали: smsbox3, pukhf2

23

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

Spell пишет:

  изменил название функции дешифровки подписи,

Как она теперь называется?

24

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

smsbox3 пишет:

Как она теперь называется?

3 символа,  vja

Отладка кода — это как охота. Только охота, на баги.
Спасибо сказали: smsbox31