Как нарезать видео с YouTube на mp3 по главам через yt-dlp

Мой отец учит греческий на прогулках и хотел заниматься по одному разделу за раз — каждую главу урока отдельным mp3. Один флаг yt-dlp, --split-chapters, режет час видео по внутренним главам на аккуратный ряд mp3-файлов, по одному на раздел, а потом то же самое — сразу по всему плейлисту. Плюс быстрый способ выхватить один кусок, чистка имён через sed и подвохи с куками.

Есть на YouTube плейлист, за которым мой отец давно следит, — «Греческий на Слух», Greek by Ear. Он учит греческий, медленно, так, как учат язык в зрелом возрасте: в щелях между делами, на прогулках, отложив экран подальше. Видео длинные. Каждое — час или больше, сшитое из кусков и разбитое на главы: отработка лексики, диалоги, грамматика, рассказ на чистом греческом — весь аппарат урока.

А заниматься он хотел так, как разучивают музыкальную пьесу, — по одному разделу за раз. Не весь час в один присест, а отработку лексики этим утром, диалог завтра, греческий рассказ на повторе, пока ухо его не схватит. Для этого ему нужна была каждая глава отдельным файлом: небольшой набор mp3 на каждый урок, по одному на раздел, — чтобы скинуть на телефон и брать на прогулку по одному, держа экран в кармане.

В этом и состоит тихая враждебность длинного видео: оболочку выстроили так, чтобы ты остался. А вот главы — это швы. Кто-то уже разметил, где каждый раздел начинается и где заканчивается. И yt-dlp с удовольствием порежет сразу по всем этим швам и отдаст целый урок пронумерованным рядом mp3 — по одному на главу — вместо одного часового монолита. Это я для него и сделал.

Сначала посмотрим, что внутри

Прежде чем что-то резать, я хочу знать, как швы называются на самом деле. yt-dlp умеет напечатать список глав видео, не скачав ни байта, — --print подразумевает --simulate, так что он просто читает метаданные и останавливается.

yt-dlp --print "%(chapters)j" "https://www.youtube.com/watch?v=58uHKLmLE5U"

Шаблон %(chapters)j даёт чистый JSON — список объектов {start_time, end_time, title}. (Можно написать и --print chapters, но тогда напечатается уродливый Python-repr того же самого; шаблон с j — это читаемая форма.) Если у видео нет глав, результат будет пустым — и сразу станет ясно, что резать не по чему. Мне важно понять, сколько в нём разделов и как они называются, — потому что через минуту yt-dlp превратит каждый из них в файл, названный по его заголовку.

Режем видео на mp3 по главам

Вот сердцевина, и держится она на одном флаге. --split-chapters читает внутренние главы видео и пишет по одному файлу на главу; -x извлекает звук, а --audio-format mp3 делает из этих файлов mp3. yt-dlp сначала стягивает весь звук в один mp3, а потом нарезает его по границам глав на отдельные mp3 — по одному на раздел.

yt-dlp \
  -x --audio-format mp3 \
  --split-chapters \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

Есть одна тонкость, которую стоит знать до запуска. Без шаблона имени каждый файл-глава получает имя вывода по умолчанию — а оно для всех одинаковое, выведенное из заголовка видео, — так что yt-dlp начинает их неудачно разводить по именам или попросту затирать. Лечится это шаблоном вывода chapter:, который именует только нарезанные файлы:

yt-dlp \
  -x --audio-format mp3 \
  --split-chapters \
  -o "chapter:%(section_number)02d - %(section_title)s.%(ext)s" \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

Префикс chapter: — это ключ. yt-dlp позволяет задать отдельный шаблон вывода для каждого типа файлов, и chapter: целится именно в нарезанные файлы. Поля, которые здесь существуют, — это группа section_*: %(section_number)s, %(section_title)s, %(section_start)s, %(section_end)s — а не %(chapter)s, которое относится к другому, никак не связанному с этим полю. %(section_number)02d дополняет номер нулями до двух разрядов, так что урок выходит как 01 - ..., 02 - ..., 03 - ..., по порядку, и папка с ними сортируется так, как задумано курсом.

И извлечение звука, и нарезка опираются на ffmpeg — это единственная жёсткая зависимость здесь. -x хочет видеть в PATH и ffmpeg, и ffprobe, а --split-chapters тоже зовёт ffmpeg, так что если yt-dlp жалуется, что не может найти бинарник, — вот она, недостающая деталь: brew install ffmpeg на Mac, ваш пакетный менеджер в остальных случаях.

Чего стоит ожидать, потому что меня это застало врасплох: --split-chapters оставляет и полноразмерный файл тоже. Вы получаете свои N mp3-глав и исходный часовой mp3 рядом с ними. Чистого флага, который удалял бы оставшийся полный файл после нарезки, нет — семейство --no-keep-video/-k управляет исходным медиа до извлечения звука, а не тем извлечённым mp3, из которого читает резак, так что его оно не удалит. На практике вы либо не обращаете внимания на лишний файл, либо отправляете файлы-главы в отдельную папку, чтобы держать их порознь, либо удаляете полный вручную потом. Я выбрал отдельную папку — её легко задать через -P и тот же префикс chapter::

yt-dlp \
  -x --audio-format mp3 \
  --split-chapters \
  -P "chapter:./chapters" \
  -o "chapter:%(section_number)02d - %(section_title)s.%(ext)s" \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

Теперь mp3 по главам ложатся в ./chapters, а оставшийся полноразмерный файл остаётся выше, в рабочем каталоге, где я разок брошу на него взгляд и отправлю в корзину.

Или хватаем один кусок вместо всех

Иногда вам нужен не весь набор — а один раздел или просто минуты с 10-й по 15-ю. Для этого есть --download-sections: он скачивает только названный вами кусок и пропускает остальную загрузку целиком.

У моего отца есть любимая глава. Ближе к концу каждого видео есть раздел «История на греческом без субтитров» — рассказ на чистом греческом, без субтитров, без английского костыля снизу. Когда он хочет просто её, саму по себе, тянуть весь час незачем:

yt-dlp \
  --download-sections "(?i)история на греческом без субтитров" \
  -x --audio-format mp3 \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

Аргумент — это регулярное выражение, которое сопоставляется с названием каждой главы, и по умолчанию сопоставление чувствительно к регистру: yt-dlp компилирует его голым re.search, без re.IGNORECASE, так что история не совпадёт с История. Флаг (?i) в начале делает поиск нечувствительным к регистру, и на плейлисте, где регистр гуляет от ролика к ролику, это не косметика — это безопасное умолчание. Экранируйте в названии любые метасимволы регэкспа (., (, ?, +), иначе их прочитают как регэксп.

А если нужный кусок — вовсе не глава, а сырой отрезок времени, поставьте перед аргументом *, и --download-sections переключается на буквальный диапазон времени:

yt-dlp --download-sections "*10:15-15:30" -x --audio-format mp3 \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

inf на конце означает «до самого финала» ("*1:30:00-inf"), а саму опцию можно передать несколько раз, собрав за один заход сразу несколько кусков. По умолчанию рез приклеивается к ближайшему ключевому кадру — быстро, но края могут прихватить секунду-другую лишнего; --force-keyframes-at-cuts делает их точными ценой перекодирования. Для аудио на прогулку мне это было безразлично.

Делаем это сразу по всему плейлисту

Вот где это перестаёт быть фокусом ради фокуса и начинает по-настоящему экономить время. Дайте yt-dlp URL плейлиста вместо одного видео — и --split-chapters отработает на каждой записи: каждое видео порежется на свои mp3 по главам, независимо.

yt-dlp \
  -x --audio-format mp3 \
  --split-chapters \
  -o "chapter:%(playlist_index)s - %(title)s/%(section_number)02d - %(section_title)s.%(ext)s" \
  "https://www.youtube.com/playlist?list=PLTV0lR0SKiEu51XnbiJ8vCXDLQDiFR9uh"

Этот шаблон строит по папке на урок — %(playlist_index)s нумерует уроки в порядке курса, — а внутри каждой папки главы пронумерованы по очереди. Целый курс возвращается аккуратным деревом: урок за уроком, раздел за разделом. У любого видео в плейлисте, где глав не оказалось, печатается Chapter information is unavailable, нарезанных файлов оно не даёт и пропускается без ошибки — в первый раз, увидев эту строчку, я решил, что что-то сломал; ничего подобного.

Приводим имена в порядок

Имена нарезанных по главам файлов могут выходить неряшливыми — заголовки глав пишут не для того, чтобы они были именами файлов, так что вам попадётся залётная пунктуация, сдвоенные пробелы, нет-нет да и эмодзи, который канал воткнул в название раздела. Шаблон chapter: чинит большую часть этого в источнике, но когда что-то проскальзывает, я подчищаю это потом маленьким циклом на sed.

for f in *.mp3; do
  new=$(echo "$f" \
    | sed -E 's/^Греческий на Слух[^)]*\)[🎧. ]*//' \
    | sed -E 's/ *\[[^]]+\]//' \
    | sed -E 's/^[ .]+//; s/[ .]+$//')
  mv -n "$f" "$new"
done

Три прохода: срезать префикс с именем канала вплоть до первой ) и любую хвостовую мишуру, выкинуть скобку с [video-id], если она уцелела, затем подрезать залётные точки и пробелы спереди и сзади. Второй крошечный цикл схлопывает сдвоенные точки, если они остались перед расширением:

for f in *.mp3; do
  new=$(echo "$f" | sed -E 's/\.+\.mp3$/.mp3/')
  mv -n "$f" "$new"
done

Первый шаблон скроен под мои конкретные имена — имя канала, за которым идёт пометка урока в скобках, — а не под универсальную чистку; на именах другой формы префикс может уцелеть нетронутым. sed -E — переносимый способ попросить расширенный регэксп: он одинаково работает и с BSD sed в macOS, и с GNU sed (GNU-шный -r — непереносимый синоним, так что предпочтительнее -E). Кавычки вокруг "$f" и "$new" не дают пробелам разорвать цикл, а mv -n отказывается затирать уже существующий файл.

Пара краёв, на которых я споткнулся. В bash с выключенным nullglob, если файлов .mp3 нет вообще, цикл прокрутится один раз с буквальной строкой *.mp3 в роли имени файла — теперь я подстраховываюсь через shopt -s nullglob. (zsh строже и просто падает с no matches found.) А mv -n пропускает конфликтующее переименование молча, без предупреждения, так что когда два вычищенных имени сошлись в одну строку, один файл тихо остался со старым именем. С нарезкой по главам это кусается сильнее, чем кажется: короткие общие названия разделов вроде «Повторение» повторяются из урока в урок — так что нумеруйте их или раскладывайте по папкам уроков, и столкновения просто не возникнут.

Когда ломаются куки

Чаще всего чтение публичных метаданных глав не требует никакой аутентификации. Но иногда YouTube выставляет стену «Sign in to confirm you're not a bot», или же контент оказывается с возрастным ограничением, только для участников или вовсе приватным. Для таких случаев yt-dlp может одолжить сессию вашего браузера:

yt-dlp --cookies-from-browser chrome \
  -x --audio-format mp3 \
  --split-chapters \
  -o "chapter:%(section_number)02d - %(section_title)s.%(ext)s" \
  "https://www.youtube.com/watch?v=58uHKLmLE5U"

Имя браузера — одно из brave, chrome, chromium, edge, firefox, opera, safari, vivaldi, whale. Сценарии отказа, на которых споткнулся я, — потому что они неочевидны:

  • «Permission denied» почти всегда означает, что браузер открыт. Закройте его полностью и попробуйте снова.
  • На macOS может выскочить запрос Keychain на ключ «Chrome Safe Storage» — это нормально, подтвердите.
  • На Windows Chrome 127 и новее наглухо сломали --cookies-from-browser chrome через App-Bound Encryption (увидите ошибку «failed to decrypt with DPAPI»). Используйте Firefox или экспортируйте cookies.txt и передайте --cookies file.txt.

Вики yt-dlp прямо говорит, что автоматический доступ с куками может привести к пометке аккаунта. Если делаете что-то на объёме, заведите одноразовый аккаунт и экспортируйте его куки из приватного окна — а не тот аккаунт, который вам дорог.

Пара слов о том, чтобы вести себя порядочно

Условия YouTube ограничивают скачивание, кроме особо разрешённых случаев, и ограничивают автоматический доступ, кроме того, что позволяет robots.txt, или с разрешения. Однозначно нормальные случаи — это ваши собственные загрузки, материалы под Creative Commons и в общественном достоянии, а также личное или образовательное использование контента, который вы и так можете смотреть, — причём последнее зависит от юрисдикции и не является индульгенцией. То, что сделал я, лежит в обычной зоне личного использования: аудио для изучения языка, разобранное на куски для ушей моего отца, на его собственных прогулках, никому не раздаваемое. Берите нужное себе, остальное оставьте. Не зеркальте чужой канал, называя это учёбой.

Что в итоге получил отец

Теперь у отца есть папка на каждый урок, а в ней — пронумерованный набор коротких mp3: отработка лексики, диалог, греческий рассказ, — которые он прорабатывает по одному куску за раз по дороге в магазин и обратно. Без браузера. Без автоплея, решающего за него, что дальше. Без часового файла, через который надо проматывать, чтобы добраться до того раздела, на котором он сегодня. Просто урок, разобранный на те куски, из которых он всегда и был сложен, в форме, которая ложится в ритм его дня.

Видео хотело его час — целиком и по порядку. Я отдал его обратно по разделам, и отец проходит их по одному за прогулку.

Понравилось? Не пропустите следующее.

Новые эссе — вам на почту. Двойное подтверждение, отписка в любой момент, без пикселей слежки.

Предпочитаете RSS? Есть лента →

Похожее