Обвиваем YouTube змеем, или как смотреть и скачивать видео с YouTube без VPN на чистом Python-е. Часть 2

в 19:38, , рубрики: dpi, nodpi, python, YouTube, обход блокировок, обход блокировок youtube, скачивание видео

Приветствую! Эта статья является продолжением (2 частью) статьи Обвиваем YouTube змеем, или как смотреть и скачивать видео с YouTube без VPN на чистом Python-е. Часть 1 Если честно, я был приятно удивлен популярностью первой части: 115К просмотров за неделю и 137 голосов, которые принесли мне 21 место в рейтинге Хабра. Учитывая, что эта статья была из песочницы (отдельное спасибо @Ilha за приглашение), для меня это большой результат. Поэтому всем, кто поставил стрелочку вверх – авторское спасибо!)

В этой статье я покажу, как можно скачивать с YouTube каналы и плейлисты. Если кто-то не читал первую часть, настоятельно рекомендую это сделать. По крайней мере, если по мере чтения у вас возникнут какие-то вопросы, скорее всего там есть на них ответы. Напомню, что у нас уже есть средство, которое решает "проблему с устаревшим и изношенным оборудованием Google Global Cache" (к сожалению, оно не у всех работает, учтите), а также мы разобрались с тем, как скачивать с YouTube видео и аудио в любом качестве. Итак, начнём!


Как там наши кэширующие сервера?

По многочисленным заявкам телезрителей читателей, даю ссылку на мой репозиторий с утилитой для обхода DPI. Там вы найдёте не только работающий код, но и скомпилированный exe для Windows, который не требует дополнительных программ/библиотек (главное - не забудьте положить рядом с exe файл blacklist.txt). Разбор исходного кода утилиты приведён в первой части статьи.

Подготовка инструментария

Для тех, кто не читал первую часть, повторю, что нам понадобится библиотека pytubefix, которая устанавливается командой:

pip install pytubefix

Также нам понадобится ffmpeg, для объединения аудио и видео (зачем и почему – см. часть 1). Если у вас Windows, то скачать его можно с моего Google Диска (ну или найдите в интернете)

Также приведу код функции, которая объединяет аудио с видео, из прошлой статьи. Я не буду дублировать его каждый раз, а просто буду ссылаться на него:

def combine(audio: str, video: str, output: str) -> None:

    if os.path.exists(output):
        os.remove(output)

    code = os.system(
        f'.\ffmpeg.exe -i "{video}" -i "{audio}" -c copy "{output}"')

    if code != 0:
        raise SystemError(code)

Простой пример загрузки плейлиста

Для работы с плейлистами в pytubefix есть объект Playlist. В принципе, кроме него нам почти больше ничего и не нужно.

Давайте посмотрим, как можно скачать плейлист:

from pytubefix import Playlist
from pytubefix.cli import on_progress

url = "https://www.youtube.com/playlist?list=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

playlist = Playlist(url,
                    proxies={"http": "http://127.0.0.1:8881",
                             "https": "http://127.0.0.1:8881"})

for video in playlist.videos:
    video.register_on_progress_callback(on_progress)
    stream = video.streams.get_highest_resolution()
    stream.download()

Давайте разберём, что делает код. Вначале мы импортируем класс Playlist и функцию on_progress, которая нужна для отображения прогрессбара во время загрузки видео. url -  это ссылка на плейлист. Далее, мы создаём экземпляр класса Playlist и передаём ему url и адрес нашего прокси, то бишь nodpi.

Затем циклом мы проходимся по списку playlist.videos (хотя вообще-то это не список, а генератор). Каждый его элемент – это экземпляр класса YouTube, непосредственно с которым можно работать и скачивать потоки (про потоки см. в 1 части). Для каждого видео мы регистрируем обработчик для on_progress_callback, который обеспечивает отображение прогрессбара во время загрузки. После этого мы получаем поток с самым высоким разрешением, который содержит и аудио, и видео, и скачиваем его. Как я уже говорил в прошлой статье, таким разрешением окажется 360p (из-за ограничений YouTube-а), но пока для нас это неважно, в более высоком разрешение мы будем скачивать позднее.

Сохраните этот код в файл, запустите nodpi, а затем запустите этот скрипт (вставив свою ссылку на плейлист). Должно работать! (но это не точно)

Что можно узнать о плейлисте

У Playlist есть несколько свойств, которые помогают получить о нём информацию. Ниже представлены самые основные из них:

  • last_updated – возвращает дату последнего обновления плейлиста (добавления в него последнего видео).

  • title -  название плейлиста.

  • description -  описание.

  • owner -  владелец плейлиста. Обычно это название канала.

  • length -  количество видео в плейлисте.

Загрузка плейлиста в хорошем качестве

Давайте объединим все наши знания и напишем скрипт, который будет скачивать все видео из плейлиста в одном из разрешений:

  1. Самое высокое разрешение (будет определяться для каждого видео автоматически)

  2. Дефолтное разрешение (360p есть у всех видео)

  3. Самое низкое разрешение (будет определяться для каждого видео автоматически)

Пишем код

Для начала импортируем библиотеки:

import os
import sys

from pytubefix import YouTube, Playlist
from pytubefix.cli import on_progress
from pytubefix.helpers import safe_filename
from pytubefix.file_system import file_system_verify

Далее вставьте код функции combine, приведенный выше.

Затем идёт функция скачивания видео:

def download(url: str, res: str, progressive: bool = False) -> None:

    yt = YouTube(
        proxies={"http": "http://127.0.0.1:8881",
                 "https": "http://127.0.0.1:8881"},
        url=url,
        on_progress_callback=on_progress,
    )

    video_stream = yt.streams.
        filter(type='video', resolution=res, progressive=progressive).
        desc().first()

    audio_stream = yt.streams.
        filter(mime_type='audio/mp4').
        order_by('filesize').
        desc().first()

    kernel = sys.platform

    if kernel == "linux":
        file_system = "ext4"
    elif kernel == "darwin":
        file_system = "APFS"
    else:
        file_system = "NTFS"

    translation_table = file_system_verify(file_system)

    audio_filename = audio_stream.default_filename.translate(translation_table)
    video_filename = video_stream.default_filename.translate(translation_table)
    output_filename = f'{safe_filename(yt.title)}.mp4'

    if output_filename == video_filename:
        output_filename = f'{safe_filename(yt.title)}_1.mp4'

    print('nInformation:')
    print("tTitle:", yt.title)
    print("tAuthor:", yt.author)
    print("tDate:", yt.publish_date)
    print("tResolution:", video_stream.resolution)
    print("tViews:", yt.views)
    print("tLength:", round(yt.length/60), "minutes")
    print("tFilename of the video:", output_filename)
    print("tFilesize of the video:", round(
        video_stream.filesize / 1000000), "MB")

    print('Download video...')
    video_stream.download()

    if not video_stream.is_progressive:
        print('nDownload audio...')
        audio_stream.download()

        combine(audio_filename, video_filename, output_filename)

Она похожа на функцию из первой части, но есть и отличия. Я добавил аргументы res (разрешение) и progressive (указывает на то, есть ли в потоке аудио и видео). После того, как мы получаем потоки, идёт блок, который преобразует имена файлов в «безопасные» для текущей файловой системе. Также в прошлой статье у меня был косяк – если имя файла с видео и имя конвертированного файла совпадали, то происходила ошибка. Здесь я поправил это, добавив соответствующую проверку. Скачивание аудио и конвертирование мы производим, только если видеопоток не содержит его. В целом код понятный, и я думаю подробные объяснения излишни.

Далее напишем функцию, которая будет выбирать разрешение для конкретного видео, исходя из выбранного режима:

def resolution(video: YouTube, type: str):

    if type == 'default':
        stream = video.streams.get_highest_resolution()
        return stream.resolution

    if type == 'worst':
        stream = video.streams.get_lowest_resolution()
        return stream.resolution

    if type == 'best':
        stream = video.streams.
            filter(type='video').
            order_by('resolution').
            desc().first()

        return stream.resolution

Надеюсь в объяснениях не нуждается.

Ну и основная часть:

url = "https://www.youtube.com/playlist?list=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

playlist = Playlist(url,
                    proxies={"http": "http://127.0.0.1:8881",
                             "https": "http://127.0.0.1:8881"})

while True:
    res = str(input(
        'In what resolution would you like to download videos from the playlist (0 - 360p, 1 - minimum resolution, 2 - maximum resolution)? '))
    if res not in ('0', '1', '2'):
        print('Incorrect value entered')
        continue
    break

print('nInformation:')
print("tTitle:", playlist.title)
print("tAuthor:", playlist.owner)
print("tVideos:", playlist.length)

input('nPress Enter to start downloading...n')

Здесь мы спрашиваем пользователя, как скачивать плейлист и выводим информацию о нём. Затем качаем:

for video in playlist.videos:
    try:
        video.check_availability()
    except Exception as e:
        print(f"Error: {e}")
        continue
    else:
        try:
            download(video.watch_url, resolution(
                video, 'default' if res == '0' else 'worst' if res == '1' else 'best'),
                progressive=True if res == '0' else False)
        except Exception as e:
            print(f"Error: {e}")
            continue

video.check_availability() проверяет доступно ли видео для скачивания и вызывает ошибку, если нет. Мы перехватываем её и уведомляем пользователя, иначе скачивание может пойти насмарку. В остальном код, я надеюсь, понятный.

Примечание

Отдельно хочу заметить, что я не претендую на красоту и продуманность кода – моя цель показать how it works, а не дать готовый идеальный инструмент.

Ну и наконец проверяем. Запускаем nodpi и наш скрипт. Не забудьте вставить свой url (просто копипастите из строки браузера).

Загрузка канала

Как это ни странно, но скачивание канала почти ничем не отличается от скачивания плейлиста. Для этого в pytubefix предназначен класс Channel (который, кстати, наследуется от Playlist).

Как известно, видео на каналах YouTube делятся на несколько категорий: просто видео, трансляции (стримы) и shorts. pytubefix позволяет скачивать все эти категории, за исключением трансляций, которые закончились совсем недавно или ещё идут.

Итак, какие изменения мы внесём.

Для начала импортируем Channel

from pytubefix import YouTube, Channel

Также мы напишем функцию, которая глобально устанавливает прокси, т. е. для всего скрипта, потому что, по какой-то причине, Channel не хочет работать с прокси. Импортируем urllib и напишем функцию:

from urllib import request


def install_proxy(proxy_handler: dict):
    proxy_support = request.ProxyHandler(proxy_handler)
    opener = request.build_opener(proxy_support)
    request.install_opener(opener)

proxy_handler - это словарь примерно в таком формате:

{"http": "http://127.0.0.1:8881",
 "https": "http://127.0.0.1:8881"}

Далее, изменим часть кода, где мы создавали экземпляр класса Playlist. Теперь здесь мы устанавливаем прокси и используем Channel:

url = "https://www.youtube.com/@xxxx"

install_proxy({"http": "http://127.0.0.1:8881",
               "https": "http://127.0.0.1:8881"})
channel = Channel(url)

Затем уберём информацию об авторе и названии, т. к. Channel не поддерживает их:

print('nInformation:')
print("tVideos:", channel.length)

input('nPress Enter to start downloading...n')

Ну а всё остальное оставляем без изменений, кроме цикла. Для итерации по видео используйте for video in channel.videos:; по трансляциям for video in channel.live:; по shorts‑ам for video in channel.shorts:

Всё! Скрипт готов к работе. Как обычно, запускаем nodpi и проверяем.

Загрузка субтитров

Здесь вообще всё просто. Простейший код, который сохраняет субтитры в файл:

from pytubefix import YouTube

yt = YouTube('http://youtube.com/watch?v=xxxxxxxxxxx')

caption = yt.captions['a.ru']
caption.save_captions("captions.txt")

yt.captions - это словарь, в котором хранятся все доступные для этого видео субтитры (caption будет объектом Caption)

caption.save_captions сохраняет субтитры в текстовом формате в файл. xml-формат можно получить с помощью caption.xml_captions

Плюсом pytubefix, является то, что он позволяет. скачивать даже те субтитры, которые были созданы YouTube-ом автоматически (в pytube такой возможности не было).

Добавить субтитры к видео можно вручную. Например в VLC это делается так: Субтиры -> Добавить файл субтитров.

Можно и с помощью ffmpeg. В данном случае команда будет выглядеть так:

ffmpeg.exe -i filename.mp4 -vf subtitles=captions.txt filename_new.mp4

В данном случае субтитры будут зашиты текстом непосредственно в видео (а не отдельным потоком).


На этом всё! В двух частях статьи я рассказал, как можно скачивать с YouTube видео, аудио, плейлисты, каналы и субтитры, а также поделился способом использования YouTube без VPN. Надеюсь статьи были полезными и интересными!

Что может из этого получится

Может кто-то скажет, что я забыл добавить хаб "Я пиарюсь", но нет - я делаю это не ради рекламы, а просто, чтобы показать, на что способен pytubefix. А дело в том, что уже около полугода я пишу пет-проект - утилита для скачивания с YouTube всего того, о чём я рассказывал. Она полностью написана на python, но исходный код я пока не выложил, несмотря на то, что он работает (работал до августа 2024), из-за "замедления" YouTube. Большинство попыток скачивания заканчиваются ошибкой Remote end closed connection Ниже я покажу несколько скриншотов программы. Возможно, кто-то заинтересуется ей и будет готов по разбираться - я не против, велком в диалоги.

Главный экран (графика - собственная либа, написанная с нуля)

Главный экран (графика - собственная либа, написанная с нуля)
Экран загрузок

Экран загрузок
Подготовка к загрузке видео

Подготовка к загрузке видео
Загрузка видео

Загрузка видео
Загрузка окончена

Загрузка окончена
Список настроек для понимания функционала

Список настроек для понимания функционала

Автор: Lord_of_Rings

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js