Привет, Хабр!
Все мы любим YouTube, но иногда хочется сохранить видео для просмотра офлайн, отправить другу в Telegram или просто вырезать интересный момент. И тут мы сталкиваемся с реальностью: десятки сайтов-загрузчиков, которые завалены агрессивной рекламой, всплывающими окнами и капчами. Простая задача — скачать ролик — превращается в пятиминутный квест. А если нужно скачать несколько видео, это становится настоящей головной болью.
Как разработчикам, нам под силу решить эту проблему раз и навсегда. Сегодня мы создадим собственный консольный загрузчик на .NET, который будет делать ровно то, что нужно, и ничего лишнего. В этой статье мы не только напишем рабочий код, но и разберемся, с какими подводными камнями можно столкнуться на этом пути, чтобы вы не тратили свое время.
Наша цель — написать утилиту, которая:
Скачивает видео по ссылке.
Позволяет выбрать качество через простой конфиг-файл.
Автоматически объединяет видео с лучшим доступным звуком.
Звучит неплохо? Тогда приступим.
Наш арсенал: выбираем правильные инструменты
Чтобы не изобретать велосипед, мы воспользуемся несколькими мощными и популярными библиотеками. Для красивого вывода в консоль возьмем Spectre.Console
, а для основной магии — YoutubeExplode
.
YoutubeExplode — это сердце нашего проекта. Она умеет "разбирать" страницы YouTube, получая доступ к метаданным и прямым ссылкам на видео- и аудиопотоки. И все это без официального API и связанных с ним квот. Вместе с дополнением YoutubeExplode.Converter
она становится универсальным комбайном, который сам скачает и объединит нужные потоки.
Шаг 1: Настройка проекта и конфигурации
Создаем новый консольный проект (dotnet new console
) и подключаем необходимые зависимости:
dotnet add package YoutubeExplode
dotnet add package YoutubeExplode.Converter
dotnet add package Spectre.Console
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
А чтобы сделать наш загрузчик гибким, воспользуемся стандартным для .NET механизмом конфигурации Microsoft.Extensions.Configuration
. Вынесем настройки в appsettings.json
. Это позволит пользователю легко менять качество видео и путь сохранения, не пересобирая программу.
appsettings.json
{
"Settings": {
"PreferredQuality": 720,
// (здесь {YourUserName} нужно заменить на ваше имя пользователя в системе)
"OutputPath": "C:\\Users\\{YourUserName}\\Downloads"
}
}
Шаг 2: Получение информации о видео и выбор качества
После того как пользователь вставил ссылку, YoutubeExplode
вступает в игру. Сначала мы получаем всю информацию о видео: название, длительность и, самое главное, "манифест" — список всех доступных потоков (стримов).
var youtube = new YoutubeClient();
var video = await youtube.Videos.GetAsync(videoUrl);
var streamManifest = await youtube.Videos.Streams.GetManifestAsync(video.Id);
Далее нам нужно выбрать подходящие видео и аудиопотоки. Может возникнуть вопрос: "А почему бы не взять готовый поток, где уже есть и видео, и звук?". Раньше YouTube так и делал, но сейчас, для оптимизации трафика, видео высокого качества (720p и выше) и аудио почти всегда передаются раздельно, а плеер объединяет их "на лету".
Для нас это даже плюс: мы можем выбрать видео нужного качества и независимо от него — аудио с самым высоким битрейтом.
Начнем с видео. Наша логика проста: сначала ищем поток с качеством, указанным в appsettings.json. Если его нет, не беда, ищем лучший из доступных вариантов качеством пониже. Для аудио все еще проще: YoutubeExplode предоставляет удобный метод, который сам найдет поток с самым высоким битрейтом.
Шаг 3: Скачивание и конвертация в один клик
Теперь, когда потоки выбраны, их нужно скачать и объединить. Для этого давно существует стандарт де-факто — ffmpeg
. Но чтобы не вызывать его вручную, мы используем YoutubeExplode.Converter. Эта библиотека-помощник берет на себя всю грязную работу.
// Определяем итоговый путь к файлу
var outputFile = Path.Combine(outputPath, $"{outputFileName}.mp4");
// Запускаем процесс
await youtube.Videos.DownloadAsync(
[audioStreamInfo, videoStreamInfo], // Передаем нужные потоки
new ConversionRequestBuilder(outputFile).Build(), // Указываем, куда сохранить
progress // И передаем обработчик для прогресс-бара
);
В это время мы можем показывать пользователю красивый прогресс-бар от Spectre.Console
. Просто и эффективно.
Вот весь код.
// Хотите запустить код? Чтобы не возиться с настройками и зависимостями, просто скачайте готовый проект с https://github.com/DNsam/YoutubeDownloader. Там уже всё настроено.
// И еще один важный момент про ffmpeg: в репозитории уже может лежать версия ffmpeg.exe для Windows (x64). Если вы используете другую операционную систему (например, Linux или macOS) или другую архитектуру или просто обновить до последней версии, вам понадобится скачать подходящую для вашего окружения сборку с официального ресурса (https://github.com/tomaszzmuda/Xabe.FFmpeg/releases/tag/executables) и заменить ею существующий файл в проекте.
using Microsoft.Extensions.Configuration;
using Spectre.Console;
using YoutubeExplode;
using YoutubeExplode.Converter;
using YoutubeExplode.Videos.Streams;
// Получим конфиг
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build()
.Get<AppSettings>() ?? new AppSettings();
// Получим ссылку на видео из youtube
Console.Write("Enter a YouTube video URL: ");
string? videoUrl = Console.ReadLine();
// 1. Получим всю информацию о видео
var youtube = new YoutubeClient();
var video = await youtube.Videos.GetAsync(videoUrl);
var streamManifest = await youtube.Videos.Streams.GetManifestAsync(video.Id);
// 2. Выберем потоки
var videoStreamInfo = streamManifest
.GetVideoStreams()
.Where(s => s.Container == Container.Mp4 && s.VideoQuality.MaxHeight == config.PreferredQuality)
.OrderByDescending(s => s.VideoQuality.Framerate)
.FirstOrDefault();
if (videoStreamInfo == null)
{
// Если видеопоток, котрый хотели не нашли, найдем доустпное, качеством по ниже
videoStreamInfo = streamManifest
.GetVideoStreams()
.Where(s => s.Container == Container.Mp4 && s.VideoQuality.MaxHeight < config.PreferredQuality)
.OrderByDescending(s => s.VideoQuality.MaxHeight + s.VideoQuality.Framerate)
.FirstOrDefault();
if (videoStreamInfo == null)
{
Console.WriteLine("Error: No suitable MP4 video stream found.");
return;
}
}
// Подберем аудиопоток с наилучшим качеством
var audioStreamInfo = streamManifest
.GetAudioStreams()
.Where(s => s.Container == Container.Mp4)
.GetWithHighestBitrate();
if (audioStreamInfo == null)
{
Console.WriteLine("Error: No audio stream found.");
return;
}
// 3. Скачаем и объединим потоки в видео файл
// Наимеониваение файла будет из названия видео. Отчистим от недопустимых символов
var outputFileName = SanitizeFilename(video.Title);
// Определим папку для скачивания, если не из конфига, пусть будет папка стандартная папка для видео
var outputPath = config.OutputPath ?? Environment.GetFolderPath(Environment.SpecialFolder.CommonVideos);
var outputFile = Path.Combine(outputPath, $"{outputFileName}.mp4");
await AnsiConsole.Progress() // Это прогрессбар для консоли
.StartAsync(async ctx =>
{
var progressTask = ctx.AddTask($"[green]Downloading Video[/]");
var progress = new Progress<double>(percent => progressTask.Increment(percent * 100 - progressTask.Percentage));
// Скачиваем и обхединяем одной командой!
await youtube.Videos.DownloadAsync(
[audioStreamInfo, videoStreamInfo],
new ConversionRequestBuilder(outputFile).Build(),
progress
);
});
Console.WriteLine("Video saved successfully!");
Console.ReadLine();
static string SanitizeFilename(string filename)
{
if (string.IsNullOrWhiteSpace(filename))
{
return $"video{Random.Shared.Next()}";
}
foreach (var c in Path.GetInvalidFileNameChars())
{
filename = filename.Replace(c.ToString(), "");
}
if (string.IsNullOrWhiteSpace(filename))
{
return $"video{Random.Shared.Next()}";
}
const int MaxFileNameLength = 200;
if (filename.Length > MaxFileNameLength)
{
filename = filename.Substring(0, MaxFileNameLength);
}
return filename;
}
public class AppSettings
{
public int PreferredQuality { get; set; } = 720;
public string? OutputPath { get; set; }
}
Важные нюансы, о которых стоит знать
А теперь что бы сэкономить ваше время если решите попробовать, есть пара важных моментов.
1. Нюанс с YoutubeExplode
и региональными ограничениями
Автор YoutubeExplode
ввел ограничения на использование библиотеки на территории России и Беларуси. При запуске кода на машине с региональными настройками этих стран приложение аварийно завершится с сообщением от автора. В самом сообщении будут инструкции, как можно обойти это ограничение (например, через переменные окружения). Если вы находитесь за пределами этих стран, код будет работать без каких-либо проблем.
2. Установка ffmpeg
Библиотека YoutubeExplode.Converter
не содержит в себе ffmpeg
, а только вызывает его. Поэтому ffmpeg.exe
должен быть доступен для вашего приложения. Напомню, это готовое решение, и одно из самых известных для работы с видео и аудио. Даже если вы используете удобную .NET-библиотеку, под капотом она все равно формирует и выполняет консольные команды для ffmpeg.exe
. Что бы его получить, самый простой способ — сделать следующее:
Скачайте FFmpeg. Перейдите на официальный сайт и скачайте статическую сборку для вашей операционной системы (для Windows популярны сборки от
Gyan.dev
илиBtbN
).Распакуйте архив и найдите в папке
bin
файлffmpeg.exe
.Поместите
ffmpeg.exe
в корневую папку вашего проекта (рядом с.csproj
файлом).
В .csproj
файле вашего проекта нужно добавить строки, чтобы ffmpeg.exe
копировался в папку со скомпилированной программой:
<ItemGroup>
<None Update="ffmpeg.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Теперь ваше приложение сможет найти и использовать ffmpeg
для своей работы.
Если вы планируете использовать приложение в коммерческих целях, обязательно изучите лицензии компонентов FFmpeg. Для личного использования никаких проблем нет.
3. Ошибка 403 Forbidden: когда YouTube говорит "нет"
При активном использовании загрузчика вы можете время от времени сталкиваться с ошибкой HTTP 403 Forbidden
. Не пугайтесь, это не баг в вашем коде или библиотеке. Это сам YouTube вежливо, но настойчиво просит вас сбавить обороты.
Почему это происходит?
YouTube, как и любой крупный сервис, защищается от ботов и чрезмерной нагрузки. Если с вашего IP-адреса поступает слишком много запросов за короткое время (например, вы пытаетесь скачать 100 видео подряд), его автоматические системы могут временно вас заблокировать. Блокировка обычно длится от нескольких минут до нескольких часов.
Если вы скачиваете по несколько видео за раз, вы, скорее всего, редко будете сталкиваться с этой проблемой. Если же ошибка возникла — просто сделайте перерыв.
Для нашей простой консольной утилиты это не критично, но знать о таком поведении YouTube очень полезно.
Заключение
Мы создали простой, но мощный инструмент, который решает реальную проблему — скачивание видео с YouTube без рекламы и лишних сложностей. Мы увидели, как современные .NET-библиотеки позволяют писать элегантный и функциональный код, который приятно не только использовать, но и разрабатывать.
Это лишь отправная точка. Проект можно развивать дальше: добавить скачивание плейлистов, выбор формата, графический интерфейс на Avalonia UI или MAUI. Но даже в таком виде он уже отлично справляется со своей задачей.
Надеюсь, эта статья вдохновит вас на создание собственных полезных утилит.
Полный исходный код проекта, как и всегда, доступен на GitHub. Заходите, ставьте звезды и предлагайте свои улучшения!
Спасибо за внимание