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.responseMessage.c_str() << DiscordCoreAPI::reset() << endl
45 << endl;
46 }
47 simdjson::ondemand::value partialSearchResultsJson{};
48
49 std::vector<DiscordCoreAPI::Song> searchResults{};
50 auto varInitFind = returnData.responseMessage.find("var ytInitialData = ");
51 if (varInitFind != std::string::npos) {
52 std::string newString00 = "var ytInitialData = ";
53 std::string newString = returnData.responseMessage.substr(varInitFind + newString00.length());
54 std::string stringSequence = ";</script><script nonce=";
55 newString = newString.substr(0, newString.find(stringSequence));
56 newString.reserve(newString.size() + simdjson::SIMDJSON_PADDING);
57 simdjson::ondemand::parser parser{};
58 partialSearchResultsJson = parser.iterate(newString.data(), newString.length(), newString.capacity());
59 simdjson::ondemand::value objectContents{};
60 if (partialSearchResultsJson["contents"].get(objectContents) == simdjson::error_code::SUCCESS) {
61 if (objectContents["twoColumnSearchResultsRenderer"].get(objectContents) == simdjson::error_code::SUCCESS) {
62 if (objectContents["primaryContents"].get(objectContents) == simdjson::error_code::SUCCESS) {
63 if (objectContents["sectionListRenderer"].get(objectContents) == simdjson::error_code::SUCCESS) {
64 if (objectContents["contents"].get(objectContents) == simdjson::error_code::SUCCESS) {
65 if (objectContents.at(0).get(objectContents) == simdjson::error_code::SUCCESS) {
66 if (objectContents["itemSectionRenderer"].get(objectContents) == simdjson::error_code::SUCCESS) {
67 if (objectContents["contents"].get(objectContents) == simdjson::error_code::SUCCESS) {
68 for (auto iterator: objectContents) {
69 DiscordCoreAPI::Song searchResult{};
70 simdjson::ondemand::value object{};
71 if (iterator["videoRenderer"].get(object) == simdjson::error_code::SUCCESS) {
72 searchResult = DiscordCoreAPI::Song{ object };
73 }
75 searchResult.viewUrl = this->baseUrl + "/watch?v=" + searchResult.songId + "&hl=en";
76 if (searchResult.description == "" || searchResult.viewUrl == "") {
77 continue;
78 }
79 searchResults.emplace_back(searchResult);
80 }
81 }
82 }
83 }
84 }
85 }
86 }
87 }
88 }
89 }
90 return searchResults;
91 }
92
93 DiscordCoreAPI::Song YouTubeRequestBuilder::constructDownloadInfo(DiscordCoreAPI::Song& newSong, int32_t currentRecursionDepth) {
94 HttpsResponseData responseData{};
95 try {
96 DiscordCoreAPI::Jsonifier request{};
97 request["videoId"] = newSong.songId;
98 request["contentCheckOk"] = true;
99 request["racyCheckOk"] = true;
100 request["context"]["client"]["clientName"] = "ANDROID";
101 request["context"]["client"]["clientScreen"] = "EMBED";
102 request["context"]["client"]["clientVersion"] = "16.46.37";
103 request["context"]["client"]["hl"] = "en";
104 request["context"]["client"]["gl"] = "US";
105 request["context"]["client"]["utcOffsetMinutes"] = 0;
106 request["context"]["embedUrl"] = "https://www.youtube.com";
107 HttpsWorkloadData dataPackage02{ HttpsWorkloadType::YouTubeGetSearchResults };
108 dataPackage02.baseUrl = YouTubeRequestBuilder::baseUrl;
109 dataPackage02.relativePath = "/youtubei/v1/player?key=" + YouTubeRequestBuilder::apiKey;
110 request.refreshString(DiscordCoreAPI::JsonifierSerializeType::Json);
111 dataPackage02.content = request.operator std::string();
112 dataPackage02.workloadClass = HttpsWorkloadClass::Post;
113 responseData = this->httpsClient->submitWorkloadAndGetResult(dataPackage02);
114 if (responseData.responseCode != 204 && responseData.responseCode != 201 && responseData.responseCode != 200 &&
115 this->configManager->doWePrintHttpsErrorMessages()) {
116 cout << DiscordCoreAPI::shiftToBrightRed() << "YouTubeRequestBuilder::constructDownloadInfo() 01 Error: " << responseData.responseCode
117 << ", " << responseData.responseMessage << DiscordCoreAPI::reset() << endl
118 << endl;
119 }
121 responseData.responseMessage.reserve(responseData.responseMessage.size() + simdjson::SIMDJSON_PADDING);
122 simdjson::ondemand::parser parser{};
123 simdjson::ondemand::value value{};
124 if (parser.iterate(responseData.responseMessage.data(), responseData.responseMessage.length(), responseData.responseMessage.capacity())
125 .get(value) == simdjson::error_code::SUCCESS) {
127 DiscordCoreAPI::YouTubeFormat format{};
128 bool isOpusFound{};
129 for (auto& value: static_cast<std::vector<DiscordCoreAPI::YouTubeFormat>>(vector)) {
130 if (value.mimeType.find("opus") != std::string::npos) {
131 if (value.audioQuality == "AUDIO_QUALITY_LOW") {
132 isOpusFound = true;
133 format = value;
134 }
135 if (value.audioQuality == "AUDIO_QUALITY_MEDIUM") {
136 isOpusFound = true;
137 format = value;
138 }
139 if (value.audioQuality == "AUDIO_QUALITY_HIGH") {
140 isOpusFound = true;
141 format = value;
142 }
143 }
144 }
145 if (isOpusFound) {
146 newSong.format = format;
147 }
148 }
149 std::string downloadBaseUrl{};
150 auto httpsFind = newSong.format.downloadUrl.find("https://");
151 auto videoPlaybackFind = newSong.format.downloadUrl.find("/videoplayback?");
152 if (httpsFind != std::string::npos && videoPlaybackFind != std::string::npos) {
153 std::string newString00 = "https://";
154 downloadBaseUrl = newSong.format.downloadUrl.substr(httpsFind + newString00.length(), videoPlaybackFind - newString00.length());
155 }
156 std::string requestNew = "GET " + newSong.format.downloadUrl +
157 " HTTP/1.1\n\rUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 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.responseMessage.find("\"innertubeApiKey\":\"") != std::string::npos) {
200 std::string newString = responseData01.responseMessage.substr(
201 responseData01.responseMessage.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, const DiscordCoreAPI::Snowflake guildIdNew) {
209 this->configManager = configManagerNew;
210 this->httpsClient = httpsClientNew;
211 this->guildId = static_cast<DiscordCoreAPI::Snowflake>(guildIdNew);
212 if (YouTubeRequestBuilder::apiKey == "") {
213 YouTubeRequestBuilder::apiKey = this->collectApiKey();
214 }
215 }
216
217 void YouTubeAPI::weFailedToDownloadOrDecode(const DiscordCoreAPI::Song& newSong, std::stop_token token, int32_t currentReconnectTries) {
218 ++currentReconnectTries;
220 DiscordCoreAPI::GuildMembers::getCachedGuildMember({ .guildMemberId = newSong.addedByUserId, .guildId = this->guildId });
221 DiscordCoreAPI::Song newerSong = newSong;
222 if (currentReconnectTries > 9) {
224 while (DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.tryReceive(frameData)) {
225 };
227 auto returnValue = DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId);
228 if (returnValue) {
229 eventData.previousSong = returnValue->getCurrentSong(this->guildId);
230 }
231 eventData.wasItAFail = true;
232 eventData.guildMember = guildMember;
233 eventData.guild = DiscordCoreAPI::Guilds::getGuildAsync({ .guildId = this->guildId }).get();
234 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->onSongCompletionEvent(eventData);
235 } else {
236 newerSong = this->collectFinalSong(newerSong);
237 YouTubeAPI::downloadAndStreamAudio(newerSong, token, currentReconnectTries);
238 }
239 }
240
241 void YouTubeAPI::downloadAndStreamAudio(const DiscordCoreAPI::Song& newSong, std::stop_token token, int32_t currentReconnectTries) {
242 try {
243 std::unique_ptr<WebSocketClient> streamSocket{ std::make_unique<WebSocketClient>(nullptr, 0, nullptr) };
244 auto bytesRead{ static_cast<int32_t>(streamSocket->getBytesRead()) };
245 if (newSong.finalDownloadUrls.size() > 0) {
246 if (!static_cast<TCPSSLClient*>(streamSocket.get())
247 ->connect(newSong.finalDownloadUrls[0].urlPath, 443, this->configManager->doWePrintWebSocketErrorMessages(), true)) {
248 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
249 return;
250 }
251 } else {
252 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
253 return;
254 }
255 bool areWeDoneHeaders{};
256 int64_t remainingDownloadContentLength{ static_cast<int64_t>(newSong.contentLength) };
257 int64_t bytesToRead{ static_cast<int64_t>(this->maxBufferSize) };
258 int64_t bytesSubmittedPrevious{};
259 int64_t bytesReadTotal{};
260 const uint8_t maxReruns{ 200 };
261 uint8_t currentReruns{};
262 uint32_t counter{};
263 uint32_t headerSize{};
264 BuildAudioDecoderData dataPackage{};
265 std::string currentString{};
266 dataPackage.totalFileSize = static_cast<uint64_t>(newSong.contentLength);
267 dataPackage.bufferMaxSize = this->maxBufferSize;
268 dataPackage.configManager = this->configManager;
269 std::unique_ptr<AudioDecoder> audioDecoder = std::make_unique<AudioDecoder>(dataPackage);
270 std::string string = newSong.finalDownloadUrls[1].urlPath;
271 streamSocket->writeData(string, true);
272 std::vector<DiscordCoreAPI::AudioFrameData> frames{};
273 streamSocket->processIO(1000);
274 if (!streamSocket->areWeStillConnected()) {
275 audioDecoder.reset(nullptr);
276 streamSocket->disconnect();
277 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
278 return;
279 }
280 while (remainingDownloadContentLength > 0) {
281 std::this_thread::sleep_for(1ms);
282 if (bytesSubmittedPrevious == bytesReadTotal) {
283 ++currentReruns;
284 } else {
285 currentReruns = 0;
286 }
287 if (currentReruns >= maxReruns) {
290 frameData.currentSize = 0;
291 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(frameData));
292 streamSocket->disconnect();
293 audioDecoder.reset(nullptr);
294 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
295 return;
296 }
297 bytesSubmittedPrevious = bytesReadTotal;
298 if (audioDecoder->haveWeFailed()) {
299 streamSocket->disconnect();
300 audioDecoder.reset(nullptr);
301 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
302 return;
303 }
304 if (token.stop_requested()) {
305 streamSocket->disconnect();
306 audioDecoder.reset(nullptr);
307 return;
308 } else {
309 if (!areWeDoneHeaders) {
310 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
311 streamSocket->processIO(10);
312 if (!streamSocket->areWeStillConnected()) {
313 streamSocket->disconnect();
314 audioDecoder.reset(nullptr);
315 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
316 return;
317 }
318 if (!token.stop_requested()) {
319 if (streamSocket->areWeStillConnected()) {
320 bytesReadTotal = streamSocket->getBytesRead() - headerSize;
321 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
322 headerSize = static_cast<int32_t>(streamBufferReal.size());
323 }
324 }
325 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
326 areWeDoneHeaders = true;
327 }
328 if (token.stop_requested()) {
329 streamSocket->disconnect();
330 audioDecoder.reset(nullptr);
331 return;
332 }
333 if (counter == 0) {
334 streamSocket->processIO(10);
335 if (!streamSocket->areWeStillConnected()) {
336 streamSocket->disconnect();
337 audioDecoder.reset(nullptr);
338 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
339 return;
340 }
341 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
342 if (streamBufferReal.size() > 0) {
343 currentString.insert(currentString.end(), streamBufferReal.data(), streamBufferReal.data() + streamBufferReal.size());
344 std::string submissionString{};
345 if (currentString.size() >= this->maxBufferSize) {
346 submissionString.insert(submissionString.begin(), currentString.data(), currentString.data() + this->maxBufferSize);
347 currentString.erase(currentString.begin(), currentString.begin() + this->maxBufferSize);
348 } else {
349 submissionString = std::move(currentString);
350 currentString.clear();
351 }
352 bytesReadTotal = streamSocket->getBytesRead();
353 audioDecoder->submitDataForDecoding(std::move(submissionString));
354 }
355 audioDecoder->startMe();
356 } else if (counter > 0) {
357 remainingDownloadContentLength = newSong.contentLength - bytesReadTotal;
358 streamSocket->processIO(10);
359 if (!streamSocket->areWeStillConnected()) {
360 streamSocket->disconnect();
361 audioDecoder.reset(nullptr);
362 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
363 return;
364 }
365 std::string streamBufferReal = static_cast<std::string>(streamSocket->getInputBuffer());
366
367 if (streamBufferReal.size() > 0) {
368 currentString.insert(currentString.end(), streamBufferReal.data(), streamBufferReal.data() + streamBufferReal.size());
369 while (currentString.size() > 0) {
370 std::string submissionString{};
371 if (currentString.size() >= this->maxBufferSize) {
372 submissionString.insert(submissionString.begin(), currentString.data(),
373 currentString.data() + this->maxBufferSize);
374 currentString.erase(currentString.begin(), currentString.begin() + this->maxBufferSize);
375 } else {
376 submissionString = std::move(currentString);
377 currentString.clear();
378 }
379 audioDecoder->submitDataForDecoding(std::move(submissionString));
380 bytesReadTotal = streamSocket->getBytesRead();
381 }
382 }
383 if (token.stop_requested()) {
384 streamSocket->disconnect();
385 audioDecoder.reset(nullptr);
386 return;
387 }
388 while (true) {
390 if (!audioDecoder->getFrame(rawFrame)) {
391 break;
392 } else {
393 if (rawFrame.currentSize == -5) {
394 break;
395 }
396 if (rawFrame.currentSize > 3) {
397 frames.emplace_back(std::move(rawFrame));
398 }
399 }
400 }
401 for (auto iterator = frames.begin(); iterator != frames.end();) {
402 iterator->guildMemberId = static_cast<DiscordCoreAPI::Song>(newSong).addedByUserId.operator size_t();
403 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(*iterator));
404 iterator = frames.erase(iterator);
405 }
406 }
407 if (remainingDownloadContentLength >= this->maxBufferSize) {
408 bytesToRead = this->maxBufferSize;
409 } else {
410 bytesToRead = remainingDownloadContentLength;
411 }
412 }
413 ++counter;
414 }
415 DiscordCoreAPI::AudioFrameData frameData01{};
416 while (audioDecoder->getFrame(frameData01)) {
417 };
418 audioDecoder.reset(nullptr);
421 frameData.currentSize = 0;
422 DiscordCoreAPI::DiscordCoreClient::getSongAPI(this->guildId)->audioDataBuffer.send(std::move(frameData));
423 } catch (...) {
424 if (this->configManager->doWePrintWebSocketErrorMessages()) {
425 DiscordCoreAPI::reportException("YouTubeAPI::downloadAndStreamAudio()");
426 }
427 this->weFailedToDownloadOrDecode(newSong, token, currentReconnectTries);
428 }
429 }
430
431 std::vector<DiscordCoreAPI::Song> YouTubeAPI::searchForSong(const std::string& searchQuery) {
432 return this->collectSearchResults(searchQuery);
433 }
434
435 DiscordCoreAPI::Song YouTubeAPI::collectFinalSong(DiscordCoreAPI::Song& newSong) {
436 return YouTubeRequestBuilder::collectFinalSong(newSong);
437 }
438
439 std::string YouTubeRequestBuilder::apiKey{};
440}
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:1498
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:1284
AudioFrameType type
The type of audio frame.
Definition: Utilities.hpp:1285
uint64_t guildMemberId
GuildMemberId for the sending GuildMember.
Definition: Utilities.hpp:1287