[CHANGE] Implementing video fetching and deciphering
This commit is contained in:
8
Manager.YouTube/Models/Innertube/ColorInfo.cs
Normal file
8
Manager.YouTube/Models/Innertube/ColorInfo.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class ColorInfo
|
||||
{
|
||||
public string Primaries { get; set; } = "";
|
||||
public string TransferCharacteristics { get; set; } = "";
|
||||
public string MatrixCoefficients { get; set; } = "";
|
||||
}
|
||||
7
Manager.YouTube/Models/Innertube/Range.cs
Normal file
7
Manager.YouTube/Models/Innertube/Range.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class Range
|
||||
{
|
||||
public uint Start { get; set; }
|
||||
public uint End { get; set; }
|
||||
}
|
||||
8
Manager.YouTube/Models/Innertube/StoryBoard.cs
Normal file
8
Manager.YouTube/Models/Innertube/StoryBoard.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class StoryBoard
|
||||
{
|
||||
public string Spec { get; set; } = "";
|
||||
public int RecommendedLevel { get; set; }
|
||||
public int HighResolutionRecommendedLevel { get; set; }
|
||||
}
|
||||
10
Manager.YouTube/Models/Innertube/StreamingData.cs
Normal file
10
Manager.YouTube/Models/Innertube/StreamingData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class StreamingData
|
||||
{
|
||||
public DateTime FetchedUtc { get; set; } = DateTime.UtcNow;
|
||||
public int ExpiresInSeconds { get; set; }
|
||||
public string ServerAbrStreamingUrl { get; set; } = "";
|
||||
public List<StreamingFormat> Formats { get; set; } = [];
|
||||
public List<StreamingFormat> AdaptiveFormats { get; set; } = [];
|
||||
}
|
||||
30
Manager.YouTube/Models/Innertube/StreamingFormat.cs
Normal file
30
Manager.YouTube/Models/Innertube/StreamingFormat.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class StreamingFormat
|
||||
{
|
||||
public int Itag { get; set; }
|
||||
public string Url { get; set; } = "";
|
||||
public string MimeType { get; set; } = "";
|
||||
public uint Bitrate { get; set; }
|
||||
public uint? Width { get; set; }
|
||||
public uint? Height { get; set; }
|
||||
public Range? InitRange { get; set; }
|
||||
public Range? IndexRange { get; set; }
|
||||
public long LastModified { get; set; }
|
||||
public long ContentLength { get; set; }
|
||||
public string Quality { get; set; } = "";
|
||||
public string? Xtags { get; set; }
|
||||
public uint Fps { get; set; }
|
||||
public string QualityLabel { get; set; } = "";
|
||||
public string ProjectionType { get; set; } = "";
|
||||
public uint? AverageBitrate { get; set; }
|
||||
public bool? HighReplication { get; set; }
|
||||
public ColorInfo? ColorInfo { get; set; }
|
||||
public string? AudioQuality { get; set; } = "";
|
||||
public long ApproxDurationMs { get; set; }
|
||||
public int? AudioSampleRate { get; set; }
|
||||
public int? AudioChannels { get; set; }
|
||||
public double? LoudnessDb { get; set; }
|
||||
public bool? IsDrc { get; set; }
|
||||
public string QualityOrdinal { get; set; } = "";
|
||||
}
|
||||
9
Manager.YouTube/Models/Parser/YouTubeVideoData.cs
Normal file
9
Manager.YouTube/Models/Parser/YouTubeVideoData.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Manager.YouTube.Models.Parser;
|
||||
|
||||
public class YouTubeVideoData
|
||||
{
|
||||
public JsonObject? YouTubePlayerData { get; set; }
|
||||
public JsonObject? YouTubeInitialData { get; set; }
|
||||
}
|
||||
12
Manager.YouTube/Models/PlayerConfig.cs
Normal file
12
Manager.YouTube/Models/PlayerConfig.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Manager.YouTube.Models;
|
||||
|
||||
public class PlayerConfig
|
||||
{
|
||||
public double AudioLoudnessDb { get; set; }
|
||||
public double AudioPerceptualLoudnessDb { get; set; }
|
||||
public bool AudioEnablePerFormatLoudness { get; set; }
|
||||
public uint MaxBitrate { get; set; }
|
||||
public uint MaxReadAheadMediaTimeMs { get; set; }
|
||||
public uint MinReadAheadMediaTimeMs { get; set; }
|
||||
public uint ReadAheadGrowthRateMs { get; set; }
|
||||
}
|
||||
36
Manager.YouTube/Models/YouTubeVideo.cs
Normal file
36
Manager.YouTube/Models/YouTubeVideo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Manager.YouTube.Models.Innertube;
|
||||
|
||||
namespace Manager.YouTube.Models;
|
||||
|
||||
public class YouTubeVideo
|
||||
{
|
||||
public required string VideoId { get; set; }
|
||||
public string Title { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string[] HashTags { get; set; } = [];
|
||||
public long ViewCount { get; set; }
|
||||
public long LikeCount { get; set; }
|
||||
public string ChannelId { get; set; } = "";
|
||||
public string Author { get; set; } = "";
|
||||
public string PlayabilityStatus { get; set; } = "";
|
||||
public long LengthSeconds { get; set; }
|
||||
public bool AllowRating { get; set; }
|
||||
public bool IsCrawlable { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
public bool IsUnpluggedCorpus { get; set; }
|
||||
public bool IsLive { get; set; }
|
||||
public bool IsFamiliySave { get; set; }
|
||||
public string[] AvailableCountries { get; set; } = [];
|
||||
public bool IsUnlisted { get; set; }
|
||||
public bool HasYpcMetadata { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
public DateTime UploadDate { get; set; }
|
||||
public bool IsShortsEligible { get; set; }
|
||||
public string Category { get; set; } = "";
|
||||
|
||||
public StreamingData StreamingData { get; set; } = new();
|
||||
public List<WebImage> Thumbnails { get; set; } = [];
|
||||
|
||||
public PlayerConfig? PlayerConfig { get; set; }
|
||||
public StoryBoard? StoryBoard { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using DotBased.Monads;
|
||||
using HtmlAgilityPack;
|
||||
using Manager.YouTube.Models.Parser;
|
||||
|
||||
namespace Manager.YouTube.Parsers;
|
||||
|
||||
@@ -31,7 +33,57 @@ public static class HtmlParser
|
||||
|
||||
return (json, isPremiumUser);
|
||||
}
|
||||
|
||||
|
||||
public static Result<YouTubeVideoData> GetVideoDataFromHtml(string html)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
{
|
||||
return ResultError.Fail("html cannot be empty!");
|
||||
}
|
||||
var htmlDocument = new HtmlDocument();
|
||||
htmlDocument.LoadHtml(html);
|
||||
|
||||
const string initialYoutubeData = "var ytInitialPlayerResponse = {";
|
||||
var initialPlayerDataNode = htmlDocument.DocumentNode.SelectSingleNode($"//script[contains(., '{initialYoutubeData}')]");
|
||||
if (string.IsNullOrWhiteSpace(initialPlayerDataNode.InnerText))
|
||||
{
|
||||
return ResultError.Fail("Could not find {initialPlayerData} in html script nodes!");
|
||||
}
|
||||
var initialPlayerDataString = ExtractJson(initialPlayerDataNode.InnerText, "var ytInitialPlayerResponse = ");
|
||||
if (string.IsNullOrWhiteSpace(initialPlayerDataString))
|
||||
{
|
||||
return ResultError.Fail("Failed to extract initial player date from JSON.");
|
||||
}
|
||||
var parsedPlayerInitialData = JsonNode.Parse(initialPlayerDataString);
|
||||
|
||||
const string initialData = "var ytInitialData = {";
|
||||
var initialDataNode = htmlDocument.DocumentNode.SelectSingleNode($"//script[contains(., '{initialData}')]");
|
||||
if (string.IsNullOrWhiteSpace(initialDataNode.InnerText))
|
||||
{
|
||||
return ResultError.Fail("Could not find {initialData} in html script nodes!");
|
||||
}
|
||||
|
||||
var initialDataJsonString = ExtractJson(initialDataNode.InnerText, "var ytInitialData = ");
|
||||
if (string.IsNullOrWhiteSpace(initialDataJsonString))
|
||||
{
|
||||
return ResultError.Fail("Failed to extract initial player date from JSON.");
|
||||
}
|
||||
var parsedInitialData = JsonNode.Parse(initialDataJsonString);
|
||||
|
||||
try
|
||||
{
|
||||
return new YouTubeVideoData
|
||||
{
|
||||
YouTubePlayerData = parsedPlayerInitialData?.AsObject(),
|
||||
YouTubeInitialData = parsedInitialData?.AsObject()
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ResultError.Error(e, "Could not parse youtube player data.");
|
||||
}
|
||||
}
|
||||
|
||||
static string? ExtractJson(string input, string marker)
|
||||
{
|
||||
var start = input.IndexOf(marker, StringComparison.Ordinal);
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed class YouTubeClient : IDisposable
|
||||
|
||||
if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId))
|
||||
{
|
||||
var datasyncResult = await GetDatasyncIds();
|
||||
var datasyncResult = await GetDatasyncIdsAsync();
|
||||
if (!datasyncResult.IsSuccess)
|
||||
{
|
||||
return datasyncResult;
|
||||
@@ -125,7 +125,24 @@ public sealed class YouTubeClient : IDisposable
|
||||
return result.Error ?? ResultError.Fail("Request failed!");
|
||||
}
|
||||
|
||||
var clientStateResult = HtmlParser.GetStateJson(result.Value);
|
||||
var stateResult = SetClientStateFromHtml(result.Value);
|
||||
if (!stateResult.IsSuccess)
|
||||
{
|
||||
return stateResult;
|
||||
}
|
||||
|
||||
var cookieRotationResult = await RotateCookiesPageAsync();
|
||||
return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success();
|
||||
}
|
||||
|
||||
private Result SetClientStateFromHtml(string html)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
{
|
||||
return ResultError.Fail("HTML is empty!!");
|
||||
}
|
||||
|
||||
var clientStateResult = HtmlParser.GetStateJson(html);
|
||||
if (clientStateResult is { IsSuccess: false, Error: not null })
|
||||
{
|
||||
return clientStateResult.Error;
|
||||
@@ -146,11 +163,10 @@ public sealed class YouTubeClient : IDisposable
|
||||
}
|
||||
|
||||
State.IsPremiumUser = clientStateResult.Value.Item2;
|
||||
|
||||
var cookieRotationResult = await RotateCookiesPageAsync();
|
||||
return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success();
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
|
||||
public async Task<Result<InnertubeChannel>> GetChannelByIdAsync(string channelId)
|
||||
{
|
||||
if (State == null)
|
||||
@@ -262,7 +278,7 @@ public sealed class YouTubeClient : IDisposable
|
||||
return JsonAccountParser.ParseAccountId(responseResult.Value);
|
||||
}
|
||||
|
||||
private async Task<Result<string[]>> GetDatasyncIds()
|
||||
private async Task<Result<string[]>> GetDatasyncIdsAsync()
|
||||
{
|
||||
if (State is not { LoggedIn: true } || CookieContainer.Count == 0)
|
||||
{
|
||||
@@ -291,4 +307,38 @@ public sealed class YouTubeClient : IDisposable
|
||||
|
||||
return ResultError.Fail("Failed to get datasyncIds! Client not logged in.");
|
||||
}
|
||||
|
||||
public async Task<Result> GetVideoByIdAsync(string videoId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoId))
|
||||
{
|
||||
return ResultError.Fail("Video id is empty!");
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/watch?v={videoId}"));
|
||||
|
||||
var response = await NetworkService.MakeRequestAsync(request, this);
|
||||
if (!response.IsSuccess && !string.IsNullOrWhiteSpace(response.Value))
|
||||
{
|
||||
return response.Error ?? ResultError.Fail("Request failed!");
|
||||
}
|
||||
|
||||
var html = response.Value;
|
||||
|
||||
var stateResult = SetClientStateFromHtml(html);
|
||||
if (!stateResult.IsSuccess)
|
||||
{
|
||||
//TODO: Log warning: failed to update client state!
|
||||
}
|
||||
|
||||
var htmlParseReult = HtmlParser.GetVideoDataFromHtml(html);
|
||||
if (!htmlParseReult.IsSuccess)
|
||||
{
|
||||
return htmlParseReult;
|
||||
}
|
||||
|
||||
//TODO: Process to YouTubeVideo model && decipher stream urls
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user