From 9d31e97efbb196bdba6da6b1757f8a01412800f5 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Sun, 20 Aug 2023 22:12:13 +0300 Subject: [PATCH] Replace reflection-based memoization with Lazy.Fody --- YoutubeExplode/Bridge/ChannelPage.cs | 37 +- .../Bridge/ClosedCaptionTrackResponse.cs | 49 +-- YoutubeExplode/Bridge/DashManifest.cs | 122 +++-- YoutubeExplode/Bridge/PlayerResponse.cs | 416 +++++++----------- YoutubeExplode/Bridge/PlayerSource.cs | 176 ++++---- .../Bridge/PlaylistBrowseResponse.cs | 93 ++-- YoutubeExplode/Bridge/PlaylistNextResponse.cs | 78 ++-- YoutubeExplode/Bridge/PlaylistVideoData.cs | 79 ++-- YoutubeExplode/Bridge/SearchResponse.cs | 238 +++++----- YoutubeExplode/Bridge/ThumbnailData.cs | 17 +- YoutubeExplode/Bridge/VideoWatchPage.cs | 98 ++--- YoutubeExplode/FodyWeavers.xml | 3 + YoutubeExplode/FodyWeavers.xsd | 26 ++ YoutubeExplode/Utils/Memo.cs | 31 -- YoutubeExplode/YoutubeExplode.csproj | 1 + 15 files changed, 650 insertions(+), 814 deletions(-) create mode 100644 YoutubeExplode/FodyWeavers.xml create mode 100644 YoutubeExplode/FodyWeavers.xsd delete mode 100644 YoutubeExplode/Utils/Memo.cs diff --git a/YoutubeExplode/Bridge/ChannelPage.cs b/YoutubeExplode/Bridge/ChannelPage.cs index cf89acb5..eabe83e8 100644 --- a/YoutubeExplode/Bridge/ChannelPage.cs +++ b/YoutubeExplode/Bridge/ChannelPage.cs @@ -1,5 +1,6 @@ using System; using AngleSharp.Html.Dom; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -9,27 +10,21 @@ internal partial class ChannelPage { private readonly IHtmlDocument _content; - public string? Url => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:url\"]")? - .GetAttribute("content") - ); - - public string? Id => Memo.Cache(this, () => - Url?.SubstringAfter("channel/", StringComparison.OrdinalIgnoreCase) - ); - - public string? Title => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:title\"]")? - .GetAttribute("content") - ); - - public string? LogoUrl => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:image\"]")? - .GetAttribute("content") - ); + [Lazy] + public string? Url => _content.QuerySelector("meta[property=\"og:url\"]")?.GetAttribute("content"); + + [Lazy] + public string? Id => Url?.SubstringAfter("channel/", StringComparison.OrdinalIgnoreCase); + + [Lazy] + public string? Title => _content + .QuerySelector("meta[property=\"og:title\"]")? + .GetAttribute("content"); + + [Lazy] + public string? LogoUrl => _content + .QuerySelector("meta[property=\"og:image\"]")? + .GetAttribute("content"); public ChannelPage(IHtmlDocument content) => _content = content; } diff --git a/YoutubeExplode/Bridge/ClosedCaptionTrackResponse.cs b/YoutubeExplode/Bridge/ClosedCaptionTrackResponse.cs index cb50da78..024a5049 100644 --- a/YoutubeExplode/Bridge/ClosedCaptionTrackResponse.cs +++ b/YoutubeExplode/Bridge/ClosedCaptionTrackResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,12 +12,11 @@ internal partial class ClosedCaptionTrackResponse { private readonly XElement _content; - public IReadOnlyList Captions => Memo.Cache(this, () => - _content - .Descendants("p") - .Select(x => new CaptionData(x)) - .ToArray() - ); + [Lazy] + public IReadOnlyList Captions => _content + .Descendants("p") + .Select(x => new CaptionData(x)) + .ToArray(); public ClosedCaptionTrackResponse(XElement content) => _content = content; } @@ -27,24 +27,20 @@ public class CaptionData { private readonly XElement _content; - public string? Text => Memo.Cache(this, () => - (string?)_content - ); + [Lazy] + public string? Text => (string?)_content; - public TimeSpan? Offset => Memo.Cache(this, () => - ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds) - ); + [Lazy] + public TimeSpan? Offset => ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds); - public TimeSpan? Duration => Memo.Cache(this, () => - ((double?)_content.Attribute("d"))?.Pipe(TimeSpan.FromMilliseconds) - ); + [Lazy] + public TimeSpan? Duration => ((double?)_content.Attribute("d"))?.Pipe(TimeSpan.FromMilliseconds); - public IReadOnlyList Parts => Memo.Cache(this, () => - _content - .Elements("s") - .Select(x => new PartData(x)) - .ToArray() - ); + [Lazy] + public IReadOnlyList Parts => _content + .Elements("s") + .Select(x => new PartData(x)) + .ToArray(); public CaptionData(XElement content) => _content = content; } @@ -56,15 +52,14 @@ public class PartData { private readonly XElement _content; - public string? Text => Memo.Cache(this, () => - (string?)_content - ); + [Lazy] + public string? Text => (string?)_content; - public TimeSpan? Offset => Memo.Cache(this, () => + [Lazy] + public TimeSpan? Offset => ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds) ?? ((double?)_content.Attribute("ac"))?.Pipe(TimeSpan.FromMilliseconds) ?? - TimeSpan.Zero - ); + TimeSpan.Zero; public PartData(XElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/DashManifest.cs b/YoutubeExplode/Bridge/DashManifest.cs index 0f85c401..fc633340 100644 --- a/YoutubeExplode/Bridge/DashManifest.cs +++ b/YoutubeExplode/Bridge/DashManifest.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text.RegularExpressions; using System.Xml.Linq; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -12,30 +13,29 @@ internal partial class DashManifest { private readonly XElement _content; - public IReadOnlyList Streams => Memo.Cache(this, () => - _content - .Descendants("Representation") - // Skip non-media representations (like "rawcc") - // https://github.com/Tyrrrz/YoutubeExplode/issues/546 - .Where(x => x - .Attribute("id")? - .Value - .All(char.IsDigit) == true - ) - // Skip segmented streams - // https://github.com/Tyrrrz/YoutubeExplode/issues/159 - .Where(x => x - .Descendants("Initialization") - .FirstOrDefault()? - .Attribute("sourceURL")? - .Value - .Contains("sq/") != true - ) - // Skip streams without codecs - .Where(x => !string.IsNullOrWhiteSpace(x.Attribute("codecs")?.Value)) - .Select(x => new StreamData(x)) - .ToArray() - ); + [Lazy] + public IReadOnlyList Streams => _content + .Descendants("Representation") + // Skip non-media representations (like "rawcc") + // https://github.com/Tyrrrz/YoutubeExplode/issues/546 + .Where(x => x + .Attribute("id")? + .Value + .All(char.IsDigit) == true + ) + // Skip segmented streams + // https://github.com/Tyrrrz/YoutubeExplode/issues/159 + .Where(x => x + .Descendants("Initialization") + .FirstOrDefault()? + .Attribute("sourceURL")? + .Value + .Contains("sq/") != true + ) + // Skip streams without codecs + .Where(x => !string.IsNullOrWhiteSpace(x.Attribute("codecs")?.Value)) + .Select(x => new StreamData(x)) + .ToArray(); public DashManifest(XElement content) => _content = content; } @@ -46,13 +46,11 @@ public class StreamData : IStreamData { private readonly XElement _content; - public int? Itag => Memo.Cache(this, () => - (int?)_content.Attribute("id") - ); + [Lazy] + public int? Itag => (int?)_content.Attribute("id"); - public string? Url => Memo.Cache(this, () => - (string?)_content.Element("BaseURL") - ); + [Lazy] + public string? Url => (string?)_content.Element("BaseURL"); // DASH streams don't have signatures public string? Signature => null; @@ -60,54 +58,46 @@ public class StreamData : IStreamData // DASH streams don't have signatures public string? SignatureParameter => null; - public long? ContentLength => Memo.Cache(this, () => + [Lazy] + public long? ContentLength => (long?)_content.Attribute("contentLength") ?? Url? .Pipe(s => Regex.Match(s, @"[/\?]clen[/=](\d+)").Groups[1].Value) .NullIfWhiteSpace()? - .ParseLongOrNull() - ); + .ParseLongOrNull(); - public long? Bitrate => Memo.Cache(this, () => - (long?)_content.Attribute("bandwidth") - ); + [Lazy] + public long? Bitrate => (long?)_content.Attribute("bandwidth"); - public string? Container => Memo.Cache(this, () => - Url? - .Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) - .Pipe(WebUtility.UrlDecode) - ); - - private bool IsAudioOnly => Memo.Cache(this, () => - _content.Element("AudioChannelConfiguration") is not null - ); - - public string? AudioCodec => Memo.Cache(this, () => - IsAudioOnly - ? (string?)_content.Attribute("codecs") - : null - ); - - public string? VideoCodec => Memo.Cache(this, () => - IsAudioOnly - ? null - : (string?)_content.Attribute("codecs") - ); + [Lazy] + public string? Container => Url? + .Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) + .Pipe(WebUtility.UrlDecode); + + [Lazy] + private bool IsAudioOnly => _content.Element("AudioChannelConfiguration") is not null; + + [Lazy] + public string? AudioCodec => IsAudioOnly + ? (string?)_content.Attribute("codecs") + : null; + + [Lazy] + public string? VideoCodec => IsAudioOnly + ? null + : (string?)_content.Attribute("codecs"); public string? VideoQualityLabel => null; - public int? VideoWidth => Memo.Cache(this, () => - (int?)_content.Attribute("width") - ); + [Lazy] + public int? VideoWidth => (int?)_content.Attribute("width"); - public int? VideoHeight => Memo.Cache(this, () => - (int?)_content.Attribute("height") - ); + [Lazy] + public int? VideoHeight => (int?)_content.Attribute("height"); - public int? VideoFramerate => Memo.Cache(this, () => - (int?)_content.Attribute("frameRate") - ); + [Lazy] + public int? VideoFramerate => (int?)_content.Attribute("frameRate"); public StreamData(XElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/PlayerResponse.cs b/YoutubeExplode/Bridge/PlayerResponse.cs index dd010327..8906f1ff 100644 --- a/YoutubeExplode/Bridge/PlayerResponse.cs +++ b/YoutubeExplode/Bridge/PlayerResponse.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -13,105 +14,72 @@ internal partial class PlayerResponse { private readonly JsonElement _content; - private JsonElement? Playability => Memo.Cache(this, () => - _content.GetPropertyOrNull("playabilityStatus") - ); + [Lazy] + private JsonElement? Playability => _content.GetPropertyOrNull("playabilityStatus"); - private string? PlayabilityStatus => Memo.Cache(this, () => - Playability? - .GetPropertyOrNull("status")? - .GetStringOrNull() - ); + [Lazy] + private string? PlayabilityStatus => Playability?.GetPropertyOrNull("status")?.GetStringOrNull(); - public string? PlayabilityError => Memo.Cache(this, () => - Playability? - .GetPropertyOrNull("reason")? - .GetStringOrNull() - ); + [Lazy] + public string? PlayabilityError => Playability?.GetPropertyOrNull("reason")?.GetStringOrNull(); - public bool IsAvailable => Memo.Cache(this, () => + [Lazy] + public bool IsAvailable => !string.Equals(PlayabilityStatus, "error", StringComparison.OrdinalIgnoreCase) && - Details is not null - ); - - public bool IsPlayable => Memo.Cache(this, () => - string.Equals(PlayabilityStatus, "ok", StringComparison.OrdinalIgnoreCase) - ); - - private JsonElement? Details => Memo.Cache(this, () => - _content.GetPropertyOrNull("videoDetails") - ); - - public string? Title => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("title")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("channelId")? - .GetStringOrNull() - ); - - public string? Author => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("author")? - .GetStringOrNull() - ); - - public DateTimeOffset? UploadDate => Memo.Cache(this, () => - _content - .GetPropertyOrNull("microformat")? - .GetPropertyOrNull("playerMicroformatRenderer")? - .GetPropertyOrNull("uploadDate")? - .GetDateTimeOffset() - ); - - public TimeSpan? Duration => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("lengthSeconds")? - .GetStringOrNull()? - .ParseDoubleOrNull()? - .Pipe(TimeSpan.FromSeconds) - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public IReadOnlyList Keywords => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("keywords")? - .EnumerateArrayOrNull()? - .Select(j => j.GetStringOrNull()) - .WhereNotNull() - .ToArray() ?? - - Array.Empty() - ); - - public string? Description => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("shortDescription")? - .GetStringOrNull() - ); - - public long? ViewCount => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("viewCount")? - .GetStringOrNull()? - .ParseLongOrNull() - ); - - public string? PreviewVideoId => Memo.Cache(this, () => + Details is not null; + + [Lazy] + public bool IsPlayable => string.Equals(PlayabilityStatus, "ok", StringComparison.OrdinalIgnoreCase); + + [Lazy] + private JsonElement? Details => _content.GetPropertyOrNull("videoDetails"); + + [Lazy] + public string? Title => Details?.GetPropertyOrNull("title")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => Details?.GetPropertyOrNull("channelId")?.GetStringOrNull(); + + [Lazy] + public string? Author => Details?.GetPropertyOrNull("author")?.GetStringOrNull(); + + [Lazy] + public DateTimeOffset? UploadDate => _content + .GetPropertyOrNull("microformat")? + .GetPropertyOrNull("playerMicroformatRenderer")? + .GetPropertyOrNull("uploadDate")? + .GetDateTimeOffset(); + + [Lazy] + public TimeSpan? Duration => Details? + .GetPropertyOrNull("lengthSeconds")? + .GetStringOrNull()? + .ParseDoubleOrNull()? + .Pipe(TimeSpan.FromSeconds); + + [Lazy] + public IReadOnlyList Thumbnails => Details? + .GetPropertyOrNull("thumbnail")? + .GetPropertyOrNull("thumbnails")? + .EnumerateArrayOrNull()? + .Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); + + public IReadOnlyList Keywords => Details? + .GetPropertyOrNull("keywords")? + .EnumerateArrayOrNull()? + .Select(j => j.GetStringOrNull()) + .WhereNotNull() + .ToArray() ?? Array.Empty(); + + [Lazy] + public string? Description => Details?.GetPropertyOrNull("shortDescription")?.GetStringOrNull(); + + [Lazy] + public long? ViewCount => Details?.GetPropertyOrNull("viewCount")?.GetStringOrNull()?.ParseLongOrNull(); + + [Lazy] + public string? PreviewVideoId => Playability? .GetPropertyOrNull("errorScreen")? .GetPropertyOrNull("playerLegacyDesktopYpcTrailerRenderer")? @@ -140,59 +108,52 @@ Details is not null .Pipe(Convert.FromBase64String) .Pipe(Encoding.UTF8.GetString) .Pipe(s => Regex.Match(s, @"video_id=(.{11})").Groups[1].Value) - .NullIfWhiteSpace() - ); - - private JsonElement? StreamingData => Memo.Cache(this, () => - _content.GetPropertyOrNull("streamingData") - ); - - public string? DashManifestUrl => Memo.Cache(this, () => - StreamingData? - .GetPropertyOrNull("dashManifestUrl")? - .GetStringOrNull() - ); - - public string? HlsManifestUrl => Memo.Cache(this, () => - StreamingData? - .GetPropertyOrNull("hlsManifestUrl")? - .GetStringOrNull() - ); - - public IReadOnlyList Streams => Memo.Cache(this, () => - { - var result = new List(); + .NullIfWhiteSpace(); - var muxedStreams = StreamingData? - .GetPropertyOrNull("formats")? - .EnumerateArrayOrNull()? - .Select(j => new StreamData(j)); + [Lazy] + private JsonElement? StreamingData => _content.GetPropertyOrNull("streamingData"); - if (muxedStreams is not null) - result.AddRange(muxedStreams); + [Lazy] + public string? DashManifestUrl => StreamingData?.GetPropertyOrNull("dashManifestUrl")?.GetStringOrNull(); - var adaptiveStreams = StreamingData? - .GetPropertyOrNull("adaptiveFormats")? - .EnumerateArrayOrNull()? - .Select(j => new StreamData(j)); + [Lazy] + public string? HlsManifestUrl => StreamingData?.GetPropertyOrNull("hlsManifestUrl")?.GetStringOrNull(); - if (adaptiveStreams is not null) - result.AddRange(adaptiveStreams); + [Lazy] + public IReadOnlyList Streams + { + get + { + var result = new List(); - return result; - }); + var muxedStreams = StreamingData? + .GetPropertyOrNull("formats")? + .EnumerateArrayOrNull()? + .Select(j => new StreamData(j)); - public IReadOnlyList ClosedCaptionTracks => Memo.Cache(this, () => - _content - .GetPropertyOrNull("captions")? - .GetPropertyOrNull("playerCaptionsTracklistRenderer")? - .GetPropertyOrNull("captionTracks")? - .EnumerateArrayOrNull()? - .Select(j => new ClosedCaptionTrackData(j)) - .ToArray() ?? + if (muxedStreams is not null) + result.AddRange(muxedStreams); - Array.Empty() - ); + var adaptiveStreams = StreamingData? + .GetPropertyOrNull("adaptiveFormats")? + .EnumerateArrayOrNull()? + .Select(j => new StreamData(j)); + + if (adaptiveStreams is not null) + result.AddRange(adaptiveStreams); + + return result; + } + } + + [Lazy] + public IReadOnlyList ClosedCaptionTracks => _content + .GetPropertyOrNull("captions")? + .GetPropertyOrNull("playerCaptionsTracklistRenderer")? + .GetPropertyOrNull("captionTracks")? + .EnumerateArrayOrNull()? + .Select(j => new ClosedCaptionTrackData(j)) + .ToArray() ?? Array.Empty(); public PlayerResponse(JsonElement content) => _content = content; } @@ -203,19 +164,14 @@ public class ClosedCaptionTrackData { private readonly JsonElement _content; - public string? Url => Memo.Cache(this, () => - _content - .GetPropertyOrNull("baseUrl")? - .GetStringOrNull() - ); + [Lazy] + public string? Url => _content.GetPropertyOrNull("baseUrl")?.GetStringOrNull(); - public string? LanguageCode => Memo.Cache(this, () => - _content - .GetPropertyOrNull("languageCode")? - .GetStringOrNull() - ); + [Lazy] + public string? LanguageCode => _content.GetPropertyOrNull("languageCode")?.GetStringOrNull(); - public string? LanguageName => Memo.Cache(this, () => + [Lazy] + public string? LanguageName => _content .GetPropertyOrNull("name")? .GetPropertyOrNull("simpleText")? @@ -227,15 +183,13 @@ public class ClosedCaptionTrackData .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - public bool IsAutoGenerated => Memo.Cache(this, () => - _content - .GetPropertyOrNull("vssId")? - .GetStringOrNull()? - .StartsWith("a.", StringComparison.OrdinalIgnoreCase) ?? false - ); + [Lazy] + public bool IsAutoGenerated => _content + .GetPropertyOrNull("vssId")? + .GetStringOrNull()? + .StartsWith("a.", StringComparison.OrdinalIgnoreCase) ?? false; public ClosedCaptionTrackData(JsonElement content) => _content = content; } @@ -247,13 +201,11 @@ public class StreamData : IStreamData { private readonly JsonElement _content; - public int? Itag => Memo.Cache(this, () => - _content - .GetPropertyOrNull("itag")? - .GetInt32OrNull() - ); + [Lazy] + public int? Itag => _content.GetPropertyOrNull("itag")?.GetInt32OrNull(); - private IReadOnlyDictionary? CipherData => Memo.Cache(this, () => + [Lazy] + private IReadOnlyDictionary? CipherData => _content .GetPropertyOrNull("cipher")? .GetStringOrNull()? @@ -262,26 +214,21 @@ public class StreamData : IStreamData _content .GetPropertyOrNull("signatureCipher")? .GetStringOrNull()? - .Pipe(UrlEx.GetQueryParameters) - ); + .Pipe(UrlEx.GetQueryParameters); - public string? Url => Memo.Cache(this, () => - _content - .GetPropertyOrNull("url")? - .GetStringOrNull() ?? + [Lazy] + public string? Url => + _content.GetPropertyOrNull("url")?.GetStringOrNull() ?? + CipherData?.GetValueOrDefault("url"); - CipherData?.GetValueOrDefault("url") - ); + [Lazy] + public string? Signature => CipherData?.GetValueOrDefault("s"); - public string? Signature => Memo.Cache(this, () => - CipherData?.GetValueOrDefault("s") - ); + [Lazy] + public string? SignatureParameter => CipherData?.GetValueOrDefault("sp"); - public string? SignatureParameter => Memo.Cache(this, () => - CipherData?.GetValueOrDefault("sp") - ); - - public long? ContentLength => Memo.Cache(this, () => + [Lazy] + public long? ContentLength => _content .GetPropertyOrNull("contentLength")? .GetStringOrNull()? @@ -290,79 +237,56 @@ public class StreamData : IStreamData Url? .Pipe(s => UrlEx.TryGetQueryParameterValue(s, "clen"))? .NullIfWhiteSpace()? - .ParseLongOrNull() - ); + .ParseLongOrNull(); - public long? Bitrate => Memo.Cache(this, () => - _content - .GetPropertyOrNull("bitrate")? - .GetInt64OrNull() - ); + [Lazy] + public long? Bitrate => _content.GetPropertyOrNull("bitrate")?.GetInt64OrNull(); - private string? MimeType => Memo.Cache(this, () => - _content - .GetPropertyOrNull("mimeType")? - .GetStringOrNull() - ); - - public string? Container => Memo.Cache(this, () => - MimeType? - .SubstringUntil(";") - .SubstringAfter("/") - ); - - private bool IsAudioOnly => Memo.Cache(this, () => - MimeType?.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ?? false - ); - - public string? Codecs => Memo.Cache(this, () => - MimeType? - .SubstringAfter("codecs=\"") - .SubstringUntil("\"") - ); - - public string? AudioCodec => Memo.Cache(this, () => - IsAudioOnly - ? Codecs - : Codecs?.SubstringAfter(", ").NullIfWhiteSpace() - ); - - public string? VideoCodec => Memo.Cache(this, () => + [Lazy] + private string? MimeType => _content.GetPropertyOrNull("mimeType")?.GetStringOrNull(); + + [Lazy] + public string? Container => MimeType?.SubstringUntil(";").SubstringAfter("/"); + + [Lazy] + private bool IsAudioOnly => MimeType?.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ?? false; + + [Lazy] + public string? Codecs => MimeType?.SubstringAfter("codecs=\"").SubstringUntil("\""); + + [Lazy] + public string? AudioCodec => IsAudioOnly + ? Codecs + : Codecs?.SubstringAfter(", ").NullIfWhiteSpace(); + + [Lazy] + public string? VideoCodec { - var codec = IsAudioOnly - ? null - : Codecs?.SubstringUntil(", ").NullIfWhiteSpace(); + get + { + var codec = IsAudioOnly + ? null + : Codecs?.SubstringUntil(", ").NullIfWhiteSpace(); - // "unknown" value indicates av01 codec - if (string.Equals(codec, "unknown", StringComparison.OrdinalIgnoreCase)) - return "av01.0.05M.08"; + // "unknown" value indicates av01 codec + if (string.Equals(codec, "unknown", StringComparison.OrdinalIgnoreCase)) + return "av01.0.05M.08"; - return codec; - }); + return codec; + } + } - public string? VideoQualityLabel => Memo.Cache(this, () => - _content - .GetPropertyOrNull("qualityLabel")? - .GetStringOrNull() - ); + [Lazy] + public string? VideoQualityLabel => _content.GetPropertyOrNull("qualityLabel")?.GetStringOrNull(); - public int? VideoWidth => Memo.Cache(this, () => - _content - .GetPropertyOrNull("width")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoWidth => _content.GetPropertyOrNull("width")?.GetInt32OrNull(); - public int? VideoHeight => Memo.Cache(this, () => - _content - .GetPropertyOrNull("height")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoHeight => _content.GetPropertyOrNull("height")?.GetInt32OrNull(); - public int? VideoFramerate => Memo.Cache(this, () => - _content - .GetPropertyOrNull("fps")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoFramerate => _content.GetPropertyOrNull("fps")?.GetInt32OrNull(); public StreamData(JsonElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/PlayerSource.cs b/YoutubeExplode/Bridge/PlayerSource.cs index 6f905f41..de42a4fe 100644 --- a/YoutubeExplode/Bridge/PlayerSource.cs +++ b/YoutubeExplode/Bridge/PlayerSource.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using Lazy; using YoutubeExplode.Bridge.Cipher; -using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -11,96 +11,100 @@ internal partial class PlayerSource { private readonly string _content; - public CipherManifest? CipherManifest => Memo.Cache(this, () => + [Lazy] + public CipherManifest? CipherManifest { - // Extract the signature timestamp - var signatureTimestamp = Regex.Match(_content, @"(?:signatureTimestamp|sts):(\d{5})") - .Groups[1] - .Value - .NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(signatureTimestamp)) - return null; - - // Find where the player calls the cipher functions - var cipherCallsite = Regex.Match( - _content, - """ - [$_\w]+=function\([$_\w]+\){([$_\w]+)=\1\.split\(['"]{2}\);.*?return \1\.join\(['"]{2}\)} - """, - RegexOptions.Singleline - ).Groups[0].Value.NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(cipherCallsite)) - return null; - - // Find the object that defines the cipher functions - var cipherContainerName = Regex.Match(cipherCallsite, @"([$_\w]+)\.[$_\w]+\([$_\w]+,\d+\);") - .Groups[1] - .Value; - - if (string.IsNullOrWhiteSpace(cipherContainerName)) - return null; - - // Find the definition of the cipher functions - var cipherDefinition = Regex.Match( - _content, - $$""" - var {{Regex.Escape(cipherContainerName)}}={.*?}; - """, - RegexOptions.Singleline - ).Groups[0].Value.NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(cipherDefinition)) - return null; - - // Identify the swap cipher function - var swapFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?%[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - // Identify the splice cipher function - var spliceFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?splice[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - // Identify the reverse cipher function - var reverseFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+\){+[^}]*?reverse[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - var operations = new List(); - - foreach (var statement in cipherCallsite.Split(';')) + get { - var calledFuncName = Regex.Match(statement, @"[$_\w]+\.([$_\w]+)\([$_\w]+,\d+\)").Groups[1].Value; - if (string.IsNullOrWhiteSpace(calledFuncName)) - continue; - - if (string.Equals(calledFuncName, swapFuncName, StringComparison.Ordinal)) - { - var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); - operations.Add(new SwapCipherOperation(index)); - } - else if (string.Equals(calledFuncName, spliceFuncName, StringComparison.Ordinal)) + // Extract the signature timestamp + var signatureTimestamp = Regex.Match(_content, @"(?:signatureTimestamp|sts):(\d{5})") + .Groups[1] + .Value + .NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(signatureTimestamp)) + return null; + + // Find where the player calls the cipher functions + var cipherCallsite = Regex.Match( + _content, + """ + [$_\w]+=function\([$_\w]+\){([$_\w]+)=\1\.split\(['"]{2}\);.*?return \1\.join\(['"]{2}\)} + """, + RegexOptions.Singleline + ).Groups[0].Value.NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(cipherCallsite)) + return null; + + // Find the object that defines the cipher functions + var cipherContainerName = Regex.Match(cipherCallsite, @"([$_\w]+)\.[$_\w]+\([$_\w]+,\d+\);") + .Groups[1] + .Value; + + if (string.IsNullOrWhiteSpace(cipherContainerName)) + return null; + + // Find the definition of the cipher functions + var cipherDefinition = Regex.Match( + _content, + $$""" + var {{Regex.Escape(cipherContainerName)}}={.*?}; + """, + RegexOptions.Singleline + ).Groups[0].Value.NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(cipherDefinition)) + return null; + + // Identify the swap cipher function + var swapFuncName = Regex.Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?%[^}]*?}", + RegexOptions.Singleline + ).Groups[1].Value.NullIfWhiteSpace(); + + // Identify the splice cipher function + var spliceFuncName = Regex.Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?splice[^}]*?}", + RegexOptions.Singleline + ).Groups[1].Value.NullIfWhiteSpace(); + + // Identify the reverse cipher function + var reverseFuncName = Regex.Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+\){+[^}]*?reverse[^}]*?}", + RegexOptions.Singleline + ).Groups[1].Value.NullIfWhiteSpace(); + + var operations = new List(); + + foreach (var statement in cipherCallsite.Split(';')) { - var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); - operations.Add(new SpliceCipherOperation(index)); + var calledFuncName = Regex.Match(statement, @"[$_\w]+\.([$_\w]+)\([$_\w]+,\d+\)").Groups[1].Value; + if (string.IsNullOrWhiteSpace(calledFuncName)) + continue; + + if (string.Equals(calledFuncName, swapFuncName, StringComparison.Ordinal)) + { + var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); + operations.Add(new SwapCipherOperation(index)); + } + else if (string.Equals(calledFuncName, spliceFuncName, StringComparison.Ordinal)) + { + var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); + operations.Add(new SpliceCipherOperation(index)); + } + else if (string.Equals(calledFuncName, reverseFuncName, StringComparison.Ordinal)) + { + operations.Add(new ReverseCipherOperation()); + } } - else if (string.Equals(calledFuncName, reverseFuncName, StringComparison.Ordinal)) - { - operations.Add(new ReverseCipherOperation()); - } - } - return new CipherManifest(signatureTimestamp, operations); - }); + return new CipherManifest(signatureTimestamp, operations); + } + } public PlayerSource(string content) => _content = content; } diff --git a/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs b/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs index 72c5cb67..041d3156 100644 --- a/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs +++ b/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,32 +12,29 @@ internal partial class PlaylistBrowseResponse : IPlaylistData { private readonly JsonElement _content; - private JsonElement? Sidebar => Memo.Cache(this, () => - _content - .GetPropertyOrNull("sidebar")? - .GetPropertyOrNull("playlistSidebarRenderer")? - .GetPropertyOrNull("items") - ); - - private JsonElement? SidebarPrimary => Memo.Cache(this, () => - Sidebar? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0)? - .GetPropertyOrNull("playlistSidebarPrimaryInfoRenderer") - ); - - private JsonElement? SidebarSecondary => Memo.Cache(this, () => - Sidebar? - .EnumerateArrayOrNull()? - .ElementAtOrNull(1)? - .GetPropertyOrNull("playlistSidebarSecondaryInfoRenderer") - ); - - public bool IsAvailable => Memo.Cache(this, () => - Sidebar is not null - ); - - public string? Title => Memo.Cache(this, () => + [Lazy] + private JsonElement? Sidebar => _content + .GetPropertyOrNull("sidebar")? + .GetPropertyOrNull("playlistSidebarRenderer")? + .GetPropertyOrNull("items"); + + [Lazy] + private JsonElement? SidebarPrimary => Sidebar? + .EnumerateArrayOrNull()? + .ElementAtOrNull(0)? + .GetPropertyOrNull("playlistSidebarPrimaryInfoRenderer"); + + [Lazy] + private JsonElement? SidebarSecondary => Sidebar? + .EnumerateArrayOrNull()? + .ElementAtOrNull(1)? + .GetPropertyOrNull("playlistSidebarSecondaryInfoRenderer"); + + [Lazy] + public bool IsAvailable => Sidebar is not null; + + [Lazy] + public string? Title => SidebarPrimary? .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -48,16 +46,15 @@ Sidebar is not null .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - private JsonElement? AuthorDetails => Memo.Cache(this, () => - SidebarSecondary? - .GetPropertyOrNull("videoOwner")? - .GetPropertyOrNull("videoOwnerRenderer") - ); + [Lazy] + private JsonElement? AuthorDetails => SidebarSecondary? + .GetPropertyOrNull("videoOwner")? + .GetPropertyOrNull("videoOwnerRenderer"); - public string? Author => Memo.Cache(this, () => + [Lazy] + public string? Author => AuthorDetails? .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -69,18 +66,17 @@ Sidebar is not null .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); + [Lazy] + public string? ChannelId => AuthorDetails? + .GetPropertyOrNull("navigationEndpoint")? + .GetPropertyOrNull("browseEndpoint")? + .GetPropertyOrNull("browseId")? + .GetStringOrNull(); - public string? Description => Memo.Cache(this, () => + [Lazy] + public string? Description => SidebarPrimary? .GetPropertyOrNull("description")? .GetPropertyOrNull("simpleText")? @@ -92,10 +88,10 @@ Sidebar is not null .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - public IReadOnlyList Thumbnails => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList Thumbnails => SidebarPrimary? .GetPropertyOrNull("thumbnailRenderer")? .GetPropertyOrNull("playlistVideoThumbnailRenderer")? @@ -114,8 +110,7 @@ Sidebar is not null .Select(j => new ThumbnailData(j)) .ToArray() ?? - Array.Empty() - ); + Array.Empty(); public PlaylistBrowseResponse(JsonElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/PlaylistNextResponse.cs b/YoutubeExplode/Bridge/PlaylistNextResponse.cs index b4e36d6c..09ae9e6c 100644 --- a/YoutubeExplode/Bridge/PlaylistNextResponse.cs +++ b/YoutubeExplode/Bridge/PlaylistNextResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,61 +12,48 @@ internal partial class PlaylistNextResponse : IPlaylistData { private readonly JsonElement _content; - private JsonElement? ContentRoot => Memo.Cache(this, () => - _content - .GetPropertyOrNull("contents")? - .GetPropertyOrNull("twoColumnWatchNextResults")? - .GetPropertyOrNull("playlist")? - .GetPropertyOrNull("playlist") - ); + [Lazy] + private JsonElement? ContentRoot => _content + .GetPropertyOrNull("contents")? + .GetPropertyOrNull("twoColumnWatchNextResults")? + .GetPropertyOrNull("playlist")? + .GetPropertyOrNull("playlist"); - public bool IsAvailable => Memo.Cache(this, () => - ContentRoot is not null - ); + [Lazy] + public bool IsAvailable => ContentRoot is not null; - public string? Title => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("title")? - .GetStringOrNull() - ); + [Lazy] + public string? Title => ContentRoot?.GetPropertyOrNull("title")?.GetStringOrNull(); - public string? Author => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("ownerName")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() - ); + [Lazy] + public string? Author => ContentRoot? + .GetPropertyOrNull("ownerName")? + .GetPropertyOrNull("simpleText")? + .GetStringOrNull(); public string? ChannelId => null; public string? Description => null; - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - Videos - .FirstOrDefault()? - .Thumbnails ?? + [Lazy] + public IReadOnlyList Thumbnails => + Videos.FirstOrDefault()?.Thumbnails ?? + Array.Empty(); - Array.Empty() - ); + [Lazy] + public IReadOnlyList Videos => ContentRoot? + .GetPropertyOrNull("contents")? + .EnumerateArrayOrNull()? + .Select(j => j.GetPropertyOrNull("playlistPanelVideoRenderer")) + .WhereNotNull() + .Select(j => new PlaylistVideoData(j)) + .ToArray() ?? Array.Empty(); - public IReadOnlyList Videos => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("contents")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("playlistPanelVideoRenderer")) - .WhereNotNull() - .Select(j => new PlaylistVideoData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public string? VisitorData => Memo.Cache(this, () => - _content - .GetPropertyOrNull("responseContext")? - .GetPropertyOrNull("visitorData")? - .GetStringOrNull() - ); + [Lazy] + public string? VisitorData => _content + .GetPropertyOrNull("responseContext")? + .GetPropertyOrNull("visitorData")? + .GetStringOrNull(); public PlaylistNextResponse(JsonElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/PlaylistVideoData.cs b/YoutubeExplode/Bridge/PlaylistVideoData.cs index e5a1d0cf..375ef29a 100644 --- a/YoutubeExplode/Bridge/PlaylistVideoData.cs +++ b/YoutubeExplode/Bridge/PlaylistVideoData.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using YoutubeExplode.Utils; +using Lazy; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -11,21 +11,18 @@ internal class PlaylistVideoData { private readonly JsonElement _content; - public int? Index => Memo.Cache(this, () => - _content - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("watchEndpoint")? - .GetPropertyOrNull("index")? - .GetInt32OrNull() - ); + [Lazy] + public int? Index => _content + .GetPropertyOrNull("navigationEndpoint")? + .GetPropertyOrNull("watchEndpoint")? + .GetPropertyOrNull("index")? + .GetInt32OrNull(); - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("videoId")? - .GetStringOrNull() - ); + [Lazy] + public string? Id => _content.GetPropertyOrNull("videoId")?.GetStringOrNull(); - public string? Title => Memo.Cache(this, () => + [Lazy] + public string? Title => _content .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -37,10 +34,10 @@ internal class PlaylistVideoData .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - private JsonElement? AuthorDetails => Memo.Cache(this, () => + [Lazy] + private JsonElement? AuthorDetails => _content .GetPropertyOrNull("longBylineText")? .GetPropertyOrNull("runs")? @@ -51,24 +48,20 @@ internal class PlaylistVideoData .GetPropertyOrNull("shortBylineText")? .GetPropertyOrNull("runs")? .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); + .ElementAtOrNull(0); - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); + [Lazy] + public string? ChannelId => AuthorDetails? + .GetPropertyOrNull("navigationEndpoint")? + .GetPropertyOrNull("browseEndpoint")? + .GetPropertyOrNull("browseId")? + .GetStringOrNull(); - public TimeSpan? Duration => Memo.Cache(this, () => + [Lazy] + public TimeSpan? Duration => _content .GetPropertyOrNull("lengthSeconds")? .GetStringOrNull()? @@ -88,19 +81,15 @@ internal class PlaylistVideoData .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() .ConcatToString() - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }); + + [Lazy] + public IReadOnlyList Thumbnails => _content + .GetPropertyOrNull("thumbnail")? + .GetPropertyOrNull("thumbnails")? + .EnumerateArrayOrNull()? + .Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public PlaylistVideoData(JsonElement content) => _content = content; } \ No newline at end of file diff --git a/YoutubeExplode/Bridge/SearchResponse.cs b/YoutubeExplode/Bridge/SearchResponse.cs index ff5702d8..42270f52 100644 --- a/YoutubeExplode/Bridge/SearchResponse.cs +++ b/YoutubeExplode/Bridge/SearchResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -14,45 +15,35 @@ internal partial class SearchResponse // Search response is incredibly inconsistent (with at least 5 variations), // so we employ descendant searching, which is inefficient but resilient. - private JsonElement? ContentRoot => Memo.Cache(this, () => + [Lazy] + private JsonElement? ContentRoot => _content.GetPropertyOrNull("contents") ?? - _content.GetPropertyOrNull("onResponseReceivedCommands") - ); - - public IReadOnlyList Videos => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("videoRenderer") - .Select(j => new VideoData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public IReadOnlyList Playlists => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("playlistRenderer") - .Select(j => new PlaylistData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public IReadOnlyList Channels => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("channelRenderer") - .Select(j => new ChannelData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public string? ContinuationToken => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("continuationCommand") - .FirstOrNull()? - .GetPropertyOrNull("token")? - .GetStringOrNull() - ); + _content.GetPropertyOrNull("onResponseReceivedCommands"); + + [Lazy] + public IReadOnlyList Videos => ContentRoot? + .EnumerateDescendantProperties("videoRenderer") + .Select(j => new VideoData(j)) + .ToArray() ?? Array.Empty(); + + [Lazy] + public IReadOnlyList Playlists => ContentRoot? + .EnumerateDescendantProperties("playlistRenderer") + .Select(j => new PlaylistData(j)) + .ToArray() ?? Array.Empty(); + + [Lazy] + public IReadOnlyList Channels => ContentRoot? + .EnumerateDescendantProperties("channelRenderer") + .Select(j => new ChannelData(j)) + .ToArray() ?? Array.Empty(); + + [Lazy] + public string? ContinuationToken => ContentRoot? + .EnumerateDescendantProperties("continuationCommand") + .FirstOrNull()? + .GetPropertyOrNull("token")? + .GetStringOrNull(); public SearchResponse(JsonElement content) => _content = content; } @@ -63,13 +54,11 @@ internal class VideoData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("videoId")? - .GetStringOrNull() - ); + [Lazy] + public string? Id => _content.GetPropertyOrNull("videoId")?.GetStringOrNull(); - public string? Title => Memo.Cache(this, () => + [Lazy] + public string? Title => _content .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -81,10 +70,10 @@ internal class VideoData .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - private JsonElement? AuthorDetails => Memo.Cache(this, () => + [Lazy] + private JsonElement? AuthorDetails => _content .GetPropertyOrNull("longBylineText")? .GetPropertyOrNull("runs")? @@ -95,24 +84,20 @@ internal class VideoData .GetPropertyOrNull("shortBylineText")? .GetPropertyOrNull("runs")? .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public TimeSpan? Duration => Memo.Cache(this, () => + .ElementAtOrNull(0); + + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => AuthorDetails? + .GetPropertyOrNull("navigationEndpoint")? + .GetPropertyOrNull("browseEndpoint")? + .GetPropertyOrNull("browseId")? + .GetStringOrNull(); + + [Lazy] + public TimeSpan? Duration => _content .GetPropertyOrNull("lengthText")? .GetPropertyOrNull("simpleText")? @@ -126,19 +111,15 @@ internal class VideoData .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() .ConcatToString() - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? + .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }); - Array.Empty() - ); + [Lazy] + public IReadOnlyList Thumbnails => _content + .GetPropertyOrNull("thumbnail")? + .GetPropertyOrNull("thumbnails")? + .EnumerateArrayOrNull()? + .Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public VideoData(JsonElement content) => _content = content; } @@ -150,13 +131,11 @@ public class PlaylistData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("playlistId")? - .GetStringOrNull() - ); + [Lazy] + public string? Id => _content.GetPropertyOrNull("playlistId")?.GetStringOrNull(); - public string? Title => Memo.Cache(this, () => + [Lazy] + public string? Title => _content .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -168,41 +147,32 @@ public class PlaylistData .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - private JsonElement? AuthorDetails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("longBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("thumbnails")? - .EnumerateDescendantProperties("thumbnails") - .SelectMany(j => j.EnumerateArrayOrEmpty()) - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ConcatToString(); + + [Lazy] + private JsonElement? AuthorDetails => _content + .GetPropertyOrNull("longBylineText")? + .GetPropertyOrNull("runs")? + .EnumerateArrayOrNull()? + .ElementAtOrNull(0); + + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => AuthorDetails? + .GetPropertyOrNull("navigationEndpoint")? + .GetPropertyOrNull("browseEndpoint")? + .GetPropertyOrNull("browseId")? + .GetStringOrNull(); + + [Lazy] + public IReadOnlyList Thumbnails => _content + .GetPropertyOrNull("thumbnails")? + .EnumerateDescendantProperties("thumbnails") + .SelectMany(j => j.EnumerateArrayOrEmpty()) + .Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public PlaylistData(JsonElement content) => _content = content; } @@ -214,13 +184,11 @@ public class ChannelData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("channelId")? - .GetStringOrNull() - ); + [Lazy] + public string? Id => _content.GetPropertyOrNull("channelId")?.GetStringOrNull(); - public string? Title => Memo.Cache(this, () => + [Lazy] + public string? Title => _content .GetPropertyOrNull("title")? .GetPropertyOrNull("simpleText")? @@ -232,19 +200,15 @@ public class ChannelData .EnumerateArrayOrNull()? .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ConcatToString(); + + [Lazy] + public IReadOnlyList Thumbnails => _content + .GetPropertyOrNull("thumbnail")? + .GetPropertyOrNull("thumbnails")? + .EnumerateArrayOrNull()? + .Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public ChannelData(JsonElement content) => _content = content; } diff --git a/YoutubeExplode/Bridge/ThumbnailData.cs b/YoutubeExplode/Bridge/ThumbnailData.cs index aceda254..b68ea6c6 100644 --- a/YoutubeExplode/Bridge/ThumbnailData.cs +++ b/YoutubeExplode/Bridge/ThumbnailData.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using YoutubeExplode.Utils; +using Lazy; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -10,15 +10,12 @@ internal class ThumbnailData public ThumbnailData(JsonElement content) => _content = content; - public string? Url => Memo.Cache(this, () => - _content.GetPropertyOrNull("url")?.GetStringOrNull() - ); + [Lazy] + public string? Url => _content.GetPropertyOrNull("url")?.GetStringOrNull(); - public int? Width => Memo.Cache(this, () => - _content.GetPropertyOrNull("width")?.GetInt32OrNull() - ); + [Lazy] + public int? Width => _content.GetPropertyOrNull("width")?.GetInt32OrNull(); - public int? Height => Memo.Cache(this, () => - _content.GetPropertyOrNull("height")?.GetInt32OrNull() - ); + [Lazy] + public int? Height => _content.GetPropertyOrNull("height")?.GetInt32OrNull(); } \ No newline at end of file diff --git a/YoutubeExplode/Bridge/VideoWatchPage.cs b/YoutubeExplode/Bridge/VideoWatchPage.cs index 89dc2b9d..df9413d0 100644 --- a/YoutubeExplode/Bridge/VideoWatchPage.cs +++ b/YoutubeExplode/Bridge/VideoWatchPage.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using AngleSharp.Dom; using AngleSharp.Html.Dom; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -13,60 +14,56 @@ internal partial class VideoWatchPage { private readonly IHtmlDocument _content; - public bool IsAvailable => Memo.Cache(this, () => - _content.QuerySelector("meta[property=\"og:url\"]") is not null - ); + [Lazy] + public bool IsAvailable => _content.QuerySelector("meta[property=\"og:url\"]") is not null; - public DateTimeOffset? UploadDate => Memo.Cache(this, () => - _content - .QuerySelector("meta[itemprop=\"datePublished\"]")? - .GetAttribute("content")? - .NullIfWhiteSpace()? - .ParseDateTimeOffsetOrNull(new[] { @"yyyy-MM-dd" }) - ); + [Lazy] + public DateTimeOffset? UploadDate => _content + .QuerySelector("meta[itemprop=\"datePublished\"]")? + .GetAttribute("content")? + .NullIfWhiteSpace()? + .ParseDateTimeOffsetOrNull(new[] { @"yyyy-MM-dd" }); - public long? LikeCount => Memo.Cache(this, () => - _content - .Source - .Text - .Pipe(s => Regex.Match( - s, - """ - "label"\s*:\s*"([\d,\.]+) likes" - """ - ).Groups[1].Value) - .NullIfWhiteSpace()? - .StripNonDigit() - .ParseLongOrNull() - ); + [Lazy] + public long? LikeCount => _content + .Source + .Text + .Pipe(s => Regex.Match( + s, + """ + "label"\s*:\s*"([\d,\.]+) likes" + """ + ).Groups[1].Value) + .NullIfWhiteSpace()? + .StripNonDigit() + .ParseLongOrNull(); - public long? DislikeCount => Memo.Cache(this, () => - _content - .Source - .Text - .Pipe(s => Regex.Match( - s, - """ - "label"\s*:\s*"([\d,\.]+) dislikes" - """ - ).Groups[1].Value) - .NullIfWhiteSpace()? - .StripNonDigit() - .ParseLongOrNull() - ); + [Lazy] + public long? DislikeCount => _content + .Source + .Text + .Pipe(s => Regex.Match( + s, + """ + "label"\s*:\s*"([\d,\.]+) dislikes" + """ + ).Groups[1].Value) + .NullIfWhiteSpace()? + .StripNonDigit() + .ParseLongOrNull(); - private JsonElement? PlayerConfig => Memo.Cache(this, () => - _content - .GetElementsByTagName("script") - .Select(e => e.Text()) - .Select(s => Regex.Match(s, @"ytplayer\.config\s*=\s*(\{.*\})").Groups[1].Value) - .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))? - .NullIfWhiteSpace()? - .Pipe(Json.Extract) - .Pipe(Json.TryParse) - ); + [Lazy] + private JsonElement? PlayerConfig => _content + .GetElementsByTagName("script") + .Select(e => e.Text()) + .Select(s => Regex.Match(s, @"ytplayer\.config\s*=\s*(\{.*\})").Groups[1].Value) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))? + .NullIfWhiteSpace()? + .Pipe(Json.Extract) + .Pipe(Json.TryParse); - public PlayerResponse? PlayerResponse => Memo.Cache(this, () => + [Lazy] + public PlayerResponse? PlayerResponse => _content .GetElementsByTagName("script") .Select(e => e.Text()) @@ -82,8 +79,7 @@ internal partial class VideoWatchPage .GetPropertyOrNull("player_response")? .GetStringOrNull()? .Pipe(Json.TryParse)? - .Pipe(j => new PlayerResponse(j)) - ); + .Pipe(j => new PlayerResponse(j)); public VideoWatchPage(IHtmlDocument content) => _content = content; } diff --git a/YoutubeExplode/FodyWeavers.xml b/YoutubeExplode/FodyWeavers.xml new file mode 100644 index 00000000..6ef70586 --- /dev/null +++ b/YoutubeExplode/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/YoutubeExplode/FodyWeavers.xsd b/YoutubeExplode/FodyWeavers.xsd new file mode 100644 index 00000000..fe819e8e --- /dev/null +++ b/YoutubeExplode/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/YoutubeExplode/Utils/Memo.cs b/YoutubeExplode/Utils/Memo.cs deleted file mode 100644 index c81d25bf..00000000 --- a/YoutubeExplode/Utils/Memo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace YoutubeExplode.Utils; - -// Helper utility used to cache the result of a function -internal static class Memo -{ - private static class For - { - private static readonly ConditionalWeakTable> CachesByOwner = new(); - - public static Dictionary GetCache(object owner) => - CachesByOwner.GetOrCreateValue(owner); - } - - public static T Cache(object owner, Func getValue) - { - var cache = For.GetCache(owner); - var key = getValue.Method.GetHashCode(); - - if (cache.TryGetValue(key, out var cachedValue)) - return cachedValue; - - var value = getValue(); - cache[key] = value; - - return value; - } -} \ No newline at end of file diff --git a/YoutubeExplode/YoutubeExplode.csproj b/YoutubeExplode/YoutubeExplode.csproj index 0f111fb8..84b0dcf7 100644 --- a/YoutubeExplode/YoutubeExplode.csproj +++ b/YoutubeExplode/YoutubeExplode.csproj @@ -20,6 +20,7 @@ +