Надоело каждый раз лезть в терминал, чтобы скачать видео с YouTube? Мне тоже. Поэтому я сделал нормальный GUI для yt-dlp - без лишних кнопок, с современным интерфейсом и чтобы просто работал. Код на GitHub, готовая сборка тоже есть.

Зачем вообще это делать?

Да, yt-dlp крутой - качает с кучи сайтов, быстрый, надёжный. Но блин, каждый раз набирать команды в консоли - это не для всех. Особенно когда нужно быстро скачать что-то и не париться с параметрами.

Посмотрел на существующие GUI - одни выглядят как из 2005 года, другие напичканы настройками, которые 99% пользователей никогда не трогают. Захотелось сделать что-то простое: вставил ссылку, выбрал качество, скачал. Всё.

Что хотел получить:

  • Простоту - минимум кликов от ссылки до файла

  • Нормальный вид - тёмная тема, без уродских кнопок из 90-х

  • Скорость - никаких тормозов и зависаний

  • Работает везде - Windows точно, остальные ОС в планах

  • Не требует установки - скачал exe и пользуешься

Что в итоге получилось

Интерфейс работает по принципу "от простого к сложному":

  1. Стартовая страница - только поле для ссылки, ничего лишнего

  2. Превью - показываем видео, даём выбрать качество

  3. Скачивание - прогресс-бар и всякая полезная инфа

Стартовая страница

Стартовая страница с полем ввода URL
Стартовая страница с полем ввода URL

Превью видео

Интерфейс выбора качества и формата
Интерфейс выбора качества и формата

Прогресс загрузки

Отображение прогресса с детальной информацией
Отображение прогресса с детальной информацией

На чём писал

CustomTkinter - почему именно он

Долго выбирал между разными вариантами. В итоге остановился на CustomTkinter - это такая современная обёртка над обычным Tkinter.

Плюсы:

  • Выглядит нормально сразу из коробки

  • Плавные анимации есть

  • Совместим с обычным Tkinter

  • Активно развивается

Что ещё рассматривал:

  • PyQt/PySide - мощно, но лицензия для коммерции геморрой

  • Kivy - больше для мобилок заточен

  • Electron - для простого даунлоадера это перебор

  • Обычный tkinter - работает, но выглядит как поделка

Как организовал код

Сразу решил не лепить всё в одну кучу, а разложить по папкам:

src/ytdlp_gui/
├── core/                    # Вся логика работы
│   ├── download_manager.py  # Качает файлы
│   ├── format_detector.py   # Разбирается с форматами
│   ├── settings_manager.py  # Настройки
│   └── cookie_manager.py    # Куки для обхода блокировок
├── gui/                     # Интерфейс
│   ├── main_window.py       # Главное окно
│   └── components/          # Отдельные части UI
└── utils/                   # Всякие полезности
    ├── logger.py           # Логи
    └── notifications.py    # Уведомления

Зачем так заморачивался:

  • Проще искать баги - каждая штука в своём файле

  • Можно тестировать части по отдельности

  • Если захочу что-то добавить, не придётся ковыряться во всём коде

  • Другим разработчикам будет понятно, что где лежит

Как это работает изнутри

Менеджер загрузок

Основная фишка - DownloadManager. Он умеет:

Качать в фоне и не тормозить интерфейс:

Воркер для загрузки
def _download_worker(self, download_item: DownloadItem):
    """Отдельный поток для скачивания"""
    try:
        ydl_opts = self._prepare_ydl_options(download_item)

        # Подключаем отслеживание прогресса
        ydl_opts['progress_hooks'] = [
            lambda d: self._progress_hook(d, download_item.id)
        ]
        ydl_opts['postprocessor_hooks'] = [
            lambda d: self._postprocessor_hook(d, download_item.id)
        ]

        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            ydl.download([download_item.url])
    except Exception as e:
        self._handle_download_error(e, download_item)

Обновлять интерфейс по ходу дела:

Система уведомлений
def add_progress_callback(self, download_id: str, callback: Callable):
    """Подписаться на обновления прогресса"""
    if download_id not in self.progress_callbacks:
        self.progress_callbacks[download_id] = []
    self.progress_callbacks[download_id].append(callback)

def _notify_progress_change(self, download_id: str):
    """Сказать интерфейсу, что что-то изменилось"""
    if download_id in self.progress_callbacks:
        for callback in self.progress_callbacks[download_id]:
            try:
                callback()
            except Exception as e:
                self.logger.error(f"Callback error: {e}")

Определение качества видео

FormatDetector разбирается, какие форматы доступны, и сортирует их по качеству:

Как считаем рейтинг качества
def _calculate_quality_score(self, fmt: Dict) -> int:
    """Считаем очки качества для сортировки"""
    score = 0

    # Очки за разрешение
    height = fmt.get('height', 0) or 0
    if height >= 2160:    # 4K
        score += 1000
    elif height >= 1440:  # 1440p
        score += 800
    elif height >= 1080:  # 1080p
        score += 600
    # ... и так далее

    # Очки за битрейт
    tbr = fmt.get('tbr', 0) or 0
    score += min(tbr, 500)  # Чтобы не было совсем диких значений

    # Бонусы за хорошие кодеки
    vcodec = fmt.get('vcodec', '')
    if 'av01' in vcodec:      # AV1
        score += 50
    elif 'vp9' in vcodec:     # VP9
        score += 30
    elif 'h264' in vcodec:    # H.264
        score += 20

    return score

Как устроен интерфейс

Сделал по принципу "показываем только то, что нужно сейчас":

  1. Стартовая - только поле для ссылки

  2. После вставки ссылки - грузим инфо о видео

  3. Превью - показываем видео и даём выбрать настройки

  4. Скачивание - прогресс и всякие детали

Каждый экран - отдельный компонент:

  • SimpleURLInputFrame - ввод ссылки

  • VideoPreviewFrame - превью и настройки

  • ProgressDisplayFrame - прогресс скачивания

Проблемы, с которыми столкнулся

YouTube и его капризы

Самая большая головная боль - получить нормальное название видео. YouTube ведёт себя по-разному в зависимости от времени, региона, есть ли VPN. Иногда вместо названия получаешь какую-то фигню.

Решил парсить HTML напрямую:

Вытаскиваем название из HTML
def _extract_title_from_html(self, url: str) -> Optional[str]:
    """Берём название прямо со страницы"""
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            # Специально не указываем язык, чтобы получить оригинал
        }

        response = requests.get(url, headers=headers, timeout=10)

        # Ищем название в разных местах
        patterns = [
            r'<meta property="og:title" content="([^"]+)"',
            r'<meta name="title" content="([^"]+)"',
            r'"title":"([^"]+)"',
        ]

        for pattern in patterns:
            match = re.search(pattern, response.text)
            if match:
                return html.unescape(match.group(1))

    except Exception as e:
        self.logger.error(f"Не смог вытащить название: {e}")

    return None

Обход блокировок через куки

Чтобы обойти всякие региональные блокировки, тащу куки из браузера:

Автоматическое извлечение cookies
def get_cookie_options(self, url: str = None) -> Dict[str, Any]:
    """Берём куки из браузера для yt-dlp"""

    # Для разных сайтов разные браузеры работают лучше
    site_browsers = self.site_browser_preferences.get(
        self._extract_domain(url),
        self.browser_priority
    )

    for browser in site_browsers:
        if self._is_browser_available(browser):
            return {
                'cookiesfrombrowser': (browser, None, None, None)
            }

    return {}

Многопоточность в Tkinter

Tkinter не умеет в асинхронность из коробки, поэтому пришлось городить threading + callback'и:

def _progress_hook(self, d: Dict, download_id: str):
    """Хук для обновления прогресса (работает в фоновом потоке)"""
    try:
        download_item = self.get_download_item(download_id)
        if not download_item:
            return

        if d['status'] == 'downloading':
            # Обновляем данные
            download_item.progress = (d.get('downloaded_bytes', 0) /
                                    d.get('total_bytes', 1)) * 100
            download_item.speed = self._clean_display_string(d.get('_speed_str', ''))

            # Говорим интерфейсу обновиться
            self._notify_progress_change(download_id)

    except Exception as e:
        self.logger.error(f"Ошибка в progress hook: {e}")

А интерфейс подписывается на изменения и обновляется в основном потоке:

def update_progress(self, download_item):
    """Обновляем прогресс-бар (в основном потоке)"""
    if download_item:
        # Обновляем полоску прогресса
        progress = download_item.progress / 100.0
        self.progress_bar.set(progress)

        # Обновляем текст
        self.percentage_label.configure(text=f"{download_item.progress:.1f}%")
        self.speed_label.configure(text=f"Скорость: {download_item.speed}")

Уведомления

Сделал всплывающие уведомления с анимацией:

class ToastNotification(ctk.CTkToplevel):
    """Всплывающее уведомление"""

    def show_animation(self):
        """Плавно появляемся"""
        # Начинаем невидимыми
        self.attributes('-alpha', 0.0)

        # Постепенно становимся видимыми
        for i in range(20):
            alpha = i / 20.0
            self.attributes('-alpha', alpha)
            self.update()
            time.sleep(0.01)

    def close_animation(self):
        """Плавно исчезаем"""
        for i in range(20, 0, -1):
            alpha = i / 20.0
            self.attributes('-alpha', alpha)
            self.update()
            time.sleep(0.01)

        self.destroy()

Сборка в exe

Чтобы не заставлять людей ставить Python, собираю всё в один exe файл через PyInstaller:

Автоматическая сборка

def create_pyinstaller_spec():
    """Создаём spec-файл для PyInstaller"""

    hidden_imports = [
        "customtkinter",
        "yt_dlp",
        "PIL._tkinter_finder",
        "tkinter",
        "sqlite3",
        "threading",
        "psutil"
    ]

    # Подключаем ресурсы
    datas = [
        ("src", "src"),
        ("assets", "assets") if Path("assets").exists() else None
    ]
    datas = [d for d in datas if d]  # Убираем пустые

    # Генерим spec-файл
    spec_content = f'''
a = Analysis(
    ['main.py'],
    pathex=['src'],
    datas={datas!r},
    hiddenimports={hidden_imports!r},
    # ... остальные настройки
)
'''

Проблемы при сборке

Проблема: CustomTkinter не может найти свои файлы в exe

Решение: Прописываем пути явно:

# В spec-файле
datas=[
    ('venv/Lib/site-packages/customtkinter', 'customtkinter'),
]

Проблема: yt-dlp пытается обновиться через интернет

Решение: Отключаем обновления:

ydl_opts = {
    'no_check_certificate': True,
    'call_home': False,  # Не проверять обновления
}

Сборка под разные ОС

Скрипт сам определяет систему и делает нужный архив:

def create_archive():
    """Создаём архив для раздачи"""
    system = platform.system().lower()

    if system == "windows":
        exe_name = f"{APP_NAME}.exe"
        archive_format = "zip"
    elif system == "darwin":  # macOS
        exe_name = APP_NAME
        archive_format = "zip"
    else:  # Linux
        exe_name = APP_NAME
        archive_format = "gztar"

    # Архив с версией и платформой в названии
    archive_name = f"{APP_NAME}-v{APP_VERSION}-{system}-{platform.machine()}"

Что в итоге

Что работает

  • ✅ Нормальный современный интерфейс

  • ✅ Быстро качает без лишней обработки

  • ✅ Поддерживает кучу сайтов через yt-dlp

  • ✅ Работает на Windows (на других ОС пока не тестил)

  • ✅ Готовый exe файл

  • ✅ Уведомления и обработка ошибок

  • ✅ Проверено на YouTube и ВКонтакте - всё ок

  • ⚠️ Надо протестить на macOS и Linux

  • ⚠️ Проверить работу с другими сайтами из списка yt-dlp

Цифры:

  • Размер: ~27MB со всеми зависимостями

  • Запуск: 2-3 секунды на нормальном компе

  • Память: ~55MB когда просто висит

  • Форматы: MP4 для видео, MP3 для аудио

Что планирую добавить

  1. Выбор папки для сохранения - пока всё сохраняется на рабочий стол

  2. Субтитры - скачивание субтитров в разных форматах

  3. Тестирование на других ОС - проверить работу на macOS и Linux

  4. Больше сайтов - протестить Одноклассники, Rutube, TikTok и прочие

Выводы

Делать GUI для консольной утилиты - интересная задачка. Главное - не переборщить с функциями и сделать так, чтобы было удобно пользоваться. CustomTkinter оказался отличным выбором: выглядит современно, работает быстро, не такой тяжёлый как Qt и не такой монстр как Electron.

Что понял в процессе:

  • Архитектура важна - если сразу всё разложить по полочкам, потом легче добавлять новые фичи

  • Простота рулит - лучше сделать 3 кнопки, которые работают, чем 30, которые никто не использует

  • Многопоточность в GUI - боль - но без неё интерфейс тормозит

  • Тестирование на разных ОС критично - что работает на Windows, может не работать на Linux

Полезные ссылки:

В общем, Python + CustomTkinter - хорошая связка для десктопных приложений. Если думаете над GUI для Python - попробуйте.

Комментарии (41)


  1. Ioanna
    23.07.2025 09:27

    У меня почему-то вот так :(


    1. Diacut
      23.07.2025 09:27

      Аналогично. То есть, не работает.


    1. vokrob Автор
      23.07.2025 09:27

      Попробуйте скачать то же видео через оригинальный yt-dlp из командной строки


  1. datacompboy
    23.07.2025 09:27

    'no_check_certificate': True,

    это такая закладка?


    1. vokrob Автор
      23.07.2025 09:27

      Это опция yt-dlp, которая отключает строгую проверку SSL-сертификатов


      1. datacompboy
        23.07.2025 09:27

        Угу, и сделано это в качечстве закладки, чтоб подмену сертификатов допускало?


        1. vokrob Автор
          23.07.2025 09:27

          Это стандартная опция yt-dlp для обхода технических проблем с SSL в корпоративных сетях и VPN. Многие пользователи без неё вообще не смогут скачивать видео


          1. andreymal
            23.07.2025 09:27

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


          1. datacompboy
            23.07.2025 09:27

            1. зачем в корпоративных сетях yt-dlp?

            2. зачем c VPN подмена сертификатов?

            это костыльное решение для редких случаев, которое НЕЛЬЗЯ включать по дефолту навсегда и для всех.
            это как предлагать удобные ручки для входной двери -- где кнопочка отключения замка всегда повернута в положение "открыто", и без разбора двери её не вернуть на место :D


  1. andreymal
    23.07.2025 09:27

    Ну не, выбор кодеков — вещь достаточно важная, чтобы без неё GUI оказывался для меня бесполезным


    1. vokrob Автор
      23.07.2025 09:27

      Согласен, для продвинутых пользователей это критично. Текущая версия рассчитана на простые задачи "скачать и посмотреть". Для точного контроля кодеков пока лучше использовать yt-dlp напрямую. Но обязательно добавлю эти настройки в будущих версиях!


  1. igrushkin
    23.07.2025 09:27

    чтобы проверить работу на MacOS нужны файлы для Мака или brew формула. Вряд ли кто-то будет заморачиваться собирать из сорсов.

    Я попробую, но не обещаю


    1. vokrob Автор
      23.07.2025 09:27

      Полностью понимаю! У меня нет Mac для тестирования, поэтому пока фокусируюсь на Windows. Если протестируете, буду очень благодарен за фидбек


      1. JerryI
        23.07.2025 09:27

        Github Actions)


  1. eyeDM
    23.07.2025 09:27

    Но блин, каждый раз набирать команды в консоли - это не для всех.

    Я создал файл конфигурации с типовыми параметрами, создал bat-ник типа

    C:\programs\yt-dlp.exe --config-location "C:\etc\yt-dlp.cfg" %*

    Назвал его yt.bat, добавил его директорию в переменную %PATH%.

    И теперь для загрузки видео достаточно нажать Win+R, cmd, yt $URL. Не сложно, работает быстро.


    1. vokrob Автор
      23.07.2025 09:27

      Да, для тех кто дружит с командной строкой - это идеальный вариант! Мой GUI рассчитан на тех, кто не хочет разбираться с конфигами и bat-файлами - просто вставил ссылку и скачал