diff --git a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor new file mode 100644 index 0000000..bd8c941 --- /dev/null +++ b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor @@ -0,0 +1,16 @@ +@using Manager.App.Models.System +@using Manager.App.Services.System + +@inject ISnackbar Snackbar +@inject ClientService ClientService + +Video data + + + + + + + Get data + \ No newline at end of file diff --git a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs new file mode 100644 index 0000000..0fa7ca1 --- /dev/null +++ b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs @@ -0,0 +1,56 @@ +using Manager.App.Models.System; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Manager.App.Components.Application.Dev; + +public partial class DevelopmentVideo : ComponentBase +{ + private YouTubeClientItem? _selectedClient; + private string _videoId = ""; + + private async Task> SearchClientsAsync(string? search, CancellationToken cancellationToken) + { + var searchResults = await ClientService.GetClientsAsync(search, cancellationToken: cancellationToken); + return !searchResults.IsSuccess ? [] : searchResults.Value; + } + + private async Task GetDataAsync(MouseEventArgs obj) + { + if (_selectedClient == null) + { + Snackbar.Add("No client selected!", Severity.Warning); + return; + } + + if (string.IsNullOrWhiteSpace(_videoId)) + { + Snackbar.Add("No video ID set!", Severity.Warning); + return; + } + + if (_videoId.Length != 11) + { + Snackbar.Add("Video ID needs to have an length of 11 chars!", Severity.Warning); + } + + var clientResult = await ClientService.LoadClientByIdAsync(_selectedClient.Id); + if (!clientResult.IsSuccess) + { + Snackbar.Add(clientResult.Error?.Description ?? $"Failed to get client with id: {_selectedClient.Id}", Severity.Error); + return; + } + + var ytClient = clientResult.Value; + var videoResult = await ytClient.GetVideoByIdAsync(_videoId); + if (!videoResult.IsSuccess) + { + Snackbar.Add(videoResult.Error?.Description ?? $"Failed to load video: {_videoId}", Severity.Error); + return; + } + + var ytVideo = videoResult.Value; + Snackbar.Add($"Loaded video {ytVideo.Title}", Severity.Success); + } +} \ No newline at end of file diff --git a/Manager.App/Components/Pages/Development.razor b/Manager.App/Components/Pages/Development.razor index 338665e..2a3a512 100644 --- a/Manager.App/Components/Pages/Development.razor +++ b/Manager.App/Components/Pages/Development.razor @@ -6,4 +6,7 @@ + + + \ No newline at end of file diff --git a/Manager.App/Services/LibraryService.cs b/Manager.App/Services/LibraryService.cs index 4e57e67..0e3ef22 100644 --- a/Manager.App/Services/LibraryService.cs +++ b/Manager.App/Services/LibraryService.cs @@ -101,6 +101,7 @@ public class LibraryService : ILibraryService } else { + context.HttpCookies.RemoveRange(context.HttpCookies.Where(x => x.ClientId == client.Id)); context.ClientAccounts.Add(dbClient); } diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs index 4b3525d..aa6330e 100644 --- a/Manager.App/Services/System/ClientService.cs +++ b/Manager.App/Services/System/ClientService.cs @@ -40,7 +40,7 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger> GetClientsAsync(string search, int offset = 0, int limit = 10, CancellationToken cancellationToken = default) + public async Task> GetClientsAsync(string? search, int offset = 0, int limit = 10, CancellationToken cancellationToken = default) { if (_libraryService == null) { diff --git a/Manager.YouTube/Models/Innertube/ColorInfo.cs b/Manager.YouTube/Models/Innertube/ColorInfo.cs index afbd20a..642fa7b 100644 --- a/Manager.YouTube/Models/Innertube/ColorInfo.cs +++ b/Manager.YouTube/Models/Innertube/ColorInfo.cs @@ -1,8 +1,13 @@ +using System.Text.Json.Serialization; + namespace Manager.YouTube.Models.Innertube; public class ColorInfo { + [JsonPropertyName("primaries")] public string Primaries { get; set; } = ""; + [JsonPropertyName("transferCharacteristics")] public string TransferCharacteristics { get; set; } = ""; + [JsonPropertyName("matrixCoefficients")] public string MatrixCoefficients { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/Range.cs b/Manager.YouTube/Models/Innertube/Range.cs index 38d6fdc..2f6c384 100644 --- a/Manager.YouTube/Models/Innertube/Range.cs +++ b/Manager.YouTube/Models/Innertube/Range.cs @@ -1,7 +1,11 @@ +using System.Text.Json.Serialization; + namespace Manager.YouTube.Models.Innertube; public class Range { + [JsonPropertyName("start")] public uint Start { get; set; } + [JsonPropertyName("end")] public uint End { get; set; } } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/StreamingData.cs b/Manager.YouTube/Models/Innertube/StreamingData.cs index a1135f6..56947d0 100644 --- a/Manager.YouTube/Models/Innertube/StreamingData.cs +++ b/Manager.YouTube/Models/Innertube/StreamingData.cs @@ -1,10 +1,16 @@ +using System.Text.Json.Serialization; + namespace Manager.YouTube.Models.Innertube; public class StreamingData { public DateTime FetchedUtc { get; set; } = DateTime.UtcNow; + [JsonPropertyName("expiresInSeconds")] public int ExpiresInSeconds { get; set; } + [JsonPropertyName("serverAbrStreamingUrl")] public string ServerAbrStreamingUrl { get; set; } = ""; + [JsonPropertyName("formats")] public List Formats { get; set; } = []; + [JsonPropertyName("adaptiveFormats")] public List AdaptiveFormats { get; set; } = []; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/StreamingFormat.cs b/Manager.YouTube/Models/Innertube/StreamingFormat.cs index 8d84adc..592738d 100644 --- a/Manager.YouTube/Models/Innertube/StreamingFormat.cs +++ b/Manager.YouTube/Models/Innertube/StreamingFormat.cs @@ -1,31 +1,59 @@ +using System.Text.Json.Serialization; + namespace Manager.YouTube.Models.Innertube; public class StreamingFormat { + [JsonPropertyName("itag")] public int Itag { get; set; } + [JsonPropertyName("url")] public string? Url { get; set; } + [JsonPropertyName("mimeType")] public string MimeType { get; set; } = ""; + [JsonPropertyName("bitrate")] public uint Bitrate { get; set; } + [JsonPropertyName("width")] public uint? Width { get; set; } + [JsonPropertyName("height")] public uint? Height { get; set; } + [JsonPropertyName("initRange")] public Range? InitRange { get; set; } + [JsonPropertyName("indexRange")] public Range? IndexRange { get; set; } + [JsonPropertyName("lastModified")] public long LastModified { get; set; } + [JsonPropertyName("contentLength")] public long ContentLength { get; set; } + [JsonPropertyName("quality")] public string Quality { get; set; } = ""; + [JsonPropertyName("xtags")] public string? Xtags { get; set; } + [JsonPropertyName("fps")] public uint Fps { get; set; } + [JsonPropertyName("qualityLabel")] public string QualityLabel { get; set; } = ""; + [JsonPropertyName("projectionType")] public string ProjectionType { get; set; } = ""; + [JsonPropertyName("averagebitrate")] public uint? AverageBitrate { get; set; } + [JsonPropertyName("highReplication")] public bool? HighReplication { get; set; } + [JsonPropertyName("colorInfo")] public ColorInfo? ColorInfo { get; set; } + [JsonPropertyName("audioQuality")] public string? AudioQuality { get; set; } = ""; + [JsonPropertyName("approxDurationMs")] public long ApproxDurationMs { get; set; } + [JsonPropertyName("audioSampleRate")] public int? AudioSampleRate { get; set; } + [JsonPropertyName("audioChannels")] public int? AudioChannels { get; set; } + [JsonPropertyName("loudnessDb")] public double? LoudnessDb { get; set; } + [JsonPropertyName("isDrc")] public bool? IsDrc { get; set; } + [JsonPropertyName("signatureCipher")] public string? SignatureCipher { get; set; } + [JsonPropertyName("qualityOrdinal")] public string QualityOrdinal { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/WebImage.cs b/Manager.YouTube/Models/Innertube/WebImage.cs index 37e282b..8cd8101 100644 --- a/Manager.YouTube/Models/Innertube/WebImage.cs +++ b/Manager.YouTube/Models/Innertube/WebImage.cs @@ -1,8 +1,13 @@ +using System.Text.Json.Serialization; + namespace Manager.YouTube.Models.Innertube; public class WebImage { + [JsonPropertyName("width")] public int Width { get; set; } + [JsonPropertyName("height")] public int Height { get; set; } + [JsonPropertyName("url")] public string Url { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Parser/YouTubeVideoData.cs b/Manager.YouTube/Models/Parser/YouTubeVideoData.cs index 190cea2..0aae6ce 100644 --- a/Manager.YouTube/Models/Parser/YouTubeVideoData.cs +++ b/Manager.YouTube/Models/Parser/YouTubeVideoData.cs @@ -4,6 +4,6 @@ namespace Manager.YouTube.Models.Parser; public class YouTubeVideoData { - public JsonObject? YouTubePlayerData { get; set; } - public JsonObject? YouTubeInitialData { get; set; } + public JsonNode? YouTubePlayerData { get; set; } + public JsonNode? YouTubeInitialData { get; set; } } \ No newline at end of file diff --git a/Manager.YouTube/Parsers/HtmlParser.cs b/Manager.YouTube/Parsers/HtmlParser.cs index cf22929..7200aba 100644 --- a/Manager.YouTube/Parsers/HtmlParser.cs +++ b/Manager.YouTube/Parsers/HtmlParser.cs @@ -74,8 +74,8 @@ public static class HtmlParser { return new YouTubeVideoData { - YouTubePlayerData = parsedPlayerInitialData?.AsObject(), - YouTubeInitialData = parsedInitialData?.AsObject() + YouTubePlayerData = parsedPlayerInitialData, + YouTubeInitialData = parsedInitialData }; } catch (Exception e) diff --git a/Manager.YouTube/Parsers/Json/JsonParser.cs b/Manager.YouTube/Parsers/Json/JsonParser.cs index def4fa8..8b4130e 100644 --- a/Manager.YouTube/Parsers/Json/JsonParser.cs +++ b/Manager.YouTube/Parsers/Json/JsonParser.cs @@ -12,45 +12,58 @@ public static class JsonParser .Select(image => new WebImage { Width = image.GetProperty("width").GetInt32(), Height = image.GetProperty("height").GetInt32(), Url = image.GetProperty("url").GetString() ?? "" }) .ToList(); - public static string ExtractTextOrHtml(JsonElement element) + public static string ExtractTextOrHtml(JsonNode? node) { + if (node is not JsonObject nodeObj) + { + return ""; + } + // Case 1: Simple text (no formatting) - if (element.TryGetProperty("simpleText", out var simpleText)) - return simpleText.GetString() ?? string.Empty; + if (nodeObj.TryGetPropertyValue("simpleText", out var simpleText)) + return simpleText?.GetValue() ?? string.Empty; // Case 2: Runs (formatted text segments) - if (element.TryGetProperty("runs", out var runs) && runs.ValueKind == JsonValueKind.Array) + if (nodeObj.TryGetPropertyValue("runs", out var runs) && runs != null && runs.GetValueKind() == JsonValueKind.Array) { var sb = new StringBuilder(); - foreach (var run in runs.EnumerateArray()) + foreach (var runNode in runs.AsArray()) { - var text = run.GetProperty("text").GetString() ?? string.Empty; + if (runNode is not JsonObject run) + { + continue; + } + + var text = runNode["text"]?.GetValue() ?? string.Empty; var formatted = System.Net.WebUtility.HtmlEncode(text); - var bold = run.TryGetProperty("bold", out var boldProp) && boldProp.GetBoolean(); - var italic = run.TryGetProperty("italic", out var italicProp) && italicProp.GetBoolean(); - var underline = run.TryGetProperty("underline", out var underlineProp) && underlineProp.GetBoolean(); - var strikethrough = run.TryGetProperty("strikethrough", out var strikeProp) && strikeProp.GetBoolean(); + var bold = run.TryGetPropertyValue("bold", out var boldNode) && boldNode is JsonValue bv && bv.GetValue(); + + var italic = run.TryGetPropertyValue("italic", out var italicNode) && italicNode is JsonValue iv && iv.GetValue(); + + var underline = run.TryGetPropertyValue("underline", out var underlineNode) && underlineNode is JsonValue uv && uv.GetValue(); + + var strikethrough = run.TryGetPropertyValue("strikethrough", out var strikeNode) && strikeNode is JsonValue sv && sv.GetValue(); if (bold) formatted = $"{formatted}"; if (italic) formatted = $"{formatted}"; if (underline) formatted = $"{formatted}"; if (strikethrough) formatted = $"{formatted}"; - if (run.TryGetProperty("navigationEndpoint", out var nav) && - nav.TryGetProperty("url", out var urlProp)) + if (run.TryGetPropertyValue("navigationEndpoint", out var nav) && nav is JsonObject navObj && + navObj.TryGetPropertyValue("url", out var urlProp)) { - var url = urlProp.GetString(); + var url = urlProp?.GetValue(); if (!string.IsNullOrEmpty(url)) formatted = $"{formatted}"; } - if (run.TryGetProperty("emoji", out var emoji) && emoji.ValueKind == JsonValueKind.Object) + if (run.TryGetPropertyValue("emoji", out var emoji) && emoji is JsonObject emojiObj) { - if (emoji.TryGetProperty("url", out var emojiUrl)) + if (emojiObj.TryGetPropertyValue("url", out var emojiUrl)) { - var src = emojiUrl.GetString(); + var src = emojiUrl?.GetValue(); if (!string.IsNullOrEmpty(src)) formatted = $"\"{text}\""; } @@ -64,10 +77,15 @@ public static class JsonParser return string.Empty; } - - public static List ExtractWebImages(JsonElement element) + + public static List ExtractWebImages(JsonNode? node) { - var thumbnailsArray = element.GetProperty("thumbnail").GetProperty("thumbnails"); - return thumbnailsArray.Deserialize>() ?? []; + if (node == null) + { + return []; + } + + var thumbnailsArray = node["thumbnails"]; + return thumbnailsArray?.Deserialize>() ?? []; } } \ No newline at end of file diff --git a/Manager.YouTube/Parsers/Json/VideoJsonParser.cs b/Manager.YouTube/Parsers/Json/VideoJsonParser.cs index eadcecb..2f17fd9 100644 --- a/Manager.YouTube/Parsers/Json/VideoJsonParser.cs +++ b/Manager.YouTube/Parsers/Json/VideoJsonParser.cs @@ -14,7 +14,7 @@ public static class VideoJsonParser public static Result ParseVideoData(YouTubeVideoData videoData) { - if (videoData.YouTubeInitialData == null || videoData.YouTubeInitialData.Count == 0) + if (videoData.YouTubePlayerData == null) { return ResultError.Fail("No initial video data found!"); } @@ -22,7 +22,7 @@ public static class VideoJsonParser YouTubeVideo? video; try { - video = videoData.YouTubeInitialData.Deserialize(VideoParserOptions); + video = videoData.YouTubePlayerData.Deserialize(VideoParserOptions); } catch (Exception e) { diff --git a/Manager.YouTube/Util/Cipher/CipherManager.cs b/Manager.YouTube/Util/Cipher/CipherManager.cs index 5db1300..3a76d07 100644 --- a/Manager.YouTube/Util/Cipher/CipherManager.cs +++ b/Manager.YouTube/Util/Cipher/CipherManager.cs @@ -39,7 +39,7 @@ public static class CipherManager private static string GetCipherVersion(string relativePlayerUrl) { - var split = relativePlayerUrl.Split('/'); + var split = relativePlayerUrl.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var v = split[2]; var lang = split[4]; return $"{v}_{lang}"; diff --git a/Manager.YouTube/Util/Converters/NumericJsonConverter.cs b/Manager.YouTube/Util/Converters/NumericJsonConverter.cs new file mode 100644 index 0000000..18e672e --- /dev/null +++ b/Manager.YouTube/Util/Converters/NumericJsonConverter.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Manager.YouTube.Util.Converters; + +public class NumericJsonConverter : JsonConverter where T : struct, IConvertible +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + if (reader.TokenType == JsonTokenType.Number) + { + // Direct numeric value + return (T)Convert.ChangeType(reader.GetDouble(), typeof(T)); + } + + if (reader.TokenType == JsonTokenType.String) + { + var str = reader.GetString(); + if (string.IsNullOrWhiteSpace(str)) + throw new JsonException("Empty string cannot be converted to a number."); + + return (T)Convert.ChangeType(str, typeof(T)); + } + + throw new JsonException($"Unexpected token {reader.TokenType} for type {typeof(T)}."); + } + catch (Exception ex) + { + throw new JsonException($"Error converting value to {typeof(T)}.", ex); + } + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteNumberValue(Convert.ToDouble(value)); + } +} \ No newline at end of file diff --git a/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs b/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs index d7f73eb..f6eea63 100644 --- a/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs +++ b/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using DotBased.Logging; using Manager.YouTube.Models; @@ -11,56 +12,74 @@ namespace Manager.YouTube.Util.Converters; public class YouTubeVideoJsonConverter : JsonConverter { private readonly ILogger _logger = LogService.RegisterLogger(); + private readonly JsonSerializerOptions _serializerOptions = new() + { + Converters = { + new NumericJsonConverter(), + new NumericJsonConverter(), + new NumericJsonConverter(), + new NumericJsonConverter(), + new NumericJsonConverter() }, + PropertyNameCaseInsensitive = true + }; public override YouTubeVideo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - using var document = JsonDocument.ParseValue(ref reader); - var root = document.RootElement; + var node = JsonNode.Parse(ref reader); + if (node == null) + { + throw new SerializationException("Failed to parse JSON reader."); + } - var playabilityStatus = root.GetProperty("playabilityStatus"); - var streamingData = root.GetProperty("streamingData"); - var videoDetails = root.GetProperty("videoDetails"); - var playerConfigJson = root.GetProperty("playerConfig"); - var microformat = root.GetProperty("microformat").GetProperty("playerMicroformatRenderer"); + var rootObject = node.AsObject(); - var videoId = videoDetails.GetProperty("videoId").GetString() ?? microformat.GetProperty("externalVideoId").GetString(); + var playabilityStatus = rootObject["playabilityStatus"]; + var streamingDataJson = rootObject["streamingData"]; + var videoDetails = rootObject["videoDetails"]; + var playerConfigJson = rootObject["playerConfig"]; + var microformat = rootObject["microformat"]?["playerMicroformatRenderer"]; + + var videoId = videoDetails?["videoId"]?.GetValue() ?? microformat?["externalVideoId"]?.GetValue(); if (string.IsNullOrEmpty(videoId)) { throw new SerializationException("Failed to get videoId"); } + + var thumbnails = JsonParser.ExtractWebImages(videoDetails?["thumbnail"]); + thumbnails.AddRange(JsonParser.ExtractWebImages(microformat?["thumbnail"])); - var thumbnails = JsonParser.ExtractWebImages(videoDetails.GetProperty("thumbnail")); - thumbnails.AddRange(JsonParser.ExtractWebImages(microformat.GetProperty("thumbnail"))); + var streamingData = streamingDataJson.Deserialize(_serializerOptions); + var playerConfig = ExtractPlayerConfig(playerConfigJson); var video = new YouTubeVideo { VideoId = videoId, - Title = JsonParser.ExtractTextOrHtml(microformat.GetProperty("title")), - Description = JsonParser.ExtractTextOrHtml(microformat.GetProperty("description")), - ViewCount = videoDetails.GetProperty("viewCount").GetInt32(), - LikeCount = videoDetails.GetProperty("likeCount").GetInt32(), - ChannelId = videoDetails.GetProperty("channelId").GetString() ?? "", - Author = JsonParser.ExtractTextOrHtml(videoDetails.GetProperty("author")), - PlayabilityStatus = playabilityStatus.GetProperty("status").GetString() ?? "", - LengthSeconds = videoDetails.GetProperty("lengthSeconds").GetInt32(), - Keywords = videoDetails.GetProperty("keywords").EnumerateArray().Select(v => v.GetString()).Cast().ToArray(), - IsOwnerViewing = videoDetails.GetProperty("isOwnerViewing").GetBoolean(), - AllowRating = videoDetails.GetProperty("allowRating").GetBoolean(), - IsCrawlable = videoDetails.GetProperty("isCrawlable").GetBoolean(), - IsPrivate = videoDetails.GetProperty("isPrivate").GetBoolean(), - IsUnpluggedCorpus = videoDetails.GetProperty("isUnpluggedCorpus").GetBoolean(), - IsLive = videoDetails.GetProperty("isLiveContent").GetBoolean(), - IsFamilySave = microformat.GetProperty("isFamilySave").GetBoolean(), - AvailableCountries = microformat.GetProperty("availableCountries").EnumerateArray().Select(v => v.GetString()).Cast().ToArray(), - IsUnlisted = microformat.GetProperty("isUnlisted").GetBoolean(), - HasYpcMetadata = microformat.GetProperty("hasYpcMetadata").GetBoolean(), - PublishDate = microformat.GetProperty("publishDate").GetDateTime(), - UploadDate = microformat.GetProperty("uploadDate").GetDateTime(), - IsShortsEligible = microformat.GetProperty("isShortsEligible").GetBoolean(), - Category = microformat.GetProperty("category").GetString() ?? "", - StreamingData = streamingData.Deserialize(), + Title = JsonParser.ExtractTextOrHtml(microformat?["title"]), + Description = JsonParser.ExtractTextOrHtml(microformat?["description"]), + ViewCount = long.TryParse(microformat?["viewCount"]?.GetValue(), out var viewCountParsed) ? viewCountParsed : -1, + LikeCount = long.TryParse(microformat?["likeCount"]?.GetValue(), out var likeCountParsed) ? likeCountParsed : -1, + ChannelId = videoDetails?["channelId"]?.GetValue() ?? "", + Author = videoDetails?["author"]?.GetValue() ?? "", + PlayabilityStatus = playabilityStatus?["status"]?.GetValue() ?? "", + LengthSeconds = long.TryParse(videoDetails?["lengthSeconds"]?.GetValue(), out var lengthSecondsParsed) ? lengthSecondsParsed : -1, + Keywords = videoDetails?["keywords"]?.AsArray().Select(v => v?.GetValue() ?? "").ToArray() ?? [], + IsOwnerViewing = videoDetails?["isOwnerViewing"]?.GetValue() ?? false, + AllowRating = videoDetails?["allowRating"]?.GetValue() ?? false, + IsCrawlable = videoDetails?["isCrawlable"]?.GetValue() ?? false, + IsPrivate = videoDetails?["isPrivate"]?.GetValue() ?? false, + IsUnpluggedCorpus = videoDetails?["isUnpluggedCorpus"]?.GetValue() ?? false, + IsLive = videoDetails?["isLiveContent"]?.GetValue() ?? false, + IsFamilySave = microformat?["isFamilySave"]?.GetValue() ?? false, + AvailableCountries = microformat?["availableCountries"]?.AsArray().Select(v => v?.GetValue() ?? "").ToArray() ?? [], + IsUnlisted = microformat?["isUnlisted"]?.GetValue() ?? false, + HasYpcMetadata = microformat?["hasYpcMetadata"]?.GetValue() ?? false, + PublishDate = DateTime.TryParse(microformat?["publishDate"]?.GetValue(), out var parsedPublishDate) ? parsedPublishDate : DateTime.MinValue, + UploadDate = DateTime.TryParse(microformat?["uploadDate"]?.GetValue(), out var parsedUploadDate) ? parsedUploadDate : DateTime.MinValue, + IsShortsEligible = microformat?["isShortsEligible"]?.GetValue() ?? false, + Category = microformat?["category"]?.GetValue() ?? "", + StreamingData = streamingData, Thumbnails = thumbnails, - PlayerConfig = ExtractPlayerConfig(playerConfigJson) + PlayerConfig = playerConfig }; return video; @@ -71,23 +90,25 @@ public class YouTubeVideoJsonConverter : JsonConverter throw new NotImplementedException("Converter only supports reading."); } - private PlayerConfig? ExtractPlayerConfig(JsonElement element) + private PlayerConfig? ExtractPlayerConfig(JsonNode? playerConfigNode) { + if (playerConfigNode == null) + { + return null; + } + try { + var playerConfigObj = playerConfigNode.AsObject(); var playerConfig = new PlayerConfig { - AudioLoudnessDb = element.GetProperty("audioConfig").GetProperty("loudnessDb").GetDouble(), - AudioPerceptualLoudnessDb = element.GetProperty("audioConfig").GetProperty("perceptualLoudnessDb").GetDouble(), - AudioEnablePerFormatLoudness = element.GetProperty("audioConfig").GetProperty("enablePerFormatLoudness") - .GetBoolean(), - MaxBitrate = element.GetProperty("streamSelectionConfig").GetProperty("maxBitrate").GetUInt32(), - MaxReadAheadMediaTimeMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") - .GetProperty("maxReadAheadMediaTimeMs").GetUInt32(), - MinReadAheadMediaTimeMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") - .GetProperty("minReadAheadMediaTimeMs").GetUInt32(), - ReadAheadGrowthRateMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") - .GetProperty("readAheadGrowthRateMs").GetUInt32(), + AudioLoudnessDb = playerConfigObj["audioConfig"]?["loudnessDb"]?.GetValue() ?? 0, + AudioPerceptualLoudnessDb = playerConfigObj["audioConfig"]?["perceptualLoudnessDb"]?.GetValue() ?? 0, + AudioEnablePerFormatLoudness = playerConfigObj["audioConfig"]?["enablePerFormatLoudness"]?.GetValue() ?? false, + MaxBitrate = uint.TryParse(playerConfigObj["streamSelectionConfig"]?["maxBitrate"]?.GetValue(), out var parsedMaxBitrate) ? parsedMaxBitrate : 0, + MaxReadAheadMediaTimeMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["maxReadAheadMediaTimeMs"]?.GetValue() ?? 0, + MinReadAheadMediaTimeMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["minReadAheadMediaTimeMs"]?.GetValue() ?? 0, + ReadAheadGrowthRateMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["readAheadGrowthRateMs"]?.GetValue() ?? 0, }; return playerConfig; } diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index f4c27d2..a5aacf2 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -175,10 +175,10 @@ public sealed class YouTubeClient : IDisposable public async Task RotateCookiesPageAsync(string origin = NetworkService.Origin, int ytPid = 1) { - if (IsAnonymous) + /*if (IsAnonymous) { return ResultError.Fail("Anonymous clients cannot rotate cookies!"); - } + }*/ if (string.IsNullOrWhiteSpace(origin)) { @@ -277,6 +277,12 @@ public sealed class YouTubeClient : IDisposable return stateResult; } + if (State is { LoggedIn: false }) + { + _logger.Warning("Client is not logged in!"); + return ResultError.Fail("Client login failed!"); + } + var cookieRotationResult = await RotateCookiesPageAsync(); return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success(); }