Repository: i3arnon/libvideo Branch: master Commit: 7dae9908d4d1 Files: 69 Total size: 137.3 KB Directory structure: gitextract_xcx1mnd_/ ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── changelog.md ├── docs/ │ └── README.md ├── samples/ │ └── Valks/ │ ├── Valks/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ └── Valks.csproj │ └── Valks.sln ├── src/ │ ├── libvideo/ │ │ ├── AdaptiveKind.cs │ │ ├── AudioFormat.cs │ │ ├── Client.cs │ │ ├── DelegatingClient.cs │ │ ├── Exceptions/ │ │ │ ├── BadQueryException.cs │ │ │ └── UnavaibleVideoException.cs │ │ ├── Helpers/ │ │ │ ├── EmptyArray.cs │ │ │ ├── Html.cs │ │ │ ├── Json.cs │ │ │ ├── KeyCollection.cs │ │ │ ├── Operations.cs │ │ │ ├── Query.cs │ │ │ ├── Require.cs │ │ │ ├── Text.cs │ │ │ ├── UnscrambledQuery.cs │ │ │ └── ValueCollection.cs │ │ ├── IAsyncService.cs │ │ ├── IService.cs │ │ ├── ServiceBase.cs │ │ ├── Video.cs │ │ ├── VideoClient.cs │ │ ├── VideoFormat.cs │ │ ├── VisitorDataTokenGenerator.cs │ │ ├── WebSites.cs │ │ ├── YouTube.cs │ │ ├── YouTubeVideo.Decrypt.cs │ │ ├── YouTubeVideo.Format.cs │ │ ├── YouTubeVideo.cs │ │ ├── YoutubeVideo.Descramble.cs │ │ └── libvideo.csproj │ ├── libvideo.compat/ │ │ ├── AdaptiveType.cs │ │ ├── AudioExtractionException.cs │ │ ├── AudioType.cs │ │ ├── DownloadUrlResolver.cs │ │ ├── VideoInfo.cs │ │ ├── VideoNotAvailableException.cs │ │ ├── VideoType.cs │ │ ├── YoutubeParseException.cs │ │ └── libvideo.compat.csproj │ ├── libvideo.debug/ │ │ ├── CustomYoutubeClient.cs │ │ ├── Program.cs │ │ └── libvideo.debug.csproj │ └── libvideo.sln └── tests/ ├── Compat/ │ ├── Compat/ │ │ ├── App.config │ │ ├── Compat.csproj │ │ ├── Program.cs │ │ └── Properties/ │ │ └── AssemblyInfo.cs │ └── Compat.sln ├── Core/ │ ├── Core/ │ │ ├── Core.csproj │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── UnitTests.cs │ │ └── packages.config │ └── Core.sln └── Speed.Test/ ├── Speed.Test/ │ ├── App.config │ ├── Program.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Speed.Test.csproj │ └── packages.config └── Speed.Test.sln ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .gitignore ================================================ # Binary storage bin/ obj/ lib/ # Binary files *.db *.dll *.exe # NuGet packages *.nupkg **/packages/* !**/packages/build/ #!**/packages/repositories.config # IDE clutter .vs/ *.suo *.user *.userprefs # Profiling sessions *.vspx *.psess # Miscellaneous *.cache ================================================ FILE: LICENSE ================================================ BSD 2-Clause License Copyright (c) 2021, OMANSAK All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # libvideo ![icon](icons/icon_200.png) [![NuGet](https://img.shields.io/nuget/dt/VideoLibrary.svg)](https://www.nuget.org/packages/VideoLibrary) [![NuGet](https://img.shields.io/nuget/v/VideoLibrary.svg)](https://www.nuget.org/packages/VideoLibrary) [![license](https://img.shields.io/github/license/i3arnon/libvideo.svg)](LICENSE) [![Join the chat at https://discord.gg/SERVhPp](https://user-images.githubusercontent.com/7288322/34429152-141689f8-ecb9-11e7-8003-b5a10a5fcb29.png)](https://discord.gg/SERVhPp) libvideo (aka VideoLibrary) is a modern .NET library for downloading YouTube videos. It is portable to most platforms and is very lightweight. ## Documentation - [Documentation](docs/README.md) - [Example Application](samples/Valks/Valks/Program.cs) - [Fast Downloader with Chunks](/src/libvideo.debug/CustomYoutubeClient.cs) ## Installation You can grab a copy of the library [on NuGet](https://www.nuget.org/packages/VideoLibrary) by running: Install-Package VideoLibrary Alternatively, you can try building the repo if you like your assemblies extra-fresh. ## Supported Platforms | Platform / Application | Minimum Supported Version | Notes | |-----------------------------------------|---------------------------|-------| | **.NET / .NET Core** | .NET Core 2.0+ / .NET 5+ | Fully supports .NET Standard 2.0 libraries. | | **.NET Framework** | 4.6.1+ | Supports .NET Standard 2.0 (does NOT support 2.1). | | **Mono** | 5.4+ | Official support for .NET Standard 2.0. | | **Unity** | 2018.1+ | Requires scripting runtime set to .NET 4.x Equivalent. | | **Xamarin.iOS** | 10.14+ | Supports .NET Standard 2.0 libraries. | | **Xamarin.Android** | 8.0+ | Supports .NET Standard 2.0 libraries. | | **Xamarin.Mac** | 3.8+ | Supports .NET Standard 2.0 libraries. | | **Universal Windows Platform (UWP)** | 10.0.16299+ | Supported starting from this Windows 10 version. | ## Getting Started Here's a small sample to help you get familiar with libvideo: ```csharp using VideoLibrary; void SaveVideoToDisk(string link) { var youTube = YouTube.Default; // starting point for YouTube actions var video = youTube.GetVideo(link); // gets a Video object with info about the video File.WriteAllBytes(@"C:\" + video.FullName, video.GetBytes()); } ``` Or, if you use Visual Basic: ```vbnet Imports VideoLibrary Sub SaveVideoToDisk(ByVal link As String) Dim video = YouTube.Default.GetVideo(link) File.WriteAllBytes("C:\" & video.FullName, video.GetBytes()) End Sub ``` If you'd like to check out some more of our features, take a look at our [docs](docs/README.md). You can also refer to our [example application](samples/Valks/Valks/Program.cs) (named Valks, yes, I know, it's a silly name) if you're looking for a more comprehensive sample. ## License libvideo is licensed under the [BSD 2-clause license](LICENSE). ================================================ FILE: changelog.md ================================================ # Changelog ## v3.3.0 - Fix Youtube: 403 Forbidden errors ([#307](https://github.com/omansak/libvideo/issues/307)) ================================================ FILE: docs/README.md ================================================ # Documentation Here you'll find a more in-depth explanation of our API. To get information about a video: ```csharp string uri = "https://www.youtube.com/watch?v=vPto6XpRq-U"; var youTube = YouTube.Default; var video = youTube.GetVideo(uri); string title = video.Title; VideoInfo info = video.Info; // (Title,Author,LengthSeconds) string fileExtension = video.FileExtension; string fullName = video.FullName; // same thing as title + fileExtension int resolution = video.Resolution; // etc. ``` You can download it like this: ```csharp byte[] bytes = video.GetBytes(); var stream = video.Stream(); ``` And save it to a file: ```csharp File.WriteAllBytes(@"C:\" + fullName, bytes); ``` --- ## Advanced YouTube exposes multiple videos for each URL- e.g. when you change the resolution of a video, you're actually watching a different video. libvideo supports downloading multiple of them: ```csharp var videos = youTube.GetAllVideos(uri); ``` Some Informations of Video ```csharp var videoInfos = Client.For(YouTube.Default).GetAllVideosAsync(uri).GetAwaiter().GetResult(); var resolutions = videoInfos.Where(j => j.AdaptiveKind == AdaptiveKind.Video).Select(j => j.Resolution); var bitRates = videoInfos.Where(j => j.AdaptiveKind == AdaptiveKind.Audio).Select(j => j.AudioBitrate); var unknownFormats = videoInfos.Where(j => j.AdaptiveKind == AdaptiveKind.None).Select(j => j.Resolution); ``` Get specific resolution, bitrate, format ```csharp var youTube = YouTube.Default; // starting point for YouTube actions var videoInfos = youTube.GetAllVideosAsync(link).GetAwaiter().GetResult(); var maxResolution = videoInfos.First(i => i.Resolution == videoInfos.Max(j => j.Resolution)); var minBitrate = videoInfos.First(i => i.AudioBitrate == videoInfos.Min(j => j.AudioBitrate)); var audioFormat = videoInfos.First(i => i.AudioFormat == AudioFormat.Aac); var videoFormat = videoInfos.First(i => i.Format == VideoFormat.Mp4); var adaptive = videoInfos.First(i => i.IsAdaptive); ``` We also have full support for async: ```csharp var video = await youTube.GetVideoAsync(uri); var videos = await youTube.GetAllVideosAsync(uri); var contents = await video.GetBytesAsync(); ``` In addition, you should be aware that for every time you visit YouTube a new `HttpClient` is created and disposed. To avoid this, use the `Client` class: ```csharp using (var cli = Client.For(new YouTube())) { cli.GetVideo(uri); cli.GetVideo("[some other video]"); // HttpClient is reused here } ``` Likewise, if you'd like to reuse `HttpClients` when downloading a video, use `VideoClient`. ```csharp using (var cli = new VideoClient()) { cli.GetBytes(video); await cli.StreamAsync(video); } ``` ### Custom HTTP Configurations If you need to custom-configure the `HttpClient` for some reason- maybe you need to increase the timeout length, or add credentials, or use [a different message handler](https://github.com/paulcbetts/ModernHttpClient)- fear not. Simply derive your class from `YouTube` and configure as necessary: ```csharp class MyYouTube : YouTube { protected override HttpMessageHandler MakeHandler() { return new BlahBlahMessageHandler(); } protected override HttpClient MakeClient(HttpMessageHandler handler) { return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(12345); }; } } ``` Use like so: ```csharp var youTube = new MyYouTube(); youTube.GetVideo("foo"); // --- OR --- using (var cli = Client.For(new MyYouTube())) { // ... } ``` Note that this does not change the HTTP behavior when downloading the video itself. To do that, inherit from `VideoClient`: ```csharp class MyVideoClient : VideoClient { protected override HttpMessageHandler MakeHandler() { ... } protected override HttpClient MakeClient(HttpMessageHandler handler) { ... } } ``` And to use it: ```csharp using (var cli = new MyVideoClient()) { byte[] contents = cli.GetBytes(video); } ``` Sample Progress ```csharp class Program { static async Task Main(string[] args) { var youtube = YouTube.Default; var video = youtube.GetVideo("https://www.youtube.com/watch?v=GNxEEyOMce4"); var client = new HttpClient(); long? totalByte = 0; using (Stream output = File.OpenWrite("C:\\Users" + video.Title)) { using (var request = new HttpRequestMessage(HttpMethod.Head, video.Uri)) { totalByte = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result.Content.Headers.ContentLength; } using (var input = await client.GetStreamAsync(video.Uri)) { byte[] buffer = new byte[16 * 1024]; int read; int totalRead = 0; Console.WriteLine("Download Started"); while ((read = input.Read(buffer, 0, buffer.Length)) > 0) { output.Write(buffer, 0, read); totalRead += read; Console.Write($"\rDownloading {totalRead}/{totalByte} ..."); } Console.WriteLine("Download Complete"); } } Console.ReadLine(); } } ``` ### Custom Downloader with Chunks (Fast) [Sample Code](https://github.com/omansak/libvideo/blob/master/src/libvideo.debug/CustomYoutubeClient.cs) --- That's it, enjoy! If you're looking for more features, feel free to raise an issue and we can discuss it with you. ================================================ FILE: samples/Valks/Valks/Program.cs ================================================ using VideoLibrary; using System; using System.IO; namespace Valks { class MainClass { public static void Main(string[] args) { Console.WriteLine("Welcome to Valks!"); Console.WriteLine("Easily save your favorite videos from YouTube."); using (var service = Client.For(YouTube.Default)) { while (true) { Console.WriteLine(); Console.Write("Enter your video's ID: "); string id = Console.ReadLine(); Console.WriteLine("Awesome! Downloading..."); var video = service.GetVideo("https://youtube.com/watch?v=" + id); Console.Write("Finished! Would you like to save the video to Downloads? [y/n] "); char opt = Console.ReadKey().KeyChar; Console.WriteLine(); string folder; if (char.ToUpper(opt) == 'Y') folder = GetDefaultFolder(); else { Console.Write("Please tell us where you'd like to save it: "); folder = Console.ReadLine(); } string path = Path.Combine(folder, video.FullName); Console.WriteLine("Saving..."); File.WriteAllBytes(path, video.GetBytes()); Console.WriteLine("Done."); } } } static string GetDefaultFolder() { var home = Environment.GetFolderPath( Environment.SpecialFolder.UserProfile); return Path.Combine(home, "Downloads"); } } } ================================================ FILE: samples/Valks/Valks/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. [assembly: AssemblyTitle("Valks")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("")] [assembly: AssemblyCopyright("i3arnon")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. [assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. //[assembly: AssemblyDelaySign(false)] //[assembly: AssemblyKeyFile("")] ================================================ FILE: samples/Valks/Valks/Valks.csproj ================================================  Debug x86 10.0.0 2.0 {A600E2F2-81C4-4C6A-B87B-342766AA773E} Exe Valks Valks v4.5 true full false bin\Debug DEBUG; prompt 4 true x86 full true bin\Release prompt 4 true x86 False ..\..\..\src\libvideo\bin\Release\netstandard1.1\libvideo.dll ================================================ FILE: samples/Valks/Valks.sln ================================================  Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Valks", "Valks\Valks.csproj", "{A600E2F2-81C4-4C6A-B87B-342766AA773E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A600E2F2-81C4-4C6A-B87B-342766AA773E}.Debug|x86.ActiveCfg = Debug|x86 {A600E2F2-81C4-4C6A-B87B-342766AA773E}.Debug|x86.Build.0 = Debug|x86 {A600E2F2-81C4-4C6A-B87B-342766AA773E}.Release|x86.ActiveCfg = Release|x86 {A600E2F2-81C4-4C6A-B87B-342766AA773E}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = Valks\Valks.csproj EndGlobalSection EndGlobal ================================================ FILE: src/libvideo/AdaptiveKind.cs ================================================ namespace VideoLibrary { public enum AdaptiveKind { None, Audio, Video } } ================================================ FILE: src/libvideo/AudioFormat.cs ================================================ namespace VideoLibrary { public enum AudioFormat { Aac = 0, Vorbis = 1, Opus = 2, Unknown = 3 } } ================================================ FILE: src/libvideo/Client.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using VideoLibrary.Helpers; namespace VideoLibrary { public static class Client { public static Client For(ServiceBase baseService) where T : Video => new Client(baseService); } public class Client : IService, IAsyncService, IDisposable where T : Video { private bool disposed = false; private readonly ServiceBase baseService; private readonly HttpClient client; private Task SourceFactory(string address) => client.GetStringAsync(address); internal Client(ServiceBase baseService) { Require.NotNull(baseService, nameof(baseService)); this.baseService = baseService; this.client = baseService.MakeClient(); } #region IDisposable ~Client() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; if (disposing) { if (client != null) client.Dispose(); } } #endregion public T GetVideo(string videoUri) => baseService.GetVideo(videoUri, SourceFactory); public IEnumerable GetAllVideos(string videoUri) => baseService.GetAllVideos(videoUri, SourceFactory); public Task GetVideoAsync(string videoUri) => baseService.GetVideoAsync(videoUri, SourceFactory); public Task> GetAllVideosAsync(string videoUri) => baseService.GetAllVideosAsync(videoUri, SourceFactory); } } ================================================ FILE: src/libvideo/DelegatingClient.cs ================================================ using System; using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; namespace VideoLibrary { public class DelegatingClient : IDisposable { private bool disposed = false; private readonly HttpClient client; public DelegatingClient() { this.client = MakeClient(); } #region IDisposable ~DelegatingClient() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; if (disposing) { if (client != null) client.Dispose(); } } #endregion #region MakeClient/MakeHandler private HttpClient MakeClient() => MakeClient(MakeHandler()); protected virtual HttpMessageHandler MakeHandler() { var handler = new HttpClientHandler(); if (handler.SupportsAutomaticDecompression) { handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } return handler; } protected virtual HttpClient MakeClient(HttpMessageHandler handler) { return new HttpClient(handler); } #endregion #region Synchronous wrappers public HttpResponseMessage Get(string uri) => GetAsync(uri).GetAwaiter().GetResult(); public byte[] GetByteArray(string uri) => GetByteArrayAsync(uri).GetAwaiter().GetResult(); public Stream GetStream(string uri) => GetStreamAsync(uri).GetAwaiter().GetResult(); public string GetString(string uri) => GetStringAsync(uri).GetAwaiter().GetResult(); #endregion #region HttpClient wrappers // TODO: Support other kinds of HTTP requests, // such as PUT, POST, DELETE, etc. public Task GetAsync(string uri) => client.GetAsync(uri); public Task GetByteArrayAsync(string uri) => client.GetByteArrayAsync(uri); public Task GetStreamAsync(string uri) => client.GetStreamAsync(uri); public Task GetStringAsync(string uri) => client.GetStringAsync(uri); #endregion } } ================================================ FILE: src/libvideo/Exceptions/BadQueryException.cs ================================================ using System; namespace VideoLibrary.Exceptions { internal class BadQueryException : Exception { public BadQueryException() : base() { } public BadQueryException(string message) : base(message) { } public BadQueryException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: src/libvideo/Exceptions/UnavaibleVideoException.cs ================================================ using System; namespace VideoLibrary.Exceptions { public class UnavailableStreamException : Exception { public UnavailableStreamException() : base() { } public UnavailableStreamException(string message) : base(message) { } public UnavailableStreamException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: src/libvideo/Helpers/EmptyArray.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal static class EmptyArray { public static readonly T[] Value = new T[0]; } } ================================================ FILE: src/libvideo/Helpers/Html.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal static class Html { // TODO: Refactor? public static string GetNode(string name, string source) => WebUtility.HtmlDecode( Text.StringBetween( '<' + name + '>', "', source)); public static IEnumerable GetUrisFromManifest(string source) { string opening = ""; string closing = ""; int start = source.IndexOf(opening); if (start != -1) { string temp = source.Substring(start); var uris = temp.Split(new string[] { opening }, StringSplitOptions.RemoveEmptyEntries) .Select(v => v.Substring(0, v.IndexOf(closing))); return uris; } throw new NotSupportedException(); } } } ================================================ FILE: src/libvideo/Helpers/Json.cs ================================================ using System; using System.Text; using System.Text.Json; namespace VideoLibrary.Helpers { internal static class Json { public static string GetKey(string key, string source) { if (GetKey(key, source, out string result)) { return result; } return null; } public static bool TryGetKey(string key, string source, out string target) { return GetKey(key, source, out target); } public static JsonElement? GetNullableProperty(this JsonElement jsonElement, string propertyName) { if (jsonElement.TryGetProperty(propertyName, out JsonElement returnElement)) { return returnElement; } return null; } public static string Extract(string source) { StringBuilder sb = new StringBuilder(); int depth = 0; int backSlashesCounter = 0; char lastChar = '\u0000'; bool isString = false; foreach (var ch in source) { sb.Append(ch); if (ch == '\\') { // count backslashes backSlashesCounter++; } else if (ch == '"') { // if current char is quote check last char and count of backslashes to be sure it is not doubled backslashes if (lastChar != '\\' || backSlashesCounter%2 == 0) { isString = !isString; } } else { // reset backslashes count if its any other char backSlashesCounter = 0; } if (!isString) { if (ch == '{' && lastChar != '\\') depth++; else if (ch == '}' && lastChar != '\\') depth--; } if (depth == 0) break; lastChar = ch; } return sb.ToString(); } private static bool GetKey(string key, string source, out string target) { // Example scenario: "key" : "value" string quotedKey = '"' + key + '"'; int index = 0; while (true) { index = source.IndexOf(quotedKey, index, StringComparison.Ordinal); // '"' if (index == -1) { target = string.Empty; return false; } index += quotedKey.Length; // ' ' int start = index; start = source.SkipWhitespace(start); // ':' if (source[start++] != ':') // ' ' continue; start = source.SkipWhitespace(start); // '"' if (source[start++] != '"') // 'v' continue; int end = start; while ((source[end - 1] == '\\' && source[end] == '"') || source[end] != '"') // "value\"" { end++; } target = source.Substring(start, end - start); return true; } } } } ================================================ FILE: src/libvideo/Helpers/KeyCollection.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal partial class Query : IDictionary { public class KeyCollection : ICollection, IReadOnlyCollection { private readonly Query query; public KeyCollection(Query query) { this.query = query; } public int Count => query.Count; public bool IsReadOnly => true; public void Add(string item) { throw new NotSupportedException(); } public void Clear() { throw new NotSupportedException(); } public bool Contains(string item) { for (int i = 0; i < query.Count; i++) { var pair = query.Pairs[i]; if (item == pair.Key) return true; } return false; } public void CopyTo(string[] array, int arrayIndex) { for (int i = 0; i < query.Count; i++) array[arrayIndex++] = query.Pairs[i].Key; } public IEnumerator GetEnumerator() { for (int i = 0; i < query.Count; i++) yield return query.Pairs[i].Key; } public bool Remove(string item) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } } ================================================ FILE: src/libvideo/Helpers/Operations.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal struct Operations { public Operations(string reverse, string swap, string splice) { this.Reverse = reverse; this.Swap = swap; this.Splice = splice; } public string Reverse { get; } public string Swap { get; } public string Splice { get; } } } ================================================ FILE: src/libvideo/Helpers/Query.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Text; namespace VideoLibrary.Helpers { internal partial class Query : IDictionary, IReadOnlyDictionary { private int count; private readonly string baseUri; private KeyValuePair[] pairs; public Query(string uri) { int divide = uri.IndexOf('?'); if (divide == -1) { int amp = uri.IndexOf('&'); if (amp == -1) { // no query parameters this.baseUri = uri; return; } // no base URL this.baseUri = null; } else { // normal URL this.baseUri = uri.Substring(0, divide); uri = uri.Substring(divide + 1); } string[] keyValues = uri.Split('&'); string[] keys = EmptyArray.Value; string[] values = EmptyArray.Value; pairs = new KeyValuePair[keyValues.Length]; for (int i = 0; i < keyValues.Length; i++) { string pair = keyValues[i]; int equals = pair.IndexOf('='); string key; string value; key = pair.Substring(0, equals); value = equals < pair.Length ? pair.Substring(equals + 1) : string.Empty; pairs[i] = new KeyValuePair(key, value); } this.count = keyValues.Length; } public string this[string key] { get { for (int i = 0; i < count; i++) { var pair = pairs[i]; if (pair.Key == key) return pair.Value; } throw new KeyNotFoundException(); } set { for (int i = 0; i < count; i++) { var pair = pairs[i]; if (pair.Key == key) { pairs[i] = new KeyValuePair(key, value); return; } } throw new KeyNotFoundException(); } } public string BaseUri => baseUri; public int Count => count; public bool IsReadOnly => false; public KeyCollection Keys => new KeyCollection(this); ICollection IDictionary.Keys => Keys; public KeyValuePair[] Pairs => pairs; public ValueCollection Values => new ValueCollection(this); ICollection IDictionary.Values => Values; IEnumerable IReadOnlyDictionary.Keys => Keys; IEnumerable IReadOnlyDictionary.Values => Values; void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); } public void Add(string key, string value) { EnsureCapacity(count + 1); pairs[count++] = new KeyValuePair(key, value); } public void Clear() { if (count == 0) return; Array.Clear(pairs, 0, count); count = 0; } bool ICollection>.Contains(KeyValuePair item) { for (int i = 0; i < count; i++) { var pair = pairs[i]; if (item.Key == pair.Key && item.Value == pair.Value) return true; } return false; } public bool ContainsKey(string key) { for (int i = 0; i < count; i++) { if (key == pairs[i].Key) return true; } return false; } void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { Array.Copy(pairs, 0, array, arrayIndex, count); } public IEnumerator> GetEnumerator() { for (int i = 0; i < count; i++) yield return pairs[i]; } bool ICollection>.Remove(KeyValuePair item) { return Remove(item.Key); } public bool Remove(string key) { for (int i = 0; i < count; i++) { var pair = pairs[i]; if (pair.Key == key) { // found it if (i != count--) Array.Copy(pairs, i + 1, pairs, i, count - i); pairs[count] = default(KeyValuePair); return true; } } return false; } public bool TryGetValue(string key, out string value) { for (int i = 0; i < count; i++) { var pair = pairs[i]; if (key == pair.Key) { value = pair.Value; return true; } } value = null; return false; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public override string ToString() { if (count == 0) return baseUri; var builder = new StringBuilder(); if (baseUri != null) builder.Append(baseUri).Append('?'); var pair = pairs[0]; // OK since we know count is at least 1 builder.Append(pair.Key) .Append('=').Append(pair.Value); for (int i = 1; i < count; i++) { pair = pairs[i]; builder.Append('&').Append(pair.Key) .Append('=').Append(pair.Value); } return builder.ToString(); } private void EnsureCapacity(int capacity) { if (capacity > pairs.Length) { capacity = Math.Max(capacity, pairs.Length * 2); Array.Resize(ref pairs, capacity); } } } } ================================================ FILE: src/libvideo/Helpers/Require.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal static class Require { public static void NotNull(T obj, string name) where T : class { if (obj == null) throw new ArgumentNullException(name); } } } ================================================ FILE: src/libvideo/Helpers/Text.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal static class Text { public static string StringBetween(string prefix, string suffix, string parent) { int start = parent.IndexOf(prefix) + prefix.Length; if (start < prefix.Length) return string.Empty; int end = parent.IndexOf(suffix, start); if (end == -1) end = parent.Length; return parent.Substring(start, end - start); } public static int SkipWhitespace(this string text, int start) { int result = start; while (char.IsWhiteSpace(text[result])) result++; return result; } } } ================================================ FILE: src/libvideo/Helpers/UnscrambledQuery.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal readonly struct UnscrambledQuery { public UnscrambledQuery(string uri, bool encrypted) { this.Uri = uri; this.IsEncrypted = encrypted; } public string Uri { get; } public bool IsEncrypted { get; } } } ================================================ FILE: src/libvideo/Helpers/ValueCollection.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary.Helpers { internal partial class Query { public class ValueCollection : ICollection, IReadOnlyCollection { private readonly Query query; public ValueCollection(Query query) { this.query = query; } public int Count => query.Count; public bool IsReadOnly => true; public void Add(string item) { throw new NotSupportedException(); } public void Clear() { throw new NotSupportedException(); } public bool Contains(string item) { for (int i = 0; i < query.Count; i++) { var pair = query.Pairs[i]; if (item == pair.Value) return true; } return false; } public void CopyTo(string[] array, int arrayIndex) { for (int i = 0; i < query.Count; i++) array[arrayIndex++] = query.Pairs[i].Value; } public IEnumerator GetEnumerator() { for (int i = 0; i < query.Count; i++) yield return query.Pairs[i].Value; } public bool Remove(string item) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } } ================================================ FILE: src/libvideo/IAsyncService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary { internal interface IAsyncService where T : Video { Task GetVideoAsync(string uri); Task> GetAllVideosAsync(string uri); } } ================================================ FILE: src/libvideo/IService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary { internal interface IService where T : Video { T GetVideo(string uri); IEnumerable GetAllVideos(string uri); } } ================================================ FILE: src/libvideo/ServiceBase.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; namespace VideoLibrary { public abstract class ServiceBase : IService, IAsyncService where T : Video { internal virtual T VideoSelector(IEnumerable videos) => videos.First(); #region Synchronous wrappers public T GetVideo(string videoUri) => GetVideoAsync(videoUri).GetAwaiter().GetResult(); internal T GetVideo(string videoUri, Func> sourceFactory) => GetVideoAsync(videoUri, sourceFactory).GetAwaiter().GetResult(); public IEnumerable GetAllVideos(string videoUri) => GetAllVideosAsync(videoUri).GetAwaiter().GetResult(); internal IEnumerable GetAllVideos(string videoUri, Func> sourceFactory) => GetAllVideosAsync(videoUri, sourceFactory).GetAwaiter().GetResult(); #endregion public async Task GetVideoAsync(string videoUri) { using (var wrapped = Client.For(this)) { return await wrapped .GetVideoAsync(videoUri) .ConfigureAwait(false); } } internal async Task GetVideoAsync( string videoUri, Func> sourceFactory) => VideoSelector(await GetAllVideosAsync( videoUri, sourceFactory).ConfigureAwait(false)); public async Task> GetAllVideosAsync(string videoUri) { using (var wrapped = Client.For(this)) { return await wrapped .GetAllVideosAsync(videoUri) .ConfigureAwait(false); } } internal abstract Task> GetAllVideosAsync( string videoUri, Func> sourceFactory); internal HttpClient MakeClient() => MakeClient(MakeHandler()); protected virtual HttpMessageHandler MakeHandler() { // Cookie var cookieContainer = new CookieContainer(); cookieContainer.Add(new Uri(YouTube.YoutubeUrl), new Cookie("CONSENT", "YES+cb", "/", ".youtube.com")); // Handler var handler = new HttpClientHandler { UseCookies = true, CookieContainer = cookieContainer }; if (handler.SupportsAutomaticDecompression) handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; return handler; } protected virtual HttpClient MakeClient(HttpMessageHandler handler) { var httpClient = new HttpClient(handler); httpClient.DefaultRequestHeaders.Add( "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36 Edg/89.0.774.76" ); return new HttpClient(handler); } } } ================================================ FILE: src/libvideo/Video.cs ================================================ using System; using System.IO; using System.Text; using System.Threading.Tasks; namespace VideoLibrary { public class VideoInfo { public VideoInfo(string title, int second, string author) { this.Title = title; this.LengthSeconds = second; this.Author = author; } public string Title { get; } public int LengthSeconds { get; } public string Author { get; } } public abstract class Video { internal Video() { } public abstract string Uri { get; } public abstract string Title { get; } public abstract VideoInfo Info { get; } public abstract WebSites WebSite { get; } public virtual VideoFormat Format => VideoFormat.Unknown; // public virtual AudioFormat AudioFormat => AudioFormat.Unknown; public virtual Task GetUriAsync() => Task.FromResult(Uri); public byte[] GetBytes() => GetBytesAsync().GetAwaiter().GetResult(); public async Task GetBytesAsync() { using (var client = new VideoClient()) { return await client .GetBytesAsync(this) .ConfigureAwait(false); } } public Stream Stream() => StreamAsync().GetAwaiter().GetResult(); public async Task StreamAsync() { using (var client = new VideoClient()) { return await client .StreamAsync(this) .ConfigureAwait(false); } } public Stream Head() => HeadAsync().GetAwaiter().GetResult(); public async Task HeadAsync() { using (var client = new VideoClient()) { return await client .StreamAsync(this) .ConfigureAwait(false); } } public virtual string FileExtension { get { switch (Format) { case VideoFormat.Mp4: return ".mp4"; case VideoFormat.WebM: return ".webm"; case VideoFormat.Unknown: return string.Empty; default: throw new NotImplementedException($"Format {Format} is unrecognized! Please file an issue at libvideo on GitHub."); } } } public string FullName { get { var builder = new StringBuilder(Title) .Append(FileExtension); foreach (char bad in Path.GetInvalidFileNameChars()) builder.Replace(bad, '_'); return builder.ToString(); } } } } ================================================ FILE: src/libvideo/VideoClient.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace VideoLibrary { public class VideoClient : IDisposable { private bool disposed = false; private readonly HttpClient client; public VideoClient() { this.client = MakeClient(); } #region IDisposable ~VideoClient() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; if (disposing) { if (client != null) client.Dispose(); } } #endregion #region MakeClient/MakeHandler private HttpClient MakeClient() => MakeClient(MakeHandler()); protected virtual HttpMessageHandler MakeHandler() => new HttpClientHandler(); protected virtual HttpClient MakeClient(HttpMessageHandler handler) { return new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(int.MaxValue) // Longest TimeSpan HttpClient will accept }; } #endregion public byte[] GetBytes(Video video) => GetBytesAsync(video).GetAwaiter().GetResult(); public async Task GetBytesAsync(Video video) { string uri = await video.GetUriAsync() .ConfigureAwait(false); return await client .GetByteArrayAsync(uri) .ConfigureAwait(false); } public Stream Stream(Video video) => StreamAsync(video).GetAwaiter().GetResult(); public async Task StreamAsync(Video video) { string uri = await video.GetUriAsync() .ConfigureAwait(false); return await client .GetStreamAsync(uri) .ConfigureAwait(false); } public async Task GetContentLengthAsync(string requestUri) { using (var response = await HeadAsync(requestUri)) { return response.Content.Headers.ContentLength; } } public async Task HeadAsync(string requestUri) { using (var request = new HttpRequestMessage(HttpMethod.Head, requestUri)) return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); } } } ================================================ FILE: src/libvideo/VideoFormat.cs ================================================ namespace VideoLibrary { public enum VideoFormat { Mp4, WebM, Unknown } } ================================================ FILE: src/libvideo/VisitorDataTokenGenerator.cs ================================================ using System; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; namespace VideoLibrary { internal class VisitorDataTokenGenerator : IDisposable { private bool _disposed; private static string _visitorData = string.Empty; public static async Task GetVisitorDataFromYouTube(HttpClient http) { // Return cached visitor data if available if (!string.IsNullOrEmpty(_visitorData)) return _visitorData; try { // Configure request headers http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); const string url = "https://www.youtube.com/sw.js_data"; var response = await http.GetAsync(url); response.EnsureSuccessStatusCode(); var jsonString = await response.Content.ReadAsStringAsync(); // Remove the ")]}'" prefix if present jsonString = jsonString.StartsWith(")]}'") ? jsonString.Substring(4) : jsonString; var doc = JsonDocument.Parse(jsonString); var value = doc.RootElement[0] .EnumerateArray() .ElementAt(2) .EnumerateArray() .ElementAt(0) .EnumerateArray() .ElementAt(0) .EnumerateArray() .ElementAt(13) .GetString(); if (value == null) throw new Exception("Failed to fetch visitor data"); _visitorData = value; return _visitorData; } catch (HttpRequestException ex) { throw new Exception("Failed to fetch data from YouTube", ex); } catch (JsonException ex) { throw new Exception("Failed to parse JSON response", ex); } } public void Dispose() { if (!_disposed) { _disposed = true; } } ~VisitorDataTokenGenerator() { Dispose(); } } } ================================================ FILE: src/libvideo/WebSites.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace VideoLibrary { public enum WebSites { YouTube = 0 } } ================================================ FILE: src/libvideo/YouTube.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using VideoLibrary.Exceptions; using VideoLibrary.Helpers; using System.IO; namespace VideoLibrary { public class YouTube : ServiceBase { private const string Playback = "videoplayback"; private static string _signatureKey; public static YouTube Default { get; } = new YouTube(); public const string YoutubeUrl = "https://youtube.com/"; internal override async Task> GetAllVideosAsync(string videoUri, Func> sourceFactory) { if (!TryNormalize(videoUri, out videoUri)) throw new ArgumentException("URL is not a valid YouTube URL!"); // TODO Remove string source = await sourceFactory(videoUri).ConfigureAwait(false); // TODO Remove string jsPlayer = ParseJsPlayer(source); if (jsPlayer == null) { throw new UnavailableStreamException($"JS Player is not found"); } var playerResponseJson = JsonDocument.Parse(Json.Extract(ParsePlayerJson(source))).RootElement; // PlayerJson from IOS content var data = await GetPlayerResponseIOSAsync(playerResponseJson.GetProperty("videoDetails").GetNullableProperty("videoId")?.GetString()) .ConfigureAwait(false); if (data != null) { playerResponseJson = JsonDocument.Parse(data).RootElement; } return ParseVideos(source, jsPlayer, playerResponseJson); } public static string GetSignatureKey() { return string.IsNullOrWhiteSpace(_signatureKey) ? "signature" : _signatureKey; } private bool TryNormalize(string videoUri, out string normalized) { // If you fix something in here, please be sure to fix in // DownloadUrlResolver.TryNormalizeYoutubeUrl as well. normalized = null; var builder = new StringBuilder(videoUri); videoUri = builder.Replace("youtu.be/", "youtube.com/watch?v=") .Replace("youtube.com/embed/", "youtube.com/watch?v=") .Replace("/v/", "/watch?v=") .Replace("/watch#", "/watch?") .Replace("youtube.com/shorts/", "youtube.com/watch?v=") .ToString(); var query = new Query(videoUri); string value; if (!query.TryGetValue("v", out value)) return false; normalized = $"{YoutubeUrl}watch?v=" + value; return true; } private IEnumerable ParseVideos(string source, string jsPlayer, JsonElement playerResponseJson) { IEnumerable queries; if (string.Equals(playerResponseJson.GetProperty("playabilityStatus").GetNullableProperty("status")?.GetString(), "error", StringComparison.OrdinalIgnoreCase)) { throw new UnavailableStreamException($"Video has unavailable stream."); } var errorReason = playerResponseJson.GetProperty("playabilityStatus").GetNullableProperty("reason")?.GetString(); if (string.IsNullOrWhiteSpace(errorReason)) { var isLiveStream = playerResponseJson.GetProperty("videoDetails").GetNullableProperty("isLive")?.GetBoolean() == true; var title = playerResponseJson.GetProperty("videoDetails").GetNullableProperty("title")?.GetString(); var lengthSeconds = playerResponseJson.GetProperty("videoDetails").GetNullableProperty("lengthSeconds")?.GetString() ?? "0"; var author = playerResponseJson.GetProperty("videoDetails").GetNullableProperty("author")?.GetString(); var videoInfo = new VideoInfo(title, int.Parse(lengthSeconds), author); if (isLiveStream) { throw new UnavailableStreamException($"This is live stream so unavailable stream."); } string map = Json.GetKey("url_encoded_fmt_stream_map", source); if (!string.IsNullOrWhiteSpace(map)) { queries = map.Split(',').Select(Unscramble); foreach (var query in queries) yield return new YouTubeVideo(videoInfo, query, jsPlayer); } else // player_response { List streamObjects = new List(); // Extract Muxed streams var streamFormat = playerResponseJson.GetNullableProperty("streamingData")?.GetNullableProperty("formats"); if (streamFormat != null) { streamObjects.AddRange(streamFormat?.EnumerateArray()); } // Extract AdaptiveFormat streams var streamAdaptiveFormats = playerResponseJson.GetNullableProperty("streamingData")?.GetNullableProperty("adaptiveFormats"); if (streamAdaptiveFormats != null) { streamObjects.AddRange(streamAdaptiveFormats?.EnumerateArray()); } foreach (var item in streamObjects) { var urlValue = item.GetNullableProperty("url")?.GetString(); if (!string.IsNullOrEmpty(urlValue)) { var query = new UnscrambledQuery(urlValue, false); yield return new YouTubeVideo(videoInfo, query, jsPlayer); continue; } var cipherValue = (item.GetNullableProperty("cipher") ?? item.GetNullableProperty("signatureCipher"))?.GetString(); if (!string.IsNullOrEmpty(cipherValue)) { yield return new YouTubeVideo(videoInfo, Unscramble(cipherValue), jsPlayer); } } } // adaptive_fmts string adaptiveMap = Json.GetKey("adaptive_fmts", source); if (!string.IsNullOrWhiteSpace(adaptiveMap)) { queries = adaptiveMap.Split(',').Select(Unscramble); foreach (var query in queries) yield return new YouTubeVideo(videoInfo, query, jsPlayer); } else { // dashmpd string dashmpdMap = Json.GetKey("dashmpd", source); if (!string.IsNullOrWhiteSpace(adaptiveMap)) { using (HttpClient hc = new HttpClient()) { IEnumerable uris = null; try { dashmpdMap = WebUtility.UrlDecode(dashmpdMap).Replace(@"\/", "/"); var manifest = hc .GetStringAsync(dashmpdMap) .GetAwaiter() .GetResult() .Replace(@"\/", "/"); uris = Html.GetUrisFromManifest(manifest); } catch (Exception e) { throw new UnavailableStreamException(e.Message); } if (uris != null) { foreach (var v in uris) { yield return new YouTubeVideo(videoInfo, UnscrambleManifestUri(v), jsPlayer); } } } } } } else { throw new UnavailableStreamException($"Error caused by Youtube.({errorReason}))"); } } private string ParsePlayerJson(string source) { string playerResponseMap = null, ytInitialPlayerPattern = @"\s*var\s*ytInitialPlayerResponse\s*=\s*(\{\""responseContext\"".*\});", ytWindowInitialPlayerResponse = @"\[\""ytInitialPlayerResponse\""\]\s*=\s*(\{.*\});", ytPlayerPattern = @"ytplayer\.config\s*=\s*(\{\"".*\""\}\});"; Match match; if ((match = Regex.Match(source, ytPlayerPattern)).Success && Json.TryGetKey("player_response", match.Groups[1].Value, out string json)) { playerResponseMap = Regex.Unescape(json); } if (string.IsNullOrWhiteSpace(playerResponseMap) && (match = Regex.Match(source, ytInitialPlayerPattern)).Success) { playerResponseMap = match.Groups[1].Value; } if (string.IsNullOrWhiteSpace(playerResponseMap) && (match = Regex.Match(source, ytWindowInitialPlayerResponse)).Success) { playerResponseMap = match.Groups[1].Value; } if (string.IsNullOrWhiteSpace(playerResponseMap)) { throw new UnavailableStreamException("Player json has no found."); } return playerResponseMap.Replace(@"\u0026", "&").Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\&", "\\\\&"); } private string ParseJsPlayer(string source) { if (Json.TryGetKey("jsUrl", source, out var jsPlayer) || Json.TryGetKey("PLAYER_JS_URL", source, out jsPlayer)) { jsPlayer = jsPlayer.Replace(@"\/", "/"); } else { // Alternative solution Match match = Regex.Match(source, "\\s*"); if (match.Success) { jsPlayer = match.Groups[1].Value.Replace(@"\/", "/"); } else { return null; } } if (jsPlayer.StartsWith("/yts") || jsPlayer.StartsWith("/s")) { return $"https://www.youtube.com{jsPlayer}"; } // Fall back on old implementation (not sure it's needed) if (!jsPlayer.StartsWith("http")) { jsPlayer = $"https:{jsPlayer}"; } return jsPlayer; } private UnscrambledQuery Unscramble(string queryString) { queryString = queryString.Replace(@"\u0026", "&"); var query = new Query(queryString); string uri = query["url"]; query.TryGetValue("sp", out _signatureKey); bool encrypted = false; string signature; if (query.TryGetValue("s", out signature)) { encrypted = true; uri += GetSignatureAndHost(GetSignatureKey(), signature, query); } else if (query.TryGetValue("sig", out signature)) uri += GetSignatureAndHost(GetSignatureKey(), signature, query); uri = WebUtility.UrlDecode( WebUtility.UrlDecode(uri)); var uriQuery = new Query(uri); if (!uriQuery.ContainsKey("ratebypass")) uri += "&ratebypass=yes"; return new UnscrambledQuery(uri, encrypted); } private string GetSignatureAndHost(string key, string signature, Query query) { string result = $"&{key}={signature}"; string host; if (query.TryGetValue("fallback_host", out host)) result += "&fallback_host=" + host; return result; } private UnscrambledQuery UnscrambleManifestUri(string manifestUri) { int start = manifestUri.IndexOf(Playback) + Playback.Length; string baseUri = manifestUri.Substring(0, start); string parametersString = manifestUri.Substring(start, manifestUri.Length - start); var parameters = parametersString.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var builder = new StringBuilder(baseUri); builder.Append("?"); for (var i = 0; i < parameters.Length; i += 2) { builder.Append(parameters[i]); builder.Append('='); builder.Append(parameters[i + 1].Replace("%2F", "/")); if (i < parameters.Length - 2) { builder.Append('&'); } } return new UnscrambledQuery(builder.ToString(), false); } private async Task GetPlayerResponseIOSAsync(string id) { var httpClient = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://www.youtube.com/youtubei/v1/player"); var content = new { videoId = id, contentCheckOk = true, context = new { client = new { clientName = "ANDROID_VR", clientVersion = "1.60.19", deviceMake = "Oculus", deviceModel = "Quest 3", osName = "Android", osVersion = "12L", platform = "MOBILE", hl = "en", gl = "US", utcOffsetMinutes = 0, visitorData = await VisitorDataTokenGenerator.GetVisitorDataFromYouTube(httpClient), } } }; request.Content = new StringContent(JsonSerializer.Serialize(content)); request.Headers.Add("User-Agent", "com.google.android.apps.youtube.vr.oculus/1.60.19 (Linux; U; Android 12L; Quest 3 Build/SQ3A.220605.009.A1) gzip"); var response = await httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); httpClient.Dispose(); request.Dispose(); request.Dispose(); return responseContent; } return null; } } } ================================================ FILE: src/libvideo/YouTubeVideo.Decrypt.cs ================================================ using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using VideoLibrary.Helpers; namespace VideoLibrary { public partial class YouTubeVideo { private async Task DecryptAsync(string uri, Func makeClient) { var query = new Query(uri); string signature; if (!query.TryGetValue(YouTube.GetSignatureKey(), out signature)) return uri; if (string.IsNullOrWhiteSpace(signature)) throw new Exception("Signature not found."); if (jsPlayer == null) { jsPlayer = await makeClient() .GetStringAsync(jsPlayerUrl) .ConfigureAwait(false); } query[YouTube.GetSignatureKey()] = DecryptSignature(jsPlayer, signature); return query.ToString(); } private string DecryptSignature(string js, string signature) { var functionNameRegex = new Regex(@"\w+(?:.|\[)(\""?\w+(?:\"")?)\]?\("); var functionLines = GetDecryptionFunctionLines(js); var decryptor = new Decryptor(); var decipherDefinitionName = Regex.Match(string.Join(";", functionLines), "([\\$_\\w]+).\\w+\\(\\w+,\\d+\\);").Groups[1].Value; if (string.IsNullOrEmpty(decipherDefinitionName)) { throw new Exception("Could not find signature decipher definition name. Please report this issue to us."); } var decipherDefinitionBody = Regex.Match(js, $@"var\s+{Regex.Escape(decipherDefinitionName)}=\{{(\w+:function\(\w+(,\w+)?\)\{{(.*?)\}}),?\}};", RegexOptions.Singleline).Groups[0].Value; if (string.IsNullOrEmpty(decipherDefinitionBody)) { throw new Exception("Could not find signature decipher definition body. Please report this issue to us."); } foreach (var functionLine in functionLines) { if (decryptor.IsComplete) { break; } var match = functionNameRegex.Match(functionLine); if (match.Success) { decryptor.AddFunction(decipherDefinitionBody, match.Groups[1].Value); } } foreach (var functionLine in functionLines) { var match = functionNameRegex.Match(functionLine); if (match.Success) { signature = decryptor.ExecuteFunction(signature, functionLine, match.Groups[1].Value); } } return signature; } private string[] GetDecryptionFunctionLines(string js) { var decipherFuncName = Regex.Match(js, @"(\w+)=function\(\w+\){(\w+)=\2\.split\(\x22{2}\);.*?return\s+\2\.join\(\x22{2}\)}"); return decipherFuncName.Success ? decipherFuncName.Groups[0].Value.Split(';') : null; } private class Decryptor { private static readonly Regex ParametersRegex = new Regex(@"\(\w+,(\d+)\)"); private readonly Dictionary _functionTypes = new Dictionary(); private readonly StringBuilder _stringBuilder = new StringBuilder(); public bool IsComplete => _functionTypes.Count == Enum.GetValues(typeof(FunctionType)).Length; public void AddFunction(string js, string function) { var escapedFunction = Regex.Escape(function); FunctionType? type = null; /* Pass "do":function(a){} or xa:function(a,b){} */ if (Regex.IsMatch(js, $@"(\"")?{escapedFunction}(\"")?:\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.")) { type = FunctionType.Slice; } else if (Regex.IsMatch(js, $@"(\"")?{escapedFunction}(\"")?:\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b")) { type = FunctionType.Swap; } if (Regex.IsMatch(js, $@"(\"")?{escapedFunction}(\"")?:\bfunction\b\(\w+\){{\w+\.reverse")) { type = FunctionType.Reverse; } if (type.HasValue) { _functionTypes[function] = type.Value; } } public string ExecuteFunction(string signature, string line, string function) { if (!_functionTypes.TryGetValue(function, out var type)) { return signature; } switch (type) { case FunctionType.Reverse: return Reverse(signature); case FunctionType.Slice: case FunctionType.Swap: var index = int.Parse( ParametersRegex.Match(line).Groups[1].Value, NumberStyles.AllowThousands, NumberFormatInfo.InvariantInfo); return type == FunctionType.Slice ? Slice(signature, index) : Swap(signature, index); default: throw new ArgumentOutOfRangeException(nameof(type)); } } private string Reverse(string signature) { _stringBuilder.Clear(); for (var index = signature.Length - 1; index >= 0; index--) { _stringBuilder.Append(signature[index]); } return _stringBuilder.ToString(); } private string Slice(string signature, int index) => signature.Substring(index); private string Swap(string signature, int index) { _stringBuilder.Clear(); _stringBuilder.Append(signature); _stringBuilder[0] = _stringBuilder[index % _stringBuilder.Length]; _stringBuilder[index % _stringBuilder.Length] = signature[0]; return _stringBuilder.ToString(); } private enum FunctionType { Reverse, Slice, Swap } } } } ================================================ FILE: src/libvideo/YouTubeVideo.Format.cs ================================================ namespace VideoLibrary { // TODO Add/Fix itags public partial class YouTubeVideo { public int Fps { get { switch (FormatCode) { case 571: case 402: case 401: case 400: case 399: case 398: case 337: case 336: case 335: case 334: case 333: case 332: case 331: case 330: case 272: case 315: case 308: case 303: case 302: case 305: case 304: case 299: case 298: return 60; case 18: case 22: case 37: case 43: case 59: case 397: case 396: case 395: case 394: case 313: case 271: case 248: case 247: case 244: case 243: case 242: case 278: case 138: case 266: case 264: case 137: case 136: case 135: case 134: case 133: case 160: return 30; default: return -1; } } } public bool IsAdaptive => this.AdaptiveKind != AdaptiveKind.None; public AdaptiveKind AdaptiveKind { get { switch (FormatCode) { case 18: case 22: case 37: case 43: case 59: case 133: case 134: case 135: case 136: case 137: case 138: case 160: case 242: case 243: case 244: case 247: case 248: case 264: case 266: case 271: case 272: case 298: case 299: case 302: case 303: case 304: case 305: case 308: case 313: case 315: case 330: case 331: case 332: case 333: case 334: case 335: case 336: case 337: case 394: case 395: case 396: case 397: case 398: case 399: case 400: case 401: case 402: case 571: case 278: case 694: case 695: case 696: case 697: case 698: case 699: case 700: case 701: return AdaptiveKind.Video; case 139: case 140: case 141: case 171: case 172: case 249: case 250: case 251: case 256: case 258: case 327: case 338: return AdaptiveKind.Audio; default: return AdaptiveKind.None; } } } public int AudioBitrate { get { switch (FormatCode) { case 139: case 249: case 250: case 599: case 600: return 48; case 18: return 96; case 37: case 43: case 59: case 140: case 171: case 251: return 128; case 22: case 256: return 192; case 141: case 172: case 327: case 774: return 256; case 258: case 325: case 328: case 380: return 384; case 338: return 480; case 773: return 960; default: return -1; } } } public int Resolution { get { switch (FormatCode) { case 394: case 330: case 278: case 160: case 694: case 597: case 598: return 144; case 395: case 331: case 242: case 133: case 695: return 240; case 18: case 43: case 396: case 332: case 243: case 134: case 696: case 167: return 360; case 59: case 397: case 333: case 244: case 135: case 697: case 168: return 480; case 22: case 398: case 334: case 302: case 247: case 298: case 136: case 698: case 169: case 612: return 720; case 37: case 399: case 335: case 303: case 248: case 299: case 137: case 699: case 170: case 216: case 616: case 721: return 1080; case 400: case 336: case 308: case 271: case 304: case 264: case 700: return 1440; case 401: case 337: case 315: case 313: case 305: case 266: case 701: return 2160; case 138: case 272: case 402: case 571: case 702: return 4320; default: return -1; } } } public override VideoFormat Format { get { switch (FormatCode) { case 18: case 22: case 37: case 59: case 133: case 134: case 135: case 136: case 137: case 138: case 160: case 264: case 266: case 298: case 299: case 304: case 305: case 394: case 395: case 396: case 397: case 398: case 399: case 400: case 401: case 402: case 571: case 168: case 169: case 170: case 216: case 278: case 694: case 695: case 696: case 697: case 698: case 699: case 700: case 701: case 702: case 721: return VideoFormat.Mp4; case 43: case 242: case 243: case 244: case 247: case 248: case 271: case 272: case 302: case 303: case 308: case 313: case 598: case 597: case 612: case 616: return VideoFormat.WebM; default: return VideoFormat.Unknown; } } } public AudioFormat AudioFormat { get { switch (FormatCode) { case 18: case 22: case 37: case 59: case 139: case 140: case 141: case 256: case 258: case 327: case 325: case 328: case 380: case 599: return AudioFormat.Aac; case 171: case 172: return AudioFormat.Vorbis; case 43: case 249: case 250: case 251: case 338: case 600: case 773: case 774: return AudioFormat.Opus; default: return AudioFormat.Unknown; } } } } } ================================================ FILE: src/libvideo/YouTubeVideo.cs ================================================ using System; using System.Threading.Tasks; using VideoLibrary.Helpers; namespace VideoLibrary { public partial class YouTubeVideo : Video { private readonly string jsPlayerUrl; private string jsPlayer; private string uri; private readonly Query _uriQuery; private bool _encrypted; private bool _needNDescramble; internal YouTubeVideo(VideoInfo info, UnscrambledQuery query, string jsPlayerUrl) { this.Info = info; this.Title = info?.Title; this.uri = query.Uri; this._uriQuery = new Query(uri); this.jsPlayerUrl = jsPlayerUrl; this._encrypted = query.IsEncrypted; this._needNDescramble = _uriQuery.ContainsKey("n"); this.FormatCode = int.Parse(_uriQuery["itag"]); } public override string Title { get; } public override VideoInfo Info { get; } public override WebSites WebSite => WebSites.YouTube; public override string Uri => GetUriAsync().GetAwaiter().GetResult(); public string GetUri(Func makeClient) => GetUriAsync(makeClient).GetAwaiter().GetResult(); public override Task GetUriAsync() => GetUriAsync(() => new DelegatingClient()); public async Task GetUriAsync(Func makeClient) { if (_encrypted) { uri = await DecryptAsync(uri, makeClient).ConfigureAwait(false); _encrypted = false; } if (_needNDescramble) { uri = await NDescrambleAsync(uri, makeClient).ConfigureAwait(false); _needNDescramble = false; } return uri; } public int FormatCode { get; } public long? ContentLength { get { if (_contentLength.HasValue) return _contentLength; _contentLength = this.GetContentLength(_uriQuery).Result; return _contentLength; } } public bool IsEncrypted => _encrypted; // Private's private long? _contentLength { get; set; } private async Task GetContentLength(Query query) { if (query.TryGetValue("clen", out string clen)) { return long.Parse(clen); } using (var client = new VideoClient()) { return await client.GetContentLengthAsync(uri); } } } } ================================================ FILE: src/libvideo/YoutubeVideo.Descramble.cs ================================================ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using VideoLibrary.Helpers; namespace VideoLibrary { public partial class YouTubeVideo { private async Task NDescrambleAsync(string uri, Func makeClient) { var query = new Query(uri); if (!query.TryGetValue("n", out var signature)) return uri; if (string.IsNullOrWhiteSpace(signature)) throw new Exception("N Signature not found."); if (jsPlayer == null) { jsPlayer = await makeClient() .GetStringAsync(jsPlayerUrl) .ConfigureAwait(false); } query["n"] = DescrambleNSignature(jsPlayer, signature); return query.ToString(); } private string DescrambleNSignature(string js, string signature) { return signature; //var func = GetDescrambleFunctionBody(js); //var asd = "var " + func.Item2.Replace("===\"undefined\"", "!=='undefined'"); //var result = new Engine() // .Execute(asd) // .Invoke(func.Item1, signature); // -> 3 //File.WriteAllText("C:\\Users\\OMANSAK\\Desktop\\asd.txt", js); //// TODO Add Native Descramble for "N" Signature //return result.ToString(); } private Tuple GetDescrambleFunctionBody(string js) { string functionName = null; var functionLine = Regex.Match(js, @"([\w\d]*)=function\(\w+?\){var \w+=\w+.split\(\w+\.slice\(0,0\)\),Z=\["); if (functionLine.Success && !functionLine.Groups[2].Success) { functionName = functionLine.Groups[1].Value; } else { var fname = Regex.Match(js, $@"var {functionLine.Groups[1]}\s*=\s*(\[.+?\]);"); if (fname.Success && fname.Groups[1].Success) { functionName = fname.Groups[1].Value .Replace("[", string.Empty) .Replace("]", string.Empty) .Split(',')[int.Parse(functionLine.Groups[2].Value)]; } } if (!string.IsNullOrWhiteSpace(functionName)) { var decipherDefinitionBody = Regex.Match(js, $@"{Regex.Escape(functionName)}=function\(\w+(,\w+)?\)\{{(?s:.*?)\}};", RegexOptions.Singleline); if (decipherDefinitionBody.Success) { return new Tuple(functionName, decipherDefinitionBody.Groups[0].Value); } } return new Tuple(functionName, null); } } } ================================================ FILE: src/libvideo/libvideo.csproj ================================================  Bar Arnon,OMANSAK Copyright 2018 Bar Arnon | 2026 OMANSAK libvideo (aka VideoLibrary) is a modern .NET library for downloading YouTube videos. It is portable to most platforms and is very lightweight. Find us on GitHub at https://github.com/omansak/libvideo true VideoLibrary https://github.com/omansak/libvideo Fix 403 Forbidden errors youtube youtubeexplode downloader libvideo lib libs video videos library libraries download extract get vimeo scrape scraping downloader extractor getter scraper youtubeextract videolibrary library winrt wp windows phone pcl portable class library .net framework core compat compatibility api apis layer layers emulator emulators emulation emulations xamarin mono monotouch xamarin.ios ios xamarin.android android youtubedownloader c# .net standart git https://github.com/omansak/libvideo VideoLibrary 3.3.1 netstandard2.0 libvideo true 3.3.1.0 3.3.1.0 LICENSE icon_586.png README.md True True True ================================================ FILE: src/libvideo.compat/AdaptiveType.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace YoutubeExtractor { public enum AdaptiveType { None, Audio, Video } } ================================================ FILE: src/libvideo.compat/AudioExtractionException.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace YoutubeExtractor { public class AudioExtractionException : Exception { public AudioExtractionException(string message) : base(message) { } } } ================================================ FILE: src/libvideo.compat/AudioType.cs ================================================ namespace YoutubeExtractor { public enum AudioType { Aac, Vorbis, Opus, Unknown } } ================================================ FILE: src/libvideo.compat/DownloadUrlResolver.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using VideoLibrary; using VideoLibrary.Helpers; namespace YoutubeExtractor { public static class DownloadUrlResolver { private static YouTube Service = new YouTube(); public static void DecryptDownloadUrl(VideoInfo info) { // Nothing to do here, URL is decrypted automatically // upon calling YouTubeVideo.Uri. } public static IEnumerable GetDownloadUrls(string videoUrl, bool decryptSignature = true) { Require.NotNull(videoUrl, nameof(videoUrl)); // GetAllVideos normalizes the URL as of libvideo v0.4.1, // don't call TryNormalizeYoutubeUrl here. return Service.GetAllVideos(videoUrl).Select(v => new VideoInfo(v)); } public async static Task> GetDownloadUrlsAsync( string videoUrl, bool decryptSignature = true) { var videos = await Service .GetAllVideosAsync(videoUrl) .ConfigureAwait(false); return videos.Select(v => new VideoInfo(v)); } public static bool TryNormalizeYoutubeUrl(string url, out string normalizedUrl) { // If you fix something in here, please be sure to fix in // YouTubeService.TryNormalize as well. normalizedUrl = null; var builder = new StringBuilder(url); url = builder.Replace("youtu.be/", "youtube.com/watch?v=") .Replace("youtube.com/embed/", "youtube.com/watch?v=") .Replace("/v/", "/watch?v=") .Replace("/watch#", "/watch?") .Replace("youtube.com/shorts/", "youtube.com/watch?v=") .ToString(); string value; var query = new Query(url); if (!query.TryGetValue("v", out value)) return false; normalizedUrl = "https://youtube.com/watch?v=" + value; return true; } } } ================================================ FILE: src/libvideo.compat/VideoInfo.cs ================================================ using System; using VideoLibrary; namespace YoutubeExtractor { public class VideoInfo { private readonly YouTubeVideo video; internal VideoInfo(YouTubeVideo video) { this.video = video; } public AdaptiveType AdaptiveType { get { switch (video.AdaptiveKind) { case AdaptiveKind.Audio: return AdaptiveType.Audio; case AdaptiveKind.Video: return AdaptiveType.Video; default: return AdaptiveType.None; } } } public int AudioBitrate { get { int result = video.AudioBitrate; return result == -1 ? 0 : result; } } public string AudioExtension { get { switch (video.AudioFormat) { case AudioFormat.Aac: return ".aac"; case AudioFormat.Vorbis: return ".ogg"; case AudioFormat.Opus: return ".ogg"; case AudioFormat.Unknown: return String.Empty; default: throw new NotImplementedException($"Audio format {video.AudioFormat} is unrecognized! Please file an issue at libvideo on GitHub."); } } } public AudioType AudioType { get { switch (video.AudioFormat) { case AudioFormat.Aac: return AudioType.Aac; case AudioFormat.Vorbis: return AudioType.Vorbis; case AudioFormat.Opus: return AudioType.Opus; default: return AudioType.Unknown; } } } public bool CanExtractAudio => false; public string DownloadUrl => video.Uri; public int FormatCode { get { int result = video.FormatCode; return result == -1 ? 0 : result; } } public int Fps { get { int fps = video.Fps; return fps == -1 ? 0 : fps; } } public bool RequiresDecryption => false; public int Resolution { get { int result = video.Resolution; return result == -1 ? 0 : result; } } public string Title => video.Title; public string VideoExtension => video.FileExtension; public VideoType VideoType { get { switch (video.Format) { case VideoFormat.Mp4: return VideoType.Mp4; case VideoFormat.WebM: return VideoType.WebM; default: return VideoType.Unknown; } } } public override string ToString() { return string.Format($"Full Title\t{Title + VideoExtension}\nType\t{VideoType}\nResolution\t{Resolution}p\nFormat\t{FormatCode}\nFPS\t{Fps}\nAudioType\t{AudioType}\nBitrate\t{AudioBitrate}\n"); } } } ================================================ FILE: src/libvideo.compat/VideoNotAvailableException.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace YoutubeExtractor { public class VideoNotAvailableException : Exception { public VideoNotAvailableException() { } public VideoNotAvailableException(string message) : base(message) { } } } ================================================ FILE: src/libvideo.compat/VideoType.cs ================================================ namespace YoutubeExtractor { public enum VideoType { Mp4, WebM, Unknown } } ================================================ FILE: src/libvideo.compat/YoutubeParseException.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace YoutubeExtractor { public class YoutubeParseException : Exception { public YoutubeParseException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: src/libvideo.compat/libvideo.compat.csproj ================================================  Bar Arnon,OMANSAK Copyright 2018 Bar Arnon | 2026 OMANSAK libvideo (aka VideoLibrary) is a modern .NET library for downloading YouTube videos. It is portable to most platforms and is very lightweight. Find us on GitHub at https://github.com/omansak/libvideo true VideoLibrary.Compat https://github.com/omansak/libvideo Fix 403 Forbidden errors youtube youtubeexplode downloader libvideo lib libs video videos library libraries download extract get vimeo scrape scraping downloader extractor getter scraper youtubeextract videolibrary library winrt wp windows phone pcl portable class library .net framework core compat compatibility api apis layer layers emulator emulators emulation emulations xamarin mono monotouch xamarin.ios ios xamarin.android android youtubedownloader git https://github.com/omansak/libvideo YoutubeExtractor 3.3.1 netstandard2.0 libvideo.compat true 3.3.1.0 3.3.1.0 icon_586.png en LICENSE README.md Exceptions\BadQueryException.cs Helpers\EmptyArray.cs Helpers\KeyCollection.cs Helpers\Query.cs Helpers\Require.cs Helpers\ValueCollection.cs True True True ================================================ FILE: src/libvideo.debug/CustomYoutubeClient.cs ================================================ using System; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; namespace VideoLibrary.Debug { class CustomHandler { public HttpMessageHandler GetHandler() { CookieContainer cookieContainer = new CookieContainer(); cookieContainer.Add(new Cookie("CONSENT", "YES+cb", "/", "youtube.com")); return new HttpClientHandler { UseCookies = true, CookieContainer = cookieContainer }; } } class CustomYouTube : YouTube { private long chunkSize = 10_485_760; private long _fileSize = 0L; private HttpClient _client = new HttpClient(); protected override HttpClient MakeClient(HttpMessageHandler handler) { return base.MakeClient(handler); } protected override HttpMessageHandler MakeHandler() { return new CustomHandler().GetHandler(); } public async Task CreateDownloadAsync(Uri uri, string filePath, IProgress> progress) { var totalBytesCopied = 0L; _fileSize = await GetContentLengthAsync(uri.AbsoluteUri) ?? 0; if (_fileSize == 0) { throw new Exception("File has no any content !"); } using (Stream output = File.OpenWrite(filePath)) { var segmentCount = (int)Math.Ceiling(1.0 * _fileSize / chunkSize); for (var i = 0; i < segmentCount; i++) { var from = i * chunkSize; var to = (i + 1) * chunkSize - 1; var request = new HttpRequestMessage(HttpMethod.Get, uri); request.Headers.Range = new RangeHeaderValue(from, to); using (request) { // Download Stream var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); //File Steam var buffer = new byte[81920]; int bytesCopied; do { bytesCopied = await stream.ReadAsync(buffer, 0, buffer.Length); output.Write(buffer, 0, bytesCopied); totalBytesCopied += bytesCopied; progress.Report(new Tuple(totalBytesCopied, _fileSize)); } while (bytesCopied > 0); } } } } private async Task GetContentLengthAsync(string requestUri, bool ensureSuccess = true) { using (var request = new HttpRequestMessage(HttpMethod.Head, requestUri)) { var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (ensureSuccess) response.EnsureSuccessStatusCode(); return response.Content.Headers.ContentLength; } } } class Test { public void Run() { // Custom Youtube var youtube = new CustomYouTube(); var videos = youtube.GetAllVideosAsync("https://www.youtube.com/watch?v=qK_NeRZOdq4").GetAwaiter().GetResult(); var maxResolution = videos.First(i => i.Resolution == videos.Max(j => j.Resolution)); youtube .CreateDownloadAsync( new Uri(maxResolution.Uri), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), maxResolution.FullName), new Progress>((Tuple v) => { var percent = (int)((v.Item1 * 100) / v.Item2); Console.Write(string.Format("Downloading.. ( % {0} ) {1} / {2} MB\r", percent, (v.Item1 / (double)(1024 * 1024)).ToString("N"), (v.Item2 / (double)(1024 * 1024)).ToString("N"))); })) .GetAwaiter().GetResult(); } } } ================================================ FILE: src/libvideo.debug/Program.cs ================================================ using System; using System.Diagnostics; namespace VideoLibrary.Debug { class Program { static void Main() { string[] queries = { "https://www.youtube.com/watch?v=jfobiCq0YUc&ab_channel=EminemMusic", //1080 "https://www.youtube.com/watch?v=LXb3EKWsInQ&ab_channel=Jacob%2BKatieSchwarz", //2060 "https://www.youtube.com/watch?v=U2XK_TJZ3PI", //JSON Parse Error }; TestVideoLib(queries); Console.WriteLine("Done."); Console.ReadKey(); } public static void TestVideoLib(string[] queries) { //new Test().Run(); using (var cli = Client.For(YouTube.Default)) { Console.WriteLine("Downloading..."); for (int i = 0; i < queries.Length; i++) { string uri = queries[i]; try { var videoInfos = cli.GetAllVideosAsync(uri).GetAwaiter().GetResult(); Console.WriteLine($"Link #{i + 1}"); foreach (YouTubeVideo v in videoInfos) { if (v.Resolution > 0 && v.AudioBitrate < 0) { Console.WriteLine(v.Uri); Console.WriteLine(string.Format($"Full Title\t{v.Title + v.FileExtension}\nType\t{v.AdaptiveKind}\nResolution\t{v.Resolution}p\nFormat\t{v.FormatCode}\nFPS\t{v.Fps}\nBitrate\t{v.AudioBitrate}\n")); Console.WriteLine("Success : " + v.Head().CanRead); Console.WriteLine(); } } } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e); Debugger.Break(); } } } } } } ================================================ FILE: src/libvideo.debug/libvideo.debug.csproj ================================================  Exe VideoLibrary.Debug net10.0 true ================================================ FILE: src/libvideo.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27703.2000 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "libvideo", "libvideo\libvideo.csproj", "{9C71A8F8-39B6-4A53-8960-AEABF72A7771}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "libvideo.compat", "libvideo.compat\libvideo.compat.csproj", "{B50E1120-908F-40E0-9E90-5CF87FBCBB71}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "libvideo.debug", "libvideo.debug\libvideo.debug.csproj", "{8EE6CE4C-B1C1-4CA1-9CBF-7C07C6BF993E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9C71A8F8-39B6-4A53-8960-AEABF72A7771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9C71A8F8-39B6-4A53-8960-AEABF72A7771}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C71A8F8-39B6-4A53-8960-AEABF72A7771}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C71A8F8-39B6-4A53-8960-AEABF72A7771}.Release|Any CPU.Build.0 = Release|Any CPU {B50E1120-908F-40E0-9E90-5CF87FBCBB71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B50E1120-908F-40E0-9E90-5CF87FBCBB71}.Debug|Any CPU.Build.0 = Debug|Any CPU {B50E1120-908F-40E0-9E90-5CF87FBCBB71}.Release|Any CPU.ActiveCfg = Release|Any CPU {B50E1120-908F-40E0-9E90-5CF87FBCBB71}.Release|Any CPU.Build.0 = Release|Any CPU {8EE6CE4C-B1C1-4CA1-9CBF-7C07C6BF993E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8EE6CE4C-B1C1-4CA1-9CBF-7C07C6BF993E}.Debug|Any CPU.Build.0 = Debug|Any CPU {8EE6CE4C-B1C1-4CA1-9CBF-7C07C6BF993E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8EE6CE4C-B1C1-4CA1-9CBF-7C07C6BF993E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F1D2D10E-73CA-4A16-A045-6F6115731093} EndGlobalSection EndGlobal ================================================ FILE: tests/Compat/Compat/App.config ================================================  ================================================ FILE: tests/Compat/Compat/Compat.csproj ================================================  Debug AnyCPU {4ABF577F-35D9-45C5-95E1-075AD73AE5E6} Exe Properties Compat Compat v4.5.2 512 true AnyCPU true full false bin\Debug\ DEBUG;TRACE prompt 4 AnyCPU pdbonly true bin\Release\ TRACE prompt 4 ..\..\..\src\libvideo.compat\bin\$(Configuration)\netstandard1.1\libvideo.compat.dll ================================================ FILE: tests/Compat/Compat/Program.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using YoutubeExtractor; namespace Compat { class Program { static void Main(string[] args) { /* Do nothing here. We just want to make sure this code compiles. */ } static void CompileThis() { AdaptiveType a = AdaptiveType.Audio; a = AdaptiveType.None; a = AdaptiveType.Video; AudioExtractionException b = new AudioExtractionException(default(string)); AudioType c = AudioType.Aac; c = AudioType.Mp3; c = AudioType.Unknown; c = AudioType.Vorbis; DownloadUrlResolver.DecryptDownloadUrl(default(VideoInfo)); IEnumerable d = DownloadUrlResolver.GetDownloadUrls(default(string)); d = DownloadUrlResolver.GetDownloadUrls(default(string), default(bool)); string e = default(string); bool f = DownloadUrlResolver.TryNormalizeYoutubeUrl(e, out e); VideoInfo g = default(VideoInfo); AdaptiveType h = g.AdaptiveType; int i = g.AudioBitrate; string j = g.AudioExtension; AudioType k = g.AudioType; bool l = g.CanExtractAudio; string m = g.DownloadUrl; int n = g.FormatCode; bool o = g.Is3D; bool p = g.RequiresDecryption; int q = g.Resolution; string r = g.Title; string s = g.VideoExtension; VideoType t = g.VideoType; VideoNotAvailableException u = new VideoNotAvailableException(); u = new VideoNotAvailableException(default(string)); VideoType v = VideoType.Flash; v = VideoType.Mobile; v = VideoType.Mp4; v = VideoType.Unknown; v = VideoType.WebM; YoutubeParseException w = new YoutubeParseException(default(string), default(Exception)); } } } ================================================ FILE: tests/Compat/Compat/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Compat")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Compat")] [assembly: AssemblyCopyright("Copyright © 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("4abf577f-35d9-45c5-95e1-075ad73ae5e6")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: tests/Compat/Compat.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compat", "Compat\Compat.csproj", "{4ABF577F-35D9-45C5-95E1-075AD73AE5E6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4ABF577F-35D9-45C5-95E1-075AD73AE5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4ABF577F-35D9-45C5-95E1-075AD73AE5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4ABF577F-35D9-45C5-95E1-075AD73AE5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4ABF577F-35D9-45C5-95E1-075AD73AE5E6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal ================================================ FILE: tests/Core/Core/Core.csproj ================================================  Debug AnyCPU {561A8EEA-46E2-4294-94D7-EDCE5FD971B7} Library Properties Core Core v4.5.2 512 true full false bin\Debug\ DEBUG;TRACE prompt 4 pdbonly true bin\Release\ TRACE prompt 4 ..\..\..\src\libvideo\bin\$(Configuration)\netstandard1.1\libvideo.dll ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll True ..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll True ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll True This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. ================================================ FILE: tests/Core/Core/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Core")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Core")] [assembly: AssemblyCopyright("Copyright © 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("561a8eea-46e2-4294-94d7-edce5fd971b7")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: tests/Core/Core/UnitTests.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using VideoLibrary; using Xunit; namespace Core { public class UnitTests { private const string YouTubeInvalidUriOne = "https://wikipedia.com"; private const string YouTubeInvalidUriTwo = "123ABC!@#"; private const string YouTubeUri = "https://www.youtube.com/watch?v=JjCaRS-CABk"; private const string YouTubeDecryptSigUri = "https://www.youtube.com/watch?v=09R8_2nJtjg"; private const string YouTubeWithDataManifest = "https://www.youtube.com/watch?v=EphGWZKtXvE"; private const string YouTubeShortsUri = "https://www.youtube.com/shorts/xuCO7-DLCaA"; // private const string VimeoUri = "https://vimeo.com/131417856"; [Theory] [InlineData(YouTubeUri)] [InlineData(YouTubeDecryptSigUri)] [InlineData(YouTubeWithDataManifest)] [InlineData(YouTubeShortsUri)] public void YouTube_GetAllVideos(string uri) { var videos = YouTube.Default.GetAllVideos(uri); Assert.NotNull(videos); Assert.DoesNotContain(null, videos); foreach (var video in videos) { Assert.NotNull(video.Uri); Assert.Equal(video.WebSite, WebSites.YouTube); } } [Theory] [InlineData(YouTubeInvalidUriOne)] [InlineData(YouTubeInvalidUriTwo)] public void YouTube_ThrowOnInvalidUri(string invalid) { Assert.Throws(() => YouTube.Default.GetVideo(invalid)); } } } ================================================ FILE: tests/Core/Core/packages.config ================================================  ================================================ FILE: tests/Core/Core.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{561A8EEA-46E2-4294-94D7-EDCE5FD971B7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {561A8EEA-46E2-4294-94D7-EDCE5FD971B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {561A8EEA-46E2-4294-94D7-EDCE5FD971B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {561A8EEA-46E2-4294-94D7-EDCE5FD971B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {561A8EEA-46E2-4294-94D7-EDCE5FD971B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal ================================================ FILE: tests/Speed.Test/Speed.Test/App.config ================================================  ================================================ FILE: tests/Speed.Test/Speed.Test/Program.cs ================================================ using VideoLibrary; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using YoutubeExtractor; namespace Speed.Test { class Program { const string Uri = "https://www.youtube.com/watch?v=NLddPakLF1I"; static void Main(string[] args) { int times = args.Length == 0 ? 3 : int.Parse(args[0]); int iterations = args.Length <= 1 ? 2 : int.Parse(args[1]); Console.WriteLine("Starting..."); Console.WriteLine($"Times: {times}."); Console.WriteLine($"Iterations: {iterations}."); Console.WriteLine("Getting results for YoutubeExtractor..."); var elapsed = TimeSpan.Zero; for (int i = 0; i < times; i++) { RunChecked(() => { var watch = Stopwatch.StartNew(); for (int j = 0; j < iterations; j++) GC.KeepAlive(DownloadUrlResolver.GetDownloadUrls(Uri)); watch.Stop(); elapsed += watch.Elapsed; }); } Console.WriteLine($"YoutubeExtractor: took {elapsed}."); Console.WriteLine("Getting results for libvideo..."); elapsed = TimeSpan.Zero; using (var service = Client.For(YouTube.Default)) { for (int i = 0; i < times; i++) { RunChecked(() => { var watch = Stopwatch.StartNew(); for (int j = 0; j < iterations; j++) GC.KeepAlive(service.GetAllVideos(Uri)); watch.Stop(); elapsed += watch.Elapsed; }); } } Console.WriteLine($"libvideo: took {elapsed}."); } static void RunChecked(Action action) { try { action(); } catch (Exception e) { string[] lines = { $"A {e.GetType()} was thrown!", "Message:", string.Empty, e.ToString(), string.Empty, "Restarting..." }; Console.WriteLine(string.Join(Environment.NewLine, lines)); RunChecked(action); } } } } ================================================ FILE: tests/Speed.Test/Speed.Test/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Speed.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Speed.Test")] [assembly: AssemblyCopyright("Copyright © 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("448bde69-df8a-4090-b0a0-81bfb9d84e28")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: tests/Speed.Test/Speed.Test/Speed.Test.csproj ================================================  Debug AnyCPU {448BDE69-DF8A-4090-B0A0-81BFB9D84E28} Exe Properties Speed.Test Speed.Test v4.5.2 512 true AnyCPU true full false bin\Debug\ DEBUG;TRACE prompt 4 AnyCPU pdbonly true bin\Release\ TRACE prompt 4 ..\..\..\src\libvideo\bin\$(Configuration)\netstandard1.1\libvideo.dll ..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll ..\packages\YoutubeExtractor.0.10.11\lib\net35\YoutubeExtractor.dll Designer ================================================ FILE: tests/Speed.Test/Speed.Test/packages.config ================================================  ================================================ FILE: tests/Speed.Test/Speed.Test.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speed.Test", "Speed.Test\Speed.Test.csproj", "{448BDE69-DF8A-4090-B0A0-81BFB9D84E28}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {448BDE69-DF8A-4090-B0A0-81BFB9D84E28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {448BDE69-DF8A-4090-B0A0-81BFB9D84E28}.Debug|Any CPU.Build.0 = Debug|Any CPU {448BDE69-DF8A-4090-B0A0-81BFB9D84E28}.Release|Any CPU.ActiveCfg = Release|Any CPU {448BDE69-DF8A-4090-B0A0-81BFB9D84E28}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal