This commit is contained in:
Ashley
2022-08-05 22:33:38 +03:00
committed by GitHub
parent f431111611
commit 72143fede3
100 changed files with 12438 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
using System;
namespace InnerTube
{
public class CacheItem<T>
{
public T Item;
public DateTimeOffset ExpireTime;
public CacheItem(T item, TimeSpan expiresIn)
{
Item = item;
ExpireTime = DateTimeOffset.Now.Add(expiresIn);
}
}
}

12
core/InnerTube/Enums.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace InnerTube
{
public enum ChannelTabs
{
Home,
Videos,
Playlists,
Community,
Channels,
About
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,380 @@
using System;
using System.Xml;
using System.Xml.Linq;
namespace InnerTube.Models
{
public class DynamicItem
{
public string Id;
public string Title;
public Thumbnail[] Thumbnails;
public virtual XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("DynamicItem");
item.SetAttribute("id", Id);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class VideoItem : DynamicItem
{
public string UploadedAt;
public long Views;
public Channel Channel;
public string Duration;
public string Description;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("duration", Duration);
item.SetAttribute("views", Views.ToString());
item.SetAttribute("uploadedAt", UploadedAt);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
if (Channel is not null)
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
if (!string.IsNullOrWhiteSpace(Description))
{
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
}
return item;
}
}
public class PlaylistItem : DynamicItem
{
public int VideoCount;
public string FirstVideoId;
public Channel Channel;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Playlist");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class RadioItem : DynamicItem
{
public string FirstVideoId;
public Channel Channel;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Radio");
item.SetAttribute("id", Id);
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ChannelItem : DynamicItem
{
public string Url;
public string Description;
public long VideoCount;
public string Subscribers;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Channel");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("subscribers", Subscribers);
if (!string.IsNullOrWhiteSpace(Url))
item.SetAttribute("customUrl", Url);
XmlElement title = doc.CreateElement("Name");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Avatar");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ContinuationItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Continuation");
item.SetAttribute("key", Id);
return item;
}
}
public class ShelfItem : DynamicItem
{
public DynamicItem[] Items;
public int CollapsedItemCount;
public BadgeItem[] Badges;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Shelf");
item.SetAttribute("title", Title);
item.SetAttribute("collapsedItemCount", CollapsedItemCount.ToString());
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
if (Badges.Length > 0)
{
XmlElement badges = doc.CreateElement("Badges");
foreach (BadgeItem badge in Badges) badges.AppendChild(badge.GetXmlElement(doc));
item.AppendChild(badges);
}
XmlElement items = doc.CreateElement("Items");
foreach (DynamicItem dynamicItem in Items) items.AppendChild(dynamicItem.GetXmlElement(doc));
item.AppendChild(items);
return item;
}
}
public class HorizontalCardListItem : DynamicItem
{
public DynamicItem[] Items;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("CardList");
item.SetAttribute("title", Title);
foreach (DynamicItem dynamicItem in Items) item.AppendChild(dynamicItem.GetXmlElement(doc));
return item;
}
}
public class CardItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Card");
item.SetAttribute("title", Title);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class PlaylistVideoItem : DynamicItem
{
public long Index;
public Channel Channel;
public string Duration;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("index", Index.ToString());
item.SetAttribute("duration", Duration);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ItemSectionItem : DynamicItem
{
public DynamicItem[] Contents;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement section = doc.CreateElement("ItemSection");
foreach (DynamicItem item in Contents) section.AppendChild(item.GetXmlElement(doc));
return section;
}
}
public class MessageItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement message = doc.CreateElement("Message");
message.InnerText = Title;
return message;
}
}
public class ChannelAboutItem : DynamicItem
{
public string Description;
public string Country;
public string Joined;
public string ViewCount;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement about = doc.CreateElement("About");
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
about.AppendChild(description);
XmlElement country = doc.CreateElement("Location");
country.InnerText = Country;
about.AppendChild(country);
XmlElement joined = doc.CreateElement("Joined");
joined.InnerText = Joined;
about.AppendChild(joined);
XmlElement viewCount = doc.CreateElement("ViewCount");
viewCount.InnerText = ViewCount;
about.AppendChild(viewCount);
return about;
}
}
public class BadgeItem : DynamicItem
{
public string Style;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement badge = doc.CreateElement("Badge");
badge.SetAttribute("style", Style);
badge.InnerText = Title;
return badge;
}
}
public class StationItem : DynamicItem
{
public int VideoCount;
public string FirstVideoId;
public string Description;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Station");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
}

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace InnerTube.Models
{
public class RequestContext
{
[JsonProperty("context")] public Context Context;
public static string BuildRequestContextJson(Dictionary<string, object> additionalFields, string language = "en",
string region = "US", string clientName = "WEB", string clientVersion = "2.20220224.07.00")
{
RequestContext ctx = new()
{
Context = new Context(
new RequestClient(language, region, clientName, clientVersion),
new RequestUser(false))
};
string json1 = JsonConvert.SerializeObject(ctx);
Dictionary<string, object> json2 = JsonConvert.DeserializeObject<Dictionary<string, object>>(json1);
foreach (KeyValuePair<string,object> pair in additionalFields) json2.Add(pair.Key, pair.Value);
return JsonConvert.SerializeObject(json2);
}
}
public class Context
{
[JsonProperty("client")] public RequestClient RequestClient { get; set; }
[JsonProperty("user")] public RequestUser RequestUser { get; set; }
public Context(RequestClient requestClient, RequestUser requestUser)
{
RequestClient = requestClient;
RequestUser = requestUser;
}
}
public class RequestClient
{
[JsonProperty("hl")] public string Language { get; set; }
[JsonProperty("gl")] public string Region { get; set; }
[JsonProperty("clientName")] public string ClientName { get; set; }
[JsonProperty("clientVersion")] public string ClientVersion { get; set; }
[JsonProperty("deviceModel")] public string DeviceModel { get; set; }
public RequestClient(string language, string region, string clientName, string clientVersion)
{
Language = language;
Region = region;
ClientName = clientName;
ClientVersion = clientVersion;
if (clientName == "IOS") DeviceModel = "iPhone14,3";
}
}
public class RequestUser
{
[JsonProperty("lockedSafetyMode")] public bool LockedSafetyMode { get; set; }
public RequestUser(bool lockedSafetyMode)
{
LockedSafetyMode = lockedSafetyMode;
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeChannel
{
public string Id;
public string Name;
public string Url;
public Thumbnail[] Avatars;
public Thumbnail[] Banners;
public string Description;
public DynamicItem[] Videos;
public string Subscribers;
public string GetHtmlDescription()
{
return Utils.GetHtmlDescription(Description);
}
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement channel = doc.CreateElement("Channel");
channel.SetAttribute("id", Id);
if (Id != Url)
channel.SetAttribute("customUrl", Url);
XmlElement metadata = doc.CreateElement("Metadata");
XmlElement name = doc.CreateElement("Name");
name.InnerText = Name;
metadata.AppendChild(name);
XmlElement avatars = doc.CreateElement("Avatars");
foreach (Thumbnail t in Avatars)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
avatars.AppendChild(thumbnail);
}
metadata.AppendChild(avatars);
XmlElement banners = doc.CreateElement("Banners");
foreach (Thumbnail t in Banners)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
banners.AppendChild(thumbnail);
}
metadata.AppendChild(banners);
XmlElement subscriberCount = doc.CreateElement("Subscribers");
subscriberCount.InnerText = Subscribers;
metadata.AppendChild(subscriberCount);
channel.AppendChild(metadata);
XmlElement contents = doc.CreateElement("Contents");
foreach (DynamicItem item in Videos) contents.AppendChild(item.GetXmlElement(doc));
channel.AppendChild(contents);
doc.AppendChild(channel);
return doc;
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeLocals
{
public Dictionary<string, string> Languages { get; set; }
public Dictionary<string, string> Regions { get; set; }
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement locals = doc.CreateElement("Locals");
XmlElement languages = doc.CreateElement("Languages");
foreach (KeyValuePair<string, string> l in Languages)
{
XmlElement language = doc.CreateElement("Language");
language.SetAttribute("hl", l.Key);
language.InnerText = l.Value;
languages.AppendChild(language);
}
locals.AppendChild(languages);
XmlElement regions = doc.CreateElement("Regions");
foreach (KeyValuePair<string, string> r in Regions)
{
XmlElement region = doc.CreateElement("Region");
region.SetAttribute("gl", r.Key);
region.InnerText = r.Value;
regions.AppendChild(region);
}
locals.AppendChild(regions);
doc.AppendChild(locals);
return doc;
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Xml;
using Newtonsoft.Json;
namespace InnerTube.Models
{
public class YoutubePlayer
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string[] Tags { get; set; }
public Channel Channel { get; set; }
public long? Duration { get; set; }
public bool IsLive { get; set; }
public Chapter[] Chapters { get; set; }
public Thumbnail[] Thumbnails { get; set; }
public Format[] Formats { get; set; }
public Format[] AdaptiveFormats { get; set; }
public string HlsManifestUrl { get; set; }
public Subtitle[] Subtitles { get; set; }
public string[] Storyboards { get; set; }
public string ExpiresInSeconds { get; set; }
public string ErrorMessage { get; set; }
public string GetHtmlDescription()
{
return Utils.GetHtmlDescription(Description);
}
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
XmlElement error = doc.CreateElement("Error");
error.InnerText = ErrorMessage;
doc.AppendChild(error);
}
else
{
XmlElement player = doc.CreateElement("Player");
player.SetAttribute("id", Id);
player.SetAttribute("duration", Duration.ToString());
player.SetAttribute("isLive", IsLive.ToString());
player.SetAttribute("expiresInSeconds", ExpiresInSeconds);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
player.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
player.AppendChild(description);
XmlElement tags = doc.CreateElement("Tags");
foreach (string tag in Tags ?? Array.Empty<string>())
{
XmlElement tagElement = doc.CreateElement("Tag");
tagElement.InnerText = tag;
tags.AppendChild(tagElement);
}
player.AppendChild(tags);
player.AppendChild(Channel.GetXmlElement(doc));
XmlElement thumbnails = doc.CreateElement("Thumbnails");
foreach (Thumbnail t in Thumbnails)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
thumbnails.AppendChild(thumbnail);
}
player.AppendChild(thumbnails);
XmlElement formats = doc.CreateElement("Formats");
foreach (Format f in Formats ?? Array.Empty<Format>()) formats.AppendChild(f.GetXmlElement(doc));
player.AppendChild(formats);
XmlElement adaptiveFormats = doc.CreateElement("AdaptiveFormats");
foreach (Format f in AdaptiveFormats ?? Array.Empty<Format>()) adaptiveFormats.AppendChild(f.GetXmlElement(doc));
player.AppendChild(adaptiveFormats);
XmlElement storyboards = doc.CreateElement("Storyboards");
foreach (string s in Storyboards)
{
XmlElement storyboard = doc.CreateElement("Storyboard");
storyboard.InnerText = s;
storyboards.AppendChild(storyboard);
}
player.AppendChild(storyboards);
XmlElement subtitles = doc.CreateElement("Subtitles");
foreach (Subtitle s in Subtitles ?? Array.Empty<Subtitle>()) subtitles.AppendChild(s.GetXmlElement(doc));
player.AppendChild(subtitles);
doc.AppendChild(player);
}
return doc;
}
}
public class Chapter
{
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("start_time")] public long StartTime { get; set; }
[JsonProperty("end_time")] public long EndTime { get; set; }
}
public class Format
{
[JsonProperty("format")] public string FormatName { get; set; }
[JsonProperty("format_id")] public string FormatId { get; set; }
[JsonProperty("format_note")] public string FormatNote { get; set; }
[JsonProperty("filesize")] public long? Filesize { get; set; }
[JsonProperty("quality")] public long Quality { get; set; }
[JsonProperty("bitrate")] public double Bitrate { get; set; }
[JsonProperty("audio_codec")] public string AudioCodec { get; set; }
[JsonProperty("video_codec")] public string VideoCodec { get; set; }
[JsonProperty("audio_sample_rate")] public long? AudioSampleRate { get; set; }
[JsonProperty("resolution")] public string Resolution { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("init_range")] public Range InitRange { get; set; }
[JsonProperty("index_range")] public Range IndexRange { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement format = doc.CreateElement("Format");
format.SetAttribute("id", FormatId);
format.SetAttribute("label", FormatName);
format.SetAttribute("filesize", Filesize.ToString());
format.SetAttribute("quality", Bitrate.ToString());
format.SetAttribute("audioCodec", AudioCodec);
format.SetAttribute("videoCodec", VideoCodec);
if (AudioSampleRate != null)
format.SetAttribute("audioSampleRate", AudioSampleRate.ToString());
else
format.SetAttribute("resolution", Resolution);
XmlElement url = doc.CreateElement("URL");
url.InnerText = Url;
format.AppendChild(url);
if (InitRange != null && IndexRange != null)
{
XmlElement initRange = doc.CreateElement("InitRange");
initRange.SetAttribute("start", InitRange.Start);
initRange.SetAttribute("end", InitRange.End);
format.AppendChild(initRange);
XmlElement indexRange = doc.CreateElement("IndexRange");
indexRange.SetAttribute("start", IndexRange.Start);
indexRange.SetAttribute("end", IndexRange.End);
format.AppendChild(indexRange);
}
return format;
}
}
public class Range
{
[JsonProperty("start")] public string Start { get; set; }
[JsonProperty("end")] public string End { get; set; }
public Range(string start, string end)
{
Start = start;
End = end;
}
}
public class Channel
{
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("subscriberCount")] public string SubscriberCount { get; set; }
[JsonProperty("avatars")] public Thumbnail[] Avatars { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement channel = doc.CreateElement("Channel");
channel.SetAttribute("id", Id);
if (!string.IsNullOrWhiteSpace(SubscriberCount))
channel.SetAttribute("subscriberCount", SubscriberCount);
XmlElement name = doc.CreateElement("Name");
name.InnerText = Name;
channel.AppendChild(name);
foreach (Thumbnail avatarThumb in Avatars ?? Array.Empty<Thumbnail>())
{
XmlElement avatar = doc.CreateElement("Avatar");
avatar.SetAttribute("width", avatarThumb.Width.ToString());
avatar.SetAttribute("height", avatarThumb.Height.ToString());
avatar.InnerText = avatarThumb.Url;
channel.AppendChild(avatar);
}
return channel;
}
}
public class Subtitle
{
[JsonProperty("ext")] public string Ext { get; set; }
[JsonProperty("name")] public string Language { get; set; }
[JsonProperty("url")] public string Url { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement subtitle = doc.CreateElement("Subtitle");
subtitle.SetAttribute("ext", Ext);
subtitle.SetAttribute("language", Language);
subtitle.InnerText = Url;
return subtitle;
}
}
public class Thumbnail
{
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("width")] public long Width { get; set; }
}
}

View File

@@ -0,0 +1,68 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubePlaylist
{
public string Id;
public string Title;
public string Description;
public string VideoCount;
public string ViewCount;
public string LastUpdated;
public Thumbnail[] Thumbnail;
public Channel Channel;
public DynamicItem[] Videos;
public string ContinuationKey;
public string GetHtmlDescription() => Utils.GetHtmlDescription(Description);
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement playlist = doc.CreateElement("Playlist");
playlist.SetAttribute("id", Id);
playlist.SetAttribute("continuation", ContinuationKey);
XmlElement metadata = doc.CreateElement("Metadata");
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
metadata.AppendChild(title);
metadata.AppendChild(Channel.GetXmlElement(doc));
XmlElement thumbnails = doc.CreateElement("Thumbnails");
foreach (Thumbnail t in Thumbnail)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
thumbnails.AppendChild(thumbnail);
}
metadata.AppendChild(thumbnails);
XmlElement videoCount = doc.CreateElement("VideoCount");
XmlElement viewCount = doc.CreateElement("ViewCount");
XmlElement lastUpdated = doc.CreateElement("LastUpdated");
videoCount.InnerText = VideoCount;
viewCount.InnerText = ViewCount;
lastUpdated.InnerText = LastUpdated;
metadata.AppendChild(videoCount);
metadata.AppendChild(viewCount);
metadata.AppendChild(lastUpdated);
playlist.AppendChild(metadata);
XmlElement results = doc.CreateElement("Videos");
foreach (DynamicItem result in Videos) results.AppendChild(result.GetXmlElement(doc));
playlist.AppendChild(results);
doc.AppendChild(playlist);
return doc;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeSearchResults
{
public string[] Refinements;
public long EstimatedResults;
public DynamicItem[] Results;
public string ContinuationKey;
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement search = doc.CreateElement("Search");
search.SetAttribute("estimatedResults", EstimatedResults.ToString());
search.SetAttribute("continuation", ContinuationKey);
if (Refinements.Length > 0)
{
XmlElement refinements = doc.CreateElement("Refinements");
foreach (string refinementText in Refinements)
{
XmlElement refinement = doc.CreateElement("Refinement");
refinement.InnerText = refinementText;
refinements.AppendChild(refinement);
}
search.AppendChild(refinements);
}
XmlElement results = doc.CreateElement("Results");
foreach (DynamicItem result in Results) results.AppendChild(result.GetXmlElement(doc));
search.AppendChild(results);
doc.AppendChild(search);
return doc;
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace InnerTube.Models
{
public class YoutubeStoryboardSpec
{
public Dictionary<string, string> Urls = new();
public YoutubeStoryboardSpec(string specStr, long duration)
{
if (specStr is null) return;
List<string> spec = new(specStr.Split("|"));
string baseUrl = spec[0];
spec.RemoveAt(0);
spec.Reverse();
int L = spec.Count - 1;
for (int i = 0; i < spec.Count; i++)
{
string[] args = spec[i].Split("#");
int width = int.Parse(args[0]);
int height = int.Parse(args[1]);
int frameCount = int.Parse(args[2]);
int cols = int.Parse(args[3]);
int rows = int.Parse(args[4]);
string N = args[6];
string sigh = args[7];
string url = baseUrl
.Replace("$L", (spec.Count - 1 - i).ToString())
.Replace("$N", N) + "&sigh=" + sigh;
float fragmentCount = frameCount / (cols * rows);
float fragmentDuration = duration / fragmentCount;
for (int j = 0; j < Math.Ceiling(fragmentCount); j++)
Urls.TryAdd($"L{spec.Count - 1 - i}", url.Replace("$M", j.ToString()));
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeTrends
{
public TrendCategory[] Categories;
public DynamicItem[] Videos;
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement explore = doc.CreateElement("Explore");
XmlElement categories = doc.CreateElement("Categories");
foreach (TrendCategory category in Categories ?? Array.Empty<TrendCategory>()) categories.AppendChild(category.GetXmlElement(doc));
explore.AppendChild(categories);
XmlElement contents = doc.CreateElement("Videos");
foreach (DynamicItem item in Videos ?? Array.Empty<DynamicItem>()) contents.AppendChild(item.GetXmlElement(doc));
explore.AppendChild(contents);
doc.AppendChild(explore);
return doc;
}
}
public class TrendCategory
{
public string Label;
public Thumbnail[] BackgroundImage;
public Thumbnail[] Icon;
public string Id;
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement category = doc.CreateElement("Category");
category.SetAttribute("id", Id);
XmlElement title = doc.CreateElement("Name");
title.InnerText = Label;
category.AppendChild(title);
XmlElement backgroundImages = doc.CreateElement("BackgroundImage");
foreach (Thumbnail t in BackgroundImage ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
backgroundImages.AppendChild(thumbnail);
}
category.AppendChild(backgroundImages);
XmlElement icons = doc.CreateElement("Icon");
foreach (Thumbnail t in Icon ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
icons.AppendChild(thumbnail);
}
category.AppendChild(icons);
return category;
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeVideo
{
public string Id;
public string Title;
public string Description;
public Channel Channel;
public string UploadDate;
public DynamicItem[] Recommended;
public string Views;
public string GetHtmlDescription() => InnerTube.Utils.GetHtmlDescription(Description);
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("views", Views);
item.SetAttribute("uploadDate", UploadDate);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
item.AppendChild(Channel.GetXmlElement(doc));
XmlElement recommendations = doc.CreateElement("Recommendations");
foreach (DynamicItem f in Recommended ?? Array.Empty<DynamicItem>()) recommendations.AppendChild(f.GetXmlElement(doc));
item.AppendChild(recommendations);
doc.AppendChild(item);
return doc;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace InnerTube
{
public static class ReturnYouTubeDislike
{
private static HttpClient _client = new();
private static Dictionary<string, YoutubeDislikes> DislikesCache = new();
// TODO: better cache
public static async Task<YoutubeDislikes> GetDislikes(string videoId)
{
if (DislikesCache.ContainsKey(videoId))
return DislikesCache[videoId];
HttpResponseMessage response = await _client.GetAsync("https://returnyoutubedislikeapi.com/votes?videoId=" + videoId);
string json = await response.Content.ReadAsStringAsync();
YoutubeDislikes dislikes = JsonConvert.DeserializeObject<YoutubeDislikes>(json);
if (dislikes is not null)
DislikesCache.Add(videoId, dislikes);
return dislikes ?? new YoutubeDislikes();
}
}
public class YoutubeDislikes
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("dateCreated")] public string DateCreated { get; set; }
[JsonProperty("likes")] public long Likes { get; set; }
[JsonProperty("dislikes")] public long Dislikes { get; set; }
[JsonProperty("rating")] public double Rating { get; set; }
[JsonProperty("viewCount")] public long Views { get; set; }
[JsonProperty("deleted")] public bool Deleted { get; set; }
public float GetLikePercentage()
{
return Likes / (float)(Likes + Dislikes) * 100;
}
}
}

457
core/InnerTube/Utils.cs Normal file
View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using InnerTube.Models;
using Newtonsoft.Json.Linq;
namespace InnerTube
{
public static class Utils
{
private static string Sapisid;
private static string Psid;
private static bool UseAuthorization;
public static string GetHtmlDescription(string description) => description?.Replace("\n", "<br>") ?? "";
public static string GetMpdManifest(this YoutubePlayer player, string proxyUrl, string videoCodec = null, string audioCodec = null)
{
XmlDocument doc = new();
XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
XmlElement root = doc.DocumentElement;
doc.InsertBefore(xmlDeclaration, root);
XmlElement mpdRoot = doc.CreateElement(string.Empty, "MPD", string.Empty);
mpdRoot.SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
mpdRoot.SetAttribute("xmlns", "urn:mpeg:dash:schema:mpd:2011");
mpdRoot.SetAttribute("xsi:schemaLocation", "urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd");
//mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-main:2011");
mpdRoot.SetAttribute("type", "static");
mpdRoot.SetAttribute("minBufferTime", "PT1.500S");
TimeSpan durationTs = TimeSpan.FromMilliseconds(double.Parse(HttpUtility
.ParseQueryString(player.Formats.First().Url.Split("?")[1])
.Get("dur")?.Replace(".", "") ?? "0"));
StringBuilder duration = new("PT");
if (durationTs.TotalHours > 0)
duration.Append($"{durationTs.Hours}H");
if (durationTs.Minutes > 0)
duration.Append($"{durationTs.Minutes}M");
if (durationTs.Seconds > 0)
duration.Append(durationTs.Seconds);
mpdRoot.SetAttribute("mediaPresentationDuration", $"{duration}.{durationTs.Milliseconds}S");
doc.AppendChild(mpdRoot);
XmlElement period = doc.CreateElement("Period");
period.AppendChild(doc.CreateComment("Audio Adaptation Set"));
XmlElement audioAdaptationSet = doc.CreateElement("AdaptationSet");
List<Format> audios;
if (audioCodec != "all")
audios = player.AdaptiveFormats
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17" &&
(audioCodec == null || x.AudioCodec.ToLower().Contains(audioCodec.ToLower())))
.GroupBy(x => x.FormatNote)
.Select(x => x.Last())
.ToList();
else
audios = player.AdaptiveFormats
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17")
.ToList();
audioAdaptationSet.SetAttribute("mimeType",
HttpUtility.ParseQueryString(audios.First().Url.Split("?")[1]).Get("mime"));
audioAdaptationSet.SetAttribute("subsegmentAlignment", "true");
audioAdaptationSet.SetAttribute("contentType", "audio");
foreach (Format format in audios)
{
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", format.FormatId);
representation.SetAttribute("codecs", format.AudioCodec);
representation.SetAttribute("startWithSAP", "1");
representation.SetAttribute("bandwidth",
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
XmlElement audioChannelConfiguration = doc.CreateElement("AudioChannelConfiguration");
audioChannelConfiguration.SetAttribute("schemeIdUri",
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011");
audioChannelConfiguration.SetAttribute("value", "2");
representation.AppendChild(audioChannelConfiguration);
XmlElement baseUrl = doc.CreateElement("BaseURL");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
representation.AppendChild(baseUrl);
if (format.IndexRange != null && format.InitRange != null)
{
XmlElement segmentBase = doc.CreateElement("SegmentBase");
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
segmentBase.SetAttribute("indexRangeExact", "true");
XmlElement initialization = doc.CreateElement("Initialization");
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
segmentBase.AppendChild(initialization);
representation.AppendChild(segmentBase);
}
audioAdaptationSet.AppendChild(representation);
}
period.AppendChild(audioAdaptationSet);
period.AppendChild(doc.CreateComment("Video Adaptation Set"));
List<Format> videos;
if (videoCodec != "all")
videos = player.AdaptiveFormats.Where(x => !x.AudioSampleRate.HasValue && x.FormatId != "17" &&
(videoCodec == null || x.VideoCodec.ToLower()
.Contains(videoCodec.ToLower())))
.GroupBy(x => x.FormatNote)
.Select(x => x.Last())
.ToList();
else
videos = player.AdaptiveFormats.Where(x => x.Resolution != "audio only" && x.FormatId != "17").ToList();
XmlElement videoAdaptationSet = doc.CreateElement("AdaptationSet");
videoAdaptationSet.SetAttribute("mimeType",
HttpUtility.ParseQueryString(videos.FirstOrDefault()?.Url?.Split("?")[1] ?? "mime=video/mp4")
.Get("mime"));
videoAdaptationSet.SetAttribute("subsegmentAlignment", "true");
videoAdaptationSet.SetAttribute("contentType", "video");
foreach (Format format in videos)
{
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", format.FormatId);
representation.SetAttribute("codecs", format.VideoCodec);
representation.SetAttribute("startWithSAP", "1");
string[] widthAndHeight = format.Resolution.Split("x");
representation.SetAttribute("width", widthAndHeight[0]);
representation.SetAttribute("height", widthAndHeight[1]);
representation.SetAttribute("bandwidth",
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
XmlElement baseUrl = doc.CreateElement("BaseURL");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
representation.AppendChild(baseUrl);
if (format.IndexRange != null && format.InitRange != null)
{
XmlElement segmentBase = doc.CreateElement("SegmentBase");
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
segmentBase.SetAttribute("indexRangeExact", "true");
XmlElement initialization = doc.CreateElement("Initialization");
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
segmentBase.AppendChild(initialization);
representation.AppendChild(segmentBase);
}
videoAdaptationSet.AppendChild(representation);
}
period.AppendChild(videoAdaptationSet);
period.AppendChild(doc.CreateComment("Subtitle Adaptation Sets"));
foreach (Subtitle subtitle in player.Subtitles ?? Array.Empty<Subtitle>())
{
period.AppendChild(doc.CreateComment(subtitle.Language));
XmlElement adaptationSet = doc.CreateElement("AdaptationSet");
adaptationSet.SetAttribute("mimeType", "text/vtt");
adaptationSet.SetAttribute("lang", subtitle.Language);
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", $"caption_{subtitle.Language.ToLower()}");
representation.SetAttribute("bandwidth", "256"); // ...why do we need this for a plaintext file
XmlElement baseUrl = doc.CreateElement("BaseURL");
string url = subtitle.Url;
url = url.Replace("fmt=srv3", "fmt=vtt");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? url : $"{proxyUrl}caption/{player.Id}/{subtitle.Language}";
representation.AppendChild(baseUrl);
adaptationSet.AppendChild(representation);
period.AppendChild(adaptationSet);
}
mpdRoot.AppendChild(period);
return doc.OuterXml.Replace(" schemaLocation=\"", " xsi:schemaLocation=\"");
}
public static async Task<string> GetHlsManifest(this YoutubePlayer player, string proxyUrl)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("#EXTM3U");
sb.AppendLine("##Generated by LightTube");
sb.AppendLine("##Video ID: " + player.Id);
sb.AppendLine("#EXT-X-VERSION:7");
sb.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
string hls = await new HttpClient().GetStringAsync(player.HlsManifestUrl);
string[] hlsLines = hls.Split("\n");
foreach (string line in hlsLines)
{
if (line.StartsWith("#EXT-X-STREAM-INF:"))
sb.AppendLine(line);
if (line.StartsWith("http"))
{
Uri u = new(line);
sb.AppendLine($"{proxyUrl}/ytmanifest?path={HttpUtility.UrlEncode(u.PathAndQuery)}");
}
}
return sb.ToString();
}
public static string ReadRuns(JArray runs)
{
string str = "";
foreach (JToken runToken in runs ?? new JArray())
{
JObject run = runToken as JObject;
if (run is null) continue;
if (run.ContainsKey("bold"))
{
str += "<b>" + run["text"] + "</b>";
}
else if (run.ContainsKey("navigationEndpoint"))
{
if (run?["navigationEndpoint"]?["urlEndpoint"] is not null)
{
string url = run["navigationEndpoint"]?["urlEndpoint"]?["url"]?.ToString() ?? "";
if (url.StartsWith("https://www.youtube.com/redirect"))
{
NameValueCollection qsl = HttpUtility.ParseQueryString(url.Split("?")[1]);
url = qsl["url"] ?? qsl["q"];
}
str += $"<a href=\"{url}\">{run["text"]}</a>";
}
else if (run?["navigationEndpoint"]?["commandMetadata"] is not null)
{
string url = run["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
?.ToString() ?? "";
if (url.StartsWith("/"))
url = "https://youtube.com" + url;
str += $"<a href=\"{url}\">{run["text"]}</a>";
}
}
else
{
str += run["text"];
}
}
return str;
}
public static Thumbnail ParseThumbnails(JToken arg) => new()
{
Height = arg["height"]?.ToObject<long>() ?? -1,
Url = arg["url"]?.ToString() ?? string.Empty,
Width = arg["width"]?.ToObject<long>() ?? -1
};
public static async Task<JObject> GetAuthorizedPlayer(string id, HttpClient client)
{
HttpRequestMessage hrm = new(HttpMethod.Post,
"https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
byte[] buffer = Encoding.UTF8.GetBytes(
RequestContext.BuildRequestContextJson(new Dictionary<string, object>
{
["videoId"] = id
}));
ByteArrayContent byteContent = new(buffer);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
hrm.Content = byteContent;
if (UseAuthorization)
{
hrm.Headers.Add("Cookie", GenerateAuthCookie());
hrm.Headers.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0");
hrm.Headers.Add("Authorization", GenerateAuthHeader());
hrm.Headers.Add("X-Origin", "https://www.youtube.com");
hrm.Headers.Add("X-Youtube-Client-Name", "1");
hrm.Headers.Add("X-Youtube-Client-Version", "2.20210721.00.00");
hrm.Headers.Add("Accept-Language", "en-US;q=0.8,en;q=0.7");
hrm.Headers.Add("Origin", "https://www.youtube.com");
hrm.Headers.Add("Referer", "https://www.youtube.com/watch?v=" + id);
}
HttpResponseMessage ytPlayerRequest = await client.SendAsync(hrm);
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
}
internal static string GenerateAuthHeader()
{
if (!UseAuthorization) return "None none";
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string hashInput = timestamp + " " + Sapisid + " https://www.youtube.com";
string hashDigest = GenerateSha1Hash(hashInput);
return $"SAPISIDHASH {timestamp}_{hashDigest}";
}
internal static string GenerateAuthCookie() => UseAuthorization ? $"SAPISID={Sapisid}; __Secure-3PAPISID={Sapisid}; __Secure-3PSID={Psid};" : ";";
private static string GenerateSha1Hash(string input)
{
using SHA1Managed sha1 = new();
byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
StringBuilder sb = new(hash.Length * 2);
foreach (byte b in hash) sb.Append(b.ToString("X2"));
return sb.ToString();
}
public static string GetExtension(this Format format)
{
if (format.VideoCodec != "none") return "mp4";
else
switch (format.FormatId)
{
case "139":
case "140":
case "141":
case "256":
case "258":
case "327":
return "mp3";
case "249":
case "250":
case "251":
case "338":
return "opus";
}
return "mp4";
}
public static void SetAuthorization(bool canUseAuthorizedEndpoints, string sapisid, string psid)
{
UseAuthorization = canUseAuthorizedEndpoints;
Sapisid = sapisid;
Psid = psid;
}
internal static string GetCodec(string mimetypeString, bool audioCodec)
{
string acodec = "";
string vcodec = "";
Match match = Regex.Match(mimetypeString, "codecs=\"([\\s\\S]+?)\"");
string[] g = match.Groups[1].ToString().Split(",");
foreach (string codec in g)
{
switch (codec.Split(".")[0].Trim())
{
case "avc1":
case "av01":
case "vp9":
case "mp4v":
vcodec = codec;
break;
case "mp4a":
case "opus":
acodec = codec;
break;
default:
Console.WriteLine("Unknown codec type: " + codec.Split(".")[0].Trim());
break;
}
}
return (audioCodec ? acodec : vcodec).Trim();
}
public static string GetFormatName(JToken formatToken)
{
string format = formatToken["itag"]?.ToString() switch
{
"160" => "144p",
"278" => "144p",
"330" => "144p",
"394" => "144p",
"694" => "144p",
"133" => "240p",
"242" => "240p",
"331" => "240p",
"395" => "240p",
"695" => "240p",
"134" => "360p",
"243" => "360p",
"332" => "360p",
"396" => "360p",
"696" => "360p",
"135" => "480p",
"244" => "480p",
"333" => "480p",
"397" => "480p",
"697" => "480p",
"136" => "720p",
"247" => "720p",
"298" => "720p",
"302" => "720p",
"334" => "720p",
"398" => "720p",
"698" => "720p",
"137" => "1080p",
"299" => "1080p",
"248" => "1080p",
"303" => "1080p",
"335" => "1080p",
"399" => "1080p",
"699" => "1080p",
"264" => "1440p",
"271" => "1440p",
"304" => "1440p",
"308" => "1440p",
"336" => "1440p",
"400" => "1440p",
"700" => "1440p",
"266" => "2160p",
"305" => "2160p",
"313" => "2160p",
"315" => "2160p",
"337" => "2160p",
"401" => "2160p",
"701" => "2160p",
"138" => "4320p",
"272" => "4320p",
"402" => "4320p",
"571" => "4320p",
var _ => $"{formatToken["height"]}p",
};
return format == "p"
? formatToken["audioQuality"]?.ToString().ToLowerInvariant()
: (formatToken["fps"]?.ToObject<int>() ?? 0) > 30
? $"{format}{formatToken["fps"]}"
: format;
}
}
}

790
core/InnerTube/Youtube.cs Normal file
View File

@@ -0,0 +1,790 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using InnerTube.Models;
using Newtonsoft.Json.Linq;
namespace InnerTube
{
public class Youtube
{
internal readonly HttpClient Client = new();
public readonly Dictionary<string, CacheItem<YoutubePlayer>> PlayerCache = new();
private readonly Dictionary<ChannelTabs, string> ChannelTabParams = new()
{
[ChannelTabs.Home] = @"EghmZWF0dXJlZA%3D%3D",
[ChannelTabs.Videos] = @"EgZ2aWRlb3M%3D",
[ChannelTabs.Playlists] = @"EglwbGF5bGlzdHM%3D",
[ChannelTabs.Community] = @"Egljb21tdW5pdHk%3D",
[ChannelTabs.Channels] = @"EghjaGFubmVscw%3D%3D",
[ChannelTabs.About] = @"EgVhYm91dA%3D%3D"
};
private async Task<JObject> MakeRequest(string endpoint, Dictionary<string, object> postData, string language,
string region, string clientName = "WEB", string clientId = "1", string clientVersion = "2.20220405", bool authorized = false)
{
HttpRequestMessage hrm = new(HttpMethod.Post,
@$"https://www.youtube.com/youtubei/v1/{endpoint}?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
byte[] buffer = Encoding.UTF8.GetBytes(RequestContext.BuildRequestContextJson(postData, language, region, clientName, clientVersion));
ByteArrayContent byteContent = new(buffer);
if (authorized)
{
hrm.Headers.Add("Cookie", Utils.GenerateAuthCookie());
hrm.Headers.Add("Authorization", Utils.GenerateAuthHeader());
hrm.Headers.Add("X-Youtube-Client-Name", clientId);
hrm.Headers.Add("X-Youtube-Client-Version", clientVersion);
hrm.Headers.Add("Origin", "https://www.youtube.com");
}
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
hrm.Content = byteContent;
HttpResponseMessage ytPlayerRequest = await Client.SendAsync(hrm);
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
}
public async Task<YoutubePlayer> GetPlayerAsync(string videoId, string language = "en", string region = "US", bool iOS = false)
{
if (PlayerCache.Any(x => x.Key == videoId && x.Value.ExpireTime > DateTimeOffset.Now))
{
CacheItem<YoutubePlayer> item = PlayerCache[videoId];
item.Item.ExpiresInSeconds = ((int)(item.ExpireTime - DateTimeOffset.Now).TotalSeconds).ToString();
return item.Item;
}
JObject player = await MakeRequest("player", new Dictionary<string, object>
{
["videoId"] = videoId,
["contentCheckOk"] = true,
["racyCheckOk"] = true
}, language, region, iOS ? "IOS" : "ANDROID", iOS ? "5" : "3", "17.13.3", true);
switch (player["playabilityStatus"]?["status"]?.ToString())
{
case "OK":
YoutubeStoryboardSpec storyboardSpec =
new(player["storyboards"]?["playerStoryboardSpecRenderer"]?["spec"]?.ToString(), player["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0);
YoutubePlayer video = new()
{
Id = player["videoDetails"]?["videoId"]?.ToString(),
Title = player["videoDetails"]?["title"]?.ToString(),
Description = player["videoDetails"]?["shortDescription"]?.ToString(),
Tags = player["videoDetails"]?["keywords"]?.ToObject<string[]>(),
Channel = new Channel
{
Name = player["videoDetails"]?["author"]?.ToString(),
Id = player["videoDetails"]?["channelId"]?.ToString(),
Avatars = Array.Empty<Thumbnail>()
},
Duration = player["videoDetails"]?["lengthSeconds"]?.ToObject<long>(),
IsLive = player["videoDetails"]?["isLiveContent"]?.ToObject<bool>() ?? false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = player["videoDetails"]?["thumbnail"]?["thumbnails"]?.Select(x => new Thumbnail
{
Height = x["height"]?.ToObject<int>() ?? -1,
Url = x["url"]?.ToString(),
Width = x["width"]?.ToObject<int>() ?? -1
}).ToArray(),
Formats = player["streamingData"]?["formats"]?.Select(x => new Format
{
FormatName = Utils.GetFormatName(x),
FormatId = x["itag"]?.ToString(),
FormatNote = x["quality"]?.ToString(),
Filesize = x["contentLength"]?.ToObject<long>(),
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
AudioCodec = Utils.GetCodec(x["mimeType"]?.ToString(), true),
VideoCodec = Utils.GetCodec(x["mimeType"]?.ToString(), false),
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
Url = x["url"]?.ToString()
}).ToArray() ?? Array.Empty<Format>(),
AdaptiveFormats = player["streamingData"]?["adaptiveFormats"]?.Select(x => new Format
{
FormatName = Utils.GetFormatName(x),
FormatId = x["itag"]?.ToString(),
FormatNote = x["quality"]?.ToString(),
Filesize = x["contentLength"]?.ToObject<long>(),
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
AudioCodec = Utils.GetCodec(x["mimeType"].ToString(), true),
VideoCodec = Utils.GetCodec(x["mimeType"].ToString(), false),
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
Url = x["url"]?.ToString(),
InitRange = x["initRange"]?.ToObject<Models.Range>(),
IndexRange = x["indexRange"]?.ToObject<Models.Range>()
}).ToArray() ?? Array.Empty<Format>(),
HlsManifestUrl = player["streamingData"]?["hlsManifestUrl"]?.ToString(),
Subtitles = player["captions"]?["playerCaptionsTracklistRenderer"]?["captionTracks"]?.Select(
x => new Subtitle
{
Ext = HttpUtility.ParseQueryString(x["baseUrl"].ToString()).Get("fmt"),
Language = Utils.ReadRuns(x["name"]?["runs"]?.ToObject<JArray>()),
Url = x["baseUrl"].ToString()
}).ToArray(),
Storyboards = storyboardSpec.Urls.TryGetValue("L0", out string sb) ? new[] { sb } : Array.Empty<string>(),
ExpiresInSeconds = player["streamingData"]?["expiresInSeconds"]?.ToString(),
ErrorMessage = null
};
PlayerCache.Remove(videoId);
PlayerCache.Add(videoId,
new CacheItem<YoutubePlayer>(video,
TimeSpan.FromSeconds(int.Parse(video.ExpiresInSeconds ?? "21600"))
.Subtract(TimeSpan.FromHours(1))));
return video;
case "LOGIN_REQUIRED":
return new YoutubePlayer
{
Id = "",
Title = "",
Description = "",
Tags = Array.Empty<string>(),
Channel = new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
},
Duration = 0,
IsLive = false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = Array.Empty<Thumbnail>(),
Formats = Array.Empty<Format>(),
AdaptiveFormats = Array.Empty<Format>(),
Subtitles = Array.Empty<Subtitle>(),
Storyboards = Array.Empty<string>(),
ExpiresInSeconds = "0",
ErrorMessage =
"This video is age-restricted. Please contact this instances authors to update their configuration"
};
default:
return new YoutubePlayer
{
Id = "",
Title = "",
Description = "",
Tags = Array.Empty<string>(),
Channel = new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
},
Duration = 0,
IsLive = false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = Array.Empty<Thumbnail>(),
Formats = Array.Empty<Format>(),
AdaptiveFormats = Array.Empty<Format>(),
Subtitles = Array.Empty<Subtitle>(),
Storyboards = Array.Empty<string>(),
ExpiresInSeconds = "0",
ErrorMessage = player["playabilityStatus"]?["reason"]?.ToString() ?? "Something has gone *really* wrong"
};
}
}
public async Task<YoutubeVideo> GetVideoAsync(string videoId, string language = "en", string region = "US")
{
JObject player = await MakeRequest("next", new Dictionary<string, object>
{
["videoId"] = videoId
}, language, region);
JToken[] contents =
(player?["contents"]?["twoColumnWatchNextResults"]?["results"]?["results"]?["contents"]
?.ToObject<JArray>() ?? new JArray())
.SkipWhile(x => !x.First.Path.EndsWith("videoPrimaryInfoRenderer")).ToArray();
YoutubeVideo video = new();
video.Id = player["currentVideoEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString();
try
{
video.Title = Utils.ReadRuns(
contents[0]
["videoPrimaryInfoRenderer"]?["title"]?["runs"]?.ToObject<JArray>());
video.Description = Utils.ReadRuns(
contents[1]
["videoSecondaryInfoRenderer"]?["description"]?["runs"]?.ToObject<JArray>());
video.Views = contents[0]
["videoPrimaryInfoRenderer"]?["viewCount"]?["videoViewCountRenderer"]?["viewCount"]?["simpleText"]?.ToString();
video.Channel = new Channel
{
Name =
contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?[
"text"]?.ToString(),
Id = contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount =
contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["subscriberCountText"]?[
"simpleText"]?.ToString(),
Avatars =
(contents[1][
"videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["thumbnail"]?[
"thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
};
video.UploadDate = contents[0][
"videoPrimaryInfoRenderer"]?["dateText"]?["simpleText"]?.ToString();
}
catch
{
video.Title ??= "";
video.Description ??= "";
video.Channel ??= new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
};
video.UploadDate ??= "";
}
video.Recommended = ParseRenderers(
player?["contents"]?["twoColumnWatchNextResults"]?["secondaryResults"]?["secondaryResults"]?
["results"]?.ToObject<JArray>() ?? new JArray());
return video;
}
public async Task<YoutubeSearchResults> SearchAsync(string query, string continuation = null,
string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
data.Add("query", query);
else
data.Add("continuation", continuation);
JObject search = await MakeRequest("search", data, language, region);
return new YoutubeSearchResults
{
Refinements = search?["refinements"]?.ToObject<string[]>() ?? Array.Empty<string>(),
EstimatedResults = search?["estimatedResults"]?.ToObject<long>() ?? 0,
Results = ParseRenderers(
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
["contents"]?[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ??
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()),
ContinuationKey =
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
["contents"]?[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?
["token"]?.ToString() ??
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?["token"]
?.ToString() ?? ""
};
}
public async Task<YoutubePlaylist> GetPlaylistAsync(string id, string continuation = null,
string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
data.Add("browseId", "VL" + id);
else
data.Add("continuation", continuation);
JObject playlist = await MakeRequest("browse", data, language, region);
DynamicItem[] renderers = ParseRenderers(
playlist?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?
["playlistVideoListRenderer"]?["contents"]?.ToObject<JArray>() ??
playlist?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]
?.ToObject<JArray>() ?? new JArray());
return new YoutubePlaylist
{
Id = id,
Title = playlist?["metadata"]?["playlistMetadataRenderer"]?["title"]?.ToString(),
Description = playlist?["metadata"]?["playlistMetadataRenderer"]?["description"]?.ToString(),
VideoCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[0]?["runs"]?[0]?["text"]?.ToString(),
ViewCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[1]?["simpleText"]?.ToString(),
LastUpdated = Utils.ReadRuns(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[2]?["runs"]?.ToObject<JArray>() ?? new JArray()),
Thumbnail = (playlist?["microformat"]?["microformatDataRenderer"]?["thumbnail"]?["thumbnails"] ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Channel = new Channel
{
Name =
playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["title"]?
["runs"]?[0]?["text"]?.ToString(),
Id = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = "",
Avatars =
(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["thumbnail"]
?["thumbnails"] ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
},
Videos = renderers.Where(x => x is not ContinuationItem).ToArray(),
ContinuationKey = renderers.FirstOrDefault(x => x is ContinuationItem)?.Id
};
}
public async Task<YoutubeChannel> GetChannelAsync(string id, ChannelTabs tab = ChannelTabs.Home,
string continuation = null, string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
{
data.Add("browseId", id);
if (string.IsNullOrWhiteSpace(continuation))
data.Add("params", ChannelTabParams[tab]);
}
else
{
data.Add("continuation", continuation);
}
JObject channel = await MakeRequest("browse", data, language, region);
JArray mainArray =
(channel?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?.ToObject<JArray>() ?? new JArray())
.FirstOrDefault(x => x?["tabRenderer"]?["selected"]?.ToObject<bool>() ?? false)?["tabRenderer"]?[
"content"]?
["sectionListRenderer"]?["contents"]?.ToObject<JArray>();
return new YoutubeChannel
{
Id = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
Name = channel?["metadata"]?["channelMetadataRenderer"]?["title"]?.ToString(),
Url = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
Avatars = (channel?["metadata"]?["channelMetadataRenderer"]?["avatar"]?["thumbnails"] ?? new JArray())
.Select(Utils.ParseThumbnails).ToArray(),
Banners = (channel?["header"]?["c4TabbedHeaderRenderer"]?["banner"]?["thumbnails"] ?? new JArray())
.Select(Utils.ParseThumbnails).ToArray(),
Description = channel?["metadata"]?["channelMetadataRenderer"]?["description"]?.ToString(),
Videos = ParseRenderers(mainArray ??
channel?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?
["continuationItems"]?.ToObject<JArray>() ?? new JArray()),
Subscribers = channel?["header"]?["c4TabbedHeaderRenderer"]?["subscriberCountText"]?["simpleText"]
?.ToString()
};
}
public async Task<YoutubeTrends> GetExploreAsync(string browseId = null, string continuation = null, string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
{
data.Add("browseId", browseId ?? "FEexplore");
}
else
{
data.Add("continuation", continuation);
}
JObject explore = await MakeRequest("browse", data, language, region);
JToken[] token =
(explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
["sectionListRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()).Skip(1).ToArray();
JArray mainArray = new(token.Select(x => x is JObject obj ? obj : null).Where(x => x is not null));
return new YoutubeTrends
{
Categories = explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?["destinationShelfRenderer"]?["destinationButtons"]?.Select(
x =>
{
JToken rendererObject = x?["destinationButtonRenderer"];
TrendCategory category = new()
{
Label = rendererObject?["label"]?["simpleText"]?.ToString(),
BackgroundImage = (rendererObject?["backgroundImage"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Icon = (rendererObject?["iconImage"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Id = $"{rendererObject?["onTap"]?["browseEndpoint"]?["browseId"]}"
};
return category;
}).ToArray(),
Videos = ParseRenderers(mainArray)
};
}
public async Task<YoutubeLocals> GetLocalsAsync(string language = "en", string region = "US")
{
JObject locals = await MakeRequest("account/account_menu", new Dictionary<string, object>(), language,
region);
return new YoutubeLocals
{
Languages =
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
["multiPageMenuSectionRenderer"]?["items"]?[1]?["compactLinkRenderer"]?["serviceEndpoint"]?
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
.ToObject<JArray>()?.ToDictionary(
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
["actions"]?[0]?["selectLanguageCommand"]?["hl"]?.ToString(),
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString()),
Regions =
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
["multiPageMenuSectionRenderer"]?["items"]?[2]?["compactLinkRenderer"]?["serviceEndpoint"]?
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
.ToObject<JArray>()?.ToDictionary(
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
["actions"]?[0]?["selectCountryCommand"]?["gl"]?.ToString(),
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString())
};
}
private DynamicItem[] ParseRenderers(JArray renderersArray)
{
List<DynamicItem> items = new();
foreach (JToken jToken in renderersArray)
{
JObject recommendationContainer = jToken as JObject;
string rendererName = recommendationContainer?.First?.Path.Split(".").Last() ?? "";
JObject rendererItem = recommendationContainer?[rendererName]?.ToObject<JObject>();
switch (rendererName)
{
case "videoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long vV) ? vV : 0,
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars =
(rendererItem?["channelThumbnailSupportedRenderers"]?[
"channelThumbnailWithLinkRenderer"]?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray()
},
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString(),
Description = Utils.ReadRuns(rendererItem?["detailedMetadataSnippets"]?[0]?[
"snippetText"]?["runs"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "gridVideoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString() ?? Utils.ReadRuns(
rendererItem?["title"]?["runs"]?.ToObject<JArray>() ?? new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long gVV) ? gVV : 0,
Channel = null,
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
});
break;
case "playlistRenderer":
items.Add(new PlaylistItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnails"]?[0]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
VideoCount = int.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
.Replace(".", "") ?? "0", out int pVC) ? pVC : 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]
?.ToString(),
SubscriberCount = null,
Avatars = null
}
});
break;
case "channelRenderer":
items.Add(new ChannelItem
{
Id = rendererItem?["channelId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails)
.ToArray(), //
Url = rendererItem?["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
?.ToString(),
Description =
Utils.ReadRuns(rendererItem?["descriptionSnippet"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
VideoCount = long.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]
?.ToString()
.Replace(",",
"")
.Replace(".",
"") ??
"0", out long cVC) ? cVC : 0,
Subscribers = rendererItem?["subscriberCountText"]?["simpleText"]?.ToString()
});
break;
case "radioRenderer":
items.Add(new RadioItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
Id = "",
SubscriberCount = null,
Avatars = null
}
});
break;
case "shelfRenderer":
items.Add(new ShelfItem
{
Title = rendererItem?["title"]?["simpleText"]
?.ToString() ??
rendererItem?["title"]?["runs"]?[0]?["text"]
?.ToString(),
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Items = ParseRenderers(
rendererItem?["content"]?["verticalListRenderer"]?["items"]
?.ToObject<JArray>() ??
rendererItem?["content"]?["horizontalListRenderer"]?["items"]
?.ToObject<JArray>() ??
rendererItem?["content"]?["expandedShelfContentsRenderer"]?["items"]
?.ToObject<JArray>() ??
new JArray()),
CollapsedItemCount =
rendererItem?["content"]?["verticalListRenderer"]?["collapsedItemCount"]
?.ToObject<int>() ?? 0,
Badges = ParseRenderers(rendererItem?["badges"]?.ToObject<JArray>() ?? new JArray())
.Where(x => x is BadgeItem).Cast<BadgeItem>().ToArray(),
});
break;
case "horizontalCardListRenderer":
items.Add(new HorizontalCardListItem
{
Title = rendererItem?["header"]?["richListHeaderRenderer"]?["title"]?["simpleText"]
?.ToString(),
Items = ParseRenderers(rendererItem?["cards"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "searchRefinementCardRenderer":
items.Add(new CardItem
{
Title = Utils.ReadRuns(rendererItem?["query"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray()
});
break;
case "compactVideoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long cVV) ? cVV : 0,
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars = null
},
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
});
break;
case "compactPlaylistRenderer":
items.Add(new PlaylistItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray(),
VideoCount = int.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
.Replace(".", "") ?? "0", out int cPVC) ? cPVC : 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]
?.ToString(),
SubscriberCount = null,
Avatars = null
}
});
break;
case "compactRadioRenderer":
items.Add(new RadioItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray(),
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
Id = "",
SubscriberCount = null,
Avatars = null
}
});
break;
case "continuationItemRenderer":
items.Add(new ContinuationItem
{
Id = rendererItem?["continuationEndpoint"]?["continuationCommand"]?["token"]?.ToString()
});
break;
case "playlistVideoRenderer":
items.Add(new PlaylistVideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Index = rendererItem?["index"]?["simpleText"]?.ToObject<long>() ?? 0,
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Channel = new Channel
{
Name = rendererItem?["shortBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["shortBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars = null
},
Duration = rendererItem?["lengthText"]?["simpleText"]?.ToString()
});
break;
case "itemSectionRenderer":
items.Add(new ItemSectionItem
{
Contents = ParseRenderers(rendererItem?["contents"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "gridRenderer":
items.Add(new ItemSectionItem
{
Contents = ParseRenderers(rendererItem?["items"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "messageRenderer":
items.Add(new MessageItem
{
Title = rendererItem?["text"]?["simpleText"]?.ToString()
});
break;
case "channelAboutFullMetadataRenderer":
items.Add(new ChannelAboutItem
{
Description = rendererItem?["description"]?["simpleText"]?.ToString(),
Country = rendererItem?["country"]?["simpleText"]?.ToString(),
Joined = Utils.ReadRuns(rendererItem?["joinedDateText"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
ViewCount = rendererItem?["viewCountText"]?["simpleText"]?.ToString()
});
break;
case "compactStationRenderer":
items.Add(new StationItem
{
Id = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["playlistId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
VideoCount = rendererItem?["videoCountText"]?["runs"]?[0]?["text"].ToObject<int>() ?? 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString(),
Description = rendererItem?["description"]?["simpleText"]?.ToString()
});
break;
case "metadataBadgeRenderer":
items.Add(new BadgeItem
{
Title = rendererItem?["label"]?.ToString(),
Style = rendererItem?["style"]?.ToString()
});
break;
case "promotedSparklesWebRenderer":
// this is an ad
// no one likes ads
break;
default:
items.Add(new DynamicItem
{
Id = rendererName,
Title = rendererItem?.ToString()
});
break;
}
}
return items.ToArray();
}
}
}