[recovery mode] Реклама — двигатель прогресса

de7f453e2e2b4d499a4446b0c3d9d938.png

«Реклама — двигатель прогресса» — эта легкая фраза, сказанная невзначай моей сестрой, описывает практически весь путь разработки простенького скрипта, который со временем вырос в небольшое клиент-серверное приложение. Итак, в данной статье я расскажу про: авторизацию на 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, картинка кликабельна.

1fcb7009d0244e61a8a1e8ca96875973.png

Для работы в 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 не умеет отрезать от видео один пиксель, поэтому где-то вам придется привести размер видео к четному. Где вы это будете делать — на свое усмотрение.


Тёмная магия

Даже не столько магия, сколько способ сложно сделать простой эффект на видео. Требовалось сделать:


  1. Оверлейный полупрозрачный бокс с названием с fade-out в альфа канал. (Иначе говоря, плавно пропадающее вместе с боксом название)
  2. 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, что на пальцах выглядит так:


  1. Используя client_id, однократно получаем auth_token. Эта операция обязательно производится с участием человека.
  2. Используя auth_token, получаем access_token и refresh_token. При этом access — истекает за час, а refresh — постоянный, по нему мы обновляем access.
  3. Если 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 = ;


Получаем 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(микроэлектроника) МИФИ, К — факультет кибернетики.


© Habrahabr.ru