1
0

Implemented DASH manifest generation, video playback data retrieval and authorization helpers

This commit is contained in:
Michael Gordeev
2019-11-30 22:08:46 +03:00
parent ff8c64e4a3
commit f55af5c276
25 changed files with 988 additions and 45 deletions
+5
View File
@@ -7,3 +7,8 @@
/YouTubeScraper/.vs/YouTubeScraper/v16 /YouTubeScraper/.vs/YouTubeScraper/v16
/YouTubeScraper/YouTubeScraper/bin/Debug/netstandard2.0 /YouTubeScraper/YouTubeScraper/bin/Debug/netstandard2.0
/YouTubeScraper/YouTubeScraper/obj /YouTubeScraper/YouTubeScraper/obj
/YouTubeScraper/.vs/YouTubeScraper/DesignTimeBuild
/YouTubeScraper/.vs/YouTube.API/v16
/YouTubeScraper/YouTube.API.Test/bin/Debug/netcoreapp3.0
/YouTubeScraper/YouTube.API.Test/obj
/YouTubeScraper/YouTubeScraper/bin/Debug/netstandard2.1
@@ -0,0 +1,27 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using YouTube.Models;
namespace YouTube.API.Test
{
public class DashManifestTest
{
[SetUp]
public void Setup()
{
}
[Test]
public void ValidManifestTest()
{
YouTubeService service = new YouTubeService();
IReadOnlyList<DashManifest> manifests = service.DashManifests.List("VC5-YkjMHuw").Execute();
foreach (var i in manifests)
Console.WriteLine(i.Label);
Assert.IsNotNull(manifests);
Assert.IsNotEmpty(manifests);
}
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using YouTube.Models;
namespace YouTube.API.Test
{
public class VideoPlaybackTest
{
[Test]
public void ValidVideoPlaybackTest()
{
YouTubeService service = new YouTubeService();
VideoPlayback info = service.VideoPlayback.List("VC5-YkjMHuw").Execute();
Assert.NotNull(info);
}
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YouTubeScraper\YouTube.API.csproj" />
</ItemGroup>
</Project>
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.29424.173 VisualStudioVersion = 16.0.29424.173
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YouTubeScraper", "YouTubeScraper\YouTubeScraper.csproj", "{F7E1AD03-B67C-4C79-BE84-682490ED05C5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YouTube.API", "YouTubeScraper\YouTube.API.csproj", "{F7E1AD03-B67C-4C79-BE84-682490ED05C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YouTube.API.Test", "YouTube.API.Test\YouTube.API.Test.csproj", "{D5E35A2A-03CA-4A5B-9F12-80E078356340}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
{F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Release|Any CPU.Build.0 = Release|Any CPU {F7E1AD03-B67C-4C79-BE84-682490ED05C5}.Release|Any CPU.Build.0 = Release|Any CPU
{D5E35A2A-03CA-4A5B-9F12-80E078356340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D5E35A2A-03CA-4A5B-9F12-80E078356340}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5E35A2A-03CA-4A5B-9F12-80E078356340}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5E35A2A-03CA-4A5B-9F12-80E078356340}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<MPD minBufferTime="PT2S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" type="static">
<Period>
<AdaptationSet>
<ContentComponent contentType="video" id="1">
</ContentComponent>
</AdaptationSet>
<AdaptationSet>
<ContentComponent contentType="audio" id="2">
</ContentComponent>
</AdaptationSet>
</Period>
</MPD>
@@ -0,0 +1,71 @@
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Oauth2.v2;
using Google.Apis.YouTube.v3;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace YouTube.Authorization
{
public static class AuthorizationHelpers
{
public static Uri Endpoint => "https://accounts.google.com/o/oauth2/approval".ToUri();
public static string RedirectUrl => Uri.EscapeDataString(redirectUrl);
const string tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
const string redirectUrl = "urn:ietf:wg:oauth:2.0:oob";
public static Uri FormQueryString(ClientSecrets clientSecrets, params string[] scopes)
{
string clientId = Uri.EscapeDataString(clientSecrets.ClientId);
string scopeStr = string.Join(' ', scopes);
return $"https://accounts.google.com/o/oauth2/auth?client_id={clientId}&redirect_uri={RedirectUrl}&response_type=code&scope={scopeStr}".ToUri();
}
public static async Task<UserCredential> ExchangeToken(ClientSecrets clientSecrets, string responseToken)
{
using HttpClient client = new HttpClient();
Dictionary<string, string> requestBody = new Dictionary<string, string>
{
{ "code", responseToken },
{ "redirect_uri", redirectUrl },
{ "grant_type", "authorization_code" },
{ "client_id", clientSecrets.ClientId },
{ "client_secret", clientSecrets.ClientSecret }
};
HttpResponseMessage response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(requestBody));
if (!response.IsSuccessStatusCode)
return null;
string responseString = await response.Content.ReadAsStringAsync();
dynamic responseData = JsonConvert.DeserializeObject(responseString);
TokenResponse tokenResponse = new TokenResponse
{
AccessToken = responseData.access_token,
ExpiresInSeconds = responseData.expires_in,
RefreshToken = responseData.refresh_token,
Scope = responseData.scope,
TokenType = responseData.token_type
};
AuthorizationCodeFlow authorizationCodeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer()
{
ClientSecrets = clientSecrets,
Scopes = responseData.scope.Split(' ')
});
return new UserCredential(authorizationCodeFlow, "user", tokenResponse);
}
}
}
@@ -0,0 +1,23 @@
using System;
namespace YouTube
{
internal static class Extensions
{
internal static Uri ToUri(this string str)
{
try { return new Uri(str); }
catch { return null; }
}
internal static int RangeOffset(int value, int min, int max)
{
if (value < min)
return -1;
else if (value > max)
return 1;
else
return 0;
}
}
}
@@ -0,0 +1,226 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Google.Apis.Services;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using YouTube.Models;
using YoutubeExplode;
using YoutubeExplode.Models;
using YoutubeExplode.Models.MediaStreams;
namespace YouTube.Generators
{
internal class ManifestGenerator
{
IClientService ClientService { get; }
YoutubeClient Client { get; }
string Id { get; }
Video Meta { get; set; }
MediaStreamInfoSet UrlsSet { get; set; }
public ManifestGenerator(IClientService service, string id)
{
ClientService = service;
Id = id;
Client = new YoutubeClient(service.HttpClient);
}
public async Task<IReadOnlyList<DashManifest>> GenerateManifestsAsync()
{
Meta = await Client.GetVideoAsync(Id);
if (Meta == null)
throw new FileNotFoundException("Video not found. Check video ID and visibility preferences");
UrlsSet = await Client.GetVideoMediaStreamInfosAsync(Id);
if (!string.IsNullOrWhiteSpace(UrlsSet.HlsLiveStreamUrl))
throw new NotSupportedException("This is livestream. Use 'YouTubeClient.VideoPlayback.List()' to get playback URLs");
List<DashManifest> list = new List<DashManifest>
{
await GenerateManifest("Auto")
};
foreach (string i in UrlsSet.GetAllVideoQualityLabels())
list.Add(await GenerateManifest(i));
return list.AsReadOnly();
}
async Task<DashManifest> GenerateManifest(string quality)
{
XmlDocument manifest = new XmlDocument();
manifest.LoadXml(Properties.Resources.DashManifestTemplate);
manifest["MPD"].SetAttribute("mediaPresentationDuration", XmlConvert.ToString(Meta.Duration));
StreamInfo streamInfo = await GetInfoAsync(quality);
foreach (var i in streamInfo.Video)
{
string rep = GetVideoRepresentation(i);
manifest.GetElementsByTagName("ContentComponent")[0].InnerXml += rep;
}
foreach (var i in streamInfo.Audio)
manifest.GetElementsByTagName("ContentComponent")[1].InnerXml += GetAudioRepresentation(i);
return new DashManifest(quality, manifest);
}
string GetVideoRepresentation(StreamInfo.VideoInfo info) =>
$@"<Representation bandwidth=""{GetBandwidth(info.Label)}"" id=""{info.Itag}"" mimeType=""{info.MimeType}"" codecs=""{info.Codecs}"" fps=""{info.Fps}"" height=""{info.Height}"" width=""{info.Width}"">
<BaseURL>{WebUtility.UrlEncode(info.Url)}</BaseURL>
<SegmentBase indexRange=""{info.IndexRange}"">
<Initialization range=""{info.InitRange}""/>
</SegmentBase>
</Representation>";
string GetAudioRepresentation(StreamInfo.AudioInfo info) =>
$@"<Representation bandwidth=""200000"" id=""{info.Itag}"" sampleRate=""{info.SampleRate}"" numChannels=""{info.ChannelsCount}"" mimeType=""{info.MimeType}"" codecs=""{info.Codecs}"">
<BaseURL>{WebUtility.UrlEncode(info.Url)}</BaseURL>
<SegmentBase indexRange=""{info.IndexRange}"">
<Initialization range=""{info.InitRange}""/>
</SegmentBase>
</Representation>";
async Task<StreamInfo> GetInfoAsync(string quality)
{
StreamInfo info = new StreamInfo();
string response = await ClientService.HttpClient.GetStringAsync($"https://youtube.com/watch?v={Id}&disable_polymer=true&bpctr=9999999999&hl=en");
IHtmlDocument videoEmbedPageHtml = new HtmlParser().ParseDocument(response);
#region I don't know what the fuck is this
string playerConfigRaw = Regex.Match(videoEmbedPageHtml.Source.Text,
@"ytplayer\.config = (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})")
.Groups["Json"].Value;
JToken playerConfigJson = JToken.Parse(playerConfigRaw);
var playerResponseRaw = playerConfigJson.SelectToken("args.player_response").Value<string>();
JToken playerResponseJson = JToken.Parse(playerResponseRaw);
string errorReason = playerResponseJson.SelectToken("playabilityStatus.reason")?.Value<string>();
if (!string.IsNullOrWhiteSpace(errorReason))
throw new InvalidDataException($"Video [{Id}] is unplayable. Reason: {errorReason}");
List<Dictionary<string, string>> adaptiveStreamInfosUrl = playerConfigJson.SelectToken("args.adaptive_fmts")?.Value<string>().Split(',').Select(SplitQuery).ToList();
List<Dictionary<string, string>> video =
quality == "Auto" ?
adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("quality_label")) :
adaptiveStreamInfosUrl.FindAll(i => i.ContainsValue(quality.Substring(0, quality.IndexOf('p'))));
List<Dictionary<string, string>> audio = adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("audio_sample_rate"));
#endregion
foreach (var i in video)
info.Video.Add(new StreamInfo.VideoInfo
{
IndexRange = i["index"],
Url = i["url"],
Itag = i["itag"],
Fps = i["fps"],
Height = i["size"].Split('x')[1],
Width = i["size"].Split('x')[0],
Codecs = i["type"].Split('"')[1],
MimeType = i["type"].Split(';')[0],
Label = i["quality_label"]
});
foreach (var i in audio)
info.Audio.Add(new StreamInfo.AudioInfo
{
ChannelsCount = i["audio_channels"],
IndexRange = i["index"],
SampleRate = i["audio_sample_rate"],
Codecs = i["type"].Split('"')[1],
MimeType = i["type"].Split(';')[0],
Url = i["url"],
Itag = i["itag"]
});
return info;
}
/// <summary>
/// I don't know what the fuck is this either
/// </summary>
public Dictionary<string, string> SplitQuery(string query)
{
Dictionary<string, string> dic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string[] paramsEncoded = query.TrimStart('?').Split("&");
foreach (string paramEncoded in paramsEncoded)
{
string param = WebUtility.UrlDecode(paramEncoded);
// Look for the equals sign
int equalsPos = param.IndexOf('=');
if (equalsPos <= 0)
continue;
// Get the key and value
string key = param.Substring(0, equalsPos);
string value = equalsPos < param.Length
? param.Substring(equalsPos + 1)
: string.Empty;
// Add to dictionary
dic[key] = value;
}
return dic;
}
string GetBandwidth(string quality) =>
quality.Split('p')[0] switch
{
"4320" => "16763040",
"3072" => "11920384",
"2880" => "11175360",
"2160" => "8381520",
"1440" => "5587680",
"1080" => "4190760",
"720" => "2073921",
"480" => "869460",
"360" => "686521",
"240" => "264835",
_ => "100000",
};
class StreamInfo
{
public class VideoInfo
{
public string IndexRange { get; set; }
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
public string Itag { get; set; }
public string Fps { get; set; }
public string Url { get; set; }
public string Codecs { get; set; }
public string MimeType { get; set; }
public string Height { get; set; }
public string Width { get; set; }
public string Label { get; set; }
}
public class AudioInfo
{
public string IndexRange { get; set; }
public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
public string SampleRate { get; set; }
public string ChannelsCount { get; set; }
public string Codecs { get; set; }
public string MimeType { get; set; }
public string Url { get; set; }
public string Itag { get; set; }
}
public List<VideoInfo> Video { get; } = new List<VideoInfo>();
public List<AudioInfo> Audio { get; } = new List<AudioInfo>();
}
}
}
@@ -0,0 +1,11 @@
using System.Globalization;
namespace YouTube.Models
{
public class ClosedCaptionInfo
{
public CultureInfo Language { get; set; }
public string Url { get; set; }
public bool AutoGenerated { get; set; }
}
}
@@ -0,0 +1,25 @@
using System;
using System.IO;
using System.Xml;
namespace YouTube.Models
{
public class DashManifest
{
public string Label { get; }
public DateTime ValidUntil { get; }
public DashManifest(string label, XmlDocument doc)
{
Label = label;
Xml = doc;
}
public string ManifestContent => Xml.OuterXml;
public XmlDocument Xml { get; }
public Uri WriteManifest(FileStream outStream)
{
Xml.Save(outStream);
return new Uri(outStream.Name);
}
}
}
@@ -0,0 +1,48 @@
using System.Drawing;
namespace YouTube.Models
{
public enum VideoFormat
{
/// <summary>
/// MPEG-4 Part 2.
/// </summary>
Mp4V = 0,
H263 = 1,
/// <summary>
/// MPEG-4 Part 10, H264, Advanced Video Coding (AVC).
/// </summary>
H264 = 2,
Vp8 = 3,
Vp9 = 4,
Av1 = 5
}
public enum AudioFormat
{
/// <summary>
/// MPEG-4 Part 3, Advanced Audio Coding (AAC).
/// </summary>
Aac = 0,
Vorbis = 1,
Opus = 2
}
public enum AudioQuality { Low, Medium, High }
public class VideoPlaybackUrl
{
public string Quality { get; set; }
public VideoFormat Format { get; set; }
public string Url { get; set; }
public Size Resolution { get; set; }
public bool HasAudio { get; set; }
public int Bitrate { get; set; }
}
public class AudioPlaybackUrl
{
public AudioQuality Quality { get; set; }
public AudioFormat Format { get; set; }
public string Url { get; set; }
public int Bitrate { get; set; }
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace YouTube.Models
{
public class VideoPlayback
{
public string Id { get; set; }
public PlaybackUrlsData PlaybackUrls { get; set; } = new PlaybackUrlsData();
public IReadOnlyList<ClosedCaptionInfo> ClosedCaptions { get; set; }
public class PlaybackUrlsData
{
public IReadOnlyList<VideoPlaybackUrl> Video { get; set; }
public IReadOnlyList<AudioPlaybackUrl> Audio { get; set; }
public string LiveStreamUrl { get; set; }
public DateTime ValidUntil { get; set; }
}
}
}
@@ -0,0 +1,86 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace YouTube.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("YouTube.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
///&lt;MPD minBufferTime=&quot;PT2S&quot; profiles=&quot;urn:mpeg:dash:profile:isoff-on-demand:2011&quot; type=&quot;static&quot;&gt;
/// &lt;Period&gt;
/// &lt;AdaptationSet&gt;
/// &lt;ContentComponent contentType=&quot;video&quot; id=&quot;1&quot;&gt;
///
/// &lt;/ContentComponent&gt;
/// &lt;/AdaptationSet&gt;
/// &lt;AdaptationSet&gt;
/// &lt;ContentComponent contentType=&quot;audio&quot; id=&quot;2&quot;&gt;
///
/// &lt;/ContentComponent&gt;
/// &lt;/AdaptationSet&gt;
/// &lt;/Period&gt;
///&lt;/MPD&gt;.
/// </summary>
internal static string DashManifestTemplate {
get {
return ResourceManager.GetString("DashManifestTemplate", resourceCulture);
}
}
}
}
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="DashManifestTemplate" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\assets\dashmanifesttemplate.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
</root>
@@ -0,0 +1,11 @@
using Google.Apis.Services;
namespace YouTube.Resources
{
public class CaptionsResource
{
IClientService Service { get; }
public CaptionsResource(IClientService service) =>
Service = service;
}
}
@@ -1,16 +1,44 @@
using System; using Google.Apis.Services;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Threading.Tasks;
using YouTube.Generators;
using YouTube.Models;
namespace YouTubeScraper.Resources namespace YouTube.Resources
{ {
public class DashManifestsResource public class DashManifestsResource
{ {
public class ListRequest { } IClientService Service { get; }
public DashManifestsResource(IClientService service) =>
Service = service;
public ListRequest List(string id) public ListRequest List(string videoId) =>
new ListRequest(Service, videoId);
public class ListRequest
{ {
return new ListRequest(); public string Id { get; set; }
IClientService Service { get; set; }
public ListRequest(IClientService service, string id)
{
Id = id;
Service = service;
}
public async Task<IReadOnlyList<DashManifest>> ExecuteAsync()
{
ManifestGenerator generator = new ManifestGenerator(Service, Id);
return await generator.GenerateManifestsAsync();
}
public IReadOnlyList<DashManifest> Execute()
{
Task<IReadOnlyList<DashManifest>> task = ExecuteAsync();
task.Wait();
return task.Result;
}
} }
} }
} }
@@ -4,7 +4,7 @@ using System.Text;
using Google.Apis.YouTube.v3; using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data; using Google.Apis.YouTube.v3.Data;
namespace YouTubeScraper.Resources namespace YouTube.Resources
{ {
public class HistoryResource public class HistoryResource
{ {
@@ -0,0 +1,110 @@
using Google.Apis.Services;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Threading.Tasks;
using YouTube.Models;
using YoutubeExplode;
using YoutubeExplode.Models.ClosedCaptions;
using YoutubeExplode.Models.MediaStreams;
namespace YouTube.Resources
{
public class VideoPlaybackResource
{
IClientService ClientService { get; }
public VideoPlaybackResource(IClientService clientService) =>
ClientService = clientService;
public ListRequest List(string videoId) =>
new ListRequest(ClientService, videoId);
public class ListRequest
{
IClientService Service { get; }
public string Id { get; set; }
public async Task<VideoPlayback> ExecuteAsync()
{
VideoPlayback item = new VideoPlayback();
YoutubeClient client = new YoutubeClient(Service.HttpClient);
MediaStreamInfoSet streamSet = await client.GetVideoMediaStreamInfosAsync(Id);
item.Id = Id;
item.PlaybackUrls.ValidUntil = streamSet.ValidUntil.DateTime;
if(!string.IsNullOrWhiteSpace(streamSet.HlsLiveStreamUrl))
{
item.PlaybackUrls.LiveStreamUrl = streamSet.HlsLiveStreamUrl;
return item;
}
List<AudioPlaybackUrl> audio = new List<AudioPlaybackUrl>();
foreach (AudioStreamInfo i in streamSet.Audio)
audio.Add(new AudioPlaybackUrl
{
Url = i.Url,
Bitrate = (int)i.Bitrate,
Format = (AudioFormat)i.AudioEncoding,
Quality = Extensions.RangeOffset((int)i.Bitrate / 1024, 128, 255) switch
{
-1 => AudioQuality.Low,
1 => AudioQuality.High,
_ => AudioQuality.Medium
}
});
item.PlaybackUrls.Audio = audio.AsReadOnly();
List<VideoPlaybackUrl> video = new List<VideoPlaybackUrl>();
foreach (VideoStreamInfo i in streamSet.Video)
video.Add(new VideoPlaybackUrl
{
Format = (VideoFormat)i.VideoEncoding,
HasAudio = false,
Quality = i.VideoQualityLabel,
Url = i.Url,
Resolution = new Size(i.Resolution.Width, i.Resolution.Height),
Bitrate = (int)i.Bitrate
});
foreach (MuxedStreamInfo i in streamSet.Muxed)
video.Add(new VideoPlaybackUrl
{
Format = (VideoFormat)i.VideoEncoding,
HasAudio = true,
Quality = i.VideoQualityLabel,
Url = i.Url,
Resolution = new Size(i.Resolution.Width, i.Resolution.Height),
Bitrate = 0
});
item.PlaybackUrls.Video = video.AsReadOnly();
var ccSet = await client.GetVideoClosedCaptionTrackInfosAsync(Id);
List<ClosedCaptionInfo> captions = new List<ClosedCaptionInfo>();
foreach (ClosedCaptionTrackInfo i in ccSet)
captions.Add(new ClosedCaptionInfo
{
AutoGenerated = i.IsAutoGenerated,
Url = i.Url,
Language = new CultureInfo(i.Language.Code)
});
item.ClosedCaptions = captions.AsReadOnly();
return item;
}
public VideoPlayback Execute()
{
Task<VideoPlayback> task = ExecuteAsync();
task.Wait();
return task.Result;
}
public ListRequest(IClientService service, string id)
{
Id = id;
Service = service;
}
}
}
}
@@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace YouTubeScraper.Resources namespace YouTube.Resources
{ {
public class WatchLaterResource public class WatchLaterResource
{ {
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace YouTube
{
public static class VideoQuality
{
public static string Auto => QualityConstants.Auto;
public static string Low144 => QualityConstants.Low144;
public static string Low240 => QualityConstants.Low240;
public static string Medium360 => QualityConstants.Medium360;
public static string Meduim480 => QualityConstants.Meduim480;
public static string High720 => QualityConstants.High720;
public static string High720p60 => QualityConstants.High720p60;
public static string High1080 => QualityConstants.High1080;
public static string High1080p60 => QualityConstants.High1080p60;
public static string High1440 => QualityConstants.High1440;
public static string High2160 => QualityConstants.High2160;
public static string High2880 => QualityConstants.High2880;
public static string High3072 => QualityConstants.High3072;
public static string High4320 => QualityConstants.High4320;
public static class QualityConstants
{
public const string Auto = "auto";
public const string Low144 = "144p";
public const string Low240 = "240p";
public const string Medium360 = "360p";
public const string Meduim480 = "480p";
public const string High720 = "720p";
public const string High720p60 = "720p60";
public const string High1080 = "1080p";
public const string High1080p60 = "1080p60";
public const string High1440 = "1440p";
public const string High2160 = "2160p";
public const string High2880 = "2880p";
public const string High3072 = "3072p";
public const string High4320 = "4320p";
}
}
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>YouTube.API</AssemblyName>
<RootNamespace>YouTube</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\DashManifestTemplate.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\DashManifestTemplate.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.13.0" />
<PackageReference Include="Google.Apis" Version="1.41.1" />
<PackageReference Include="Google.Apis.Auth" Version="1.41.1" />
<PackageReference Include="Google.Apis.Core" Version="1.41.1" />
<PackageReference Include="Google.Apis.Oauth2.v2" Version="1.41.1.1602" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.41.1.1699" />
<PackageReference Include="Microsoft.CSharp" Version="4.6.0" />
<PackageReference Include="YoutubeExplode" Version="4.7.10" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
@@ -1,19 +0,0 @@
using System;
using Google.Apis.YouTube.v3;
using YouTubeScraper.Resources;
namespace YouTubeScraper
{
public class YouTubeScraper : YouTubeService
{
public HistoryResource History { get; }
public WatchLaterResource WatchLater { get; }
public DashManifestsResource DashManifests { get; set; }
// TODO: Add Activities override for recomendations and subscriptions
public YouTubeScraper()
{
}
}
}
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.13.0" />
<PackageReference Include="Google.Apis" Version="1.41.1" />
<PackageReference Include="Google.Apis.Auth" Version="1.41.1" />
<PackageReference Include="Google.Apis.Core" Version="1.41.1" />
<PackageReference Include="Google.Apis.Oauth2.v2" Version="1.41.1.1602" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.41.1.1699" />
<PackageReference Include="YoutubeExplode" Version="4.7.10" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
using YouTube.Resources;
namespace YouTube
{
public partial class YouTubeService : Google.Apis.YouTube.v3.YouTubeService
{
public DashManifestsResource DashManifests => new DashManifestsResource(this);
public VideoPlaybackResource VideoPlayback => new VideoPlaybackResource(this);
public HistoryResource History { get; }
public WatchLaterResource WatchLater { get; }
// TODO: Add Activities override for recomendations and subscriptions and implementation of cc retrieval
public YouTubeService() : base()
{
}
public YouTubeService(Initializer initializer) : base(initializer) { }
}
}