"Реклама — двигатель прогресса" — эта легкая фраза, сказанная невзначай моей сестрой, описывает практически весь путь разработки простенького скрипта, который со временем вырос в небольшое клиент-серверное приложение. Итак, в данной статье я расскажу про: авторизацию на youtube с помощью perl, сложные приёмчики с ffmpeg, мимоходом пройдусь по json и sqlite, и покажу, чего стоят подборки видео на youtube.
С чего всё началось
Идея родилась достаточно просто. Просматривая как-то вечером на youtube очередную подборку прикольных видео, я поймал себя на раздражающей мысли, что не хочу смотреть рекламу, а еще — не хочу видеть одно и то же видео дважды. Эта мысль развилась в идею о том, что, вероятно, множество процессов создания подобных видео можно автоматизировать. Прикинув свои возможности, я понял, что мне вполне по силам накидать небольшой скрипт, который меня освободит от рекламы и баянов.
Disclaimer: я не программист, а инженер-микроэлектронщик, так что при оценке кода делайте скидку на этот момент.
Получение данных
У меня было на выбор два источника видео: coub.com и vine.co. Просмотрев контент с обоих сайтов, был сделан выбор в пользу coub.com, что было активно поддержано моей девушкой.
У coub.com относительно недавно появился API, который позволяет тягать с него много всяких данных. Надо сказать, что я не сразу подумал о возможности авторизации на этом сайте, ведь доступ к ендпойнтам открыт для всех желающих. А вот когда авторизовался, то понял, что делать этого не надо было: для авторизованных пользователей открывается куча контента NSFW(not safe for work, 18+), который, вообще говоря, не понятно что делает на этом сайте. Итак, работаем без авторизации.
Пример эндпойнта:
http://coub.com/api/v2/timeline/hot?page=${page_number}&per_page=${per_page}&order_by=newest_popular
Не буду приводить тут код функции, которая тягает с означенного эндпойнта JSON, так как они тривиальна и не интересна.
Работа с данными
Сначала я просто банально смотрел лидеров по количеству просмотров, потом пытался написать хитрые метрики для определения популярности видео, но все это не работало, как хотелось. Попробовал поиграть с кластеризацией, но тоже требуемого эффекта не получил.
В итоге я решил, что надо отслеживать динамику процесса, а для этого написал маленькую базу sqlite на две таблички, которая позволяет мне отслеживать просмотры по различным видео. Все манипуляции с базой лежат на плечах скрипта, который тягает JSON’ы с эндпойнтов, занимается разбором полученных данных и прочее. Также этот скрипт генерирует красивые картинки для понимания динамики процесса и создает JSON с конечными данными для последующего использования. Запускается скрипт раз в полчаса по cron’у.
На картинке хорошо виден набор просмотров днем и ночью, а также моменты публикации ссылок на видео на популярных порталах (ну или включения ботов накрутки просмотров, хе-хе). Время на графике — UTC, картинка кликабельна.
Для работы в perl со всем этим хозяйством мне потребовались следующие модули:
use LWP::Simple; use JSON::XS qw( decode_json ); use Time::Local; use DBI; use Chart::Gnuplot;
Надо отметить, что для работы с sqlite в дистрибутиве должен быть установлен DBD::Sqlite.
Формирование видео
Для формирования красивого видеоряда требуется некоторое время освоиться с одной замечательной утилитой — ffmpeg. Но когда вы научитесь ей пользоваться, возвращаться ко всяким avidemux’ам не захочется. Итак, какие полезные приемы я выучил за время написания скрипта? Начнем с простого.
Подготовка музыки
$local_batch = "$converter -t $audio_dur -i $music_source -af \"afade=t=out:st=$start_t_plus:d=$diff,afade=t=in:ss=0:d=$diff,volume=$volume_scale\" $res_dir/starter.mp3 -y"; system( $local_batch );
Данная команда отрезает от $music_source кусочек длиной $audio_dur с применением фильтров afade и volume, и сохраняет это в starter.mp3. Фильтр afade позволяет получить эффект повышения(fade-in) и понижения(fade-out) громкости, а volume изменяет громкость всей дорожки целиком.
Превращаем картинку в видео со звуком
$local_batch = "$converter -loop 1 -i $picture_source -i $res_dir/end.mp3 -c:v libx264 -t $end_t $res_dir/ending.mkv -y"; system( $local_batch );
Решаем проблему кривого разрешения
$local_batch = "$converter -i ./video_source/source-video-$i.mp4 "; $local_batch .= "-filter_complex \""; $local_batch .= "[0]scale=iw*$scale:ih*$scale [sharp]; "; $local_batch .= "[0]scale=trunc(iw*$blur_scale/2)*2:trunc(ih*$blur_scale/2)*2,crop=$max_w:$max_h,boxblur=30 [blur]; "; $local_batch .= "[blur][sharp] overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2\" "; $local_batch .= "-q:v 0 -vb 20M ./video_source/source-video-$i.mpg -y"; system( $local_batch );
Вы много раз видели вертикальное видео с красиво размытым фоном, сделанным из этого же видео. Теперь вы знаете, как это сделать 🙂
Итак, что же за магия здесь происходит? На вход мы подаем наш ролик и включаем —filter_complex. Дальше мы берем это же видео и приводим к требуемому размеру с заранее рассчитанными коэффициентами и сохраняем его как [sharp]. Потом опять же входное видео приводим к размеру несколько больше требуемого, потом обрезаем его до требуемого размера и применяем размытие, сохраняем как [blur]. Финальный шаг — размещаем видео [sharp] поверх [blur] строго по центру — готово!
Зачем нужна возня с trunc? Дело в том, что ffmpeg не умеет отрезать от видео один пиксель, поэтому где-то вам придется привести размер видео к четному. Где вы это будете делать — на свое усмотрение.
Тёмная магия
Даже не столько магия, сколько способ сложно сделать простой эффект на видео. Требовалось сделать:
- Оверлейный полупрозрачный бокс с названием с fade-out в альфа канал. (Иначе говоря, плавно пропадающее вместе с боксом название)
- Fade-in, fade-out на видео дорожку, переход в белый цвет.
Я не ручаюсь, что этот способ оптимальный, но я нашел только этот.
my $opacity = '@0.4'; $local_batch = "$converter -i ./video_source/video-$i.mpg -i ./audio_misc/cut-audio-$i.mp3 "; $local_batch .= " -vf \"drawbox=enable=\'between(t,0,$title_dur)\':y=(ih/1.3):color=black$opacity:width=iw:height=100:t=max, "; $local_batch .= " drawtext=enable=\'between(t,0,$title_dur)\':fontfile=$font:text=\'$title[$i]\':fontcolor=white:fontsize=50:x=(w-tw)/2:y=(h/1.3)+30, format=yuv444p \""; $local_batch .= " -codec:a copy -q:v 0 -vb 20M ./video_music/inter$i.mpg -y"; system( $local_batch ); $local_batch = "$converter -i ./video_source/video-$i.mpg -i ./video_music/inter$i.mpg -filter_complex \""; $local_batch .= "[1]fade=out:st=$title_subt:d=$title_fade:alpha=1 [ovr]; "; $local_batch .= "[0][ovr] overlay=0:0:repeatlast=0, fade=in:st=0:d=$diff:color=white, fade=out:st=$video_duration_diff:d=$diff:color=white\" "; $local_batch .= "-codec:a copy -q:v 0 -vb 20M ./video_music/faded_inter$i.mpg -y"; system( $local_batch );
Разберем подробно, что здесь происходит. В первой части мы добавляем на видео в отведенные временные рамки полупрозрачный черный бокс, а поверх него — белый текст.
Во второй части, используя уже знакомый —filter_complex мы берем видео с боксом и надписью и используем на нем fade-out в альфа канал. Затем берем видео без бокса и надписи и накладываем поверх него [ovr], одновременно применяя к полученному результату fade-in, fade-out видео канала с переходом в белый цвет.
Склеивая вряд полученные таким образом видео, получается единый видеоряд с плавным переходом от одного ролика к другому через fade белого цвета.
Disclaimer: Мне потребовалось некоторое время, чтобы понять, что делать паузы между роликами на что-либо совершенно неуместно — это отнимает время, рассеивает концентрацию… а уж отсутствие fade-in/out по звуковому каналу это вообще насилие над ушами слушателя.
Окончание ролика
На youtube принято в конце ролика дать зрителю послушать какую-нибудь странную музыку и посмотреть под нее превью своих прошлых выпусков. Ок, сделаем это:
$local_batch = "$converter -i $res_dir/ending.mkv -i $res_dir/OVRL1.mkv -i $res_dir/OVRL2.mkv -loop 1 -i $res_dir/sub.png "; $local_batch .= "-filter_complex \""; $local_batch .= "[1]scale=iw/$scale_factor:ih/$scale_factor,drawbox=0:0:iw:ih:color=white:t=5 [pip0]; "; $local_batch .= "[2]scale=iw/$scale_factor:ih/$scale_factor,drawbox=0:0:iw:ih:color=white:t=5 [pip1]; "; $local_batch .= "[3]scale=iw/$scale_factor:ih/$scale_factor [pip2]; "; $local_batch .= "[0][pip0] overlay=(main_w-2*overlay_w)/3:main_h/($scale_factor-1)-overlay_h-50:repeatlast=0 [pip_m]; "; $local_batch .= "[pip_m][pip1] overlay=2*(main_w-2*overlay_w)/3+overlay_w:main_h/($scale_factor-1)-overlay_h-50 [sum]; "; $local_batch .= "[sum][pip2] overlay=main_w/2-overlay_w/2:2*main_h/3:shortest=1\" "; $local_batch .= "-crf $quality -vb 20M $res_dir/ending.mp4 -y"; system( $local_batch );
Применяя всё тот же —filter_complex и превращение картинки в видео ряд, получаем финишную заставку. Не буду разбирать подробно, механизм работы всё тот же, просто несколько другое использование.
Работа с youtube
Возникает вопрос, что делать с полученным видео? Смотреть самому это, конечно, здорово, но можно и друзьям показать. Решено — запилим канал на ютубе.
Первые мысли были такие: ютуб — это гугл, значит, наверняка, есть библиотека под perl, а документация отменная. Вторые мысли: почему нет библиотеки под perl? Третьи: откуда ошибки в доках? Четвертые: чтоб я еще раз…
🙂
В общем пришлось самостоятельно разбираться как работать с ютубом из perl. Граблей я собрал немерянно, так как работать с web из perl’а мне еще не приходилось.
Авторизация на ютубе сделана через oauth2, что на пальцах выглядит так:
- Используя client_id, однократно получаем auth_token. Эта операция обязательно производится с участием человека.
- Используя auth_token, получаем access_token и refresh_token. При этом access — истекает за час, а refresh — постоянный, по нему мы обновляем access.
- Если access_token истек, обновляем его с использованием refresh_token.
Звучит просто, но есть нюансы. Не буду предлагать вам собирать все грабли повторно, просто предложу свой код.
Получаем auth_token
################################################################### ### Одноразовый запрос на получение одобрения от пользователя ### ### Вместе с одобрением получаем auth_token ### ################################################################### $ua = LWP::UserAgent->new(); open( RESPONSE, ">", $response_file ); $req = POST 'https://accounts.google.com/o/oauth2/auth', [ scope => "https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.readonly https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube", response_type => "code", include_granted_scopes => "true", access_type => "offline", redirect_uri => "http://localhost/oauth2callback", client_id => "$client_id" ]; $content = $ua->request($req)->as_string; print RESPONSE $content; system("$browser $response_file"); print "Enter auth_token:\n"; my $the_code = <STDIN>;
Получаем access и refresh токены
################################################################### ### Одноразовый запрос на получение access и refresh tokens ### ################################################################### $req = POST 'https://accounts.google.com/o/oauth2/token', [ code => "$the_code", ### Это и есть auth_token с прошлого шага client_secret => "$client_secret", redirect_uri => "http://localhost/oauth2callback", client_id => "$client_id", grant_type => "authorization_code", ]; $json = $ua->request( $req )->decoded_content; $json_text = decode_json( $json ); $refresh_token = $json_text->{'refresh_token'}; $access_token = $json_text->{'access_token'}; print LOG $json; close RESPONSE;
Обновление доступа
################################################################### ### Многоразовый запрос на получение access token ### ### Получаем access по существующему refresh token ### ################################################################### $req = POST 'https://accounts.google.com/o/oauth2/token', [ client_id => "$client_id", client_secret => "$client_secret", refresh_token => "$refresh_token", grant_type => "refresh_token" ]; $content = $ua->request($req)->as_string; $content =~ m/"access_token"\s+:\s+"(.*)",.*/; $access_token = $1; print "Access token succesfully refreshed: $access_token\n";
Проверка доступа
################################################################### ### Многоразовый запрос на проверку access token ### ################################################################### if( $check_access == 1 ){ $req = POST 'https://www.googleapis.com/oauth2/v3/tokeninfo', [ access_token => "$access_token", ]; $content = $ua->request($req)->decoded_content; print "$content\n"; }
На этом приключения с ютубом не заканчиваются, так как мы пока только получили авторизацию, а нам надо еще и залить свое видео на канал. И тут появляется очередной нюанс, связанный с тем, что я писал скрипт под windows, а он в известной степени не совместим с linux, в то время как мне нужна была стабильная работа скрипта и там, и там.
Если вы не знали, то сообщаю: нельзя просто так взять и залить видео на ютуб (с).
Сперва нужно сделать запрос, в котором предоставить информацию о предстоящей загрузке, и только потом по известному линку можно будет заливать.
Получение загрузочного линка
$file_size = -s $file; $headers = HTTP::Headers->new( 'Content-Type' => 'application/json; charset=utf-8', 'Authorization' => "Bearer $access_token", 'x-upload-content-type' => 'video/mp4', 'X-Upload-Content-Length' => $file_size ); $r = HTTP::Request->new( 'POST', $url, $headers ); $r->content( $message ); $response = $ua->request( $r ); $upload_url = $response->header("Location");
В качестве сообщения мы отправляем корректно сформированный JSON. Тут важно обратить внимание на то, что в документации гугла бинарные опции JSON в некоторых примерах указываются как True/False, но внутренний парсер гугла воспринимает, та-дам!, бинарные опции как true/false. Одна большая буква из копипастного примера может стоить вам приличного количества нервов, ведь возвращаемая ошибка: Parser error.
Загрузка видео
$file_content = read_file( $file, binmode => ':raw', scalar_ref => 1 ); $headers = HTTP::Headers->new( 'Content_Length' => "$file_size", 'Content-Type' => 'video/mp4', 'Authorization' => "Bearer $access_token" ); $r = HTTP::Request->new('PUT', $upload_url, $headers, $file_content); $response = $ua->request( $r ); $json = $response->decoded_content; $json_text = decode_json( $json ); $resp_code = $response->status_line; $video_id = $json_text->{'id'};
Здесь важна самая первая строчка. Конечно, сослаться на файл можно многими разными способами, но только так perl не влезает в файл и не пытается его открыть, одновременно модифицируя его. По сути, мы делаем ссылку на файл и указываем, как с ней работать: бинарно.
Загрузка превью
$url = "https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=$video_id"; $headers = HTTP::Headers->new( 'Content_Length' => $thumbnail_size, 'Content-Type' => 'image/jpeg', 'Authorization' => "Bearer $access_token", ); $r = HTTP::Request->new('POST', $url, $headers, $thumbnail_content); $response = $ua->request($r); $upload_url = $response->header("Location"); $resp_code = $response->status_line; print LOG $response->decoded_content; print "Thumbnail upload init status: $resp_code\n";
Заключение
На данный момент я использую клиент-серверный подход для создания роликов. Скрипт, отвечающий за базу данных, крутится на VPS’ке от digitalocean, доступ к которой мне предоставил друг. Кодирование видео — весьма ресурсозатратная штука, поэтому эта задача оставлена на мой домашний ПК. Также из дома я могу по желанию проверить видео, которые пойдут в выпуск, поменять их количество, добавить зацикливание и так далее.
Смотреть контент с других развлекательных каналов я перестал, так как, очевидно, ручная работа других ютуберов значительно отстает в скорости от моего скрипта. А смотреть на древние баяны и рекламу у меня теперь причин нет.
От автора
-
Не бойтесь писать на perl’е — это просто.
-
Когда я только начал писать скрипт, было много головной боли, связанной с тем, что я привык к понятию переменной типа "регистр", и не сразу сообразил, что в perl’e надо использовать ссылки.
-
— Почему ты не пошел на "К", ведь ты классно программируешь?
— Я не пошел на "К", именно потому, что люблю программировать.Разговор двух студентов с кафедры №27(микроэлектроника) МИФИ, К — факультет кибернетики.
ссылка на оригинал статьи https://habrahabr.ru/post/281182/
Добавить комментарий