-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathYouTube.cs
More file actions
232 lines (191 loc) · 8.71 KB
/
Copy pathYouTube.cs
File metadata and controls
232 lines (191 loc) · 8.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
using AngryMonkey.POCO;
using QuadSpinner.Adjunct;
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Spectre.Console;
namespace AngryMonkey
{
internal class YouTube
{
private Hive _hive;
private HttpClient _yt;
private string _youtubeApiKey;
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
internal void MakeYouTubePages(Hive hive)
{
try
{
_hive = hive;
_yt = new HttpClient();
_youtubeApiKey = File.ReadAllText("youtube.txt");
_yt.DefaultRequestHeaders.Accept.Clear();
_yt.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
ImportConfig imports = YouTubeImporter.ReadFromFile($"{Program.RootFolder}\\youtube.json");
foreach (Channel item in imports.Channels)
{
Import(item.IDs, item.Directory, item.Authorized, item.Official);
}
}
catch (Exception ex)
{
AnsiConsole.WriteLine("YouTube pages failed: " + ex.Message);
}
}
public void Import(IEnumerable<string> youtubeUrls, string directory = "", bool authorized = false, bool official = false)
{
// Create in chronological order (optional but usually nicer for topic lists)
var vids = new List<(string Url, string VideoId, VideoSnippet Snippet)>();
foreach (var url in youtubeUrls)
{
try
{
if (url is null)
throw new ArgumentException($"Could not extract YouTube video id from: {url}");
if (Program.Pages.Any(x => x.Tag == url))
continue;
var snippet = GetYoutubeSnippet(url);
Console.WriteLine($"Getting video {url}...");
vids.Add(("https://www.youtube.com/watch?v=" + url, url, snippet));
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
int order = 1;
foreach (var v in vids.OrderByDescending(x => x.Snippet.PublishedAtUtc))
{
string title = "";
if (official)
{
if (v.Snippet.Title.Contains(":")) { title = v.Snippet.Title.Split(':')[^1]; }
if (v.Snippet.Title.Contains("-")) { title = v.Snippet.Title.Split('-')[^1]; }
}
if (v.Snippet.Title.Contains("#")) { title = v.Snippet.Title.Split('#')[0]; }
title = title.Strip(":").Trim();
if (title == "")
title = v.Snippet.Title;
var createdAt = v.Snippet.PublishedAtUtc.ToString("D", CultureInfo.InvariantCulture);
StringBuilder sb = new();
string slug = $"yt-{FrontMatter.Slugify(title)}";
if (!official)
{
slug = "yt-" + v.VideoId;
}
sb.AppendLine("---");
sb.AppendLine("title: " + title);
sb.AppendLine($"uid: {slug}");
sb.AppendLine($"tag: {v.VideoId}");
sb.AppendLine($"order: {order:000}");
order++;
if (authorized)
{
sb.AppendLine("icon: certificate");
}
if (official)
{
sb.AppendLine("icon: kit fa-qs-logo");
}
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine($"})");
sb.AppendLine();
if (authorized || official)
{
sb.AppendLine(v.Snippet.Description ?? string.Empty);
}
if (!authorized && !official)
{
sb.AppendLine(":::warning\n" +
"Unofficial Training: This video is contributed by the community." +
"\n:::");
}
sb.AppendLine();
if (v.Snippet.PublishedAtUtc.Date < DateTime.UtcNow.AddMonths(-8))
{
sb.AppendLine(":::note\n" +
"This video is older than 8 months and some nodes may have changed since then. Please check the latest @changelogs-home." +
"\n:::");
}
sb.AppendLine();
sb.AppendLine($"Published on {createdAt}");
if (official)
{
if (v.Snippet.Title.Contains("Node:"))
{
Directory.CreateDirectory($@"{_hive.Source}\official\nodes\");
File.WriteAllText($@"{_hive.Source}\official\nodes\{slug}.md", sb.ToString());
}
else if (v.Snippet.Title.Contains("Breakdown") || v.Snippet.Title.Contains("Deep Dive"))
{
Directory.CreateDirectory($@"{_hive.Source}\official\deepdives\");
File.WriteAllText($@"{_hive.Source}\official\deepdives\{slug}.md", sb.ToString());
}
else
{
Directory.CreateDirectory($@"{_hive.Source}\official\tutorials\");
File.WriteAllText($@"{_hive.Source}\official\tutorials\{slug}.md", sb.ToString());
}
}
else
{
if (authorized)
{
Directory.CreateDirectory($@"{_hive.Source}\partners\{directory}\");
File.WriteAllText($@"{_hive.Source}\partners\{directory}\{v.VideoId}.md", sb.ToString());
}
else
{
Directory.CreateDirectory($@"{_hive.Source}\community\{directory}\");
File.WriteAllText($@"{_hive.Source}\community\{directory}\{v.VideoId}.md", sb.ToString());
}
}
//File.WriteAllText(Program.RootFolder + @"\Source\.videos\" + v.VideoId, "");
}
}
// -------- YouTube --------
private VideoSnippet GetYoutubeSnippet(string videoId)
{
try
{
var url = $"https://www.googleapis.com/youtube/v3/videos?part=snippet&id={Uri.EscapeDataString(videoId)}&key={Uri.EscapeDataString(_youtubeApiKey)}";
using var resp = _yt.GetAsync(url).Result;
var json = resp.Content.ReadAsStringAsync().Result;
if (!resp.IsSuccessStatusCode)
throw new HttpRequestException($"YouTube API error {(int)resp.StatusCode}: {json}");
var parsed = JsonSerializer.Deserialize<YoutubeVideosListResponse>(json, JsonOpts)
?? throw new InvalidOperationException("Failed to parse YouTube response.");
var item = parsed.Items?.FirstOrDefault()
?? throw new InvalidOperationException($"YouTube video not found: {videoId}");
var sn = item.Snippet ?? throw new InvalidOperationException("YouTube snippet missing.");
// publishedAt is RFC3339; parse as UTC.
var published = DateTime.Parse(sn.PublishedAt, CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
return new VideoSnippet(
Title: sn.Title ?? $"YouTube Video {videoId}",
Description: sn.Description ?? string.Empty,
PublishedAtUtc: published);
}
catch (Exception ex)
{
return new VideoSnippet();
}
}
// -------- Models --------
public readonly record struct VideoSnippet(string Title, string Description, DateTime PublishedAtUtc);
private sealed class YoutubeVideosListResponse
{
public List<YoutubeVideoItem> Items { get; init; }
}
private sealed class YoutubeVideoItem
{ public YoutubeSnippet Snippet { get; set; } }
private sealed class YoutubeSnippet
{
public string Title { get; set; }
public string Description { get; set; }
public string PublishedAt { get; set; } = "";
}
}
}