DiscordCoreAPI
A Discord bot library written in C++, with custom asynchronous coroutines.
Loading...
Searching...
No Matches
YouTubeAPI.cpp
Go to the documentation of this file.
1/*
2 DiscordCoreAPI, A bot library for Discord, written in C++, and featuring explicit multithreading through the usage of custom, asynchronous C++ CoRoutines.
3
4 Copyright 2021, 2022 Chris M. (RealTimeChris)
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
19 USA
20*/
21/// YouTubeAPI.cpp - Soure file for the YouTube API related stuff.
22/// Jun 30, 2021
23/// https://discordcoreapi.com
24/// \file YouTubeAPI.cpp
25
33
34namespace DiscordCoreInternal {
35
36 std::vector<DiscordCoreAPI::Song> YouTubeRequestBuilder::collectSearchResults(const std::string& searchQuery) {
37 HttpsWorkloadData dataPackage{ HttpsWorkloadType::YouTubeGetSearchResults };
38 dataPackage.baseUrl = this->baseUrl;
39 dataPackage.relativePath = "/results?search_query=" + DiscordCoreAPI::urlEncode(searchQuery.c_str());
40 dataPackage.workloadClass = HttpsWorkloadClass::Get;
41 HttpsResponseData returnData = this->httpsClient->submitWorkloadAndGetResult(dataPackage);
42 if (returnData.responseCode != 200 && this->configManager->doWePrintHttpsErrorMessages()) {
43 cout << DiscordCoreAPI::shiftToBrightRed() << "YouTubeRequestBuilder::collectSearchResults() Error: " << returnData.responseCode
44 << returnData.responseData.c_str() << DiscordCoreAPI::reset() << endl
45 << endl;
46 }
47 Jsonifier::Value partialSearchResultsJson{};
48
49 std::vector<DiscordCoreAPI::Song> searchResults{};
50 auto varInitFind = returnData.responseData.find("var ytInitialData = ");
51 if (varInitFind != std::string::npos) {
52 std::string newString00 = "var ytInitialData = ";
53 std::string newString = returnData.responseData.substr(varInitFind + newString00.length());
54 std::string stringSequence = ";</script><script nonce=";
55 newString = newString.substr(0, newString.find(stringSequence));
56 Jsonifier::Parser parser{};
57 partialSearchResultsJson = parser.parseJson(newString);
58 Jsonifier::Value objectContents{};
59 if (partialSearchResultsJson["contents"].get(objectContents) == Jsonifier::ErrorCode::Success) {
60 if (objectContents["twoColumnSearchResultsRenderer"].get(objectContents) == Jsonifier::ErrorCode::Success) {
61 if (objectContents["primaryContents"].get(objectContents) == Jsonifier::ErrorCode::Success) {
62 if (objectContents["sectionListRenderer"].get(objectContents) == Jsonifier::ErrorCode::Success) {
63 if (objectContents["contents"].get(objectContents) == Jsonifier::ErrorCode::Success) {
64 if (objectContents.at(0).get(objectContents) == Jsonifier::ErrorCode::Success) {
65 if (objectContents["itemSectionRenderer"].get(objectContents) == Jsonifier::ErrorCode::Success) {
66 if (objectContents["contents"].get(objectContents) == Jsonifier::ErrorCode::Success) {
67 for (auto iterator: objectContents) {
68 DiscordCoreAPI::Song searchResult{};
69 Jsonifier::Value object{};
70 if (iterator["videoRenderer"].get(object) == Jsonifier::ErrorCode::Success) {
71 searchResult = DiscordCoreAPI::Song{ object };
72 }
74 searchResult.viewUrl = this->baseUrl + "/watch?v=" + searchResult.songId + "&hl=en";
75 if (searchResult.description == "" || searchResult.viewUrl == "") {
76 continue;
77 }
78 searchResults.emplace_back(searchResult);
79 }
80 }
81 }
82 }
83 }
84 }
85 }
86 }
87 }
88 }
89 return searchResults;
90 }
91
92 DiscordCoreAPI::Song YouTubeRequestBuilder::constructDownloadInfo(DiscordCoreAPI::Song& newSong, int32_t currentRecursionDepth) {
93 HttpsResponseData responseData{};
94 try {
95 Jsonifier::Serializer request{};
96 request["videoId"] = newSong.songId;
97 request["contentCheckOk"] = true;
98 request["racyCheckOk"] = true;
99 request["context"]["client"]["clientName"] = "ANDROID";
100 request["context"]["client"]["clientScreen"] = "EMBED";
101 request["context"]["client"]["clientVersion"] = "16.46.37";
102 request["context"]["client"]["hl"] = "en";
103 request["context"]["client"]["gl"] = "US";
104 request["context"]["client"]["utcOffsetMinutes"] = 0;
105 request["context"]["embedUrl"] = "https://www.youtube.com";
106 HttpsWorkloadData dataPackage02{ HttpsWorkloadType::YouTubeGetSearchResults };
107 dataPackage02.baseUrl = YouTubeRequestBuilder::baseUrl;
108 dataPackage02.relativePath = "/youtubei/v1/player?key=" + YouTubeRequestBuilder::apiKey;
109 request.refreshString(DiscordCoreAPI::JsonifierSerializeType::Json);
110 dataPackage02.content = request.operator std::string();
111 dataPackage02.workloadClass = HttpsWorkloadClass::Post;
112 responseData = this->httpsClient->submitWorkloadAndGetResult(dataPackage02);
113 if (responseData.responseCode != 204 && responseData.responseCode != 201 && responseData.responseCode != 200 &&
114 this->configManager->doWePrintHttpsErrorMessages()) {
115 cout << DiscordCoreAPI::shiftToBrightRed()
116 << "YouTubeRequestBuilder::constructDownloadInfo() 01 Error: " << responseData.responseCode << ", "
117 << responseData.responseData << DiscordCoreAPI::reset() << endl
118 << endl;
119 }
121 Jsonifier::Parser parser{};
122 Jsonifier::Value value{};
123 if (parser.parseJson(responseData.responseData).get(value) == Jsonifier::ErrorCode::Success) {
125 DiscordCoreAPI::YouTubeFormat format{};
126 bool isOpusFound{};
127 for (auto& value: static_cast<std::vector<DiscordCoreAPI::YouTubeFormat>>(vector)) {
128 if (value.mimeType.find("opus") != std::string::npos) {
129 if (value.audioQuality == "AUDIO_QUALITY_LOW") {
130 isOpusFound = true;
131 format = value;
132 }
133 if (value.audioQuality == "AUDIO_QUALITY_MEDIUM") {
134 isOpusFound = true;
135 format = value;
136 }
137 if (value.audioQuality == "AUDIO_QUALITY_HIGH") {
138 isOpusFound = true;
139 format = value;
140 }
141 }
142 }
143 if (isOpusFound) {
144 newSong.format = format;
145 }
146 }
147 std::string downloadBaseUrl{};
148 auto httpsFind = newSong.format.downloadUrl.find("https://");
149 auto videoPlaybackFind = newSong.format.downloadUrl.find("/videoplayback?");
150 if (httpsFind != std::string::npos && videoPlaybackFind != std::string::npos) {
151 std::string newString00 = "https://";
152 downloadBaseUrl =
153 newSong.format.downloadUrl.substr(httpsFind + newString00.length(), videoPlaybackFind - newString00.length());
154 }
155 std::string requestNew = "GET " + newSong.format.downloadUrl +
156 " HTTP/1.1\n\rUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
157 "Chrome/107.0.0.0 "
158 "Safari/537.36\n\r";
159 requestNew += "Host: " + downloadBaseUrl + "\n\r\n\r";
160 newSong.finalDownloadUrls.resize(2);
161 DiscordCoreAPI::DownloadUrl downloadUrl01{};
162 downloadUrl01.contentSize = newSong.contentLength;
163 downloadUrl01.urlPath = downloadBaseUrl;
164 DiscordCoreAPI::DownloadUrl downloadUrl02{};
165 downloadUrl02.contentSize = newSong.contentLength;
166 downloadUrl02.urlPath = requestNew;
167 newSong.finalDownloadUrls[0] = downloadUrl01;
168 newSong.finalDownloadUrls[1] = downloadUrl02;
169 newSong.viewUrl = newSong.firstDownloadUrl;
170 newSong.contentLength = newSong.format.contentLength;
172 return newSong;
173 } catch (...) {
174 if (currentRecursionDepth <= 10) {
175 ++currentRecursionDepth;
176 return this->constructDownloadInfo(newSong, currentRecursionDepth);
177 } else {
178 if (this->configManager->doWePrintHttpsErrorMessages()) {
179 DiscordCoreAPI::reportException("YouTubeRequestBuilder::constructDownloadInfo()");
180 }
181 return {};
182 }
183 }
184 return {};
185 }
186
187 DiscordCoreAPI::Song YouTubeRequestBuilder::collectFinalSong(DiscordCoreAPI::Song& newSong) {
188 newSong.firstDownloadUrl = this->baseUrl + "/watch?v=" + newSong.songId + "&hl=en";
189 newSong = this->constructDownloadInfo(newSong, 0);
190 return newSong;
191 }
192
193 std::string YouTubeRequestBuilder::collectApiKey() {
194 HttpsWorkloadData dataPackage01{ HttpsWorkloadType::YouTubeGetSearchResults };
195 dataPackage01.baseUrl = YouTubeRequestBuilder::baseUrl;
196 dataPackage01.workloadClass = HttpsWorkloadClass::Get;
197 HttpsResponseData responseData01 = this->httpsClient->submitWorkloadAndGetResult(dataPackage01);
198 std::string apiKey{};
199 if (responseData01.responseData.find("\"innertubeApiKey\":\"") != std::string::npos) {
200 std::string newString = responseData01.responseData.substr(
201 responseData01.responseData.find("\"innertubeApiKey\":\"") + std::string{ "\"innertubeApiKey\":\"" }.size());
202 std::string apiKeyNew = newString.substr(0, newString.find_first_of('"'));
203 apiKey = apiKeyNew;
204 }
205 return apiKey;
206 }
207
208 YouTubeAPI::YouTubeAPI(DiscordCoreAPI::ConfigManager* configManagerNew, HttpsClient* httpsClientNew,
209 const DiscordCoreAPI::Snowflake guildIdNew) {
210 this->configManager = configManagerNew;
211 this->httpsClient = httpsClientNew;
212 this->guildId = static_cast<DiscordCoreAPI::Snowflake>(guildIdNew);
213 if (YouTubeRequestBuilder::apiKey == "") {
214 YouTubeRequestBuilder::apiKey = this->collectApiKey();
215 }
216 }
217
218 void YouTubeAPI::weFailedToDownloadOrDecode(const DiscordCoreAPI::Song& newSong, std::stop_token token, int32_t currentReconnectTries) {
219 ++currentReconnectTries;
221 DiscordCoreAPI::GuildMembers::getCachedGuildMember({ .guildMemberId = newSong.addedByUserId, .guildId = this->guildId });
222 DiscordCoreAPI::Song newerSong = newSong;
223 if (currentReconnectTries > 9) {
225 while (DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.tryReceive(frameData)) {
226 };
228 auto returnValue = DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId);
229 if (returnValue) {
230 eventData.previousSong = returnValue->getCurrentSong(this->guildId);
231 }
232 eventData.wasItAFail = true;
233 eventData.guildMember = guildMember;
234 eventData.guild = DiscordCoreAPI::Guilds::getGuildAsync({ .guildId = this->guildId }).get();
235 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->onSongCompletionEvent(eventData);
236 } else {
237 newerSong = this->collectFinalSong(newerSong);
238 YouTubeAPI::downloadAndStreamAudio(newerSong, token, currentReconnectTries);
239 }
240 }
241
242 void YouTubeAPI::downloadAndStreamAudio(const DiscordCoreAPI::Song& newSong, std::stop_token token, int32_t currentReconnectTries) {
243 try {
244 std::unique_ptr<WebSocketClient> streamSocket{ std::make_unique<WebSocketClient>(nullptr, 0, nullptr) };
245 auto bytesRead{ static_cast<int32_t>(streamSocket->getBytesRead()) };
246 if (newSong.finalDownloadUrls.size() > 0) {
247 if (!static_cast<TCPSSLClient*>(streamSocket.get())
248 ->connect(newSong.finalDownloadUrls[0].urlPath, 443, this->configManager->doWePrintWebSocketErrorMessages(),
249 true)) {
250 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
251 return;
252 }
253 } else {
254 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
255 return;
256 }
257 bool areWeDoneHeaders{};
258 int64_t remainingDownloadContentLength{ static_cast<int64_t>(newSong.contentLength) };
259 int64_t bytesToRead{ static_cast<int64_t>(this->maxBufferSize) };
260 int64_t bytesSubmittedPrevious{};
261 int64_t bytesReadTotal{};
262 const uint8_t maxReruns{ 200 };
263 uint8_t currentReruns{};
264 uint32_t counter{};
265 uint32_t headerSize{};
266 BuildAudioDecoderData dataPackage{};
267 std::string currentString{};
268 dataPackage.totalFileSize = static_cast<uint64_t>(newSong.contentLength);
269 dataPackage.bufferMaxSize = this->maxBufferSize;
270 dataPackage.configManager = this->configManager;
271 std::unique_ptr<AudioDecoder> audioDecoder = std::make_unique<AudioDecoder>(dataPackage);
272 std::string string = newSong.finalDownloadUrls[1].urlPath;
273 streamSocket->writeData(string, true);
274 std::vector<DiscordCoreAPI::AudioFrameData> frames{};
275 streamSocket->processIO(1000);
276 if (!streamSocket->areWeStillConnected()) {
277 audioDecoder.reset(nullptr);
278 streamSocket->disconnect();
279 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
280 return;
281 }
282 while (remainingDownloadContentLength > 0) {
283 std::this_thread::sleep_for(1ms);
284 if (bytesSubmittedPrevious == bytesReadTotal) {
285 ++currentReruns;
286 } else {
287 currentReruns = 0;
288 }
289 if (currentReruns >= maxReruns) {
292 frameData.currentSize = 0;
293 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(frameData));
294 streamSocket->disconnect();
295 audioDecoder.reset(nullptr);
296 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
297 return;
298 }
299 bytesSubmittedPrevious = bytesReadTotal;
300 if (audioDecoder->haveWeFailed()) {
301 streamSocket->disconnect();
302 audioDecoder.reset(nullptr);
303 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
304 return;
305 }
306 if (token.stop_requested()) {
307 streamSocket->disconnect();
308 audioDecoder.reset(nullptr);
309 return;
310 } else {
311 if (!areWeDoneHeaders) {
312 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
313 streamSocket->processIO(10);
314 if (!streamSocket->areWeStillConnected()) {
315 streamSocket->disconnect();
316 audioDecoder.reset(nullptr);
317 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
318 return;
319 }
320 if (!token.stop_requested()) {
321 if (streamSocket->areWeStillConnected()) {
322 bytesReadTotal = streamSocket->getBytesRead() - headerSize;
323 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
324 headerSize = static_cast<int32_t>(streamBufferReal.size());
325 }
326 }
327 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
328 areWeDoneHeaders = true;
329 }
330 if (token.stop_requested()) {
331 streamSocket->disconnect();
332 audioDecoder.reset(nullptr);
333 return;
334 }
335 if (counter == 0) {
336 streamSocket->processIO(10);
337 if (!streamSocket->areWeStillConnected()) {
338 streamSocket->disconnect();
339 audioDecoder.reset(nullptr);
340 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
341 return;
342 }
343 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
344 if (streamBufferReal.size() > 0) {
345 currentString.insert(currentString.end(), streamBufferReal.data(),
346 streamBufferReal.data() + streamBufferReal.size());
347 std::string submissionString{};
348 if (currentString.size() >= this->maxBufferSize) {
349 submissionString.insert(submissionString.begin(), currentString.data(),
350 currentString.data() + this->maxBufferSize);
351 currentString.erase(currentString.begin(), currentString.begin() + this->maxBufferSize);
352 } else {
353 submissionString = std::move(currentString);
354 currentString.clear();
355 }
356 bytesReadTotal = streamSocket->getBytesRead();
357 audioDecoder->submitDataForDecoding(std::move(submissionString));
358 }
359 audioDecoder->startMe();
360 } else if (counter > 0) {
361 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
362 streamSocket->processIO(10);
363 if (!streamSocket->areWeStillConnected()) {
364 streamSocket->disconnect();
365 audioDecoder.reset(nullptr);
366 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
367 return;
368 }
369 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
370
371 if (streamBufferReal.size() > 0) {
372 currentString.insert(currentString.end(), streamBufferReal.data(),
373 streamBufferReal.data() + streamBufferReal.size());
374 while (currentString.size() > 0) {
375 std::string submissionString{};
376 if (currentString.size() >= this->maxBufferSize) {
377 submissionString.insert(submissionString.begin(), currentString.data(),
378 currentString.data() + this->maxBufferSize);
379 currentString.erase(currentString.begin(), currentString.begin() + this->maxBufferSize);
380 } else {
381 submissionString = std::move(currentString);
382 currentString.clear();
383 }
384 audioDecoder->submitDataForDecoding(std::move(submissionString));
385 bytesReadTotal = streamSocket->getBytesRead();
386 }
387 }
388 if (token.stop_requested()) {
389 streamSocket->disconnect();
390 audioDecoder.reset(nullptr);
391 return;
392 }
393 while (true) {
395 if (!audioDecoder->getFrame(rawFrame)) {
396 break;
397 } else {
398 if (rawFrame.currentSize == -5) {
399 break;
400 }
401 if (rawFrame.currentSize > 3) {
402 frames.emplace_back(std::move(rawFrame));
403 }
404 }
405 }
406 for (auto iterator = frames.begin(); iterator != frames.end();) {
407 iterator->guildMemberId = static_cast<DiscordCoreAPI::Song>(newSong).addedByUserId.operator size_t();
408 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(*iterator));
409 iterator = frames.erase(iterator);
410 }
411 }
412 if (remainingDownloadContentLength >= this->maxBufferSize) {
413 bytesToRead = this->maxBufferSize;
414 } else {
415 bytesToRead = remainingDownloadContentLength;
416 }
417 }
418 ++counter;
419 }
420 DiscordCoreAPI::AudioFrameData frameData01{};
421 while (audioDecoder->getFrame(frameData01)) {
422 };
423 audioDecoder.reset(nullptr);
426 frameData.currentSize = 0;
427 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(frameData));
428 } catch (...) {
429 if (this->configManager->doWePrintWebSocketErrorMessages()) {
430 DiscordCoreAPI::reportException("YouTubeAPI::downloadAndStreamAudio()");
431 }
432 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
433 }
434 }
435
436 std::vector<DiscordCoreAPI::Song> YouTubeAPI::searchForSong(const std::string& searchQuery) {
437 return this->collectSearchResults(searchQuery);
438 }
439
440 DiscordCoreAPI::Song YouTubeAPI::collectFinalSong(DiscordCoreAPI::Song& newSong) {
441 return YouTubeRequestBuilder::collectFinalSong(newSong);
442 }
443
444 std::string YouTubeRequestBuilder::apiKey{};
445}
DiscordCoreAPI_Dll void reportException(const std::string &currentFunctionName, std::source_location location=std::source_location::current())
Prints the current file, line, and column from which the function is being called - typically from wi...
Definition: Utilities.cpp:763
Data structure representing a single GuildMember.
Represents a download Url.
A song from the various platforms.
std::string viewUrl
The url for listening to this Song through a browser.
Snowflake addedByUserId
The User id of the individual who added this Song to the playlist.
SongType type
The type of song.
static CoRoutine< Guild > getGuildAsync(GetGuildData dataPackage)
Collects a Guild from the Discord servers.
static GuildMemberData getCachedGuildMember(GetGuildMemberData dataPackage)
Collects a GuildMember from the library's cache.
Represents a single frame of audio data.
Definition: Utilities.hpp:832
AudioFrameType type
The type of audio frame.
Definition: Utilities.hpp:833
uint64_t guildMemberId
GuildMemberId for the sending GuildMember.
Definition: Utilities.hpp:835