Надоело каждый раз лезть в терминал, чтобы скачать видео с YouTube? Мне тоже. Поэтому я сделал нормальный GUI для yt-dlp - без лишних кнопок, с современным интерфейсом и чтобы просто работал. Код на GitHub, готовая сборка тоже есть.
Зачем вообще это делать?
Да, yt-dlp крутой - качает с кучи сайтов, быстрый, надёжный. Но блин, каждый раз набирать команды в консоли - это не для всех. Особенно когда нужно быстро скачать что-то и не париться с параметрами.
Посмотрел на существующие GUI - одни выглядят как из 2005 года, другие напичканы настройками, которые 99% пользователей никогда не трогают. Захотелось сделать что-то простое: вставил ссылку, выбрал качество, скачал. Всё.
Что хотел получить:
Простоту - минимум кликов от ссылки до файла
Нормальный вид - тёмная тема, без уродских кнопок из 90-х
Скорость - никаких тормозов и зависаний
Работает везде - Windows точно, остальные ОС в планах
Не требует установки - скачал exe и пользуешься
Что в итоге получилось
Интерфейс работает по принципу "от простого к сложному":
Стартовая страница - только поле для ссылки, ничего лишнего
Превью - показываем видео, даём выбрать качество
Скачивание - прогресс-бар и всякая полезная инфа
Стартовая страница

Превью видео

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

На чём писал
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
Как устроен интерфейс
Сделал по принципу "показываем только то, что нужно сейчас":
Стартовая - только поле для ссылки
После вставки ссылки - грузим инфо о видео
Превью - показываем видео и даём выбрать настройки
Скачивание - прогресс и всякие детали
Каждый экран - отдельный компонент:
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 для аудио
Что планирую добавить
Выбор папки для сохранения - пока всё сохраняется на рабочий стол
Субтитры - скачивание субтитров в разных форматах
Тестирование на других ОС - проверить работу на macOS и Linux
Больше сайтов - протестить Одноклассники, Rutube, TikTok и прочие
Выводы
Делать GUI для консольной утилиты - интересная задачка. Главное - не переборщить с функциями и сделать так, чтобы было удобно пользоваться. CustomTkinter оказался отличным выбором: выглядит современно, работает быстро, не такой тяжёлый как Qt и не такой монстр как Electron.
Что понял в процессе:
Архитектура важна - если сразу всё разложить по полочкам, потом легче добавлять новые фичи
Простота рулит - лучше сделать 3 кнопки, которые работают, чем 30, которые никто не использует
Многопоточность в GUI - боль - но без неё интерфейс тормозит
Тестирование на разных ОС критично - что работает на Windows, может не работать на Linux
Полезные ссылки:
В общем, Python + CustomTkinter - хорошая связка для десктопных приложений. Если думаете над GUI для Python - попробуйте.
Комментарии (41)
datacompboy
23.07.2025 09:27'no_check_certificate': True,
это такая закладка?
vokrob Автор
23.07.2025 09:27Это опция yt-dlp, которая отключает строгую проверку SSL-сертификатов
datacompboy
23.07.2025 09:27Угу, и сделано это в качечстве закладки, чтоб подмену сертификатов допускало?
vokrob Автор
23.07.2025 09:27Это стандартная опция yt-dlp для обхода технических проблем с SSL в корпоративных сетях и VPN. Многие пользователи без неё вообще не смогут скачивать видео
andreymal
23.07.2025 09:27Технические проблемы нужно решать, а не безальтернативно подвергать пользователя опасности
datacompboy
23.07.2025 09:27зачем в корпоративных сетях yt-dlp?
зачем c VPN подмена сертификатов?
это костыльное решение для редких случаев, которое НЕЛЬЗЯ включать по дефолту навсегда и для всех.
это как предлагать удобные ручки для входной двери -- где кнопочка отключения замка всегда повернута в положение "открыто", и без разбора двери её не вернуть на место :D
andreymal
23.07.2025 09:27Ну не, выбор кодеков — вещь достаточно важная, чтобы без неё GUI оказывался для меня бесполезным
vokrob Автор
23.07.2025 09:27Согласен, для продвинутых пользователей это критично. Текущая версия рассчитана на простые задачи "скачать и посмотреть". Для точного контроля кодеков пока лучше использовать yt-dlp напрямую. Но обязательно добавлю эти настройки в будущих версиях!
igrushkin
23.07.2025 09:27чтобы проверить работу на MacOS нужны файлы для Мака или brew формула. Вряд ли кто-то будет заморачиваться собирать из сорсов.
Я попробую, но не обещаю
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. Не сложно, работает быстро.
vokrob Автор
23.07.2025 09:27Да, для тех кто дружит с командной строкой - это идеальный вариант! Мой GUI рассчитан на тех, кто не хочет разбираться с конфигами и bat-файлами - просто вставил ссылку и скачал
Ioanna
У меня почему-то вот так :(
Diacut
Аналогично. То есть, не работает.
vokrob Автор
Попробуйте скачать то же видео через оригинальный yt-dlp из командной строки