Skip to content
This repository was archived by the owner on Nov 27, 2024. It is now read-only.

Commit 0b1d52a

Browse files
authored
The great bug-fixening (#21)
- Fix HTML renderers to escape HTML tags - Implement MovieRenderer - Implement CompactMovieRenderer - Fix the video cache breaking for videos longer than 6 hours - Implement chapters
1 parent f8699d7 commit 0b1d52a

File tree

12 files changed

+204
-19
lines changed

12 files changed

+204
-19
lines changed

InnerTube.Tests/PlayerTests.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ public void FailPlayer(string videoId, bool contentCheckOk, bool includeHls)
132132
[TestCase("t6cZn-Fvwa0", Description = "Video with comments disabled")]
133133
[TestCase("jPhJbKBuNnA", Description = "Video with watchEndpoint in attributedDescription")]
134134
[TestCase("UoBFuLMlDkw", Description = "Video with more special stuff in attributedDescription")]
135+
[TestCase("llrBX6FpMpM", Description = "compactMovieRenderer")]
136+
[TestCase("jUUe6TuRlgU", Description = "Chapters")]
135137
public async Task GetVideoNext(string videoId)
136138
{
137139
InnerTubeNextResponse next = await _innerTube.GetVideoAsync(videoId);
@@ -147,24 +149,37 @@ public async Task GetVideoNext(string videoId)
147149
.AppendLine("LikeCount: " + next.LikeCount)
148150
.AppendLine("Description:\n" + string.Join('\n', next.Description.Split("\n").Select(x => $"\t{x}")));
149151

152+
sb.AppendLine("\n== CHAPTERS");
153+
if (next.Chapters != null)
154+
{
155+
foreach (ChapterRenderer chapter in next.Chapters)
156+
sb.AppendLine($"- [{TimeSpan.FromMilliseconds(chapter.TimeRangeStartMillis)}] {chapter.Title}");
157+
}
158+
else
159+
{
160+
sb.AppendLine("No chapters available");
161+
}
162+
150163
sb.AppendLine("\n== COMMENTS")
151164
.AppendLine("CommentCount: " + next.CommentCount)
152165
.AppendLine("CommentsContinuation: " + next.CommentsContinuation);
153166

154167
sb.AppendLine("\n== RECOMMENDED");
155168
foreach (IRenderer renderer in next.Recommended)
156169
{
157-
sb.AppendLine("->\t" + string.Join("\n\t", (renderer.ToString() ?? "UNKNOWN RENDERER " + renderer.Type).Split("\n")));
170+
sb.AppendLine("->\t" + string.Join("\n\t",
171+
(renderer.ToString() ?? "UNKNOWN RENDERER " + renderer.Type).Split("\n")));
158172
}
159-
173+
160174
Assert.Pass(sb.ToString());
161175
}
162176

163177
[TestCase("3BR7-AzE2dQ", "OLAK5uy_l6pEkEJgy577R-aDlJ3Gkp5rmlgIOu8bc", null, null)]
164178
[TestCase("o0tky2O8NlY", "OLAK5uy_l6pEkEJgy577R-aDlJ3Gkp5rmlgIOu8bc", null, null)]
165179
[TestCase("NZwS7Cja6oE", "PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ", null, null)]
166180
[TestCase("k_nLHgIM4yE", "PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ", null, null)]
167-
public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, int? playlistIndex, string? playlistParams)
181+
public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, int? playlistIndex,
182+
string? playlistParams)
168183
{
169184
InnerTubeNextResponse next = await _innerTube.GetVideoAsync(videoId, playlistId, playlistIndex, playlistParams);
170185
if (next.Playlist is null)
@@ -190,7 +205,7 @@ public async Task GetVideoNextWithPlaylist(string videoId, string playlistId, in
190205

191206
Assert.Pass(sb.ToString());
192207
}
193-
208+
194209
[TestCase("1234567890a", Description = "An ID I just made up")]
195210
[TestCase("a62882basgl", Description = "Another ID I just made up")]
196211
[TestCase("32nkdvLq3oQ", Description = "A deleted video")]
@@ -214,8 +229,12 @@ public async Task DontGetVideoNext(string videoId)
214229
}
215230

216231
[TestCase("BaW_jenozKc", Description = "Regular video comments")]
217-
[TestCase("Eg0SC3F1STZnNEhwZVBjGAYyVSIuIgtxdUk2ZzRIcGVQYzAAeAKqAhpVZ3p3MnBIQXR1VW9xamRLbUtWNEFhQUJBZzABQiFlbmdhZ2VtZW50LXBhbmVsLWNvbW1lbnRzLXNlY3Rpb24%3D", Description = "Contains pinned & hearted comments")]
218-
[TestCase("Eg0SC2tZd0Ita1p5TlU0GAYyJSIRIgtrWXdCLWtaeU5VNDAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D", Description = "Contains authors with badges")]
232+
[TestCase(
233+
"Eg0SC3F1STZnNEhwZVBjGAYyVSIuIgtxdUk2ZzRIcGVQYzAAeAKqAhpVZ3p3MnBIQXR1VW9xamRLbUtWNEFhQUJBZzABQiFlbmdhZ2VtZW50LXBhbmVsLWNvbW1lbnRzLXNlY3Rpb24%3D",
234+
Description = "Contains pinned & hearted comments")]
235+
[TestCase("Eg0SC2tZd0Ita1p5TlU0GAYyJSIRIgtrWXdCLWtaeU5VNDAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D",
236+
Description = "Contains authors with badges")]
237+
[TestCase("5UCz9i2K9gY", Description = "Has unescaped HTML tags")]
219238
public async Task GetVideoComments(string videoId)
220239
{
221240
InnerTubeContinuationResponse comments;
@@ -229,6 +248,7 @@ public async Task GetVideoComments(string videoId)
229248
{
230249
comments = await _innerTube.GetVideoCommentsAsync(videoId!);
231250
}
251+
232252
StringBuilder sb = new();
233253

234254
foreach (IRenderer renderer in comments.Contents) sb.AppendLine(renderer.ToString());

InnerTube.Tests/SearchTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public void Setup()
2222
[TestCase("EvCZ9W2xAMQ", null, Description = "Premiere video")]
2323
[TestCase("technoblade", null, Description = "didYouMeanRenderer")]
2424
[TestCase("O'zbekcha Kuylar 2020, Vol. 2", null, Description = "epic broken playlist")]
25+
[TestCase("llrBX6FpMpM", "QgIIAQ%253D%253D", Description = "movieRenderer")]
2526
public async Task Search(string query, string param)
2627
{
2728
InnerTubeSearchResults results = await _innerTube.SearchAsync(query, param);

InnerTube/Formatters/HtmlFormatter.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
namespace InnerTube.Formatters;
1+
using System.Web;
2+
3+
namespace InnerTube.Formatters;
24

35
/// <summary>
46
/// Formatter used to output rich texts in HTML format
57
/// </summary>
68
public class HtmlFormatter : IFormatter
79
{
810
/// <inheritdoc />
9-
public string FormatBold(string text) => $"<b>{Sanitize(text)}</b>";
11+
public string FormatBold(string text) => $"<b>{text}</b>";
1012

1113
/// <inheritdoc />
12-
public string FormatItalics(string text) => $"<i>{Sanitize(text)}</i>";
14+
public string FormatItalics(string text) => $"<i>{text}</i>";
1315

1416
/// <inheritdoc />
15-
public string FormatUrl(string text, string url) => $"<a href=\"{url}\">{Sanitize(text)}</a>";
17+
public string FormatUrl(string text, string url) => $"<a href=\"{url}\">{text}</a>";
1618

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

20-
private string Sanitize(string text) => text
21-
.Replace("<", "&lt;")
22-
.Replace(">", "&gt;");
22+
/// <inheritdoc />
23+
public string Sanitize(string text) => HttpUtility.HtmlEncode(text);
2324
}

InnerTube/Formatters/IFormatter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,11 @@ public interface IFormatter
3636
/// <param name="text">Full text to fix line breaks in</param>
3737
/// <returns>The same text with line breaks fixed</returns>
3838
public string HandleLineBreaks(string text);
39+
40+
/// <summary>
41+
/// Sanitize non-special content
42+
/// </summary>
43+
/// <param name="text">Full text to sanitize</param>
44+
/// <returns>Sanitized text</returns>
45+
public string Sanitize(string text);
3946
}

InnerTube/Formatters/MarkdownFormatter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ public class MarkdownFormatter : IFormatter
1616

1717
/// <inheritdoc />
1818
public string HandleLineBreaks(string text) => text;
19+
20+
/// <inheritdoc />
21+
public string Sanitize(string text) => text;
1922
}

InnerTube/InnerTube.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public async Task<InnerTubePlayer> GetPlayerAsync(string videoId, bool contentCh
116116
Size = 1,
117117
SlidingExpiration = TimeSpan.FromSeconds(Math.Max(600, player.Details.Length.TotalSeconds)),
118118
AbsoluteExpirationRelativeToNow =
119-
TimeSpan.FromSeconds(player.ExpiresInSeconds - player.Details.Length.TotalSeconds)
119+
TimeSpan.FromSeconds(Math.Max(3600, player.ExpiresInSeconds - player.Details.Length.TotalSeconds))
120120
});
121121
return player;
122122
}

InnerTube/InnerTube.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
</PropertyGroup>
2020

2121
<ItemGroup>
22-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1"/>
23-
<PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
24-
<PackageReference Include="Serilog" Version="2.11.0"/>
22+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
23+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
24+
<PackageReference Include="Serilog" Version="2.11.0" />
2525
</ItemGroup>
2626

2727
<ItemGroup>
28-
<None Include="../README.md" Pack="true" PackagePath=""/>
28+
<None Include="../README.md" Pack="true" PackagePath="" />
2929
</ItemGroup>
3030

3131
</Project>

InnerTube/Models/InnerTubeNextResponse.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class InnerTubeNextResponse
1717
public string? CommentCount { get; }
1818
public IEnumerable<IRenderer> Recommended { get; }
1919
public InnerTubePlaylistInfo? Playlist { get; }
20+
public IEnumerable<ChapterRenderer>? Chapters { get; }
2021

2122
public InnerTubeNextResponse(JObject playerResponse)
2223
{
@@ -83,5 +84,15 @@ public InnerTubeNextResponse(JObject playerResponse)
8384
playerResponse.GetFromJsonPath<JObject>("contents.twoColumnWatchNextResults.playlist.playlist");
8485
if (playlistObject != null)
8586
Playlist = new InnerTubePlaylistInfo(playlistObject);
87+
88+
JArray? markersMap =
89+
playerResponse.GetFromJsonPath<JArray>(
90+
"playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer.decoratedPlayerBarRenderer.playerBar.multiMarkersPlayerBarRenderer.markersMap");
91+
JArray? chaptersList = markersMap
92+
?.FirstOrDefault(x => x.GetFromJsonPath<string>("key") == "DESCRIPTION_CHAPTERS")
93+
?.GetFromJsonPath<JArray>("value.chapters");
94+
Chapters = chaptersList != null
95+
? RendererManager.ParseRenderers(chaptersList).Cast<ChapterRenderer>()
96+
: null;
8697
}
8798
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text;
2+
using Newtonsoft.Json.Linq;
3+
4+
namespace InnerTube.Renderers;
5+
6+
public class ChapterRenderer : IRenderer
7+
{
8+
public string Type => "chapterRenderer";
9+
public string Title { get; }
10+
public IEnumerable<Thumbnail> Thumbnails { get; }
11+
public ulong TimeRangeStartMillis { get; }
12+
13+
public ChapterRenderer(JToken renderer)
14+
{
15+
Title = Utils.ReadText(renderer.GetFromJsonPath<JObject>("title"));
16+
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
17+
TimeRangeStartMillis = renderer.GetFromJsonPath<ulong>("timeRangeStartMillis");
18+
}
19+
20+
public override string ToString()
21+
{
22+
StringBuilder sb = new StringBuilder()
23+
.AppendLine($"[{Type}] {Title}")
24+
.AppendLine($"- TimeRangeStartMillis: ({TimeSpan.FromMilliseconds(TimeRangeStartMillis)}) {TimeRangeStartMillis}")
25+
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}");
26+
27+
return sb.ToString();
28+
}
29+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Text;
2+
using Newtonsoft.Json.Linq;
3+
4+
namespace InnerTube.Renderers;
5+
6+
public class CompactMovieRenderer : IRenderer
7+
{
8+
public string Type => "compactMovieRenderer";
9+
10+
public string Id { get; }
11+
public string Title { get; }
12+
public IEnumerable<string> TopMetadataItems { get; }
13+
public TimeSpan Duration { get; }
14+
public IEnumerable<Thumbnail> Thumbnails { get; }
15+
public Channel Channel { get; }
16+
17+
public CompactMovieRenderer(JToken renderer)
18+
{
19+
Id = renderer["videoId"]!.ToString();
20+
Title = renderer.GetFromJsonPath<string>("title.simpleText")!;
21+
TopMetadataItems =
22+
renderer.GetFromJsonPath<JArray>("topMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
23+
Array.Empty<string>();
24+
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
25+
Channel = new Channel
26+
{
27+
Id = renderer.GetFromJsonPath<string>("shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId")!,
28+
Title = renderer.GetFromJsonPath<string>("shortBylineText.runs[0].text")!,
29+
Avatar = null,
30+
Subscribers = null,
31+
Badges = Array.Empty<Badge>()
32+
};
33+
34+
Duration = Utils.ParseDuration(renderer["lengthText"]?["simpleText"]?.ToString()!);
35+
}
36+
37+
public override string ToString()
38+
{
39+
StringBuilder sb = new StringBuilder()
40+
.AppendLine($"[{Type}] {Title}")
41+
.AppendLine($"- Id: {Id}")
42+
.AppendLine($"- Duration: {Duration}")
43+
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}")
44+
.AppendLine($"- Channel: {Channel}")
45+
.AppendLine($"- TopMetadataItems:\n\t{string.Join("\n\t", TopMetadataItems.Select(x => $"- {x}"))}");
46+
47+
return sb.ToString();
48+
}
49+
}

InnerTube/Renderers/MovieRenderer.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text;
2+
using System.Text.Json.Nodes;
3+
using Newtonsoft.Json.Linq;
4+
5+
namespace InnerTube.Renderers;
6+
7+
public class MovieRenderer : IRenderer
8+
{
9+
public string Type => "movieRenderer";
10+
11+
public string Id { get; }
12+
public string Title { get; }
13+
public string DescriptionSnippet { get; }
14+
public IEnumerable<string> BottomMetadataItems { get; }
15+
public IEnumerable<string> TopMetadataItems { get; }
16+
public TimeSpan Duration { get; }
17+
public IEnumerable<Thumbnail> Thumbnails { get; }
18+
public Channel Channel { get; }
19+
public IEnumerable<Badge> Badges { get; }
20+
21+
public MovieRenderer(JToken renderer)
22+
{
23+
Id = renderer["videoId"]!.ToString();
24+
Title = Utils.ReadText(renderer.GetFromJsonPath<JObject>("title") ?? new JObject());
25+
BottomMetadataItems =
26+
renderer.GetFromJsonPath<JArray>("bottomMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
27+
Array.Empty<string>();
28+
TopMetadataItems =
29+
renderer.GetFromJsonPath<JArray>("topMetadataItems")?.Select(x => Utils.ReadText((JObject)x)) ??
30+
Array.Empty<string>();
31+
DescriptionSnippet = Utils.ReadText(renderer.GetFromJsonPath<JObject>("descriptionSnippet") ??
32+
new JObject(), true);
33+
Thumbnails = Utils.GetThumbnails(renderer.GetFromJsonPath<JArray>("thumbnail.thumbnails") ?? new JArray());
34+
Channel = new Channel
35+
{
36+
Id = null,
37+
Title = renderer.GetFromJsonPath<string>("longBylineText.runs[0].text")!,
38+
Avatar = null,
39+
Subscribers = null,
40+
Badges = renderer.GetFromJsonPath<JArray>("ownerBadges")
41+
?.Select(x => new Badge(x["metadataBadgeRenderer"]!)) ?? Array.Empty<Badge>()
42+
};
43+
Badges = renderer["badges"]?.ToObject<JArray>()?.Select(x => new Badge(x["metadataBadgeRenderer"]!)) ??
44+
Array.Empty<Badge>();
45+
46+
Duration = Utils.ParseDuration(renderer["lengthText"]?["simpleText"]?.ToString()!);
47+
}
48+
49+
public override string ToString()
50+
{
51+
StringBuilder sb = new StringBuilder()
52+
.AppendLine($"[{Type}] {Title}")
53+
.AppendLine($"- Id: {Id}")
54+
.AppendLine($"- Duration: {Duration}")
55+
.AppendLine($"- Thumbnail count: {Thumbnails.Count()}")
56+
.AppendLine($"- Channel: {Channel}")
57+
.AppendLine($"- Badges: {string.Join(" | ", Badges.Select(x => x.ToString()))}")
58+
.AppendLine($"- TopMetadataItems:\n\t{string.Join("\n\t", TopMetadataItems.Select(x => $"- {x}"))}")
59+
.AppendLine($"- BottomMetadataItems:\n\t{string.Join("\n\t", BottomMetadataItems.Select(x => $"- {x}"))}")
60+
.AppendLine(DescriptionSnippet);
61+
62+
return sb.ToString();
63+
}
64+
}

InnerTube/Utils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static string ReadText(JObject? richText, bool includeFormatting = false)
5757
continue;
5858
}
5959

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

6262
if (run.ContainsKey("bold"))
6363
currentString = Formatter.FormatBold(currentString);

0 commit comments

Comments
 (0)