Skip to content

Commit

Permalink
The great bug-fixening (#21)
Browse files Browse the repository at this point in the history
- Fix HTML renderers to escape HTML tags
- Implement MovieRenderer
- Implement CompactMovieRenderer
- Fix the video cache breaking for videos longer than 6 hours
- Implement chapters
  • Loading branch information
kuylar authored Jun 11, 2023
1 parent f8699d7 commit 0b1d52a
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 19 deletions.
32 changes: 26 additions & 6 deletions InnerTube.Tests/PlayerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public void FailPlayer(string videoId, bool contentCheckOk, bool includeHls)
[TestCase("t6cZn-Fvwa0", Description = "Video with comments disabled")]
[TestCase("jPhJbKBuNnA", Description = "Video with watchEndpoint in attributedDescription")]
[TestCase("UoBFuLMlDkw", Description = "Video with more special stuff in attributedDescription")]
[TestCase("llrBX6FpMpM", Description = "compactMovieRenderer")]
[TestCase("jUUe6TuRlgU", Description = "Chapters")]
public async Task GetVideoNext(string videoId)
{
InnerTubeNextResponse next = await _innerTube.GetVideoAsync(videoId);
Expand All @@ -147,24 +149,37 @@ public async Task GetVideoNext(string videoId)
.AppendLine("LikeCount: " + next.LikeCount)
.AppendLine("Description:\n" + string.Join('\n', next.Description.Split("\n").Select(x => $"\t{x}")));

sb.AppendLine("\n== CHAPTERS");
if (next.Chapters != null)
{
foreach (ChapterRenderer chapter in next.Chapters)
sb.AppendLine($"- [{TimeSpan.FromMilliseconds(chapter.TimeRangeStartMillis)}] {chapter.Title}");
}
else
{
sb.AppendLine("No chapters available");
}

sb.AppendLine("\n== COMMENTS")
.AppendLine("CommentCount: " + next.CommentCount)
.AppendLine("CommentsContinuation: " + next.CommentsContinuation);

sb.AppendLine("\n== RECOMMENDED");
foreach (IRenderer renderer in next.Recommended)
{
sb.AppendLine("->\t" + string.Join("\n\t", (renderer.ToString() ?? "UNKNOWN RENDERER " + renderer.Type).Split("\n")));
sb.AppendLine("->\t" + string.Join("\n\t",
(renderer.ToString() ?? "UNKNOWN RENDERER " + renderer.Type).Split("\n")));
}

Assert.Pass(sb.ToString());
}

[TestCase("3BR7-AzE2dQ", "OLAK5uy_l6pEkEJgy577R-aDlJ3Gkp5rmlgIOu8bc", null, null)]
[TestCase("o0tky2O8NlY", "OLAK5uy_l6pEkEJgy577R-aDlJ3Gkp5rmlgIOu8bc", null, null)]
[TestCase("NZwS7Cja6oE", "PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ", null, null)]
[TestCase("k_nLHgIM4yE", "PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ", null, null)]
public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, int? playlistIndex, string? playlistParams)
public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, int? playlistIndex,
string? playlistParams)
{
InnerTubeNextResponse next = await _innerTube.GetVideoAsync(videoId, playlistId, playlistIndex, playlistParams);
if (next.Playlist is null)
Expand All @@ -190,7 +205,7 @@ public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, in

Assert.Pass(sb.ToString());
}

[TestCase("1234567890a", Description = "An ID I just made up")]
[TestCase("a62882basgl", Description = "Another ID I just made up")]
[TestCase("32nkdvLq3oQ", Description = "A deleted video")]
Expand All @@ -214,8 +229,12 @@ public async Task DontGetVideoNext(string videoId)
}

[TestCase("BaW_jenozKc", Description = "Regular video comments")]
[TestCase("Eg0SC3F1STZnNEhwZVBjGAYyVSIuIgtxdUk2ZzRIcGVQYzAAeAKqAhpVZ3p3MnBIQXR1VW9xamRLbUtWNEFhQUJBZzABQiFlbmdhZ2VtZW50LXBhbmVsLWNvbW1lbnRzLXNlY3Rpb24%3D", Description = "Contains pinned & hearted comments")]
[TestCase("Eg0SC2tZd0Ita1p5TlU0GAYyJSIRIgtrWXdCLWtaeU5VNDAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D", Description = "Contains authors with badges")]
[TestCase(
"Eg0SC3F1STZnNEhwZVBjGAYyVSIuIgtxdUk2ZzRIcGVQYzAAeAKqAhpVZ3p3MnBIQXR1VW9xamRLbUtWNEFhQUJBZzABQiFlbmdhZ2VtZW50LXBhbmVsLWNvbW1lbnRzLXNlY3Rpb24%3D",
Description = "Contains pinned & hearted comments")]
[TestCase("Eg0SC2tZd0Ita1p5TlU0GAYyJSIRIgtrWXdCLWtaeU5VNDAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D",
Description = "Contains authors with badges")]
[TestCase("5UCz9i2K9gY", Description = "Has unescaped HTML tags")]
public async Task GetVideoComments(string videoId)
{
InnerTubeContinuationResponse comments;
Expand All @@ -229,6 +248,7 @@ public async Task GetVideoComments(string videoId)
{
comments = await _innerTube.GetVideoCommentsAsync(videoId!);
}

StringBuilder sb = new();

foreach (IRenderer renderer in comments.Contents) sb.AppendLine(renderer.ToString());
Expand Down
1 change: 1 addition & 0 deletions InnerTube.Tests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public void Setup()
[TestCase("EvCZ9W2xAMQ", null, Description = "Premiere video")]
[TestCase("technoblade", null, Description = "didYouMeanRenderer")]
[TestCase("O'zbekcha Kuylar 2020, Vol. 2", null, Description = "epic broken playlist")]
[TestCase("llrBX6FpMpM", "QgIIAQ%253D%253D", Description = "movieRenderer")]
public async Task Search(string query, string param)
{
InnerTubeSearchResults results = await _innerTube.SearchAsync(query, param);
Expand Down
15 changes: 8 additions & 7 deletions InnerTube/Formatters/HtmlFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
namespace InnerTube.Formatters;
using System.Web;

namespace InnerTube.Formatters;

/// <summary>
/// Formatter used to output rich texts in HTML format
/// </summary>
public class HtmlFormatter : IFormatter
{
/// <inheritdoc />
public string FormatBold(string text) => $"<b>{Sanitize(text)}</b>";
public string FormatBold(string text) => $"<b>{text}</b>";

/// <inheritdoc />
public string FormatItalics(string text) => $"<i>{Sanitize(text)}</i>";
public string FormatItalics(string text) => $"<i>{text}</i>";

/// <inheritdoc />
public string FormatUrl(string text, string url) => $"<a href=\"{url}\">{Sanitize(text)}</a>";
public string FormatUrl(string text, string url) => $"<a href=\"{url}\">{text}</a>";

/// <inheritdoc />
public string HandleLineBreaks(string text) => text.Replace("\n", "<br>");

private string Sanitize(string text) => text
.Replace("<", "&lt;")
.Replace(">", "&gt;");
/// <inheritdoc />
public string Sanitize(string text) => HttpUtility.HtmlEncode(text);
}
7 changes: 7 additions & 0 deletions InnerTube/Formatters/IFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ public interface IFormatter
/// <param name="text">Full text to fix line breaks in</param>
/// <returns>The same text with line breaks fixed</returns>
public string HandleLineBreaks(string text);

/// <summary>
/// Sanitize non-special content
/// </summary>
/// <param name="text">Full text to sanitize</param>
/// <returns>Sanitized text</returns>
public string Sanitize(string text);
}
3 changes: 3 additions & 0 deletions InnerTube/Formatters/MarkdownFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public class MarkdownFormatter : IFormatter

/// <inheritdoc />
public string HandleLineBreaks(string text) => text;

/// <inheritdoc />
public string Sanitize(string text) => text;
}
2 changes: 1 addition & 1 deletion InnerTube/InnerTube.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task<InnerTubePlayer> GetPlayerAsync(string videoId, bool contentCh
Size = 1,
SlidingExpiration = TimeSpan.FromSeconds(Math.Max(600, player.Details.Length.TotalSeconds)),
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(player.ExpiresInSeconds - player.Details.Length.TotalSeconds)
TimeSpan.FromSeconds(Math.Max(3600, player.ExpiresInSeconds - player.Details.Length.TotalSeconds))
});
return player;
}
Expand Down
8 changes: 4 additions & 4 deletions InnerTube/InnerTube.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
<PackageReference Include="Serilog" Version="2.11.0"/>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.11.0" />
</ItemGroup>

<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath=""/>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions InnerTube/Models/InnerTubeNextResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class InnerTubeNextResponse
public string? CommentCount { get; }
public IEnumerable<IRenderer> Recommended { get; }
public InnerTubePlaylistInfo? Playlist { get; }
public IEnumerable<ChapterRenderer>? Chapters { get; }

public InnerTubeNextResponse(JObject playerResponse)
{
Expand Down Expand Up @@ -83,5 +84,15 @@ public InnerTubeNextResponse(JObject playerResponse)
playerResponse.GetFromJsonPath<JObject>("contents.twoColumnWatchNextResults.playlist.playlist");
if (playlistObject != null)
Playlist = new InnerTubePlaylistInfo(playlistObject);

JArray? markersMap =
playerResponse.GetFromJsonPath<JArray>(
"playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer.decoratedPlayerBarRenderer.playerBar.multiMarkersPlayerBarRenderer.markersMap");
JArray? chaptersList = markersMap
?.FirstOrDefault(x => x.GetFromJsonPath<string>("key") == "DESCRIPTION_CHAPTERS")
?.GetFromJsonPath<JArray>("value.chapters");
Chapters = chaptersList != null
? RendererManager.ParseRenderers(chaptersList).Cast<ChapterRenderer>()
: null;
}
}
29 changes: 29 additions & 0 deletions InnerTube/Renderers/ChapterRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text;
using Newtonsoft.Json.Linq;

namespace InnerTube.Renderers;

public class ChapterRenderer : IRenderer
{
public string Type => "chapterRenderer";
public string Title { get; }
public IEnumerable<Thumbnail> Thumbnails { get; }
public ulong TimeRangeStartMillis { get; }

public ChapterRenderer(JToken renderer)
{
Title = Utils.ReadText(renderer.GetFromJsonPath<JObject>("title"));
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
TimeRangeStartMillis = renderer.GetFromJsonPath<ulong>("timeRangeStartMillis");
}

public override string ToString()
{
StringBuilder sb = new StringBuilder()
.AppendLine($"[{Type}] {Title}")
.AppendLine($"- TimeRangeStartMillis: ({TimeSpan.FromMilliseconds(TimeRangeStartMillis)}) {TimeRangeStartMillis}")
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}");

return sb.ToString();
}
}
49 changes: 49 additions & 0 deletions InnerTube/Renderers/CompactMovieRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Text;
using Newtonsoft.Json.Linq;

namespace InnerTube.Renderers;

public class CompactMovieRenderer : IRenderer
{
public string Type => "compactMovieRenderer";

public string Id { get; }
public string Title { get; }
public IEnumerable<string> TopMetadataItems { get; }
public TimeSpan Duration { get; }
public IEnumerable<Thumbnail> Thumbnails { get; }
public Channel Channel { get; }

public CompactMovieRenderer(JToken renderer)
{
Id = renderer["videoId"]!.ToString();
Title = renderer.GetFromJsonPath<string>("title.simpleText")!;
TopMetadataItems =
renderer.GetFromJsonPath<JArray>("topMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
Array.Empty<string>();
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
Channel = new Channel
{
Id = renderer.GetFromJsonPath<string>("shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId")!,
Title = renderer.GetFromJsonPath<string>("shortBylineText.runs[0].text")!,
Avatar = null,
Subscribers = null,
Badges = Array.Empty<Badge>()
};

Duration = Utils.ParseDuration(renderer["lengthText"]?["simpleText"]?.ToString()!);
}

public override string ToString()
{
StringBuilder sb = new StringBuilder()
.AppendLine($"[{Type}] {Title}")
.AppendLine($"- Id: {Id}")
.AppendLine($"- Duration: {Duration}")
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}")
.AppendLine($"- Channel: {Channel}")
.AppendLine($"- TopMetadataItems:\n\t{string.Join("\n\t", TopMetadataItems.Select(x => $"- {x}"))}");

return sb.ToString();
}
}
64 changes: 64 additions & 0 deletions InnerTube/Renderers/MovieRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text;
using System.Text.Json.Nodes;
using Newtonsoft.Json.Linq;

namespace InnerTube.Renderers;

public class MovieRenderer : IRenderer
{
public string Type => "movieRenderer";

public string Id { get; }
public string Title { get; }
public string DescriptionSnippet { get; }
public IEnumerable<string> BottomMetadataItems { get; }
public IEnumerable<string> TopMetadataItems { get; }
public TimeSpan Duration { get; }
public IEnumerable<Thumbnail> Thumbnails { get; }
public Channel Channel { get; }
public IEnumerable<Badge> Badges { get; }

public MovieRenderer(JToken renderer)
{
Id = renderer["videoId"]!.ToString();
Title = Utils.ReadText(renderer.GetFromJsonPath<JObject>("title") ?? new JObject());
BottomMetadataItems =
renderer.GetFromJsonPath<JArray>("bottomMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
Array.Empty<string>();
TopMetadataItems =
renderer.GetFromJsonPath<JArray>("topMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
Array.Empty<string>();
DescriptionSnippet = Utils.ReadText(renderer.GetFromJsonPath<JObject>("descriptionSnippet") ??
new JObject(), true);
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
Channel = new Channel
{
Id = null,
Title = renderer.GetFromJsonPath<string>("longBylineText.runs[0].text")!,
Avatar = null,
Subscribers = null,
Badges = renderer.GetFromJsonPath<JArray>("ownerBadges")
?.Select(x => new Badge(x["metadataBadgeRenderer"]!)) ?? Array.Empty<Badge>()
};
Badges = renderer["badges"]?.ToObject<JArray>()?.Select(x => new Badge(x["metadataBadgeRenderer"]!)) ??
Array.Empty<Badge>();

Duration = Utils.ParseDuration(renderer["lengthText"]?["simpleText"]?.ToString()!);
}

public override string ToString()
{
StringBuilder sb = new StringBuilder()
.AppendLine($"[{Type}] {Title}")
.AppendLine($"- Id: {Id}")
.AppendLine($"- Duration: {Duration}")
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}")
.AppendLine($"- Channel: {Channel}")
.AppendLine($"- Badges: {string.Join(" | ", Badges.Select(x => x.ToString()))}")
.AppendLine($"- TopMetadataItems:\n\t{string.Join("\n\t", TopMetadataItems.Select(x => $"- {x}"))}")
.AppendLine($"- BottomMetadataItems:\n\t{string.Join("\n\t", BottomMetadataItems.Select(x => $"- {x}"))}")
.AppendLine(DescriptionSnippet);

return sb.ToString();
}
}
2 changes: 1 addition & 1 deletion InnerTube/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static string ReadText(JObject? richText, bool includeFormatting = false)
continue;
}

string currentString = run["text"]!.ToString();
string currentString = Formatter.Sanitize(run["text"]!.ToString());

if (run.ContainsKey("bold"))
currentString = Formatter.FormatBold(currentString);
Expand Down

0 comments on commit 0b1d52a

Please sign in to comment.