1
0

Repository rearrangement

This commit is contained in:
Michael Gordeev
2019-12-05 13:34:34 +03:00
parent 8e348ad0f3
commit 56b9999342
25 changed files with 6 additions and 2 deletions
@@ -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,105 @@
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace YouTube.Authorization
{
public static class AuthorizationHelpers
{
public static Uri Endpoint => "https://accounts.google.com/o/oauth2/approval".ToUri();
const string refreshEndpoint = "https://oauth2.googleapis.com/token";
const string tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
public static Uri FormQueryString(ClientSecrets clientSecrets, Uri redirectUri, 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={redirectUri.AbsoluteUri}&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.ToString().Split(' ')
});
UserCredential credential = new UserCredential(authorizationCodeFlow, "user", tokenResponse);
return credential;
}
public static async Task<UserCredential> RestoreUser(ClientSecrets clientSecrets, string refreshToken)
{
using HttpClient client = new HttpClient();
Dictionary<string, string> requestBody = new Dictionary<string, string>
{
{ "client_id", clientSecrets.ClientId },
{ "client_secret", clientSecrets.ClientSecret },
{ "refresh_token", refreshToken },
{ "grant_type", "refresh_token" }
};
HttpResponseMessage response = await client.PostAsync(refreshEndpoint, 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 = refreshToken,
TokenType = responseData.token_type
};
AuthorizationCodeFlow authorizationCodeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets
});
UserCredential credential = new UserCredential(authorizationCodeFlow, "user", tokenResponse);
return credential;
}
}
}
+23
View File
@@ -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;
}
}
}
+226
View File
@@ -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>();
}
}
}
+14
View File
@@ -0,0 +1,14 @@
using System.Globalization;
using YoutubeExplode.Models.ClosedCaptions;
namespace YouTube.Models
{
public class ClosedCaptionInfo
{
public CultureInfo Language { get; set; }
public string Url { get; set; }
public bool AutoGenerated { get; set; }
internal ClosedCaptionTrackInfo TrackInfo { get; set; }
}
}
+19
View File
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace YouTube.Models
{
public class ClosedCaptionTrack
{
public ClosedCaptionInfo Info { get; set; }
public IReadOnlyList<ClosedCaption> Captions { get; set; }
public class ClosedCaption
{
public TimeSpan Offset { get; set; }
public TimeSpan Duration { get; set; }
public string Content { get; set; }
}
}
}
+25
View File
@@ -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);
}
}
}
+48
View File
@@ -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; }
}
}
+20
View File
@@ -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; }
}
}
}
+86
View File
@@ -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);
}
}
}
}
+124
View File
@@ -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>
+57
View File
@@ -0,0 +1,57 @@
using Google.Apis.Services;
using System.Collections.Generic;
using System.Threading.Tasks;
using YouTube.Models;
using YoutubeExplode;
namespace YouTube.Resources
{
public class CaptionsResource : Google.Apis.YouTube.v3.CaptionsResource
{
IClientService Service { get; }
public CaptionsResource(IClientService service) : base(service) =>
Service = service;
public LoadRequest Load(ClosedCaptionInfo captionInfo) =>
new LoadRequest(Service, captionInfo);
public class LoadRequest
{
public ClosedCaptionInfo CaptionInfo { get; set; }
IClientService Service { get; set; }
public LoadRequest(IClientService service, ClosedCaptionInfo captionInfo)
{
CaptionInfo = captionInfo;
Service = service;
}
public async Task<ClosedCaptionTrack> ExecuteAsync()
{
YoutubeClient client = new YoutubeClient(Service.HttpClient);
var response = await client.GetClosedCaptionTrackAsync(CaptionInfo.TrackInfo);
List<ClosedCaptionTrack.ClosedCaption> captions = new List<ClosedCaptionTrack.ClosedCaption>();
foreach (var i in response.Captions)
captions.Add(new ClosedCaptionTrack.ClosedCaption
{
Offset = i.Offset,
Duration = i.Duration,
Content = i.Text
});
return new ClosedCaptionTrack
{
Info = CaptionInfo,
Captions = captions.AsReadOnly()
};
}
public ClosedCaptionTrack Execute()
{
Task<ClosedCaptionTrack> task = ExecuteAsync();
task.Wait();
return task.Result;
}
}
}
}
@@ -0,0 +1,44 @@
using Google.Apis.Services;
using System.Collections.Generic;
using System.Threading.Tasks;
using YouTube.Generators;
using YouTube.Models;
namespace YouTube.Resources
{
public class DashManifestsResource
{
IClientService Service { get; }
public DashManifestsResource(IClientService service) =>
Service = service;
public ListRequest List(string videoId) =>
new ListRequest(Service, videoId);
public class 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;
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
namespace YouTube.Resources
{
public class HistoryResource
{
public class ListRequest { }
public class InsertRequest { }
public class DeleteRequest { }
public class ClearRequest { }
public ListRequest List()
{
return new ListRequest();
}
public InsertRequest Insert(string videoId, TimeSpan? leftOn)
{
return new InsertRequest();
}
public DeleteRequest Delete(string videoId)
{
return new DeleteRequest();
}
public ClearRequest Clear()
{
return new ClearRequest();
}
}
}
@@ -0,0 +1,111 @@
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),
TrackInfo = i
});
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;
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Google.Apis.Http;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace YouTube.Resources
{
public class WatchLaterResource
{
IClientService Service { get; }
public WatchLaterResource(IClientService service) =>
Service = service;
public ListRequest List()
{
return new ListRequest();
}
public InsertRequest Insert(string videoId, string part) =>
new InsertRequest(Service, videoId, part);
public DeleteRequest Delete(string videoId) =>
new DeleteRequest(Service, videoId);
public class ListRequest { }
public class InsertRequest
{
IClientService Service { get; set; }
public string Id { get; set; }
public string Part { get; set; }
public InsertRequest(IClientService service, string videoId, string part)
{
Service = service;
Id = videoId;
Part = part;
}
public async Task<PlaylistItem> ExecuteAsync()
{
PlaylistItem playlist = new PlaylistItem
{
Snippet = new PlaylistItemSnippet
{
PlaylistId = "WL",
ResourceId = new ResourceId
{
VideoId = Id,
Kind = "youtube#video"
}
}
};
PlaylistItemsResource.InsertRequest request = (Service as YouTubeService).PlaylistItems.Insert(playlist, Part);
return await request.ExecuteAsync();
}
public PlaylistItem Execute()
{
Task<PlaylistItem> task = ExecuteAsync();
task.Wait();
return task.Result;
}
}
public class DeleteRequest
{
IClientService Service { get; set; }
public string Id { get; set; }
public DeleteRequest(IClientService service, string videoId)
{
Service = service;
Id = videoId;
}
public async Task ExecuteAsync()
{
ConfigurableHttpClient client = Service.HttpClient;
string data = await client.GetStringAsync($"https://youtube.com/watch?v={Id}&disable_polymer=true&bpctr=9999999999&hl=en");
string plid = Regex.Match(data, @"(?<=plid=).?\w+").Value;
IHtmlDocument html = await new HtmlParser().ParseDocumentAsync(data);
string sessionToken = html.GetElementsByTagName("input").FirstOrDefault(i => i.GetAttribute("name") == "session_token")?.GetAttribute("value");
Dictionary<string, string> body = new Dictionary<string, string>
{
{ "video_ids", Id },
{ "full_list_id", "WL" },
{ "plid", plid },
{ "session_token", sessionToken }
};
HttpResponseMessage response = await client.PostAsync("https://www.youtube.com/playlist_video_ajax?action_delete_from_playlist=1", new FormUrlEncodedContent(body));
string responseStr = await response.Content.ReadAsStringAsync();
if (!responseStr.Contains("SUCCESS"))
throw new Exception(responseStr);
}
public void Execute() =>
ExecuteAsync().Wait();
}
}
}
+42
View File
@@ -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";
}
}
}
+50
View File
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
<AssemblyName>YouTube.API</AssemblyName>
<RootNamespace>YouTube</RootNamespace>
<Company>Michael "XFox" Gordeev</Company>
<Copyright>Copyright ©2019 Michael "XFox" Gordeev</Copyright>
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<FileVersion>1.0.1.0</FileVersion>
<Version>1.0.1</Version>
<PackageId>ExtendedYouTubeAPI</PackageId>
<Authors>Michael "XFox" Gordeev</Authors>
<Product>ExtendedYouTubeAPI</Product>
<RepositoryUrl>https://github.com/xfox111/YouTubeScraper</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\DashManifestTemplate.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.13.0" />
<PackageReference Include="Google.Apis" Version="1.42.0" />
<PackageReference Include="Google.Apis.Auth" Version="1.42.0" />
<PackageReference Include="Google.Apis.Core" Version="1.42.0" />
<PackageReference Include="Google.Apis.Oauth2.v2" Version="1.42.0.1602" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.42.0.1758" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
<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>
+18
View File
@@ -0,0 +1,18 @@
using YouTube.Resources;
namespace YouTube
{
public partial class ExtendedYouTubeService : Google.Apis.YouTube.v3.YouTubeService
{
public DashManifestsResource DashManifests => new DashManifestsResource(this);
public VideoPlaybackResource VideoPlayback => new VideoPlaybackResource(this);
public new CaptionsResource Captions => new CaptionsResource(this);
public HistoryResource History { get; }
public WatchLaterResource WatchLater => new WatchLaterResource(this);
// TODO: Add Activities override for recomendations and subscriptions
public ExtendedYouTubeService() : base() { }
public ExtendedYouTubeService(Initializer initializer) : base(initializer) { }
}
}