- PVSM.RU - https://www.pvsm.ru -
Было время, когда у меня уже были дети, стабильная работа, камри, уроки английского и три раза в неделю бассейн. В общем такой себе состоявшийся мужчина. И я размышлял "Ну зачем мне питон? На работе совсем нет задач для автоматизации". Ни единого раза с того момента я не оказывался на столь высоком пике Даннинга-Крюгера (брата Баадера-Майнхофа, если что).
Но на курсы я тогда всё же сходил.
С тех пор прошли годы, число детей выросло, работу поменял, бассейн бросил, а автоматизирую теперь всё, до чего дотягиваются беспокойные руки.
Ниже старая как кости мамонта история о появлении yet another убер-скрипта для механизации рутины. Началась она с попытки распарсить RSS, а закончилась системой полного релизного цикла подкаста.

Итак, всё началось с того, что никак нам не давал покоя тот факт, что на ютубе [1] наши подкасты слушают (сами в шоке, честно!), но эпизодов за предыдущие 8 лет там нет.
А меж тем наши неустанные рты наговорили порядка двухсот штук по всем рубрикам, коих тоже уже больше полудюжины.
Настолько намозолило это наш коллективный натруженный , что вызвался самоотверженный доброволец готовить по одному эпизоду в день. Но для этого нужно обладать космическим уровнем дисциплины и резиновым временем.
Ведь как выглядит релиз видео:
Неупомянутым здесь особняком стоит релиз подкаста на сайте и в RSS, где тоже длинный список операций.
И я подумал, почему бы не помочь товарищу дружеским скриптом, который соберёт всё в одном месте в удобном виде.
Примерно таком.
Жисон:
{
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"title": "telecom №89. Транспорт и управление перегрузками",
"body": "Some body that I used to know"
}
Хотя бы копировать из одного места, и картинка — вот она — уже подспорье.
Говорят, мир едет на ленивых. Но в самом низу ногами гребут ленивые и глупые. Зачем искать существующие [3] парсеры [4] RSS [5], если можно написать чуть похуже, но свой?
Так появился get_podcasts.py [6].
Всё что он делал: собирал в один словарь title, podcast_url, body и kdpv. В качестве последнего он брал первый попавшийся img src, если им была не картинка с патреон.
Словарь пишется в файл all_podcasts.json [7].
Следующая светлая мысль, обещающая непыльную работу и драматическое упрощение труда энтузиаста, не заставила себя ждать:
Ну когда уже URL есть, почему бы файлики не скачать и не положить рядышком?
Так появился download_podcasts.py [8], который скачивал звук и изображение, выделял им случайное имя, сохранял в соответствующих директориях, и обогащал словарь all_podcasts этими именами, сохраняя результат в all_podcasts_w_files.json [9].
И тут же ещё категорию подкаста укажем.
Жисон:
{
"category": "telecom",
"img": "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg",
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"mp3": "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"title": "telecom №89. Транспорт и управление перегрузками",
"body": "Some body that I used to know"
}
На случай если картинки нет, я нарисовал шаблоны, которые можно взять в качестве обложки.
![]()
И вот казалось бы всё — бери ffmpeg двумя руками на полчасика в день — и ты чуть ближе к счастью.
Ну вот ещё командочку сгенерю, чтобы уж совсем просто было вставлять.
ffmpeg -loop 1 -i "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg" -i "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3" -c:a copy -c:v libx264 -shortest "mp4/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp4"
"Постой" — скажет практикующий скриптинг читатель — "Ну ты же ленивый, но не настолько глупый — запусти команду через subprocess".
Ну вот и я так подумал. И отсюда начинается захватывающая дух история.
"Только «Hello world» может быть проще, чем запустить команду linux из питона" — считал я на основе своего более чем скромного опыта. Импорт сабпроцесс, сабпроцесс.попен — и погнали.
Но засада оказалась там, где её не ждали: ffmpeg отдавал какой-то код возврата питону, а сам продолжал считать в фоне. Питон же считал, что команда отработала, и исполнял дальше. А дальше — цикл по всем эпизодам.
При первом же запуске окно терминала превратилось в какую-то манку с комочками. Я подозреваю, что наши 200 подкастов даже отрендерелись бы за какой-то необозримый промежуток времени.
Но такой жгучий дискомфорт я почувствовал в области органа тотального контроля, что пришлось стопнуть и поставить input(). Чего уж там — 200 раз нажать на энтер — мне не сложно.
render_video.py [10]
И я запустил РЕНДЕР. Ноутбук зашуршал вентиляторами, пытаясь остудить горячее своё нутро. На следующие несколько суток этот звук стал верным моим спутником.
Он считал видео и обогащал all_podcasts_w_mp4.json [11] именем файла mp4.
Жисон:
{
"category": "telecom",
"img": "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg",
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"mp3": "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3",
"mp4": "mp4/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp4",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"title": "telecom №89. Транспорт и управление перегрузками",
"body": "Some body that I used to know"
}
Но тут наступила ночь — время, когда я обычно не нажимаю на клавишу энтер. Но нельзя ведь терять столько драгоценное время — поэтому меняем input() на time.sleep(1800) — за полчаса должно же посчитаться.
Добавил условие, что рендерить видео нужно, только если mp4 в словаре ещё отсутствует, и ушёл спать.
Нет, тут всё хорошо — без подвоха, за недолгих три дня оно посчиталось. Ни один вентилятор не пострадал. Но без подключения к сети электропитания ноутбук можно было перенести только в другую комнату, батер выживал не более получаса и рассыпался в благодарностях, когда чувствовал поддержку 220В.
За это время я научился по звенящей тишине определять, что пора нажимать энтер и запускать расчёт дальше.
Я настолько привык к этому фоновому шуму, отсылавшему меня назад в нулевые, когда на ЭЛТ-мониторе NFS Porsche Unleashed сменял Max Payne, а в ногах шумел неистово биг-тауэр c селероном, что весь день, когда последний эпизод отрендерился, и в комнате повисла зловещая тишина, мне навязчиво казалось, что что-то вышло из строя.
К этому моменту у меня были директории с mp3, img, mp4, и заполненный файлик all_podcast_w_mp4.json [11] со всеми необходимыми ссылками.
Но вместо заслуженного отдыха я уже 2 дня к этому моменту изучал YouTube Data API.
Самый поверхностный гуглёжь навёл на готовый скрипт на питон2 upload_video.py [12], который можно запустить с ограниченным списком аргументов.
python upload_video.py --file="/tmp/test_video_file.flv"
--title="Summer vacation in California"
--description="Had fun surfing in Santa Cruz"
--keywords="surfing,Santa Cruz"
--category="22"
--privacyStatus="private"
Не без бубенчика я его попробовал, залил тестовое видео на тестовый аккаунт, получил свою порцию дофамина, достаточную, чтобы начать борьбу с фейспалмом от python2. Ну не может в 2021 не быть нормального богатого API.
И да, конечно, это я был кротслеповат. А по ссылке [13] открывается богатейшая документация к API на все ручки. Тут вам и дата публикации, и дата записи, и плейлист, и тегами можно всё обмазать. И какой хошь ЯП.
И мы подумали, что будем загружать все видосы в скрытом режиме и публиковать раз в месяц все эпизоды одного года.
Поэтому появился скрипт get_pub_dates.py [14], который ещё раз парсит RSS и собирает из него даты публикации эпизода на сайте, которые затем транслируются в запланированную дату публикации на ютубе.
Жисон:
{
"category": "telecom",
"img": "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg",
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"mp3": "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3",
"mp4": "mp4/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp4",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"publishAt": "2021-09-30T12:00:03.000Z",
"recordingDate": "2020-07-25T08:25:14.000Z",
"title": "telecom №89. Транспорт и управление перегрузками",
"body": "Some body that I used to know"
}
И вот готов скриптецкий, заливающий килотонны видео в наш доселе камерный канал: upload_video.py [16] (на python3).
И вот что получилось.
Жисон:
{
"category": "telecom",
"img": "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg",
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"mp3": "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3",
"mp4": "mp4/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp4",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"publishAt": "2021-09-30T12:00:03.000Z",
"recordingDate": "2020-07-25T08:25:14.000Z",
"title": "telecom №89. Транспорт и управление перегрузками",
"youtube_id": "jFvFIpWDjKM",
"body": "Some body that I used to know"
}
Кстати, пользуясь случаем, шлю лучи добра в корпорацию зла за дружелюбные интерфейсы. Более удобных мест и способов получения ключей я нигде не видел. Получил качественное эстетическое удовольствие.
Ну и ещё, чтобы вы поняли, насколько вам хорошо живётся, если не приходится использовать YouTube API:
— Нельзя добавить видео в плейлист, если он отсортирован по дате добавления.googleapiclient.errors.HttpError: <HttpError 400 when requesting https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&alt=json returned "Playlist should use manual sorting to support position.". Details: "Playlist should use manual sorting to support position.">— Зато если уж можно, то добавить можно хоть 1000 раз одно и то же видео.
— Если видео добавлены в плейлисты, то в админке они исчезают с главной (а у обычного пользователя — нет).
— Официальные видео по IAM и документация к API не совсем соответствует действительности.
К слову, каждый сеанс связи с гугл-АПИ авторизуется отдельно. Вам выдаётся ссылка, на которую нужно тыкнуть, а потом в интерфейсе тыкнуть ещё 6 (6, НАТАША!!!) раз, чтобы получить код авторизации. Действует он только на этот запуск скрипта, потом нужно получать новый.
Я читал, что есть некий REFRESH Token, который не протухает, но, спасибо интерфейсам, я так и не постиг мрачную тайну его использования.
Ещё у ютуба, оказалось, имеется квота на количество операций по API. 10 000 енотов в день.
И прайс такой: загрузка видео — 1600 за каждую попытку, обновление плейлиста — 50. Узнал я об этом после 6-го видео. Весь список, пожалуйста [17].
googleapiclient.errors.HttpError: <HttpError 403 when requesting https://youtube.googleapis.com/upload/youtube/v3/videos?part=snippet%2Cstatus%2CrecordingDetails&alt=json&uploadType=multipart returned "The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.". Details: "The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.">
И я должен сказать, хорошо, что эта квота была. Потому что в канал нагрянула миссия от нашей команды и признала, что коллективное чувство прекрасного ущемлено такими безликими изображениями. И предложила не заливать пока дальше.

Мне потребовалось время, чтобы смириться с мыслью, что трое суток рендера и мучений моего ноутбука были напрасны. Что 200 моих детищ ждёт /dev/null с распростёртыми файл-дескрипторам. И их нужно будет в муках рожать заново.
Но!
Воистину большим препятствием была лень рисовать обложки. Их же не 1 и не 2 — их 200!
Шаблон обложки у нас лежит в фигме. И набор действий для создания оной под каждый эпизод в целом механический.
"Ну наверняка есть API у фигмы, через который можно проделать те же манипуляции" — опять подумал я (всё же, похоже, что думать — это не моё). Цена тому — научиться в ещё один API. Что? Мало разве их было в моей грешной жизни?
Увы, API фигмы Read Only — надежды рассыпались.
Мы сели с нашим дизайнером и начали думать что же можно поделать. Он сказал, что может написать веб-сервис, который нам такую картинку сгенерит. И после мгновения радости от найденного решения, сразу в голову полезли мысли о том, что его нужно где-то разворачивать, поддерживать, мониторить — чуть больше головняка, чем хотелось бы. Но эта идея склонила мою мысль на подушку, в смысле Pillow — форк библиотеки PIL (Python Imaging Library) для работы с изображениями. Там вообще чудеса можно творить и на голове стоять.
Всё, что было нужно сделать:

Делов-то: сесть и сделать.
Думал я так, и это даже оказалось недалеко от истины — дело техники.
И опыта, которого у меня не было. Я с удивлением для себя открыл, как работает прозрачность в png. Я около часа боролся с этой квадратурой круга: все углы у изображения скруглил, а оно вставляется в шаблон обложки ровным прямоугольником. Я уже проверял из каких файлов берутся эти изображения, пробовал другие, сохранял картинку со скруглёнными углами в файл, чтобы проверить, что они действительно скруглены. Я даже сам в гимпе нарисовал пнг с большим прозрачным пятном посередине. Но чертовщина продолжалась.
Пока не пришло просветление, что прозрачность задаётся маской поверх полного изображения, а Image.paste по умолчанию это дело игнорирует (или не умеет работать).
В итоге нужно было маску извлечь из изображения и накладывать с её учётом. Фьюх.
r, g, b, m = img.split()
top = Image.merge("RGB", (r, g, b))
mask = Image.merge("L", (m,))
cover.paste(top, (44,83), mask)

Вторая техническая задачка — вписать текст в обложку.
Тут всё сравнительно легко — в зависимости от количества символов выбрать шрифт и рассчитать расстановку слов — сколько символов допустимо в одной строке.
if len(text) < 50:
letters = 17
elif len(text) < 80:
letters = 21
else:
letters = 26
text = ''
row = ''
for word in words:
if len(row+word)+1 <= letters:
row += f'{word} '
text += f'{word} '
else:
row += f'n{word} '
text += f'n{word} '
row = word

Полный код скрипта: gen_kdpv.py [18].
Жисон:
{
"category": "telecom",
"cover": "img/covers/afc5a3cf-cbcf-4c3f-8a64-9cc8df589ba3.png",
"img": "img/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.jpg",
"kdpv": "https://fs.linkmeup.ru/images/podcasts/linkmeup-V089(2020-07).jpg",
"link": "https://linkmeup.ru/blog/571.html",
"mp3": "mp3/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp3",
"mp4": "mp4/ef684cbd-9fb0-4226-8a8c-b1c6ff4ae908.mp4",
"podcast_url": "https://dts.podtrac.com/redirect.mp3/https://fs.linkmeup.ru/podcasts/telecom/linkmeup-V089(2020-07).mp3",
"publishAt": "2021-09-30T12:00:03.000Z",
"recordingDate": "2020-07-25T08:25:14.000Z",
"title": "telecom №89. Транспорт и управление перегрузками",
"youtube_id": "jFvFIpWDjKM",
"body": "Some body that I used to know"
}
Я пожалел своего Боливара и решил воспользоваться виртуалочкой, любезно запущенной моим соратником на производственных мощностях linkmeup.
Но для этого нужно было решить один вопрос — как не нажимать на проклятый энтер. Не то чтобы это было невозможно на вируталке или я смертельно устал. Нет! Это был уже вопрос моей чести как скриптоинвестора — нельзя было после такой работы идти на компромиссы.
Для питона, конечно же, есть батарейка с ffmpeg. Этому я даже не удивился, я шёл на stackoverflow в поисках именно её.
Вообще пусть славятся в веках имена создателей stackoverflow, которые обеспечили бутерброд с икрой таким как я!
Но как-то то ли контакта на батарейке не два, то ли я просто не догадался, какой стороной её вставлять. Одним словом переложить знакомые команды даже после обращения к более опытным в питоне товарищам, не получилось.
И тут элегантное, как валенки, решение осветило внутреннюю поверхность черепа. Subprocess, наверняка, возвращает ID процесса. А что если просто дождаться его завершения, прежде чем выполнять код дальше?
И вот это озарило своим присутствием мой скрипт:
while True:
if psutil.Process(run_cmd.pid).status() == 'zombie':
break
render_video_norm.py [19]
После этого я со спокойной душой залил всё это барахло на виртуалочку, в tmux'е запустил скрипт и ушёл писать этот пост на следующие 5 дней.
Единственное, что оставалось ручного — это каждый день запускать скрипт загрузки видосов, потому что квота, и потому что сессионные ключи.
Однако довольно быстро была найдена секретная форма [20] с заявкой на расширение квоты.
Сделана она прицельно так, чтобы ты дошёл до её конца, только если у тебя действительно очень острая необходимость квоту увеличить.
Без какой-либо надежды я заполнил эту форму минут за 20, расписав каждого необходимого мне енота, и без надежды ушёл спать. 14 раз. А через 2 недели мне пришло письмо счастья от гугла, где моя квота была повышена до 350000. И далее за один день я уже залил всё, что хотел.

Теперь последний день каждого месяца в течение полугода пачка видосов будет сама по себе улетать в паблик. Первая партия — завтра.
Ну и апофеозом всей этой волнительной истории должен стать готовый продукт для релиза новых подкастов. Ведь мы же не прекращаем ртом в микрофон говорить. А для продукта уже почти всё готово.
В итоге получился вот такой скрипт, написанный в лучших традициях экскриментального кодинга.
podcast_release [21].
Теперь, чтобы зарелизить подкаст, надо (кроме его записи и обработки, конечно же):
Найти картинку к выпуску, придумать название и подготовить описание.
А дальше:
./release.py -i 'img/test.jpg' -m 'mp3/test.mp3' -t 'telecom №196. Лист и феоктист' -d description.html
И оно само:
Последнее — это временная полумера, пока мы не переедем на ипси-дипси новый сайт на вордпрессе с адекватным REST API.
И в целом останется только вопрос кросспостинга его в вк, телегу и менее привлекательные каналы.
И да, автор отдаёт себе отчёт в том, что в сети есть сервисы, делающие нечто подобное. Но это для слабаков и за деньги любой дурак сможет. Впрочем немножечко специфичных задач у нас есть — например, обложка, зависящая от категории подкаста, которую можно извлечь только из названия.
Всем спасибо за внимание. На то, что мой скрипт будет полезен кому-то я надежды не питаю, однако любой обратной связи и пуэрам в репу linkmeup [23] я буду рад.
Автор: Марат
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/361984
Ссылки в тексте:
[1] ютубе: https://www.youtube.com/c/linkmeup-podcast
[2] мозг: http://www.braintools.ru
[3] существующие: https://github.com/zatarra/python-rss-parser
[4] парсеры: https://pypi.org/project/feedparser/
[5] RSS: https://internet--technologies-ru.turbopages.org/internet-technologies.ru/s/articles/sozdanie-skrapera-rss-kanala-s-pomoschyu-python.html
[6] get_podcasts.py: https://github.com/linkmeup/parse_rss/blob/master/get_podcasts.py
[7] all_podcasts.json: https://github.com/linkmeup/parse_rss/blob/master/all_podcasts.json
[8] download_podcasts.py: https://github.com/linkmeup/parse_rss/blob/master/download_podcasts.py
[9] all_podcasts_w_files.json: https://github.com/linkmeup/parse_rss/blob/master/all_podcasts_w_files.json
[10] render_video.py: https://github.com/linkmeup/parse_rss/blob/master/render_video.py
[11] all_podcasts_w_mp4.json: https://github.com/linkmeup/parse_rss/blob/master/all_podcasts_w_mp4.json
[12] upload_video.py: https://developers.google.com/youtube/v3/guides/uploading_a_video?hl=ru
[13] ссылке: https://developers.google.com/youtube/v3/docs?hl=ru
[14] get_pub_dates.py: https://github.com/linkmeup/parse_rss/blob/master/get_pub_dates.py
[15] all_podcasts_w_pd.json: https://github.com/linkmeup/parse_rss/blob/master/all_podcasts_w_pd.json
[16] upload_video.py: https://github.com/linkmeup/parse_rss/blob/master/upload_video.py
[17] Весь список, пожалуйста: https://developers.google.com/youtube/v3/determine_quota_cost
[18] gen_kdpv.py: https://github.com/linkmeup/parse_rss/blob/master/gen_kdpv.py
[19] render_video_norm.py: https://github.com/linkmeup/parse_rss/blob/master/render_video_norm.py
[20] секретная форма: https://support.google.com/youtube/contact/yt_api_form?hl=en
[21] podcast_release: https://github.com/linkmeup/podcast_release
[22] хостинг: https://www.reg.ru/?rlink=reflink-717
[23] репу linkmeup: https://github.com/linkmeup/
[24] Источник: https://habr.com/ru/post/542352/?utm_source=habrahabr&utm_medium=rss&utm_campaign=542352
Нажмите здесь для печати.