mirror of
https://codeberg.org/ashley/poke
synced 2026-03-03 16:53:49 +00:00
owo
This commit is contained in:
16
core/InnerTube/CacheItem.cs
Normal file
16
core/InnerTube/CacheItem.cs
Normal 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
12
core/InnerTube/Enums.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace InnerTube
|
||||
{
|
||||
public enum ChannelTabs
|
||||
{
|
||||
Home,
|
||||
Videos,
|
||||
Playlists,
|
||||
Community,
|
||||
Channels,
|
||||
About
|
||||
}
|
||||
}
|
||||
11
core/InnerTube/InnerTube.csproj
Normal file
11
core/InnerTube/InnerTube.csproj
Normal 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>
|
||||
380
core/InnerTube/Models/DynamicItem.cs
Normal file
380
core/InnerTube/Models/DynamicItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
core/InnerTube/Models/RequestContext.cs
Normal file
67
core/InnerTube/Models/RequestContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
core/InnerTube/Models/YoutubeChannel.cs
Normal file
71
core/InnerTube/Models/YoutubeChannel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
core/InnerTube/Models/YoutubeLocals.cs
Normal file
40
core/InnerTube/Models/YoutubeLocals.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
230
core/InnerTube/Models/YoutubePlayer.cs
Normal file
230
core/InnerTube/Models/YoutubePlayer.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
68
core/InnerTube/Models/YoutubePlaylist.cs
Normal file
68
core/InnerTube/Models/YoutubePlaylist.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
core/InnerTube/Models/YoutubeSearchResults.cs
Normal file
39
core/InnerTube/Models/YoutubeSearchResults.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
core/InnerTube/Models/YoutubeStoryboardSpec.cs
Normal file
38
core/InnerTube/Models/YoutubeStoryboardSpec.cs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
core/InnerTube/Models/YoutubeTrends.cs
Normal file
70
core/InnerTube/Models/YoutubeTrends.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
core/InnerTube/Models/YoutubeVideo.cs
Normal file
45
core/InnerTube/Models/YoutubeVideo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
core/InnerTube/ReturnYouTubeDislike.cs
Normal file
44
core/InnerTube/ReturnYouTubeDislike.cs
Normal 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
457
core/InnerTube/Utils.cs
Normal 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
790
core/InnerTube/Youtube.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user