javascript

Свой родной GUI для yt-dlp на Electron и React

  • понедельник, 18 августа 2025 г. в 00:00:03
https://habr.com/ru/articles/937952/
POKE
POKE

Честно, я устал каждый месяц искать новый способ для скачивания различного контента с youtube. Что сегодня работает — завтра уже не подает признаков жизни. После очередной смерти одного из загрузчиков — я понял, что проще уже сделать свой, чем вот так тратить время на постоянные поиски.

Зачем, если уже есть готовые решения?

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

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

  • Загрузка видео в разных форматах + форматирование

  • Возможность скачать аудио отдельно

  • Скачивание отдельной части видео или аудио

  • Работа со всеми видами ссылок (в том числе плейлисты, джемы, live и shorts)

  • Отслеживание кодека и битрейта перед скачиванием

Ну и конечно, чтобы это визуально не выглядело - как взлом терминала из fallout.

Почему именно electron?

Давно хотел попробовать что-нибудь кроме python, и раз уж тут подвернулась такая возможность - то почему мы бы и нет.

Хватит лишней болтовни - переходим к главному.

И да, интерфейс пока работает только на WINDOWS.

Интерфейс

Встречают по одежке, как говорится.

Поначалу не хотел тратить много времени и остановиться на чем-нибудь однотонном. Но в процессе увлекся и меня немного занесло. Сделал две темы (светлая и тёмная).

Светлая тема
Светлая тема
Тёмная тема
Тёмная тема

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

Настройки
Настройки

Сам коддля «настроек» вывел в отдельные файлы (settings.js и settings.html) чтобы не путаться.

Далее идет основной интерфейс для выбора настроек скачивания. Я не буду показывать каждую кнопку, чтобы не растягивать статью.

Основной интерфейс
Основной интерфейс

Основной код

Приложение построил на Electron для десктопа и React для фронтенда. Electron обрабатывает IPC, процессы и внешние вызовы, React — UI с состояниями.

Функционал

  • Проверка URL видео

    Функция проверки URL реализована в main.js через IPC-событие check-url. Она использует yt-dlp для получения метаданных видео.

ipcMain.handle('check-url', async (event, url) => {
  try {
    const ytDlpPath = getYtDlpPath();
    await fs.access(ytDlpPath); // Проверка наличия yt-dlp.exe
    const args = ['--dump-single-json', url];
    const process = spawn(ytDlpPath, args, { shell: true });
    
    let output = '';
    process.stdout.on('data', (data) => {
      output += data.toString();
    });

    return new Promise((resolve, reject) => {
      process.on('close', (code) => {
        if (code === 0) {
          try {
            const json = JSON.parse(output);
            resolve(json); // Возвращает метаданные: миниатюра, форматы, длительность
          } catch (err) {
            reject(new Error('Ошибка парсинга JSON'));
          }
        } else {
          reject(new Error(`yt-dlp завершился с кодом ${code}`));
        }
      });
    });
  } catch (err) {
    console.error('Ошибка проверки URL:', err);
    throw err;
  }
});

Вызывается yt-dlp с параметром --dump-single-json, который возвращает JSON с информацией о видео (название, форматы, длительность). В App.jsx результат отображается в интерфейсе (миниатюра, список форматов).

Выбор качества в интерфейсе (с указанием исходного контейнера и битрейтом)
Выбор качества в интерфейсе (с указанием исходного контейнера и битрейтом)
  • Скачивание

Скачивание реализовано в main.js через IPC start-download. Формируются аргументы для yt-dlp и запускается процесс.

ipcMain.handle('start-download', async (event, options) => {
  const { url, format, quality, startTime, endTime, downloadMode } = options;
  const ytDlpPath = getYtDlpPath();
  const ffmpegPath = getFfmpegPath();
  let args = [url, '-o', '%(title)s.%(ext)s'];

  if (format === 'mp3' || format === 'm4a') {
    args.push('-x', `--audio-format=${format}`, `--audio-quality=${quality || 0}`);
  } else {
    args.push(`-f bestvideo[height<=${quality}]+bestaudio/best[height<=${quality}]`);
  }

  if (startTime && endTime) {
    args.push(`--download-sections=*${startTime}-${endTime}`);
  }

  const process = spawn(ytDlpPath, args, { shell: true, env: { ...process.env, PATH: `${process.env.PATH};${path.dirname(ffmpegPath)}` } });
  currentDownloadProcess = process;

  process.stdout.on('data', (data) => {
    const output = data.toString();
    const match = output.match(/(\d+\.\d)%/); // Парсинг прогресса
    if (match) {
      mainWindow.webContents.send('download-progress', { percent: parseFloat(match[1]) });
    }
  });

  return new Promise((resolve, reject) => {
    process.on('close', (code) => {
      if (code === 0) {
        mainWindow.webContents.send('download-finished');
        resolve();
      } else if (!isCancelled) {
        mainWindow.webContents.send('download-error', { message: `Ошибка скачивания, код ${code}` });
        reject(new Error(`Ошибка скачивания, код ${code}`));
      }
    });
  });
});

Формируются аргументы для yt-dlp в зависимости от формата и качества. Прогресс парсится из stdout через regex. События download-progress и download-finished отправляются в React для обновления UI.

И на сладкое — скачивание аудио или видео частями.

Буду честен — я до конца не верил, что смогу такое реализовать. Штука крайне удобная, но не без минусов. Она хорошо себя показывает в длинных роликах (от 30–40 минут), но в коротких проще скачать видео полностью и отрезать лишнее (короткий ролик загрузиться быстрее, чем его часть. Окак :-)

Принцип работы

Интерфейс (фронтенд):

  • Пользователь включает опцию "Скачать часть" кнопкой, активируя слайдер (react-range).

  • Слайдер задаёт startTime и endTime (в секундах) в пределах длительности видео (videoInfo.duration).

  • Временные метки отображаются в формате HH:MM:SS.

Слайдер
Слайдер
const [rangeValues, setRangeValues] = useState([0, 0]);
const [showRange, setShowRange] = useState(false);

<Range
  values={rangeValues}
  step={1}
  min={0}
  max={videoInfo?.duration || 100}
  onChange={(values) => setRangeValues(values)}
/>

При нажатии "Скачать" данные передаются в бэкенд через IPC:

const handleDownload = async () => {
  const options = {
    url, format, quality,
    startTime: showRange ? Math.floor(rangeValues[0]) : null,
    endTime: showRange ? Math.floor(rangeValues[1]) : null
  };
  await window.electronAPI.startDownload(options);
};

Бэкенд (main.js):

  • Получает startTime и endTime через IPC start-download.

  • Добавляет аргумент --download-sections=*${startTime}-${endTime} для yt-dlp.

  • Запускает yt-dlp с ffmpeg для обрезки.

Зачем ffmpeg?

ffmpeg (бинарник в bin/) используется yt-dlp для обрезки медиа и конвертации (например, видео в MP3). Он обеспечивает точную обрезку, но границы могут слегка варьироваться из-за ключевых кадров

Cookies

Чтобы получать корректные данные о видео приходится цеплять cookies файл через встроенный electron браузер (вход в аккаунт google). Я пытался обойти это сменой способа извлечения данных о видео с tv_embedded на mweb. Два дня адских мучений спустя (связанных с интеграцией плагина для извлечения PO Token сначала на VPS, а потом и в само приложение) я решил отказаться от этой затеи.

Итоги

Это были веселые пару недель в мои жизни, будем честны. Все функции, которые хотел увидеть - я смог реализовать. Да, есть некоторые некритичные баги, которые нужно в скором времени починить. Если кому-то будет интересно протестировать GUI - буду только рад.
Из того, что хотел бы ещё добавить в будущем - это поддержка linux/mac и возможность полноценной работы с плейлистами (пока можно скачивать только конкретное видео из плейлиста)
Проект планируется платным, но для желающих потыкаться - PROMO_0Y1NE52T

P.S - Для использования приложения на территории РФ необходимо включать методы для обхода замедления (галочка в настройках не спасает)


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