• Познакомьтесь с пентестом веб-приложений на практике в нашем новом бесплатном курсе

    «Анализ защищенности веб-приложений»

    🔥 Записаться бесплатно!

  • CTF с учебными материалами Codeby Games

    Обучение кибербезопасности в игровой форме. Более 200 заданий по Active Directory, OSINT, PWN, Веб, Стеганографии, Реверс-инжинирингу, Форензике и Криптографии. Школа CTF с бесплатными курсами по всем категориям.

Статья Скачиваем видео с YouTube с помощью Python, без использования pytube

Что вы делаете, когда вам нужно сохранить определенное видео с YouTube, чтобы оно не потерялось? Ну, тут логично. Можно в самом YouTube создать плейлист и добавлять туда все, что нужно. Можно просто добавить страницу в закладки. Да много чего можно сделать. А еще скачать видео себе на жесткий диск. Мало ли чего, всякое бывает. А так под рукой и в сохранности. Вот только способов для скачивания становится все меньше и меньше. Давайте попробуем скачать видео с YouTube с помощью питона. Нет, здесь не будет очередного руководства по pytube. Все интереснее и печальнее.

000.jpg

Не так давно промелькнула новость о том, что проект YouTube Vanced закрывается из-за юридического давления со стороны Google. И вроде бы ладно. Сколько еще таких проектов. Тем более, что для скачивания видео с сервиса я пользовался скриптом на питоне, в котором с помощью библиотеки pytube все благополучно скачивалось. Но, через какое-то время после этой новости pytube вдруг перестал работать. Раз и все. Ладно. Я подумал, что это всего лишь ошибка в моей программе. Сейчас поправлю и все заработает. Но, не тут-то было. Ошибок я особо-то и не нашел. А вот pytube не работал даже на простейших примерах, которые у него описаны на странице загрузки. Я решил, что это временные трудности. На время оставил данный проект в покое. Но, вот спустя почти месяц я снова к нему вернулся. И ничего не изменилось.

Тут, скорее всего, все просто. У YouTube слегка изменился код, а следовательно, и алгоритм поиска ссылок у pytube тоже должен было обновиться. Но, этого не произошло. Покопавшись в интернете, я нашел несколько решений. Нужно заменить регулярные выражения в коде модуля и все должно было заработать. Но, у меня не получилось. Хотя, делал все так как сказано, вплоть до строчек кода. Кстати, в самом коде видно, что алгоритм поиска по регулярным выражениям менялся уже не один раз, так как много строк просто закомментировано. В общем и целом, не знаю, «восстанет ли этот проект из пепла», так сказать. Будем надеяться, что да. Хороший был проект. Полезный.

screenshot1.png

К чему это я веду. А все просто. Время проходит, библиотека не работает, а автоматизировать скачивание надо. Значит придется искать другие решения. Но, что-то с поиском библиотек у меня не задалось. youtube-dl использовать не хочется. Да и не уверен, что он сейчас работает. Вот его не проверял. Каюсь. Впрочем, я просто нашел свое, несколько «костыльное» решение, но оно работает и довольно неплохо. Именно им я и хочу с вами поделиться. Давайте напишем свой «YouTube Downloader» на Python.

И вот какой Франкенштейн у меня получился в итоге…


Что потребуется?

Для отправки запросов нужно установить библиотеку requests, а также я использовал библиотеку tqdm для добавления в скрипт индикатора загрузки. Все же с ней веселее. Установка стандартная. Пишем в терминале:

Код:
pip install requests
pip install tqdm

После установки импортируем библиотеки в наш скрипт. Так же понадобиться импортировать библиотеку os для проверки и объединения путей к папкам и файлам. Вот блок импорта, который должен получиться в итоге:

Python:
import os.path
import time

import requests
from tqdm import tqdm

Как видите, на этот раз кучи библиотек не потребуется. В основном все будет совершаться с помощью запросов. Ими можно довольно много сделать. Нужно только понять, где и что спрашивать )))

Для начала я, конечно же, полез на сам YouTube. И немного поковырявшись в инструментах разработчика накопал довольно интересный запрос, в котором, в виде JSON прилетала самая разнообразная информация о видео, в том числе и ссылки.

screenshot2.png

Но, наличие ссылок на видео не дало особого результата. Да, по ним можно было открыть видео и даже его скачать. Вот только скачивание происходило со скоростью черепахи, по чайной ложке в час. Я понял, что это не тот вариант, что нужен. Но вот то, что в данном запросе содержалась информация о видео, это могло пригодиться в будущем. Поэтому я взял его себе на заметку.

Тут надо пояснить, что данного запроса, сколько бы я не ковырял код в браузерах, которые у меня установлены для теста, а значит я не пользуюсь ими постоянно, нет.

screenshot3.png

Немного подумав и поэкспериментировав, я понял, что в появлении данного интересного запроса «виновно» расширение, которое установлено у меня для кастомизации просмотра видео, а именно: Enhancer for YouTube. Именно оно отправляет этот запрос для каких-то своих «злодейских» целей. Но, тем не менее, скачать видео этот запрос не поможет, а значит надо искать дальше.

Я поступил просто. Ввел запрос в поисковике и стал смотреть, как скачивается видео в популярных загрузчиках с сайтов. Большинство ничего внятного не предоставляли. Но, набрел я на сайт Freemake, тот, что разрабатывает Freemake Video Downloader. Оказывается, на нем тоже можно скачать видео с YouTube. Залез я в запросы и понял, это оно. А походив по ссылкам убедился, что это оно еще больше.

screenshot4.png

Тогда я скопировал cURL данного запроса с помощью правой кнопки мыши. Там нужно выбрать пункт меню: Copy -> Copy as cURL (bash)». Убедился, что это GET-запрос и пошел на сайт curlconverter.com добывать код из скопированного безобразия. Там все просто. Выбираем тип запроса, get или post, вставляем скопированный запрос и получаем код питона. Ну или одного из тех языков, что представлены на этом сайте.

Скопировал полученный код полностью и стал делать функцию загрузки. Причем, хочу обратить внимание на то, что из данного запроса я не убирал вообще ничего. Для начала, я конечно же, попытался сократить заголовки. И до какого-то момента вроде бы все шло хорошо. Но, это пока я не менял идентификатор видео. Как только это происходило, начиналась ерунда. Очевидно, сервер понимал, что его желают налюбить и в ответ отправлял JSON с совершенно случайными видео, которые к требуемому не имели никакого отношения. Так что, немного подумав, я оставил заголовки полностью. Что же, давайте приступим к написанию кода функции.

Я создал функцию с именем get_video_download(vid_id, channel_name), которая на входе получает идентификатор видео, он содержится в ссылке, после знака «=», и имя канала, которое нужно в данном случае для того, чтобы скачиваемое видео помещать в отдельную папку, а не просто создавать папку типа Video Download и кидать все в кучу. Заголовки я пока опущу. Их можно будет увидеть в полном коде функции. Начнем с того момента, где уже и происходит получение JSON и загрузка видео.

Для начала вывожу принт в терминал, чтобы не было скучно, а после отправляю запрос на получение JSON, в котором параметр vid_id получается из ссылки, которую ввел пользователь для загрузки. Далее, уже в полученном JSON нахожу секцию, где указывается качество видео. Данный сайт позволяет скачивать видео в качестве 720р и 360р, именно mp4. Ниже есть еще пара пунктов, но они относятся к форматам 3gp и mp4a. Если все в порядке, и тэг соответствует, получаю название видео. Как вы видели сами, в названии видео содержится большое количество всяческих символов, которые просто не совместимы с тем, чтобы сохранять в операционной системе. А так, как название видео нужно будет именно для того, чтобы не скачивать его в обезличенном виде, а сохранять с тем названием, что и на сервисе, требуется его очистить от всякого мусора, что я и делаю в цикле, перебирая словарь. В теории, туда можно загнать еще больше символов. Так как я загнал только те, с которыми столкнулся. А кто его знает, что будет в голове у автора, когда он будет давать название. Ну и следом получаю ссылку на загрузку.

Python:
print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']

Двигаемся дальше. Теперь для того, чтобы сохранить видео, я создаю папку, если ее конечно не существует, с названием канала. Где я беру название канала, чуть позже, так как в этом запросе его нет. Ну и вывожу разные принты, чтобы было не скучно.

Python:
print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')

А теперь, собственно, основная, самая большая часть функции по загрузке видео. На самом деле, загрузка видео тут в несколько строчек кода. Основная часть отведена на различные проверки и запросы информации, если что-то не так у пользователя, чтобы программа просто не закрывалась без объяснения причин. Отправляю запрос на загрузку. Оказывается, у request есть интересный параметр stream. С его помощью можно переписать поведение загрузки тела ответа, которое по умолчанию загружается сразу же, а при указании параметра делает отсрочку загрузки, пока не будет получен доступ к атрибуту content. Это все нужно для того, чтобы реализовать индикатор загрузки, который здесь представлен библиотекой tqdm. Для начала устанавливаем количество заголовков запроса в переменную total. А далее, по мере загрузки контента и получения заголовков, увеличиваем данный параметр на количество заголовков. И выводим в терминал в удобоваримом виде.

Python:
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)

Ну, а если нет видео в качестве 720р, то сообщаем об этом пользователю, спрашиваем, желает ли он загрузить видео в том качестве, что есть. Если да, то загружаем. Если нет, прерываем выполнение функции и выводим первоначальное меню. Ну, а если пользователь ввел совсем не то, то посылаем его в незабываемое путешествие по экзотическим странам (перечеркнуть) сообщаем, что он ввел чушь и прерываем работу скрипта.

Python:
def get_video_download(vid_id, channel_name):
    headers = {
        'authority': 'downloader.freemake.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'dnt': '1',
        'x-cf-country': 'RU',
        'sec-ch-ua-mobile': '?0',
        'x-user-platform': 'Win32',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'x-user-browser': 'YaBrowser',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'x-analytics-header': 'UA-18256617-1',
        'x-request-attempt': '1',
        'x-user-id': '94119398-e27a-3e13-be17-bbe7fbc25874',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://www.freemake.com',
        'sec-fetch-site': 'same-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://www.freemake.com/ru/free_video_downloader/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']
        print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
            return
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)

Однако, как вы понимаете перед тем, как приступить к загрузке видео, нужно получить название канала. А где его взять, вот в чем вопрос. И я вспомнил о том запросе, который отправляло расширение на самой странице с видео. Тогда я так же скопировал его cURL, получил код и выполнил. В ответ прилетел JSON, в котором была информация о видео, в том числе и название канала. Знаю, это выглядит, как если бы я стрелял из пушки по воробьям. Но использовать BeautifulSoup и искать название канала на странице видео не хотелось. Тем более, что вот так вот, с получением JSON оказалось проще. Поэтому, я отправляю запрос на получение. В запросе, в свою очередь, отправляется JSON с кучей параметров, которые нас в общем-то не интересуют. Здесь нужно поменять только идентификатор видео. Меняется он вот тут:

Python:
json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',

У меня он обозначен как vid_id и передается в функцию при ее вызове. Таким образом я создал функцию get_channel_name(vid_id), которая на входе получает идентификатор, делает запрос. Выковыривает из него название канала. Чистит от «мусора» в виде символов и возвращает очищенное название туда, откуда вызывалась функция.

Python:
def get_channel_name(vid_id):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 Safari/537.36',
        'accept': '*/*',
    }

    params = {
        'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
        'prettyPrint': 'false',
    }

    json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',
                'deviceMake': '',
                'deviceModel': '',
                'visitorData': 'CgtrdUNhZ3U2VGNEOCiDndSTBg%3D%3D',
                'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                             'Chrome/98.0.4758.141 Safari/537.36,gzip(gfe)',
                'clientName': 'WEB',
                'clientVersion': '2.20220502.01.00',
                'osName': 'Windows',
                'osVersion': '10.0',
                'originalUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'platform': 'DESKTOP',
                'clientFormFactor': 'UNKNOWN_FORM_FACTOR',
                'configInfo': {
                    'appInstallData': 'CIOd1JMGELiLrgUQmN79EhCUj64FEOqQrgUQw_KtBRCY6q0FELfLrQUQ8IKuBRC7ka4FENSDrgUQ6JCu'
                                      'BRCw7q0FEK_yrQUQgub9EhCR-PwSENi-rQU%3D',
                },
                'userInterfaceTheme': 'USER_INTERFACE_THEME_DARK',
                'timeZone': 'Europe/Moscow',
                'browserName': 'Chrome',
                'browserVersion': '98.0.4758.141',
                'screenWidthPoints': 1137,
                'screenHeightPoints': 870,
                'screenPixelDensity': 1,
                'screenDensityFloat': 1,
                'utcOffsetMinutes': 360,
                'connectionType': 'CONN_CELLULAR_4G',
                'memoryTotalKbytes': '8000000',
                'mainAppWebInfo': {
                    'graftUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                    'webDisplayMode': 'WEB_DISPLAY_MODE_BROWSER',
                    'isWebNativeShareAvailable': True,
                },
                'playerType': 'UNIPLAYER',
                'tvAppInfo': {
                    'livingRoomAppMode': 'LIVING_ROOM_APP_MODE_UNSPECIFIED',
                },
                'clientScreen': 'WATCH_FULL_SCREEN',
            },
            'user': {
                'lockedSafetyMode': False,
            },
            'request': {
                'useSsl': True,
                'internalExperimentFlags': [],
                'consistencyTokenJars': [],
            },
            'adSignalsInfo': {
                'params': [
                    {
                        'key': 'dt',
                        'value': '1651838604229',
                    },
                    {
                        'key': 'flash',
                        'value': '0',
                    },
                    {
                        'key': 'frm',
                        'value': '0',
                    },
                    {
                        'key': 'u_tz',
                        'value': '360',
                    },
                    {
                        'key': 'u_his',
                        'value': '5',
                    },
                    {
                        'key': 'u_h',
                        'value': '1080',
                    },
                    {
                        'key': 'u_w',
                        'value': '1920',
                    },
                    {
                        'key': 'u_ah',
                        'value': '1032',
                    },
                    {
                        'key': 'u_aw',
                        'value': '1920',
                    },
                    {
                        'key': 'u_cd',
                        'value': '24',
                    },
                    {
                        'key': 'bc',
                        'value': '31',
                    },
                    {
                        'key': 'bih',
                        'value': '870',
                    },
                    {
                        'key': 'biw',
                        'value': '1121',
                    },
                    {
                        'key': 'brdim',
                        'value': '43,12,43,12,1920,0,1708,991,1137,870',
                    },
                    {
                        'key': 'vis',
                        'value': '1',
                    },
                    {
                        'key': 'wgl',
                        'value': 'true',
                    },
                    {
                        'key': 'ca_type',
                        'value': 'image',
                    },
                ],
            },
        },
        'playbackContext': {
            'contentPlaybackContext': {
                'html5Preference': 'HTML5_PREF_WANTS',
                'lactMilliseconds': '2979',
                'referer': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'signatureTimestamp': 19117,
                'autonavState': 'STATE_OFF',
                'autoCaptionsDefaultOn': False,
                'mdxContext': {},
                'playerWidthPixels': 647,
                'playerHeightPixels': 364,
            },
        },
        'cpn': 'pwy4NMkpT8PY63hl',
        'captionParams': {
            'deviceCaptionsOn': True,
        },
        'attestationRequest': {
            'omitBotguardData': True,
        },
    }

    print('\n[+] Получаю название канала...')
    channel_name = str(requests.post('https://www.youtube.com/youtubei/v1/player', params=params, headers=headers,
                                     json=json_data).json()['videoDetails']['author'])

    for m in ["?", '"', "/", ":", "#", "|", ",", " ?", "?!", "?!", "? ", " / ", " | "]:
        channel_name = channel_name.replace(m, " ")
    print(f'[+] Название канала получено: "{channel_name}"')
    return channel_name

Не стоит пугаться ее размерами. Большую часть здесь занимает JSON с параметрами, которые передаются с запросом и заголовками. Но избавиться от них не получилось. Потому, чтобы все работало без сбоев, я оставил их без изменения. И использовал так, как они и отправляются в запросе на странице.

И по сути, большая часть скрипта уже готова. Но, меня не оставляло смутное чувство, что я что-то упустил. И да, немного подумав, я понял, что есть еще ведь плейлисты, которые тоже неплохо бы выкачивать полностью, только лишь введя на них ссылку. Но для того, чтобы их выкачать, для начала со страницы плейлиста нужно получить все ссылки на видео, которые содержаться в нем. Но фишка тут в том, что бывают такие плейлисты, количество видео в которых не влезает на одну страницу. И тогда они просто подгружаются по мере прокрутки. То есть, использованием простого запроса тут не обойдешься. Нужно подключать что-то посерьезнее. Но использовать selenium мне не хотелось. Да, с помощью этой библиотеки можно было бы в безголовом режиме прокрутить страницу до конца, собрать все ссылки на видео, сложить в список и вернуть для дальнейшего использования. Но, я подумал, что должен быть другой способ получения ссылок. И он таки нашелся. Стоило ввести запрос на загрузку плейлиста, как нашелся сайт, который и предоставлял этот функционал. Я покопался в его запросах и нашел один, в котором отправляется ссылка на плейлист, а в ответ прилетает JSON уже с готовыми идентификаторами видео. Ну не красота ли.

screenshot5.png

Тут все по накатанной. Копируется cURL, получается код, делается запрос и забирается JSON, из которого выкорчевываются идентификаторы, помещаются в список и возвращают его туда, откуда запрос был сделан.

Кстати, здесь, прямо на сайте есть специальное поле, куда помещаются ссылки на загрузку видео из плейлиста уже после выполнения запроса. В самом запросе они «медленные», то есть загрузка по ним происходит со скоростью черепахи. А вот на сайте они уже переформатируются в нужный вид и по ним видео загружается хорошо. Но, чтобы забрать их отсюда нужен опять же selenium, а это снова усложняет скрипт. А так, ссылки интересные. Если понять, как преобразуются исходные ссылки именно в такой вид, то можно и самому их конвертировать. Но, пока что я не стал разбираться. Может быть в будущем.

screenshot6.png

Создаю функцию playlist_item(url). Здесь в запросе так же передается небольшой словарик с параметрами, одним из которых является ссылка на плейлист. Вот ее и получает данная функция на входе и передает в словарик.

Python:
params = {
        'url': url,
        'nextPageToken': '',
    }

Ну, а больше особо и пояснять нечего. Все довольно просто и понятно. Цикл и перебор.

Python:
def playlist_item(url):
    headers = {
        'authority': 'api.youtubemultidownloader.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'dnt': '1',
        'sec-ch-ua-mobile': '?0',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://youtubemultidownloader.net',
        'sec-fetch-site': 'cross-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://youtubemultidownloader.net/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    params = {
        'url': url,
        'nextPageToken': '',
    }

    response = requests.get('https://api.youtubemultidownloader.com/playlist', params=params, headers=headers).json()
    list_items = []
    for item in range(0, len(response['items'])):
        list_items.append(response['items'][item]['id'])
    return list_items

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

Создаю функцию get_target_path(user_input), которая на входе принимает первоначальный пользовательский ввод и обрабатывает его в зависимости от того, что ввел пользователь.

Думаю, что подробно на этом останавливаться не стоит, так как тут не используется чего-то сверхъестественного. Всего лишь проверки if, elif, else. Ну и, если пользователь ввел что-то не так, в бесконечном цикле запрос правильного параметра. Тут, конечно, я предусмотрел возможность выхода в основное меню, так как пользователь может не понять, что не так и попросту запутаться. А цикл будет долбить его снова и снова. А потому, лучше предоставить ему, то есть пользователю, небольшую лазейку, возможность сбежать из бесконечного цикла. А обрабатываются здесь запросы, которые представлены на скриншоте ниже.


screenshot7.png


Python:
def get_target_path(user_input):
    if user_input == "1":
        vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com" in vid_id:
            vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        if '&list' in vid_id:
            vid_id = vid_id.split("&")[0].split("=")[-1]
        else:
            vid_id = vid_id.split("=")[-1]
        channel_name = get_channel_name(vid_id)
        get_video_download(vid_id, channel_name)
        main()
    elif user_input == "2":
        while not os.path.isfile(user_path := input("\t[+] Введите путь к списку\n\t[+] Для выхода в меню введите: ex\n"
                                                    "\t>>> ").replace('"', '')):
            if user_path == 'ex':
                main()
                return
            print(f"\n\t[+] Список {user_path} не найден\n")
        with open(f'{user_path}', 'r', encoding='utf-8') as file:
            video_list = file.readlines()
        for video in video_list:
            if '&list' in video:
                vid_id = video.split("&")[0].split("=")[-1]
            else:
                vid_id = video.split("=")[-1].strip()
            if video.strip() == "":
                continue
            else:
                channel_name = get_channel_name(vid_id)
                get_video_download(vid_id, channel_name)
        main()
    elif user_input == "3":
        vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com/playlist" in vid_id:
            vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        list_items = playlist_item(vid_id)
        print(f'[+] Видео в плейлисте: {len(list_items)}\n[+] Загружаю плейлист...')
        for item in list_items:
            channel_name = get_channel_name(item)
            get_video_download(item, channel_name)
        main()
    elif user_input == "4":
        exit(0)
    else:
        main()

И остается функция main(), в которой и делается первоначальный выбор и осуществляется пользовательский ввод.

Python:
def main():
    get_target_path(input(f'\n[+] Выберите варианты загрузки:\n\t[1] Загрузить видео\n'
                          f'\t[2] Загрузить видео из списка\n\t[3] Загрузить плейлист\n\t[4] Выход\n\t>>> '))

Вот, в принципе и все. Если собрать все эти функции в кучку в одном скрипте, то получиться годный загрузчик видео. Данный код работает на обеих платформах. Как на Windows, так и на Linux. На MacOS не проверял, ибо нет у меня этой «заразы». Но, думаю, что и на ней будет работать. А ниже небольшое видео, которое демонстрирует работу скрипта.


Python:
import os.path
import time

import requests
from tqdm import tqdm


def playlist_item(url):
    headers = {
        'authority': 'api.youtubemultidownloader.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'dnt': '1',
        'sec-ch-ua-mobile': '?0',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://youtubemultidownloader.net',
        'sec-fetch-site': 'cross-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://youtubemultidownloader.net/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    params = {
        'url': url,
        'nextPageToken': '',
    }

    response = requests.get('https://api.youtubemultidownloader.com/playlist', params=params, headers=headers).json()
    list_items = []
    for item in range(0, len(response['items'])):
        list_items.append(response['items'][item]['id'])
    return list_items


def get_channel_name(vid_id):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 Safari/537.36',
        'accept': '*/*',
    }

    params = {
        'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
        'prettyPrint': 'false',
    }

    json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',
                'deviceMake': '',
                'deviceModel': '',
                'visitorData': 'CgtrdUNhZ3U2VGNEOCiDndSTBg%3D%3D',
                'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                             'Chrome/98.0.4758.141 Safari/537.36,gzip(gfe)',
                'clientName': 'WEB',
                'clientVersion': '2.20220502.01.00',
                'osName': 'Windows',
                'osVersion': '10.0',
                'originalUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'platform': 'DESKTOP',
                'clientFormFactor': 'UNKNOWN_FORM_FACTOR',
                'configInfo': {
                    'appInstallData': 'CIOd1JMGELiLrgUQmN79EhCUj64FEOqQrgUQw_KtBRCY6q0FELfLrQUQ8IKuBRC7ka4FENSDrgUQ6JCu'
                                      'BRCw7q0FEK_yrQUQgub9EhCR-PwSENi-rQU%3D',
                },
                'userInterfaceTheme': 'USER_INTERFACE_THEME_DARK',
                'timeZone': 'Europe/Moskow',
                'browserName': 'Chrome',
                'browserVersion': '98.0.4758.141',
                'screenWidthPoints': 1137,
                'screenHeightPoints': 870,
                'screenPixelDensity': 1,
                'screenDensityFloat': 1,
                'utcOffsetMinutes': 360,
                'connectionType': 'CONN_CELLULAR_4G',
                'memoryTotalKbytes': '8000000',
                'mainAppWebInfo': {
                    'graftUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                    'webDisplayMode': 'WEB_DISPLAY_MODE_BROWSER',
                    'isWebNativeShareAvailable': True,
                },
                'playerType': 'UNIPLAYER',
                'tvAppInfo': {
                    'livingRoomAppMode': 'LIVING_ROOM_APP_MODE_UNSPECIFIED',
                },
                'clientScreen': 'WATCH_FULL_SCREEN',
            },
            'user': {
                'lockedSafetyMode': False,
            },
            'request': {
                'useSsl': True,
                'internalExperimentFlags': [],
                'consistencyTokenJars': [],
            },
            'adSignalsInfo': {
                'params': [
                    {
                        'key': 'dt',
                        'value': '1651838604229',
                    },
                    {
                        'key': 'flash',
                        'value': '0',
                    },
                    {
                        'key': 'frm',
                        'value': '0',
                    },
                    {
                        'key': 'u_tz',
                        'value': '360',
                    },
                    {
                        'key': 'u_his',
                        'value': '5',
                    },
                    {
                        'key': 'u_h',
                        'value': '1080',
                    },
                    {
                        'key': 'u_w',
                        'value': '1920',
                    },
                    {
                        'key': 'u_ah',
                        'value': '1032',
                    },
                    {
                        'key': 'u_aw',
                        'value': '1920',
                    },
                    {
                        'key': 'u_cd',
                        'value': '24',
                    },
                    {
                        'key': 'bc',
                        'value': '31',
                    },
                    {
                        'key': 'bih',
                        'value': '870',
                    },
                    {
                        'key': 'biw',
                        'value': '1121',
                    },
                    {
                        'key': 'brdim',
                        'value': '43,12,43,12,1920,0,1708,991,1137,870',
                    },
                    {
                        'key': 'vis',
                        'value': '1',
                    },
                    {
                        'key': 'wgl',
                        'value': 'true',
                    },
                    {
                        'key': 'ca_type',
                        'value': 'image',
                    },
                ],
            },
        },
        'playbackContext': {
            'contentPlaybackContext': {
                'html5Preference': 'HTML5_PREF_WANTS',
                'lactMilliseconds': '2979',
                'referer': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'signatureTimestamp': 19117,
                'autonavState': 'STATE_OFF',
                'autoCaptionsDefaultOn': False,
                'mdxContext': {},
                'playerWidthPixels': 647,
                'playerHeightPixels': 364,
            },
        },
        'cpn': 'pwy4NMkpT8PY63hl',
        'captionParams': {
            'deviceCaptionsOn': True,
        },
        'attestationRequest': {
            'omitBotguardData': True,
        },
    }

    print('\n[+] Получаю название канала...')
    channel_name = str(requests.post('https://www.youtube.com/youtubei/v1/player', params=params, headers=headers,
                                     json=json_data).json()['videoDetails']['author'])

    for m in ["?", '"', "/", ":", "#", "|", ",", " ?", "?!", "?!", "? ", " / ", " | "]:
        channel_name = channel_name.replace(m, " ")
    print(f'[+] Название канала получено: "{channel_name}"')
    return channel_name


def get_video_download(vid_id, channel_name):
    headers = {
        'authority': 'downloader.freemake.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'dnt': '1',
        'x-cf-country': 'RU',
        'sec-ch-ua-mobile': '?0',
        'x-user-platform': 'Win32',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'x-user-browser': 'YaBrowser',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'x-analytics-header': 'UA-18256617-1',
        'x-request-attempt': '1',
        'x-user-id': '94119398-e27a-3e13-be17-bbe7fbc25874',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://www.freemake.com',
        'sec-fetch-site': 'same-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://www.freemake.com/ru/free_video_downloader/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']
        print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
            return
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)


def get_target_path(user_input):
    if user_input == "1":
        vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com" in vid_id:
            vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        if '&list' in vid_id:
            vid_id = vid_id.split("&")[0].split("=")[-1]
        else:
            vid_id = vid_id.split("=")[-1]
        channel_name = get_channel_name(vid_id)
        get_video_download(vid_id, channel_name)
        main()
    elif user_input == "2":
        while not os.path.isfile(user_path := input("\t[+] Введите путь к списку\n\t[+] Для выхода в меню введите: ex\n"
                                                    "\t>>> ").replace('"', '')):
            if user_path == 'ex':
                main()
                return
            print(f"\n\t[+] Список {user_path} не найден\n")
        with open(f'{user_path}', 'r', encoding='utf-8') as file:
            video_list = file.readlines()
        for video in video_list:
            if '&list' in video:
                vid_id = video.split("&")[0].split("=")[-1]
            else:
                vid_id = video.split("=")[-1].strip()
            if video.strip() == "":
                continue
            else:
                channel_name = get_channel_name(vid_id)
                get_video_download(vid_id, channel_name)
        main()
    elif user_input == "3":
        vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com/playlist" in vid_id:
            vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        list_items = playlist_item(vid_id)
        print(f'[+] Видео в плейлисте: {len(list_items)}\n[+] Загружаю плейлист...')
        for item in list_items:
            channel_name = get_channel_name(item)
            time.sleep(0.3)
            get_video_download(item, channel_name)
            time.sleep(0.3)
        main()
    elif user_input == "4":
        exit(0)
    else:
        main()


def main():
    get_target_path(input(f'\n[+] Выберите варианты загрузки:\n\t[1] Загрузить видео\n'
                          f'\t[2] Загрузить видео из списка\n\t[3] Загрузить плейлист\n\t[4] Выход\n\t>>> '))


if __name__ == "__main__":
    main()

Спасибо за внимание. Надеюсь, что данная информация будет кому-нибудь полезной
 

Вложения

  • video_youtube_downloader.zip
    3,9 КБ · Просмотры: 468
Последнее редактирование:

MLNK

Mod. Ethical Hacking
Red Team
23.01.2018
560
706
BIT
1
Не плохая статья, хорошо для понимания что как что искать и решать проблему.
Хотя я бы просто использовал youtube-dl
 
  • Нравится
Реакции: Johan Van

MLNK

Mod. Ethical Hacking
Red Team
23.01.2018
560
706
BIT
1
И ещё заведи себе какой нить гит чтобы туда выкладывать подобные проекты. Не всегда удобно качать архивы ;)
 

Johan Van

Green Team
13.06.2020
350
656
BIT
135
И ещё заведи себе какой нить гит чтобы туда выкладывать подобные проекты. Не всегда удобно качать архивы ;)

Я попытаюсь )) Надо научиться с ним обращаться. Он ведь как женщина, нежный и нервный )) Ну, а так да, наверное уже надо. А то я сам уже начинаю путаться в своих архивах )
 

MLNK

Mod. Ethical Hacking
Red Team
23.01.2018
560
706
BIT
1
для начала достаточно инит коммит и пуш. Остальное по ходу уже освоишь.
 
  • Нравится
Реакции: Johan Van

toldrin

New member
07.05.2022
2
0
BIT
0
Начал читать, дошёл до момента, где ты json смотришь, решил у себя тоже глянуть его. Открыл хром - нету, фокс - тоже, опера - сейм и только потом прочитал про то, что этот запрос отправляло расширение))))
Кстати, а где вообще можно бы узнать как устроены под капотом эти сервисы для скачивания видео?
 

Johan Van

Green Team
13.06.2020
350
656
BIT
135
Начал читать, дошёл до момента, где ты json смотришь, решил у себя тоже глянуть его. Открыл хром - нету, фокс - тоже, опера - сейм и только потом прочитал про то, что этот запрос отправляло расширение))))
Кстати, а где вообще можно бы узнать как устроены под капотом эти сервисы для скачивания видео?

Честно говоря, пока не знаю. Но, очень хотелось бы узнать. Скорее всего, они каким-то образом, но это лишь мое предположение, со ссылкой работают. Ведь если посмотреть в том сервисе, где можно скачивать плейлисты, там на страницу ссылки для загрузки выводятся. Так вот они похожи на те, что и ведут на медленные ссылки для плеера. Только каким-то образом модифицированные. Тут надо больше информации получить о том, где храниться видео в гугле, чтобы от этого уже отталкиваться )) Но, ведь кто-то же все это знает )))
 

toldrin

New member
07.05.2022
2
0
BIT
0
Появилось желание скачивать таким же образом только аудиоряд с видео. В json'е от freemake есть url на m4a, попробовал использовать его, но почему-то загрузка такого файла идёт в разы медленее, чем у полноценного видео. Кто знает с чем это может быть связано?
 

Johan Van

Green Team
13.06.2020
350
656
BIT
135
Появилось желание скачивать таким же образом только аудиоряд с видео. В json'е от freemake есть url на m4a, попробовал использовать его, но почему-то загрузка такого файла идёт в разы медленее, чем у полноценного видео. Кто знает с чем это может быть связано?

Возможно потому, что аудио по ссылке скачивается со скоростью воспроизведения. Но это только мое предположение. Так как скорость загрузки около 30 кбит, то есть, как и скорость воспроизведения.

Я тут посмотрел несколько загрузчиков именно mp3. Они работают немного по другому принципу. То есть, к примеру, если посмотреть на странице медиа, в инспекторе, то даже короткое видео загружается частями. Так вот загрузчики делают несколько запросов, похоже, что выкачивают их себе на сервер, там объединяют и конвертируют. А потом уже предоставляют ссылку на загрузку. Обратите внимание на скриншот.

screenshot1.png

Тут делается изначальный запрос на сервер для конвертации. А потом запросы на наличие уже готового файла. Вот в последнем запросе только ссылка на загрузку. Это уже после того, как сервер скачал все фрагменты, объединил и конвертировал. Может быть поэтому... Точнее не могу сказать. Тут надо еще глубже копать )
 
Последнее редактирование:
  • Нравится
Реакции: toldrin

dream to perfection

Green Team
11.06.2022
59
35
BIT
0
pytube работает, проверял на GUI, качает видосы как надо, но такая тема тоже топ, автор как всегда на высоте....
 
  • Нравится
Реакции: Johan Van

Johan Van

Green Team
13.06.2020
350
656
BIT
135
pytube работает, проверял на GUI, качает видосы как надо, но такая тема тоже топ, автор как всегда на высоте....

Значит автор библиотеки поправил скрипт. Можно использовать. И это хорошо
 
  • Нравится
Реакции: dream to perfection

dream to perfection

Green Team
11.06.2022
59
35
BIT
0
Значит автор библиотеки поправил скрипт. Можно использовать. И это хорошо


Python:
from tkinter import *
from pytube import YouTube

# настройка окна
root = Tk()
root.geometry("700x300")
root.resizable(0, 0)
root.title("YouTube Video Downloader")


Label(root, text="YouTube Downloader Videos",
      font = "arial 26 bold").pack()

# линк
link = StringVar()


Label(root, text="Вставьте ссылку здесь:", font="arial 15 bold").place(x=270, y=60)
Entry(root, width=80, textvariable=link).place(x=32, y=90)


# функция скачивание видео


def Downloader():
    url = YouTube(str(link.get()))
    video = url.streams.first()
    video.download()
    Label(root, text="ВИДЕО ЗАГРУЖЕНО!", font = "arial 15").place(x=270, y=210)


# кнопка скачивание

Button(root, text="Скачать видео", font = "arial 15 bold", bg="white", padx=2, command=Downloader).place(x=280, y=150)


root.mainloop()

Screenshot_20220613_142741.png


После скачивание видео, появится файл с форматом MP4 где находится ваш Py Файл
Screenshot_20220613_143055.png

P.S оформление окошки жутко согласен, можно чуток по красивее сделать.
 
  • Нравится
Реакции: vov76 и Johan Van

Johan Van

Green Team
13.06.2020
350
656
BIT
135
Python:
from tkinter import *
from pytube import YouTube

# настройка окна
root = Tk()
root.geometry("700x300")
root.resizable(0, 0)
root.title("YouTube Video Downloader")


Label(root, text="YouTube Downloader Videos",
      font = "arial 26 bold").pack()

# линк
link = StringVar()


Label(root, text="Вставьте ссылку здесь:", font="arial 15 bold").place(x=270, y=60)
Entry(root, width=80, textvariable=link).place(x=32, y=90)


# функция скачивание видео


def Downloader():
    url = YouTube(str(link.get()))
    video = url.streams.first()
    video.download()
    Label(root, text="ВИДЕО ЗАГРУЖЕНО!", font = "arial 15").place(x=270, y=210)


# кнопка скачивание

Button(root, text="Скачать видео", font = "arial 15 bold", bg="white", padx=2, command=Downloader).place(x=280, y=150)


root.mainloop()



После скачивание видео, появится файл с форматом MP4 где находится ваш Py Файл

P.S оформление окошки жутко согласен, можно чуток по красивее сделать.

У меня есть что-то похожее. Но без GUI )) Я тогда видео активно скачивал. Потому делал себе скриптик, который качает не только по одному видео, а и плейлистами, и по списку ))) Просто время такое было, скорее всего ютуб обновлял алгоритмы. И у них в pytube перестала регулярка работать. Вот потому я и не использовал его, а стал искать другие варианты. По одному я и без питона могу )) Надо будет проверить, работает тот скрипт или нет. Как время появиться проверю ))
 
  • Нравится
Реакции: dream to perfection

dream to perfection

Green Team
11.06.2022
59
35
BIT
0
У меня есть что-то похожее. Но без GUI )) Я тогда видео активно скачивал. Потому делал себе скриптик, который качает не только по одному видео, а и плейлистами, и по списку ))) Просто время такое было, скорее всего ютуб обновлял алгоритмы. И у них в pytube перестала регулярка работать. Вот потому я и не использовал его, а стал искать другие варианты. По одному я и без питона могу )) Надо будет проверить, работает тот скрипт или нет. Как время появиться проверю ))
с pytube возникают постоянные проблемы, но твой скрипт лучше чем этот, пользователь может выбрать любой вариант для скачивание видосов, такой вариант тоже идеальный... Продолжай в том же духе!
 

Johan Van

Green Team
13.06.2020
350
656
BIT
135
P.S оформление окошки жутко согласен, можно чуток по красивее сделать.

Да нормальное окошко, учитывая, что на Tkinter )) Я его почему-то не очень. Не знаю почему, но как изначально не понравился... мне больше PyQt подходит. Может быть потому, что я уже довольно давно был избалован Delphi )) А там ведь сделать окошко можно было вообще не напрягаясь )
 

dream to perfection

Green Team
11.06.2022
59
35
BIT
0
Да нормальное окошко, учитывая, что на Tkinter )) Я его почему-то не очень. Не знаю почему, но как изначально не понравился... мне больше PyQt подходит. Может быть потому, что я уже довольно давно был избалован Delphi )) А там ведь сделать окошко можно было вообще не напрягаясь )
Здесь ты уже прав, PyQt5 намного лучше чем Tkinter, но почему-то все использует tkinter, он легче чем PyQt5...)
 

vov76

One Level
13.07.2019
2
1
BIT
0
Что вы делаете, когда вам нужно сохранить определенное видео с YouTube, чтобы оно не потерялось? Ну, тут логично. Можно в самом YouTube создать плейлист и добавлять туда все, что нужно. Можно просто добавить страницу в закладки. Да много чего можно сделать. А еще скачать видео себе на жесткий диск. Мало ли чего, всякое бывает. А так под рукой и в сохранности. Вот только способов для скачивания становится все меньше и меньше. Давайте попробуем скачать видео с YouTube с помощью питона. Нет, здесь не будет очередного руководства по pytube. Все интереснее и печальнее.


Не так давно промелькнула новость о том, что проект YouTube Vanced закрывается из-за юридического давления со стороны Google. И вроде бы ладно. Сколько еще таких проектов. Тем более, что для скачивания видео с сервиса я пользовался скриптом на питоне, в котором с помощью библиотеки pytube все благополучно скачивалось. Но, через какое-то время после этой новости pytube вдруг перестал работать. Раз и все. Ладно. Я подумал, что это всего лишь ошибка в моей программе. Сейчас поправлю и все заработает. Но, не тут-то было. Ошибок я особо-то и не нашел. А вот pytube не работал даже на простейших примерах, которые у него описаны на странице загрузки. Я решил, что это временные трудности. На время оставил данный проект в покое. Но, вот спустя почти месяц я снова к нему вернулся. И ничего не изменилось.

Тут, скорее всего, все просто. У YouTube слегка изменился код, а следовательно, и алгоритм поиска ссылок у pytube тоже должен было обновиться. Но, этого не произошло. Покопавшись в интернете, я нашел несколько решений. Нужно заменить регулярные выражения в коде модуля и все должно было заработать. Но, у меня не получилось. Хотя, делал все так как сказано, вплоть до строчек кода. Кстати, в самом коде видно, что алгоритм поиска по регулярным выражениям менялся уже не один раз, так как много строк просто закомментировано. В общем и целом, не знаю, «восстанет ли этот проект из пепла», так сказать. Будем надеяться, что да. Хороший был проект. Полезный.


К чему это я веду. А все просто. Время проходит, библиотека не работает, а автоматизировать скачивание надо. Значит придется искать другие решения. Но, что-то с поиском библиотек у меня не задалось. youtube-dl использовать не хочется. Да и не уверен, что он сейчас работает. Вот его не проверял. Каюсь. Впрочем, я просто нашел свое, несколько «костыльное» решение, но оно работает и довольно неплохо. Именно им я и хочу с вами поделиться. Давайте напишем свой «YouTube Downloader» на Python.

И вот какой Франкенштейн у меня получился в итоге…


Что потребуется?

Для отправки запросов нужно установить библиотеку requests, а также я использовал библиотеку tqdm для добавления в скрипт индикатора загрузки. Все же с ней веселее. Установка стандартная. Пишем в терминале:

Код:
pip install requests
pip install tqdm

После установки импортируем библиотеки в наш скрипт. Так же понадобиться импортировать библиотеку os для проверки и объединения путей к папкам и файлам. Вот блок импорта, который должен получиться в итоге:

Python:
import os.path
import time

import requests
from tqdm import tqdm

Как видите, на этот раз кучи библиотек не потребуется. В основном все будет совершаться с помощью запросов. Ими можно довольно много сделать. Нужно только понять, где и что спрашивать )))

Для начала я, конечно же, полез на сам YouTube. И немного поковырявшись в инструментах разработчика накопал довольно интересный запрос, в котором, в виде JSON прилетала самая разнообразная информация о видео, в том числе и ссылки.


Но, наличие ссылок на видео не дало особого результата. Да, по ним можно было открыть видео и даже его скачать. Вот только скачивание происходило со скоростью черепахи, по чайной ложке в час. Я понял, что это не тот вариант, что нужен. Но вот то, что в данном запросе содержалась информация о видео, это могло пригодиться в будущем. Поэтому я взял его себе на заметку.

Тут надо пояснить, что данного запроса, сколько бы я не ковырял код в браузерах, которые у меня установлены для теста, а значит я не пользуюсь ими постоянно, нет.


Немного подумав и поэкспериментировав, я понял, что в появлении данного интересного запроса «виновно» расширение, которое установлено у меня для кастомизации просмотра видео, а именно: Enhancer for YouTube. Именно оно отправляет этот запрос для каких-то своих «злодейских» целей. Но, тем не менее, скачать видео этот запрос не поможет, а значит надо искать дальше.

Я поступил просто. Ввел запрос в поисковике и стал смотреть, как скачивается видео в популярных загрузчиках с сайтов. Большинство ничего внятного не предоставляли. Но, набрел я на сайт Freemake, тот, что разрабатывает Freemake Video Downloader. Оказывается, на нем тоже можно скачать видео с YouTube. Залез я в запросы и понял, это оно. А походив по ссылкам убедился, что это оно еще больше.


Тогда я скопировал cURL данного запроса с помощью правой кнопки мыши. Там нужно выбрать пункт меню: Copy -> Copy as cURL (bash)». Убедился, что это GET-запрос и пошел на сайт curlconverter.com добывать код из скопированного безобразия. Там все просто. Выбираем тип запроса, get или post, вставляем скопированный запрос и получаем код питона. Ну или одного из тех языков, что представлены на этом сайте.

Скопировал полученный код полностью и стал делать функцию загрузки. Причем, хочу обратить внимание на то, что из данного запроса я не убирал вообще ничего. Для начала, я конечно же, попытался сократить заголовки. И до какого-то момента вроде бы все шло хорошо. Но, это пока я не менял идентификатор видео. Как только это происходило, начиналась ерунда. Очевидно, сервер понимал, что его желают налюбить и в ответ отправлял JSON с совершенно случайными видео, которые к требуемому не имели никакого отношения. Так что, немного подумав, я оставил заголовки полностью. Что же, давайте приступим к написанию кода функции.

Я создал функцию с именем get_video_download(vid_id, channel_name), которая на входе получает идентификатор видео, он содержится в ссылке, после знака «=», и имя канала, которое нужно в данном случае для того, чтобы скачиваемое видео помещать в отдельную папку, а не просто создавать папку типа Video Download и кидать все в кучу. Заголовки я пока опущу. Их можно будет увидеть в полном коде функции. Начнем с того момента, где уже и происходит получение JSON и загрузка видео.

Для начала вывожу принт в терминал, чтобы не было скучно, а после отправляю запрос на получение JSON, в котором параметр vid_id получается из ссылки, которую ввел пользователь для загрузки. Далее, уже в полученном JSON нахожу секцию, где указывается качество видео. Данный сайт позволяет скачивать видео в качестве 720р и 360р, именно mp4. Ниже есть еще пара пунктов, но они относятся к форматам 3gp и mp4a. Если все в порядке, и тэг соответствует, получаю название видео. Как вы видели сами, в названии видео содержится большое количество всяческих символов, которые просто не совместимы с тем, чтобы сохранять в операционной системе. А так, как название видео нужно будет именно для того, чтобы не скачивать его в обезличенном виде, а сохранять с тем названием, что и на сервисе, требуется его очистить от всякого мусора, что я и делаю в цикле, перебирая словарь. В теории, туда можно загнать еще больше символов. Так как я загнал только те, с которыми столкнулся. А кто его знает, что будет в голове у автора, когда он будет давать название. Ну и следом получаю ссылку на загрузку.

Python:
print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']

Двигаемся дальше. Теперь для того, чтобы сохранить видео, я создаю папку, если ее конечно не существует, с названием канала. Где я беру название канала, чуть позже, так как в этом запросе его нет. Ну и вывожу разные принты, чтобы было не скучно.

Python:
print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')

А теперь, собственно, основная, самая большая часть функции по загрузке видео. На самом деле, загрузка видео тут в несколько строчек кода. Основная часть отведена на различные проверки и запросы информации, если что-то не так у пользователя, чтобы программа просто не закрывалась без объяснения причин. Отправляю запрос на загрузку. Оказывается, у request есть интересный параметр stream. С его помощью можно переписать поведение загрузки тела ответа, которое по умолчанию загружается сразу же, а при указании параметра делает отсрочку загрузки, пока не будет получен доступ к атрибуту content. Это все нужно для того, чтобы реализовать индикатор загрузки, который здесь представлен библиотекой tqdm. Для начала устанавливаем количество заголовков запроса в переменную total. А далее, по мере загрузки контента и получения заголовков, увеличиваем данный параметр на количество заголовков. И выводим в терминал в удобоваримом виде.

Python:
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)

Ну, а если нет видео в качестве 720р, то сообщаем об этом пользователю, спрашиваем, желает ли он загрузить видео в том качестве, что есть. Если да, то загружаем. Если нет, прерываем выполнение функции и выводим первоначальное меню. Ну, а если пользователь ввел совсем не то, то посылаем его в незабываемое путешествие по экзотическим странам (перечеркнуть) сообщаем, что он ввел чушь и прерываем работу скрипта.

Python:
def get_video_download(vid_id, channel_name):
    headers = {
        'authority': 'downloader.freemake.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'dnt': '1',
        'x-cf-country': 'RU',
        'sec-ch-ua-mobile': '?0',
        'x-user-platform': 'Win32',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'x-user-browser': 'YaBrowser',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'x-analytics-header': 'UA-18256617-1',
        'x-request-attempt': '1',
        'x-user-id': '94119398-e27a-3e13-be17-bbe7fbc25874',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://www.freemake.com',
        'sec-fetch-site': 'same-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://www.freemake.com/ru/free_video_downloader/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']
        print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
            return
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)

Однако, как вы понимаете перед тем, как приступить к загрузке видео, нужно получить название канала. А где его взять, вот в чем вопрос. И я вспомнил о том запросе, который отправляло расширение на самой странице с видео. Тогда я так же скопировал его cURL, получил код и выполнил. В ответ прилетел JSON, в котором была информация о видео, в том числе и название канала. Знаю, это выглядит, как если бы я стрелял из пушки по воробьям. Но использовать BeautifulSoup и искать название канала на странице видео не хотелось. Тем более, что вот так вот, с получением JSON оказалось проще. Поэтому, я отправляю запрос на получение. В запросе, в свою очередь, отправляется JSON с кучей параметров, которые нас в общем-то не интересуют. Здесь нужно поменять только идентификатор видео. Меняется он вот тут:

Python:
json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',

У меня он обозначен как vid_id и передается в функцию при ее вызове. Таким образом я создал функцию get_channel_name(vid_id), которая на входе получает идентификатор, делает запрос. Выковыривает из него название канала. Чистит от «мусора» в виде символов и возвращает очищенное название туда, откуда вызывалась функция.

Python:
def get_channel_name(vid_id):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 Safari/537.36',
        'accept': '*/*',
    }

    params = {
        'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
        'prettyPrint': 'false',
    }

    json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',
                'deviceMake': '',
                'deviceModel': '',
                'visitorData': 'CgtrdUNhZ3U2VGNEOCiDndSTBg%3D%3D',
                'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                             'Chrome/98.0.4758.141 Safari/537.36,gzip(gfe)',
                'clientName': 'WEB',
                'clientVersion': '2.20220502.01.00',
                'osName': 'Windows',
                'osVersion': '10.0',
                'originalUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'platform': 'DESKTOP',
                'clientFormFactor': 'UNKNOWN_FORM_FACTOR',
                'configInfo': {
                    'appInstallData': 'CIOd1JMGELiLrgUQmN79EhCUj64FEOqQrgUQw_KtBRCY6q0FELfLrQUQ8IKuBRC7ka4FENSDrgUQ6JCu'
                                      'BRCw7q0FEK_yrQUQgub9EhCR-PwSENi-rQU%3D',
                },
                'userInterfaceTheme': 'USER_INTERFACE_THEME_DARK',
                'timeZone': 'Europe/Moscow',
                'browserName': 'Chrome',
                'browserVersion': '98.0.4758.141',
                'screenWidthPoints': 1137,
                'screenHeightPoints': 870,
                'screenPixelDensity': 1,
                'screenDensityFloat': 1,
                'utcOffsetMinutes': 360,
                'connectionType': 'CONN_CELLULAR_4G',
                'memoryTotalKbytes': '8000000',
                'mainAppWebInfo': {
                    'graftUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                    'webDisplayMode': 'WEB_DISPLAY_MODE_BROWSER',
                    'isWebNativeShareAvailable': True,
                },
                'playerType': 'UNIPLAYER',
                'tvAppInfo': {
                    'livingRoomAppMode': 'LIVING_ROOM_APP_MODE_UNSPECIFIED',
                },
                'clientScreen': 'WATCH_FULL_SCREEN',
            },
            'user': {
                'lockedSafetyMode': False,
            },
            'request': {
                'useSsl': True,
                'internalExperimentFlags': [],
                'consistencyTokenJars': [],
            },
            'adSignalsInfo': {
                'params': [
                    {
                        'key': 'dt',
                        'value': '1651838604229',
                    },
                    {
                        'key': 'flash',
                        'value': '0',
                    },
                    {
                        'key': 'frm',
                        'value': '0',
                    },
                    {
                        'key': 'u_tz',
                        'value': '360',
                    },
                    {
                        'key': 'u_his',
                        'value': '5',
                    },
                    {
                        'key': 'u_h',
                        'value': '1080',
                    },
                    {
                        'key': 'u_w',
                        'value': '1920',
                    },
                    {
                        'key': 'u_ah',
                        'value': '1032',
                    },
                    {
                        'key': 'u_aw',
                        'value': '1920',
                    },
                    {
                        'key': 'u_cd',
                        'value': '24',
                    },
                    {
                        'key': 'bc',
                        'value': '31',
                    },
                    {
                        'key': 'bih',
                        'value': '870',
                    },
                    {
                        'key': 'biw',
                        'value': '1121',
                    },
                    {
                        'key': 'brdim',
                        'value': '43,12,43,12,1920,0,1708,991,1137,870',
                    },
                    {
                        'key': 'vis',
                        'value': '1',
                    },
                    {
                        'key': 'wgl',
                        'value': 'true',
                    },
                    {
                        'key': 'ca_type',
                        'value': 'image',
                    },
                ],
            },
        },
        'playbackContext': {
            'contentPlaybackContext': {
                'html5Preference': 'HTML5_PREF_WANTS',
                'lactMilliseconds': '2979',
                'referer': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'signatureTimestamp': 19117,
                'autonavState': 'STATE_OFF',
                'autoCaptionsDefaultOn': False,
                'mdxContext': {},
                'playerWidthPixels': 647,
                'playerHeightPixels': 364,
            },
        },
        'cpn': 'pwy4NMkpT8PY63hl',
        'captionParams': {
            'deviceCaptionsOn': True,
        },
        'attestationRequest': {
            'omitBotguardData': True,
        },
    }

    print('\n[+] Получаю название канала...')
    channel_name = str(requests.post('https://www.youtube.com/youtubei/v1/player', params=params, headers=headers,
                                     json=json_data).json()['videoDetails']['author'])

    for m in ["?", '"', "/", ":", "#", "|", ",", " ?", "?!", "?!", "? ", " / ", " | "]:
        channel_name = channel_name.replace(m, " ")
    print(f'[+] Название канала получено: "{channel_name}"')
    return channel_name

Не стоит пугаться ее размерами. Большую часть здесь занимает JSON с параметрами, которые передаются с запросом и заголовками. Но избавиться от них не получилось. Потому, чтобы все работало без сбоев, я оставил их без изменения. И использовал так, как они и отправляются в запросе на странице.

И по сути, большая часть скрипта уже готова. Но, меня не оставляло смутное чувство, что я что-то упустил. И да, немного подумав, я понял, что есть еще ведь плейлисты, которые тоже неплохо бы выкачивать полностью, только лишь введя на них ссылку. Но для того, чтобы их выкачать, для начала со страницы плейлиста нужно получить все ссылки на видео, которые содержаться в нем. Но фишка тут в том, что бывают такие плейлисты, количество видео в которых не влезает на одну страницу. И тогда они просто подгружаются по мере прокрутки. То есть, использованием простого запроса тут не обойдешься. Нужно подключать что-то посерьезнее. Но использовать selenium мне не хотелось. Да, с помощью этой библиотеки можно было бы в безголовом режиме прокрутить страницу до конца, собрать все ссылки на видео, сложить в список и вернуть для дальнейшего использования. Но, я подумал, что должен быть другой способ получения ссылок. И он таки нашелся. Стоило ввести запрос на загрузку плейлиста, как нашелся сайт, который и предоставлял этот функционал. Я покопался в его запросах и нашел один, в котором отправляется ссылка на плейлист, а в ответ прилетает JSON уже с готовыми идентификаторами видео. Ну не красота ли.


Тут все по накатанной. Копируется cURL, получается код, делается запрос и забирается JSON, из которого выкорчевываются идентификаторы, помещаются в список и возвращают его туда, откуда запрос был сделан.

Кстати, здесь, прямо на сайте есть специальное поле, куда помещаются ссылки на загрузку видео из плейлиста уже после выполнения запроса. В самом запросе они «медленные», то есть загрузка по ним происходит со скоростью черепахи. А вот на сайте они уже переформатируются в нужный вид и по ним видео загружается хорошо. Но, чтобы забрать их отсюда нужен опять же selenium, а это снова усложняет скрипт. А так, ссылки интересные. Если понять, как преобразуются исходные ссылки именно в такой вид, то можно и самому их конвертировать. Но, пока что я не стал разбираться. Может быть в будущем.


Создаю функцию playlist_item(url). Здесь в запросе так же передается небольшой словарик с параметрами, одним из которых является ссылка на плейлист. Вот ее и получает данная функция на входе и передает в словарик.

Python:
params = {
        'url': url,
        'nextPageToken': '',
    }

Ну, а больше особо и пояснять нечего. Все довольно просто и понятно. Цикл и перебор.

Python:
def playlist_item(url):
    headers = {
        'authority': 'api.youtubemultidownloader.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'dnt': '1',
        'sec-ch-ua-mobile': '?0',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://youtubemultidownloader.net',
        'sec-fetch-site': 'cross-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://youtubemultidownloader.net/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    params = {
        'url': url,
        'nextPageToken': '',
    }

    response = requests.get('https://api.youtubemultidownloader.com/playlist', params=params, headers=headers).json()
    list_items = []
    for item in range(0, len(response['items'])):
        list_items.append(response['items'][item]['id'])
    return list_items

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

Создаю функцию get_target_path(user_input), которая на входе принимает первоначальный пользовательский ввод и обрабатывает его в зависимости от того, что ввел пользователь.

Думаю, что подробно на этом останавливаться не стоит, так как тут не используется чего-то сверхъестественного. Всего лишь проверки if, elif, else. Ну и, если пользователь ввел что-то не так, в бесконечном цикле запрос правильного параметра. Тут, конечно, я предусмотрел возможность выхода в основное меню, так как пользователь может не понять, что не так и попросту запутаться. А цикл будет долбить его снова и снова. А потому, лучше предоставить ему, то есть пользователю, небольшую лазейку, возможность сбежать из бесконечного цикла. А обрабатываются здесь запросы, которые представлены на скриншоте ниже.


Посмотреть вложение 59475

Python:
def get_target_path(user_input):
    if user_input == "1":
        vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com" in vid_id:
            vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        if '&list' in vid_id:
            vid_id = vid_id.split("&")[0].split("=")[-1]
        else:
            vid_id = vid_id.split("=")[-1]
        channel_name = get_channel_name(vid_id)
        get_video_download(vid_id, channel_name)
        main()
    elif user_input == "2":
        while not os.path.isfile(user_path := input("\t[+] Введите путь к списку\n\t[+] Для выхода в меню введите: ex\n"
                                                    "\t>>> ").replace('"', '')):
            if user_path == 'ex':
                main()
                return
            print(f"\n\t[+] Список {user_path} не найден\n")
        with open(f'{user_path}', 'r', encoding='utf-8') as file:
            video_list = file.readlines()
        for video in video_list:
            if '&list' in video:
                vid_id = video.split("&")[0].split("=")[-1]
            else:
                vid_id = video.split("=")[-1].strip()
            if video.strip() == "":
                continue
            else:
                channel_name = get_channel_name(vid_id)
                get_video_download(vid_id, channel_name)
        main()
    elif user_input == "3":
        vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com/playlist" in vid_id:
            vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        list_items = playlist_item(vid_id)
        print(f'[+] Видео в плейлисте: {len(list_items)}\n[+] Загружаю плейлист...')
        for item in list_items:
            channel_name = get_channel_name(item)
            get_video_download(item, channel_name)
        main()
    elif user_input == "4":
        exit(0)
    else:
        main()

И остается функция main(), в которой и делается первоначальный выбор и осуществляется пользовательский ввод.

Python:
def main():
    get_target_path(input(f'\n[+] Выберите варианты загрузки:\n\t[1] Загрузить видео\n'
                          f'\t[2] Загрузить видео из списка\n\t[3] Загрузить плейлист\n\t[4] Выход\n\t>>> '))

Вот, в принципе и все. Если собрать все эти функции в кучку в одном скрипте, то получиться годный загрузчик видео. Данный код работает на обеих платформах. Как на Windows, так и на Linux. На MacOS не проверял, ибо нет у меня этой «заразы». Но, думаю, что и на ней будет работать. А ниже небольшое видео, которое демонстрирует работу скрипта.


Python:
import os.path
import time

import requests
from tqdm import tqdm


def playlist_item(url):
    headers = {
        'authority': 'api.youtubemultidownloader.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'dnt': '1',
        'sec-ch-ua-mobile': '?0',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://youtubemultidownloader.net',
        'sec-fetch-site': 'cross-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://youtubemultidownloader.net/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    params = {
        'url': url,
        'nextPageToken': '',
    }

    response = requests.get('https://api.youtubemultidownloader.com/playlist', params=params, headers=headers).json()
    list_items = []
    for item in range(0, len(response['items'])):
        list_items.append(response['items'][item]['id'])
    return list_items


def get_channel_name(vid_id):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 Safari/537.36',
        'accept': '*/*',
    }

    params = {
        'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
        'prettyPrint': 'false',
    }

    json_data = {
        'videoId': vid_id,
        'context': {
            'client': {
                'hl': 'ru',
                'gl': 'RU',
                'remoteHost': '31.173.242.98',
                'deviceMake': '',
                'deviceModel': '',
                'visitorData': 'CgtrdUNhZ3U2VGNEOCiDndSTBg%3D%3D',
                'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                             'Chrome/98.0.4758.141 Safari/537.36,gzip(gfe)',
                'clientName': 'WEB',
                'clientVersion': '2.20220502.01.00',
                'osName': 'Windows',
                'osVersion': '10.0',
                'originalUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'platform': 'DESKTOP',
                'clientFormFactor': 'UNKNOWN_FORM_FACTOR',
                'configInfo': {
                    'appInstallData': 'CIOd1JMGELiLrgUQmN79EhCUj64FEOqQrgUQw_KtBRCY6q0FELfLrQUQ8IKuBRC7ka4FENSDrgUQ6JCu'
                                      'BRCw7q0FEK_yrQUQgub9EhCR-PwSENi-rQU%3D',
                },
                'userInterfaceTheme': 'USER_INTERFACE_THEME_DARK',
                'timeZone': 'Europe/Moskow',
                'browserName': 'Chrome',
                'browserVersion': '98.0.4758.141',
                'screenWidthPoints': 1137,
                'screenHeightPoints': 870,
                'screenPixelDensity': 1,
                'screenDensityFloat': 1,
                'utcOffsetMinutes': 360,
                'connectionType': 'CONN_CELLULAR_4G',
                'memoryTotalKbytes': '8000000',
                'mainAppWebInfo': {
                    'graftUrl': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                    'webDisplayMode': 'WEB_DISPLAY_MODE_BROWSER',
                    'isWebNativeShareAvailable': True,
                },
                'playerType': 'UNIPLAYER',
                'tvAppInfo': {
                    'livingRoomAppMode': 'LIVING_ROOM_APP_MODE_UNSPECIFIED',
                },
                'clientScreen': 'WATCH_FULL_SCREEN',
            },
            'user': {
                'lockedSafetyMode': False,
            },
            'request': {
                'useSsl': True,
                'internalExperimentFlags': [],
                'consistencyTokenJars': [],
            },
            'adSignalsInfo': {
                'params': [
                    {
                        'key': 'dt',
                        'value': '1651838604229',
                    },
                    {
                        'key': 'flash',
                        'value': '0',
                    },
                    {
                        'key': 'frm',
                        'value': '0',
                    },
                    {
                        'key': 'u_tz',
                        'value': '360',
                    },
                    {
                        'key': 'u_his',
                        'value': '5',
                    },
                    {
                        'key': 'u_h',
                        'value': '1080',
                    },
                    {
                        'key': 'u_w',
                        'value': '1920',
                    },
                    {
                        'key': 'u_ah',
                        'value': '1032',
                    },
                    {
                        'key': 'u_aw',
                        'value': '1920',
                    },
                    {
                        'key': 'u_cd',
                        'value': '24',
                    },
                    {
                        'key': 'bc',
                        'value': '31',
                    },
                    {
                        'key': 'bih',
                        'value': '870',
                    },
                    {
                        'key': 'biw',
                        'value': '1121',
                    },
                    {
                        'key': 'brdim',
                        'value': '43,12,43,12,1920,0,1708,991,1137,870',
                    },
                    {
                        'key': 'vis',
                        'value': '1',
                    },
                    {
                        'key': 'wgl',
                        'value': 'true',
                    },
                    {
                        'key': 'ca_type',
                        'value': 'image',
                    },
                ],
            },
        },
        'playbackContext': {
            'contentPlaybackContext': {
                'html5Preference': 'HTML5_PREF_WANTS',
                'lactMilliseconds': '2979',
                'referer': 'https://www.youtube.com/watch?v=4MPWVKFaLD8',
                'signatureTimestamp': 19117,
                'autonavState': 'STATE_OFF',
                'autoCaptionsDefaultOn': False,
                'mdxContext': {},
                'playerWidthPixels': 647,
                'playerHeightPixels': 364,
            },
        },
        'cpn': 'pwy4NMkpT8PY63hl',
        'captionParams': {
            'deviceCaptionsOn': True,
        },
        'attestationRequest': {
            'omitBotguardData': True,
        },
    }

    print('\n[+] Получаю название канала...')
    channel_name = str(requests.post('https://www.youtube.com/youtubei/v1/player', params=params, headers=headers,
                                     json=json_data).json()['videoDetails']['author'])

    for m in ["?", '"', "/", ":", "#", "|", ",", " ?", "?!", "?!", "? ", " / ", " | "]:
        channel_name = channel_name.replace(m, " ")
    print(f'[+] Название канала получено: "{channel_name}"')
    return channel_name


def get_video_download(vid_id, channel_name):
    headers = {
        'authority': 'downloader.freemake.com',
        'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Yandex";v="22"',
        'dnt': '1',
        'x-cf-country': 'RU',
        'sec-ch-ua-mobile': '?0',
        'x-user-platform': 'Win32',
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'x-user-browser': 'YaBrowser',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/98.0.4758.141 YaBrowser/22.3.3.852 Yowser/2.5 Safari/537.36',
        'x-analytics-header': 'UA-18256617-1',
        'x-request-attempt': '1',
        'x-user-id': '94119398-e27a-3e13-be17-bbe7fbc25874',
        'sec-ch-ua-platform': '"Windows"',
        'origin': 'https://www.freemake.com',
        'sec-fetch-site': 'same-site',
        'sec-fetch-mode': 'cors',
        'sec-fetch-dest': 'empty',
        'referer': 'https://www.freemake.com/ru/free_video_downloader/',
        'accept-language': 'ru,en;q=0.9,uk;q=0.8',
    }

    print(f'[+] Получаю название и ссылку на видео...')
    response = requests.get(f'https://downloader.freemake.com/api/videoinfo/{vid_id}', headers=headers).json()
    if response['qualities'][0]['qualityInfo']['itag'] == 22:
        video_title = str(response['metaInfo']['title'])
        for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
            video_title = video_title.replace(m, "")
        url = response['qualities'][0]['url']
        print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
        if not os.path.isdir(f'{channel_name}'):
            os.mkdir(f'{channel_name}')
            print(f'[+] Создаю папку для сохранения видео...\n')
        else:
            print(f'[+] Папка для сохранения существует...\n')
        req = requests.get(url=url, headers=headers, stream=True)
        total = int(req.headers.get('content-length', 0))
        with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                total=total,
                unit='iB',
                unit_scale=True,
                unit_divisor=1024,
        ) as bar:
            for data in req.iter_content(chunk_size=1024):
                size = file.write(data)
                bar.update(size)
        print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
    else:
        user_change = input('\n[+] Нет видео в качестве 720р...\n[+] Загрузить в доступном качестве?:\n'
                            '\t[1]: Да\n\t[2]: Нет\n\t>>> ')
        if user_change == "1":
            video_title = str(response['metaInfo']['title'])
            for m in ["?", '"', "'", "/", ":", "#", "|", ",", " | "]:
                video_title = video_title.replace(m, "")
            url = response['qualities'][0]['url']
            print(f'[+] Название и ссылка получены. Начинаю загрузку: "{video_title}"...')
            if not os.path.isdir(f'{channel_name}'):
                os.mkdir(f'{channel_name}')
                print(f'[+] Создаю папку для сохранения видео...\n')
            else:
                print(f'[+] Папка для сохранения существует...\n')
            req = requests.get(url=url, headers=headers, stream=True)
            total = int(req.headers.get('content-length', 0))
            with open(f'{os.path.join(channel_name, f"{video_title}.mp4")}', 'wb') as file, tqdm(
                    desc=f"{video_title[0:int(len(video_title) / 2)]}...",
                    total=total,
                    unit='iB',
                    unit_scale=True,
                    unit_divisor=1024,
            ) as bar:
                for data in req.iter_content(chunk_size=1024):
                    size = file.write(data)
                    bar.update(size)
            print(f'\n[+] Видео сохранено в папку: "{channel_name}".\n[+] Загрузка завершена.\n')
        elif user_change == "2":
            main()
            return
        else:
            print('[-] Вы ввели чушь. Закрываю программу...')
            exit(0)


def get_target_path(user_input):
    if user_input == "1":
        vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com" in vid_id:
            vid_id = input('\t[+] Введите ссылку на видео\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        if '&list' in vid_id:
            vid_id = vid_id.split("&")[0].split("=")[-1]
        else:
            vid_id = vid_id.split("=")[-1]
        channel_name = get_channel_name(vid_id)
        get_video_download(vid_id, channel_name)
        main()
    elif user_input == "2":
        while not os.path.isfile(user_path := input("\t[+] Введите путь к списку\n\t[+] Для выхода в меню введите: ex\n"
                                                    "\t>>> ").replace('"', '')):
            if user_path == 'ex':
                main()
                return
            print(f"\n\t[+] Список {user_path} не найден\n")
        with open(f'{user_path}', 'r', encoding='utf-8') as file:
            video_list = file.readlines()
        for video in video_list:
            if '&list' in video:
                vid_id = video.split("&")[0].split("=")[-1]
            else:
                vid_id = video.split("=")[-1].strip()
            if video.strip() == "":
                continue
            else:
                channel_name = get_channel_name(vid_id)
                get_video_download(vid_id, channel_name)
        main()
    elif user_input == "3":
        vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
        if vid_id == 'ex':
            main()
            return
        while not "https://www.youtube.com/playlist" in vid_id:
            vid_id = input('\t[+] Введите ссылку на плейлист\n\t[+] Для выхода в меню введите: ex\n\t>>> ')
            if vid_id == 'ex':
                main()
                return
        list_items = playlist_item(vid_id)
        print(f'[+] Видео в плейлисте: {len(list_items)}\n[+] Загружаю плейлист...')
        for item in list_items:
            channel_name = get_channel_name(item)
            time.sleep(0.3)
            get_video_download(item, channel_name)
            time.sleep(0.3)
        main()
    elif user_input == "4":
        exit(0)
    else:
        main()


def main():
    get_target_path(input(f'\n[+] Выберите варианты загрузки:\n\t[1] Загрузить видео\n'
                          f'\t[2] Загрузить видео из списка\n\t[3] Загрузить плейлист\n\t[4] Выход\n\t>>> '))


if __name__ == "__main__":
    main()

Спасибо за внимание. Надеюсь, что данная информация будет кому-нибудь полезной

Какая версия Python использовалась при написании данного скрипта?
 

Johan Van

Green Team
13.06.2020
350
656
BIT
135
Какая версия Python использовалась при написании данного скрипта?

Если я не ошибаюсь, 3.10. Просто точно уже не помню. Прошло уже достаточно много времени с того момента, и у меня сменилась ОС. Но, если мне не изменяет память, то тогда была версия 3.10
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!