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.Videos;
using YoutubeExplode.Videos.Streams;
namespace YouTube.Generators
{
internal class ManifestGenerator
{
IClientService ClientService { get; }
YoutubeClient Client { get; }
string Id { get; }
Video Meta { get; set; }
StreamManifest UrlsSet { get; set; }
public ManifestGenerator(IClientService service, string id)
{
ClientService = service;
Id = id;
Client = new YoutubeClient(service.HttpClient);
}
public async Task> GenerateManifestsAsync()
{
Meta = await Client.Videos.GetAsync(Id);
if (Meta == null)
throw new FileNotFoundException("Video not found. Check video ID and visibility preferences");
if (!string.IsNullOrWhiteSpace(await Client.Videos.Streams.GetHttpLiveStreamUrlAsync(Id)))
throw new NotSupportedException("This is livestream. Use 'YouTubeClient.VideoPlayback.List()' to get playback URLs");
UrlsSet = await Client.Videos.Streams.GetManifestAsync(Id);
List list = new List
{
await GenerateManifest("Auto")
};
foreach (string i in UrlsSet.GetVideo().Select(k => k.VideoQualityLabel).Distinct())
list.Add(await GenerateManifest(i));
return list.AsReadOnly();
}
async Task 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) =>
$@"
{WebUtility.UrlEncode(info.Url)}
";
string GetAudioRepresentation(StreamInfo.AudioInfo info) =>
$@"
{WebUtility.UrlEncode(info.Url)}
";
async Task 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 = (?\{[^\{\}]*(((?\{)[^\{\}]*)+((?\})[^\{\}]*)+)*(?(Open)(?!))\})")
.Groups["Json"].Value;
JToken playerConfigJson = JToken.Parse(playerConfigRaw);
var playerResponseRaw = playerConfigJson.SelectToken("args.player_response").Value();
JToken playerResponseJson = JToken.Parse(playerResponseRaw);
string errorReason = playerResponseJson.SelectToken("playabilityStatus.reason")?.Value();
if (!string.IsNullOrWhiteSpace(errorReason))
throw new InvalidDataException($"Video [{Id}] is unplayable. Reason: {errorReason}");
List> adaptiveStreamInfosUrl = playerConfigJson.SelectToken("args.adaptive_fmts")?.Value().Split(',').Select(SplitQuery).ToList();
List> video =
quality == "Auto" ?
adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("quality_label")) :
adaptiveStreamInfosUrl.FindAll(i => i.ContainsValue(quality.Substring(0, quality.IndexOf('p'))));
List> 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;
}
///
/// I don't know what the fuck is this either
///
public Dictionary SplitQuery(string query)
{
Dictionary dic = new Dictionary(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 Video { get; } = new List();
public List Audio { get; } = new List();
}
}
}