C# - Youtube 동영상 다운로드 (YoutubeExplode 패키지)
사실 예전에도 한 번 소개해 드렸는데,
YoutubeExplode
; https://www.sysnet.pe.kr/2/0/13637#youtube_explode
github/YoutubeExplode
; https://github.com/Tyrrrz/YoutubeExplode
nuget/YoutubeExplode
; https://www.nuget.org/packages/YoutubeExplode
근래 들어 Youtube 측의 정책에 변화가 있어 사용법이 쉽지 않아졌습니다. (그러니까, 이 문제는 비단 YoutubeExplode 패키지뿐만 아니라 다른 라이브러리들도 모두 동일한 현상을 겪게 됩니다.)
Unable to download video (403 Forbidden) #853
; https://github.com/Tyrrrz/YoutubeExplode/issues/853
위의 이슈를 정리해 보면, Youtube 측에서 Rate Limit(요청 제한)을 적용했기 때문에 요청이 차단되는 현상이 발생한다는 건데요, 실제로 아래의 코드로만 테스트를 해도,
YoutubeClient youtube = new YoutubeClient();
var streamManifest = await youtube.Videos.Streams.GetManifestAsync(url);
어떤 경우에는 정상적으로 동작하지만, 어떤 경우에는 아래와 같은 오류가 발생합니다.
System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at YoutubeExplode.Videos.Streams.StreamClient.TryGetContentLengthAsync(IStreamData streamData, String url, CancellationToken cancellationToken) in /_/YoutubeExplode/Videos/Streams/StreamClient.cs:line 82
at YoutubeExplode.Videos.Streams.StreamClient.GetStreamInfosAsync(IEnumerable`1 streamDatas, CancellationToken cancellationToken)+MoveNext() in /_/YoutubeExplode/Videos/Streams/StreamClient.cs:line 115
at YoutubeExplode.Videos.Streams.StreamClient.GetStreamInfosAsync(IEnumerable`1 streamDatas, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
at YoutubeExplode.Utils.Extensions.AsyncCollectionExtensions.ToListAsync[T](IAsyncEnumerable`1 source) in /_/YoutubeExplode/Utils/Extensions/AsyncCollectionExtensions.cs:line 49
at YoutubeExplode.Utils.Extensions.AsyncCollectionExtensions.ToListAsync[T](IAsyncEnumerable`1 source) in /_/YoutubeExplode/Utils/Extensions/AsyncCollectionExtensions.cs:line 49
at YoutubeExplode.Videos.Streams.StreamClient.GetStreamInfosAsync(VideoId videoId, PlayerResponse playerResponse, CancellationToken cancellationToken) in /_/YoutubeExplode/Videos/Streams/StreamClient.cs:line 230
at YoutubeExplode.Videos.Streams.StreamClient.GetStreamInfosAsync(VideoId videoId, CancellationToken cancellationToken) in /_/YoutubeExplode/Videos/Streams/StreamClient.cs:line 275
at YoutubeExplode.Videos.Streams.StreamClient.GetManifestAsync(VideoId videoId, CancellationToken cancellationToken) in /_/YoutubeExplode/Videos/Streams/StreamClient.cs:line 307
at ConsoleApp1.Program.GetLink(String url)
이로 인해, 연속적인 요청에 대해 정상 동작한다는 보장이 없으므로 귀찮아도 재시도를 수행하도록 코드를 작성해야 합니다.
YoutubeClient youtube = new YoutubeClient();
StreamManifest streamManifest = await GetStreamManifest(youtube, url);
private static async Task<StreamManifest> GetStreamManifest(YoutubeClient youtube, string url)
{
while (true)
{
try
{
return await youtube.Videos.Streams.GetManifestAsync(url);
}
catch (Exception)
{
}
await Task.Delay(5000);
}
}
이 외에도 정책 변경이 하나 더 있다면,
Audio + Video가 합쳐진 스트림이 더 이상 제공되지 않는다는 점입니다.
var muxedStreams = streamManifest.GetMuxedStreams(); // 결과가 없음
즉, 각각 다운로드를 해야 하는데, 어쩌면 그것 역시 Rate Limit 정책이 걸려 있을지 모르니 재시도를 고려해야 할 것 같습니다. 그래서... 결국 이런 식으로 코드가 나와야 합니다.
using YoutubeExplode;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.Streams;
namespace ConsoleApp1;
// Install-Package YoutubeExplode
internal class Program
{
static async Task Main(string[] args)
{
YoutubeClient youtube = new YoutubeClient();
string url = "https://youtu.be/90HFIm2Reqk";
string title = await GetVideoTitle(youtube, url);
StreamManifest streamManifest = await GetStreamManifest(youtube, url);
string videoFile = await DownloadVideo(youtube, streamManifest, title, url);
string audioFile = await DownloadAudio(youtube, streamManifest, title, url);
}
private static async Task<StreamManifest> GetStreamManifest(YoutubeClient youtube, string url)
{
StreamManifest? streamManifest = null;
Console.WriteLine("Fetching stream manifest...");
while (true)
{
try
{
streamManifest = await youtube.Videos.Streams.GetManifestAsync(url);
break;
}
catch (Exception)
{
Console.Write($".");
}
await Task.Delay(5000);
}
Console.WriteLine();
return streamManifest;
}
static async Task<string> GetVideoTitle(YoutubeClient youtube, string url)
{
Video? video = null;
Console.WriteLine("Fetching video info...");
while (true)
{
try
{
video = await youtube.Videos.GetAsync(url);
break;
}
catch (Exception)
{
Console.Write($".");
}
await Task.Delay(5000);
}
if (video == null)
{
return "";
}
string title = video.Title;
foreach (char c in Path.GetInvalidFileNameChars())
{
title = title.Replace(c, '_');
}
return title;
}
static async Task<string> DownloadVideo(YoutubeClient youtube, StreamManifest streamManifest, string title, string url)
{
var videoStreams = streamManifest.GetVideoStreams();
IVideoStreamInfo? candidate = null;
IVideoStreamInfo? mp4stream = null;
foreach (var streamInfo in videoStreams)
{
VideoOnlyStreamInfo? vosi = streamInfo as VideoOnlyStreamInfo;
if (vosi == null)
{
continue;
}
candidate = streamInfo;
if (streamInfo.Container == Container.Mp4 && streamInfo.VideoResolution.Width == 1920)
{
mp4stream = streamInfo;
break;
}
}
if (mp4stream == null)
{
if (candidate == null)
{
return string.Empty;
}
mp4stream = candidate;
}
Console.WriteLine($"{mp4stream}: Downloading Video...");
string fileName = $"{title}.{mp4stream.Container}";
while (true)
{
try
{
await youtube.Videos.Streams.DownloadAsync(mp4stream, fileName);
break;
}
catch (Exception)
{
Console.Write($".");
}
await Task.Delay(5000);
}
return fileName;
}
private static async Task<string> DownloadAudio(YoutubeClient youtube, StreamManifest streamManifest, string title, string url)
{
var audioStreams = streamManifest.GetAudioOnlyStreams();
IAudioStreamInfo? candidate = null;
IAudioStreamInfo? mp3stream = null;
foreach (var streamInfo in audioStreams)
{
AudioOnlyStreamInfo? aosi = streamInfo as AudioOnlyStreamInfo;
if (aosi == null)
{
continue;
}
candidate = streamInfo;
if (streamInfo.Container == Container.Mp3 && streamInfo.Bitrate.KiloBitsPerSecond >= 128)
{
mp3stream = streamInfo;
break;
}
}
if (mp3stream == null)
{
if (candidate == null)
{
return string.Empty;
}
mp3stream = candidate;
}
Console.WriteLine($"{mp3stream}: Downloading Audio...");
string fileName = $"{title}.{mp3stream.Container}";
while (true)
{
try
{
await youtube.Videos.Streams.DownloadAsync(mp3stream, fileName);
break;
}
catch (Exception)
{
Console.Write($".");
}
await Task.Delay(5000);
}
return fileName;
}
}
이렇게 하면... 뭐 그런대로 잘 동작합니다. ^^ 단지,
좀 멋있게 하고 싶다면 Polly 등을 이용해 재시도를 구현해도 좋을 것입니다. ^^
마지막으로, Audio와 Video를 따로 받았으니 이제 합쳐야 할 텐데요, 이 부분은
지난 글에서 설명한 FFMpegCore 패키지를 이용해 간단하게 해결할 수 있습니다.
// Install-Package FFmpegCore
using FFMpegCore;
FFMpeg.ReplaceAudio(videoFile, audioFile, $"{title}-final.mp4");
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]