Надоело каждый раз лезть в терминал, чтобы скачать видео с 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 — попробуйте.
ссылка на оригинал статьи https://habr.com/ru/articles/930260/
Добавить комментарий