From 94d51f66cb60ddd35bd9be28a1acf27706b34917 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:52:49 +0200 Subject: [PATCH 01/58] Adding video statistics functionality -> able to fetch video statistics by id -> new datatype to hold information -> adding example -> adding keywords --- examples/VideoStatistics/VideoStatistics.ino | 107 +++++++++++++++++++ keywords.txt | 1 + platformio.ini | 9 +- src/YoutubeApi.cpp | 57 ++++++++++ src/YoutubeApi.h | 12 +++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 examples/VideoStatistics/VideoStatistics.ino diff --git a/examples/VideoStatistics/VideoStatistics.ino b/examples/VideoStatistics/VideoStatistics.ino new file mode 100644 index 0000000..deb2485 --- /dev/null +++ b/examples/VideoStatistics/VideoStatistics.ino @@ -0,0 +1,107 @@ +/******************************************************************* + Read YouTube Video statistics from the YouTube API + and print them to the serial monitor + + Compatible Boards: + * Any ESP8266 board + * Any ESP32 board + + Recommended Board: D1 Mini ESP8266 + http://s.click.aliexpress.com/e/uzFUnIe (affiliate) + + If you find what I do useful and would like to support me, + please consider becoming a sponsor on Github + https://github.com/sponsors/witnessmenow/ + + Written by Brian Lough and modified by Colum31 + YouTube: https://www.youtube.com/brianlough + Tindie: https://www.tindie.com/stores/brianlough/ + Twitter: https://twitter.com/witnessmenow + *******************************************************************/ + +// ---------------------------- +// Standard Libraries +// ---------------------------- + +#if defined(ESP8266) + #include +#elif defined(ESP32) + #include +#endif + +#include + +// ---------------------------- +// Additional Libraries - each of these will need to be installed +// ---------------------------- + +// Library for connecting to the YouTube API +// https://github.com/witnessmenow/arduino-youtube-api +// (search for "youtube" in the Arduino Library Manager) +#include + +// Library used for parsing Json from the API responses +// https://github.com/bblanchon/ArduinoJson +// (search for "Arduino Json" in the Arduino Library Manager) +#include + +//------- Replace the following! ------ +const char ssid[] = "xxx"; // your network SSID (name) +const char password[] = "yyyy"; // your network key +#define API_KEY "zzzz" // your Google API key +#define VIDEO_ID "dQw4w9WgXcQ" // part of the video url +//------- ---------------------- ------ + +WiFiClientSecure client; +YoutubeApi api(API_KEY, client); + +unsigned long timeBetweenRequests = 60 * 1000; // 60 seconds, in milliseconds + +void setup() { + Serial.begin(115200); + + // Set WiFi to 'station' mode and disconnect + // from the AP if it was previously connected + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + // Connect to the WiFi network + Serial.print("\nConnecting to WiFi: "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.println("\nWiFi connected!"); + Serial.print("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); + + #ifdef ESP8266 + // Required if you are using ESP8266 V2.5 or above + client.setInsecure(); + #endif + + // Uncomment for extra debugging info + // api._debug = true; +} + +void loop() { + if(api.getVideoStatistics(VIDEO_ID)){ + Serial.println("\n--------Video Stats---------"); + + Serial.print("Video views: "); + Serial.println(api.videoStats.viewCount); + + Serial.print("Likes: "); + Serial.println(api.videoStats.likeCount); + + Serial.print("Comments: "); + Serial.println(api.videoStats.commentCount); + + Serial.println("------------------------"); + } + delay(timeBetweenRequests); +} diff --git a/keywords.txt b/keywords.txt index f8a30fe..3d106e8 100644 --- a/keywords.txt +++ b/keywords.txt @@ -11,6 +11,7 @@ YoutubeApi KEYWORD1 # API Data channelStatistics KEYWORD1 +videoStatistics KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) diff --git a/platformio.ini b/platformio.ini index 0702987..2bd6fb3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,7 +6,8 @@ ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples -; http://docs.platformio.org/page/projectconf.html +; https://docs.platformio.org/page/projectconf.html + [common] lib_deps_external = ArduinoJson @@ -15,3 +16,9 @@ platform = espressif8266 board = d1_mini framework = arduino lib_deps = ${common.lib_deps_external} + +[env:esp32_devkit] +platform = espressif32 +board = esp32doit-devkit-v1 +framework = arduino +lib_deps = ${common.lib_deps_external} diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 59977c3..c31c791 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -133,6 +133,63 @@ bool YoutubeApi::getChannelStatistics(const String& channelId) { return getChannelStatistics(channelId.c_str()); } +bool YoutubeApi::getVideoStatistics(const char *videoId){ + char command[150] = YTAPI_VIDEO_ENDPOINT; + char params[120]; + sprintf(params, "?part=statistics&id=%s&key=%s", videoId, apiKey.c_str()); + strcat(command, params); + + if (_debug) + { + Serial.println(command); + } + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(2) + + 2*JSON_OBJECT_SIZE(4) + + JSON_OBJECT_SIZE(5) + + 330; + + int httpStatus = sendGetToYoutube(command); + + if (httpStatus == 200) + { + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client); + if (!error) + { + wasSuccessful = true; + + JsonObject itemStatistics = doc["items"][0]["statistics"]; + + videoStats.viewCount = itemStatistics["viewCount"].as(); + videoStats.likeCount = itemStatistics["likeCount"].as(); + videoStats.commentCount= itemStatistics["commentCount"].as(); + } + else + { + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + } else { + Serial.print("Unexpected HTTP Status Code: "); + Serial.println(httpStatus); + } + closeClient(); + + return wasSuccessful; +} + +bool YoutubeApi::getVideoStatistics(const String& videoId){ + return getVideoStatistics(videoId.c_str()); +} + void YoutubeApi::skipHeaders() { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 28169ad..fc3d1a0 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -36,6 +36,7 @@ #define YTAPI_TIMEOUT 1500 #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" +#define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" struct channelStatistics { long viewCount; @@ -45,6 +46,14 @@ struct channelStatistics { long videoCount; }; +struct videoStatistics { + long viewCount; + long commentCount; + long likeCount; +// long dislikeCount; +// In Memory of the old dislike count. +}; + class YoutubeApi { public: @@ -54,7 +63,10 @@ class YoutubeApi int sendGetToYoutube(const String& command); bool getChannelStatistics(const char *channelId); bool getChannelStatistics(const String& channelId); + bool getVideoStatistics(const char *videoId); + bool getVideoStatistics(const String& videoId); channelStatistics channelStats; + videoStatistics videoStats; bool _debug = false; private: From ed6a161764df9851780a927467fec23bd272b621 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 12 Apr 2022 20:45:05 +0200 Subject: [PATCH 02/58] adding new datatype to hold video info -> adding struct -> adding functions to fill structs --- src/YoutubeApi.cpp | 8 ++++++++ src/YoutubeApi.h | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index c31c791..5babc4f 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -190,6 +190,14 @@ bool YoutubeApi::getVideoStatistics(const String& videoId){ return getVideoStatistics(videoId.c_str()); } + +bool YoutubeApi::getVideoInfo(const char *videoId){ + +} +bool YoutubeApi::getVideoInfo(const String& videoId){ + return getVideoInfo(videoId.c_str()); +} + void YoutubeApi::skipHeaders() { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index fc3d1a0..5241f77 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -30,6 +30,7 @@ #include #include #include +#include #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 @@ -38,6 +39,8 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" +#define YT_VIDEO_TITLE_MAX_LENGTH 100 + struct channelStatistics { long viewCount; long commentCount; /* DEPRECATED */ @@ -46,6 +49,16 @@ struct channelStatistics { long videoCount; }; +// some of the most important informations about a video +struct videoInformation{ + char videoTitle[YT_VIDEO_TITLE_MAX_LENGTH + 1]; + tm videoDuration; + tm publishedAt; + int categoryId; + char defaultLanguage[4]; +}; + + struct videoStatistics { long viewCount; long commentCount; @@ -65,8 +78,11 @@ class YoutubeApi bool getChannelStatistics(const String& channelId); bool getVideoStatistics(const char *videoId); bool getVideoStatistics(const String& videoId); + bool getVideoInfo(const char *videoId); + bool getVideoInfo(const String& videoId); channelStatistics channelStats; videoStatistics videoStats; + videoInformation videoInfo; bool _debug = false; private: From c4173297fff955d01f7449d62309643f70117b77 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 16 Apr 2022 01:02:33 +0200 Subject: [PATCH 03/58] Implement video.list:contentDetails -> also added some error checking and documentation --- src/YoutubeApi.cpp | 211 ++++++++++++++++++++++++++++++++++++++++++--- src/YoutubeApi.h | 21 ++--- 2 files changed, 211 insertions(+), 21 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 5babc4f..854e0c5 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -133,6 +133,13 @@ bool YoutubeApi::getChannelStatistics(const String& channelId) { return getChannelStatistics(channelId.c_str()); } + +/** + * @brief Gets the statistics of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ bool YoutubeApi::getVideoStatistics(const char *videoId){ char command[150] = YTAPI_VIDEO_ENDPOINT; char params[120]; @@ -162,8 +169,16 @@ bool YoutubeApi::getVideoStatistics(const char *videoId){ // Parse JSON object DeserializationError error = deserializeJson(doc, client); - if (!error) - { + + if (error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.print("No results found for video id "); + Serial.println(videoId); + } + else{ wasSuccessful = true; JsonObject itemStatistics = doc["items"][0]["statistics"]; @@ -172,11 +187,6 @@ bool YoutubeApi::getVideoStatistics(const char *videoId){ videoStats.likeCount = itemStatistics["likeCount"].as(); videoStats.commentCount= itemStatistics["commentCount"].as(); } - else - { - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } } else { Serial.print("Unexpected HTTP Status Code: "); Serial.println(httpStatus); @@ -190,12 +200,191 @@ bool YoutubeApi::getVideoStatistics(const String& videoId){ return getVideoStatistics(videoId.c_str()); } +/** + * @brief Parses the ISO8601 duration string into a tm time struct. + * + * @param duration Pointer to string + * @return tm Time struct corresponding to duration. When sucessful, it's non zero. + */ +tm YoutubeApi::parseDuration(const char *duration){ + + tm ret; + memset(&ret, 0, sizeof(tm)); + + if(!duration){ + return ret; + } + + char temp[3]; + + int len = strlen(duration); + int marker = len - 1; + + bool secondsSet, minutesSet, hoursSet, daysSet; + secondsSet = minutesSet = hoursSet = daysSet = false; + + for(int i = len - 1; i >= 0; i--){ + + char c = duration[i]; + + if(c == 'S'){ + secondsSet = true; + marker = i - 1; + continue; + } + + if(c == 'M'){ + minutesSet = true; + + if(secondsSet){ + memcpy(&temp, &duration[i + 1], marker - i + 1); + ret.tm_sec = atoi(temp); + + secondsSet = false; + } + marker = i - 1; + continue; + } + + if(c == 'H'){ + hoursSet = true; + + if(secondsSet || minutesSet){ + + memcpy(&temp, &duration[i + 1], marker - i + 1); + int time = atoi(temp); + + if(secondsSet){ + ret.tm_sec = time; + secondsSet = false; + }else{ + ret.tm_min = time; + minutesSet = false; + } + } + + marker = i - 1; + continue; + } + + if(c == 'T'){ + + if(secondsSet || minutesSet || hoursSet){ + + memcpy(&temp, &duration[i + 1], marker - i + 1); + int time = atoi(temp); + + if(secondsSet){ + ret.tm_sec = time; + }else if (minutesSet){ + ret.tm_min = time; + }else{ + ret.tm_hour = time; + } + } + } + // a video can be as long as days + if(c == 'D'){ + marker = i - 1; + daysSet = true; + } + + if(c == 'P' && daysSet){ + + memcpy(&temp, &duration[i + 1], marker - i + 1); + int time = atoi(temp); -bool YoutubeApi::getVideoInfo(const char *videoId){ - + ret.tm_mday = time; + + } + } + return ret; +} + + +/** + * @brief Gets the content details of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeApi::getContentDetails(const char *videoId){ + char command[150] = YTAPI_VIDEO_ENDPOINT; + char params[120]; + sprintf(params, "?part=contentDetails&id=%s&key=%s", videoId, apiKey.c_str()); + strcat(command, params); + + if (_debug) + { + Serial.println(command); + } + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 768; + + int httpStatus = sendGetToYoutube(command); + + if (httpStatus == 200) + { + // Creating a filter to filter out + // region restrictions, content rating and metadata + DynamicJsonDocument filter(144); + + JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); + filterItems["duration"] = true; + filterItems["dimension"] = true; + filterItems["definition"] = true; + filterItems["caption"] = true; + filterItems["licensedContent"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.print("No results found for video id "); + Serial.println(videoId); + } + else{ + wasSuccessful = true; + + JsonObject itemcontentDetails = doc["items"][0]["contentDetails"]; + + memcpy(contentDets.defintion, itemcontentDetails["definition"].as(), 3); + + memcpy(contentDets.dimension, itemcontentDetails["dimension"].as(), 3); + + if(strcmp("false", itemcontentDetails["caption"].as()) != 0){ + contentDets.caption = true; + } + else{ + contentDets.caption = false; + } + + contentDets.licensedContent = itemcontentDetails["licensedContent"].as(); + contentDets.duration = parseDuration(itemcontentDetails["duration"].as()); + } + + } else { + Serial.print("Unexpected HTTP Status Code: "); + Serial.println(httpStatus); + } + closeClient(); + + return wasSuccessful; } -bool YoutubeApi::getVideoInfo(const String& videoId){ - return getVideoInfo(videoId.c_str()); +bool YoutubeApi::getContentDetails(const String& videoId){ + return getContentDetails(videoId.c_str()); } void YoutubeApi::skipHeaders() { diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 5241f77..d6a09ec 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -49,13 +49,13 @@ struct channelStatistics { long videoCount; }; -// some of the most important informations about a video -struct videoInformation{ - char videoTitle[YT_VIDEO_TITLE_MAX_LENGTH + 1]; - tm videoDuration; - tm publishedAt; - int categoryId; - char defaultLanguage[4]; + +struct contentDetails{ + tm duration; + char dimension[3]; + char defintion[3]; + bool caption; + bool licensedContent; }; @@ -78,16 +78,17 @@ class YoutubeApi bool getChannelStatistics(const String& channelId); bool getVideoStatistics(const char *videoId); bool getVideoStatistics(const String& videoId); - bool getVideoInfo(const char *videoId); - bool getVideoInfo(const String& videoId); + bool getContentDetails(const char *videoId); + bool getContentDetails(const String& videoId); channelStatistics channelStats; videoStatistics videoStats; - videoInformation videoInfo; + contentDetails contentDets; bool _debug = false; private: const String apiKey; Client &client; + tm parseDuration(const char *duration); int getHttpStatusCode(); void skipHeaders(); void closeClient(); From d17aba0206c21345ac059cbe0fb44401898a3c83 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:52:34 +0200 Subject: [PATCH 04/58] Implement video.list:snippet -> although not final, library now able to fetch important informations about videos (title, description, publish date etc.) --- src/YoutubeApi.cpp | 213 ++++++++++++++++++++++++++++++++++++++++++++- src/YoutubeApi.h | 22 ++++- 2 files changed, 230 insertions(+), 5 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 854e0c5..fd40694 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -24,6 +24,13 @@ * */ +// TODO +// address code duplication +// +// add video.list:status +// video.list:topicDetails +// +// store retrieved data in heap instead of stack #include "YoutubeApi.h" @@ -138,7 +145,7 @@ bool YoutubeApi::getChannelStatistics(const String& channelId) { * @brief Gets the statistics of a specific video. Stores them in the calling object. * * @param videoId videoID of the video to get the information from - * @return wasSuccesssful true, if there were no errors and the video was found + * @return true, if there were no errors and the video was found */ bool YoutubeApi::getVideoStatistics(const char *videoId){ char command[150] = YTAPI_VIDEO_ENDPOINT; @@ -204,10 +211,11 @@ bool YoutubeApi::getVideoStatistics(const String& videoId){ * @brief Parses the ISO8601 duration string into a tm time struct. * * @param duration Pointer to string - * @return tm Time struct corresponding to duration. When sucessful, it's non zero. + * @return tm time struct corresponding to duration. When sucessful, it's non zero. */ tm YoutubeApi::parseDuration(const char *duration){ - + // FIXME + // rewrite this with strtok? tm ret; memset(&ret, 0, sizeof(tm)); @@ -302,11 +310,41 @@ tm YoutubeApi::parseDuration(const char *duration){ } +/** + * @brief Parses the ISO8601 date time string into a tm time struct. + * + * @param dateTime Pointer to string + * @return tm time struct corresponding to specified time. When sucessful, it's non zero. + */ +tm YoutubeApi::parseUploadDate(const char* dateTime){ + + tm ret; + memset(&ret, 0, sizeof(tm)); + + if(!dateTime){ + return ret; + } + + int checksum = sscanf(dateTime, "%4d-%2d-%2dT%2d:%2d:%2dZ", &ret.tm_year, &ret.tm_mon, &ret.tm_mday, + &ret.tm_hour, &ret.tm_min, &ret.tm_sec); + + if(checksum != 6){ + printf("sscanf didn't scan in correctly\n"); + memset(&ret, 0, sizeof(tm)); + return ret; + } + + ret.tm_year -= 1900; + + return ret; +} + + /** * @brief Gets the content details of a specific video. Stores them in the calling object. * * @param videoId videoID of the video to get the information from - * @return wasSuccesssful true, if there were no errors and the video was found + * @return true, if there were no errors and the video was found */ bool YoutubeApi::getContentDetails(const char *videoId){ char command[150] = YTAPI_VIDEO_ENDPOINT; @@ -383,10 +421,177 @@ bool YoutubeApi::getContentDetails(const char *videoId){ return wasSuccessful; } + bool YoutubeApi::getContentDetails(const String& videoId){ return getContentDetails(videoId.c_str()); } +/** + * @brief Frees memory used by strings in snippet struct. Initializes it with zeros. + * + * @param s Pointer to snippet struct to free + */ +void YoutubeApi::freeSnippet(snippet *s){ + + if(!s->set){ + return; + } + + free(s->channelId); + free(s->title); + free(s->description); + free(s->channelTitle); + free(s->liveBroadcastContent); + free(s->defaultLanguage); + free(s->defaultAudioLanguage); + + memset(s, 0, sizeof(snippet)); + s->set = false; + + return; +} + +/** + * @brief Allocates memory and copies a string into it. + * + * @param pos where to store a pointer of the allocated memory to + * @param data pointer of data to copy + * @return int 0 on success, 1 on error + */ +int YoutubeApi::allocAndCopy(char **pos, const char *data){ + + if(!data){ + Serial.println("data is a NULL pointer!"); + return 1; + } + + size_t size = strlen(data) + 1; + char *space = (char*) malloc(size); + + if(!space){ + Serial.println("malloc returned NULL pointer!"); + return 1; + } + + *pos = space; + + memcpy(space, data, size); + space[size - 1] = '\0'; + + return 0; +} + +/** + * @brief Gets the snippet of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return true, if there were no errors and the video was found + */ +bool YoutubeApi::getSnippet(const char *videoId){ + char command[150] = YTAPI_VIDEO_ENDPOINT; + char params[120]; + + sprintf(params, "?part=snippet&id=%s&key=%s", videoId, apiKey.c_str()); + strcat(command, params); + + if (_debug) + { + Serial.println(command); + } + + bool wasSuccessful = false; + + // should be more, just to test + // description can be as large as 5kb, title 400 bytes + + const size_t bufferSize = 4096; + + int httpStatus = sendGetToYoutube(command); + + if (httpStatus == 200) + { + // Creating a filter to filter out + // metadata, thumbnail links, tags, localized information + DynamicJsonDocument filter(256); + + JsonObject filterItems = filter["items"][0].createNestedObject("snippet"); + filterItems["publishedAt"] = true; + filterItems["channelId"] = true; + filterItems["channelTitle"] = true; + filterItems["title"] = true; + filterItems["description"] = true; + filterItems["categoryId"] = true; + filterItems["liveBroadcastContent"] = true; + filterItems["defaultLanguage"] = true; + filterItems["defaultAudioLanguage"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.print("No results found for video id "); + Serial.println(videoId); + Serial.println(doc["pageInfo"]["totalResults"].as()); + } + else{ + JsonObject itemsSnippet = doc["items"][0]["snippet"]; + + if(snip.set){ + freeSnippet(&snip); + } + int checksum = 0; + + snip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); + snip.categoryId = itemsSnippet["categoryId"].as(); + + checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); + checksum += allocAndCopy(&snip.title, itemsSnippet["title"].as()); + checksum += allocAndCopy(&snip.description, itemsSnippet["description"].as()); + checksum += allocAndCopy(&snip.channelTitle, itemsSnippet["channelTitle"].as()); + checksum += allocAndCopy(&snip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); + checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); + checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + + if(checksum){ + // don't set snip.set flag in order to avoid false free + Serial.print("Error reading in response values. Checksum: "); + Serial.println(checksum); + snip.set = false; + wasSuccessful = false; + }else{ + snip.set = true; + wasSuccessful = true; + } + } + + } else { + Serial.print("Unexpected HTTP Status Code: "); + Serial.println(httpStatus); + } + closeClient(); + + return wasSuccessful; +} + +/** + * @brief Gets the snippet of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeApi::getSnippet(const String& videoId){ + return getSnippet(videoId.c_str()); +} + void YoutubeApi::skipHeaders() { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index d6a09ec..a77dcb8 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -67,6 +67,20 @@ struct videoStatistics { // In Memory of the old dislike count. }; + +struct snippet{ + bool set; + tm publishedAt; + char *channelId; + char *title; + char *description; + char *channelTitle; + int categoryId; + char *liveBroadcastContent; + char *defaultLanguage; + char *defaultAudioLanguage; +}; + class YoutubeApi { public: @@ -80,6 +94,9 @@ class YoutubeApi bool getVideoStatistics(const String& videoId); bool getContentDetails(const char *videoId); bool getContentDetails(const String& videoId); + bool getSnippet(const char *videoId); + bool getSnippet(const String& videoId); + snippet snip; channelStatistics channelStats; videoStatistics videoStats; contentDetails contentDets; @@ -89,9 +106,12 @@ class YoutubeApi const String apiKey; Client &client; tm parseDuration(const char *duration); + tm parseUploadDate(const char *dateTime); + void freeSnippet(snippet *s); + int allocAndCopy(char **pos, const char *data); int getHttpStatusCode(); void skipHeaders(); - void closeClient(); + void closeClient(); }; #endif From c81fb750ce8b2e3acc218c645b99d66d35a7c241 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 18 Apr 2022 13:54:59 +0200 Subject: [PATCH 05/58] fixed bug -> getSnippet would exit, when language informations are not set --- src/YoutubeApi.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index fd40694..334a069 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -553,13 +553,25 @@ bool YoutubeApi::getSnippet(const char *videoId){ snip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); snip.categoryId = itemsSnippet["categoryId"].as(); - checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); + checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); checksum += allocAndCopy(&snip.title, itemsSnippet["title"].as()); checksum += allocAndCopy(&snip.description, itemsSnippet["description"].as()); checksum += allocAndCopy(&snip.channelTitle, itemsSnippet["channelTitle"].as()); checksum += allocAndCopy(&snip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); - checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); - checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + + // language informations appears to be optional, so it is being checked if it is in response + // if not, a placeholder will be set + if(!itemsSnippet["defaultLanguage"].as()){ + checksum += allocAndCopy(&snip.defaultLanguage, ""); + }else{ + checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); + } + + if(!itemsSnippet["defaultAudioLanguage"].as()){ + checksum += allocAndCopy(&snip.defaultAudioLanguage, ""); + }else{ + checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + } if(checksum){ // don't set snip.set flag in order to avoid false free From 4c1e1cb7e9ebc826e36288702af89813d4ac4c34 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:57:50 +0200 Subject: [PATCH 06/58] refactoring code -> it is now easier to implement more functionality -> api calls are handled by a different calling and parsing function --- src/YoutubeApi.cpp | 618 ++++++++++++++++++++++++--------------------- src/YoutubeApi.h | 33 ++- 2 files changed, 359 insertions(+), 292 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 334a069..ffcc3d1 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -25,12 +25,9 @@ */ // TODO -// address code duplication // // add video.list:status // video.list:topicDetails -// -// store retrieved data in heap instead of stack #include "YoutubeApi.h" @@ -38,10 +35,12 @@ YoutubeApi::YoutubeApi(const char* key, Client &client) : apiKey(key), client(client) {} + YoutubeApi::YoutubeApi(const String &apiKey, Client &client) : YoutubeApi(apiKey.c_str(), client) // passing the key as c-string to force a copy {} + int YoutubeApi::sendGetToYoutube(const char *command) { client.flush(); client.setTimeout(YTAPI_TIMEOUT); @@ -77,21 +76,59 @@ int YoutubeApi::sendGetToYoutube(const char *command) { return statusCode; } + int YoutubeApi::sendGetToYoutube(const String& command) { return sendGetToYoutube(command.c_str()); } -bool YoutubeApi::getChannelStatistics(const char *channelId) { - char command[150] = YTAPI_CHANNEL_ENDPOINT; - char params[120]; - sprintf(params, "?part=statistics&id=%s&key=%s", channelId, apiKey.c_str()); - strcat(command, params); - if (_debug) - { - Serial.println(command); +/** + * @brief Parses the channel statistics from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeApi::parseChannelStatistics() { + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(2) + + 2*JSON_OBJECT_SIZE(4) + + JSON_OBJECT_SIZE(5) + + 330; + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client); + if (!error){ + JsonObject itemStatistics = doc["items"][0]["statistics"]; + + channelStats.viewCount = itemStatistics["viewCount"].as(); + channelStats.subscriberCount = itemStatistics["subscriberCount"].as(); + channelStats.commentCount = itemStatistics["commentCount"].as(); + channelStats.hiddenSubscriberCount = itemStatistics["hiddenSubscriberCount"].as(); + channelStats.videoCount = itemStatistics["videoCount"].as(); + + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); } + closeClient(); + return wasSuccessful; +} + + +/** + * @brief Parses the video statistics from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeApi::parseVideoStatistics(){ + bool wasSuccessful = false; // Get from https://arduinojson.org/v6/assistant/ @@ -101,112 +138,325 @@ bool YoutubeApi::getChannelStatistics(const char *channelId) { + JSON_OBJECT_SIZE(5) + 330; - int httpStatus = sendGetToYoutube(command); + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); - if (httpStatus == 200) - { - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); + // Parse JSON object + DeserializationError error = deserializeJson(doc, client); - // Parse JSON object - DeserializationError error = deserializeJson(doc, client); - if (!error) - { - wasSuccessful = true; + if (error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + JsonObject itemStatistics = doc["items"][0]["statistics"]; + + videoStats.viewCount = itemStatistics["viewCount"].as(); + videoStats.likeCount = itemStatistics["likeCount"].as(); + videoStats.commentCount= itemStatistics["commentCount"].as(); + + wasSuccessful = true; + } + + closeClient(); + return wasSuccessful; +} + + +/** + * @brief Parses the video content details from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeApi::parseContentDetails(){ + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 768; - JsonObject itemStatistics = doc["items"][0]["statistics"]; + + // Creating a filter to filter out + // region restrictions, content rating and metadata + StaticJsonDocument<144> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); + filterItems["duration"] = true; + filterItems["dimension"] = true; + filterItems["definition"] = true; + filterItems["caption"] = true; + filterItems["licensedContent"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + wasSuccessful = true; - channelStats.viewCount = itemStatistics["viewCount"].as(); - channelStats.subscriberCount = itemStatistics["subscriberCount"].as(); - channelStats.commentCount = itemStatistics["commentCount"].as(); - channelStats.hiddenSubscriberCount = itemStatistics["hiddenSubscriberCount"].as(); - channelStats.videoCount = itemStatistics["videoCount"].as(); + JsonObject itemcontentDetails = doc["items"][0]["contentDetails"]; + + memcpy(contentDets.defintion, itemcontentDetails["definition"].as(), 3); + memcpy(contentDets.dimension, itemcontentDetails["dimension"].as(), 3); + + if("false" == itemcontentDetails["caption"]){ + contentDets.caption = true; } - else - { - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); + else{ + contentDets.caption = false; } - } else { - Serial.print("Unexpected HTTP Status Code: "); - Serial.println(httpStatus); + + contentDets.licensedContent = itemcontentDetails["licensedContent"].as(); + contentDets.duration = parseDuration(itemcontentDetails["duration"].as()); } + closeClient(); - return wasSuccessful; } -bool YoutubeApi::getChannelStatistics(const String& channelId) { - return getChannelStatistics(channelId.c_str()); + +/** + * @brief Parses the video snippet from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeApi::parseSnippet(){ + + bool wasSuccessful = false; + + // should be more, just to test + // description can be as large as 5kb, title 400 bytes + const size_t bufferSize = 4096; + + // Creating a filter to filter out + // metadata, thumbnail links, tags, localized information + StaticJsonDocument<256> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("snippet"); + filterItems["publishedAt"] = true; + filterItems["channelId"] = true; + filterItems["channelTitle"] = true; + filterItems["title"] = true; + filterItems["description"] = true; + filterItems["categoryId"] = true; + filterItems["liveBroadcastContent"] = true; + filterItems["defaultLanguage"] = true; + filterItems["defaultAudioLanguage"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + JsonObject itemsSnippet = doc["items"][0]["snippet"]; + + if(snip.set){ + freeSnippet(&snip); + } + int checksum = 0; + + snip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); + snip.categoryId = itemsSnippet["categoryId"].as(); + + checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); + checksum += allocAndCopy(&snip.title, itemsSnippet["title"].as()); + checksum += allocAndCopy(&snip.description, itemsSnippet["description"].as()); + checksum += allocAndCopy(&snip.channelTitle, itemsSnippet["channelTitle"].as()); + checksum += allocAndCopy(&snip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); + + // language informations appears to be optional, so it is being checked if it is in response + // if not, a placeholder will be set + if(!itemsSnippet["defaultLanguage"].as()){ + checksum += allocAndCopy(&snip.defaultLanguage, ""); + }else{ + checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); + } + + if(!itemsSnippet["defaultAudioLanguage"].as()){ + checksum += allocAndCopy(&snip.defaultAudioLanguage, ""); + }else{ + checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + } + + if(checksum){ + // don't set snip.set flag in order to avoid false free + Serial.print("Error reading in response values. Checksum: "); + Serial.println(checksum); + snip.set = false; + wasSuccessful = false; + }else{ + snip.set = true; + wasSuccessful = true; + } + } + + closeClient(); + return wasSuccessful; } /** - * @brief Gets the statistics of a specific video. Stores them in the calling object. + * @brief Makes an API request for a specific endpoint and type. Calls a parsing function + * to handle parsing. * - * @param videoId videoID of the video to get the information from - * @return true, if there were no errors and the video was found + * @param op API request type to make + * @param id video or channel id + * @return Returns parsing function return value, or false in case of an unexpected HTTP status code. */ -bool YoutubeApi::getVideoStatistics(const char *videoId){ - char command[150] = YTAPI_VIDEO_ENDPOINT; - char params[120]; - sprintf(params, "?part=statistics&id=%s&key=%s", videoId, apiKey.c_str()); - strcat(command, params); +bool YoutubeApi::getRequestedType(int op, const char *id) { - if (_debug) + char command[150]; + bool wasSuccessful = false; + int httpStatus; + + switch (op) { - Serial.println(command); + case videoListStats: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey.c_str()); + break; + + case videoListContentDetails: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey.c_str()); + break; + + case videoListSnippet: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey.c_str()); + break; + + case channelListStats: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey.c_str()); + break; + + default: + return false; } - bool wasSuccessful = false; - - // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = JSON_ARRAY_SIZE(1) - + JSON_OBJECT_SIZE(2) - + 2*JSON_OBJECT_SIZE(4) - + JSON_OBJECT_SIZE(5) - + 330; + if (_debug){ + Serial.println(command); + } - int httpStatus = sendGetToYoutube(command); + httpStatus = sendGetToYoutube(command); if (httpStatus == 200) { - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client); - - if (error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.print("No results found for video id "); - Serial.println(videoId); - } - else{ - wasSuccessful = true; + switch(op) + { + case channelListStats: + wasSuccessful = parseChannelStatistics(); + break; + + case videoListStats: + wasSuccessful = parseVideoStatistics(); + break; - JsonObject itemStatistics = doc["items"][0]["statistics"]; + case videoListContentDetails: + wasSuccessful = parseContentDetails(); + break; + + case videoListSnippet: + wasSuccessful = parseSnippet(); + break; - videoStats.viewCount = itemStatistics["viewCount"].as(); - videoStats.likeCount = itemStatistics["likeCount"].as(); - videoStats.commentCount= itemStatistics["commentCount"].as(); + default: + wasSuccessful = false; + break; } } else { Serial.print("Unexpected HTTP Status Code: "); Serial.println(httpStatus); } - closeClient(); - return wasSuccessful; } + +/** + * @brief Gets the statistics of a specific channel. Stores them in the calling object. + * + * @param channelId channelID of the channel to get the information from + * @return true, if there were no errors and the channel was found + */ +bool YoutubeApi::getChannelStatistics(const String& channelId) { + return getRequestedType(channelListStats, channelId.c_str()); +} + + +bool YoutubeApi::getChannelStatistics(const char *channelId) { + return getRequestedType(channelListStats, channelId); +} + + +/** + * @brief Gets the statistics of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return true, if there were no errors and the video was found + */ bool YoutubeApi::getVideoStatistics(const String& videoId){ - return getVideoStatistics(videoId.c_str()); + return getRequestedType(videoListStats, videoId.c_str()); +} + + +bool YoutubeApi::getVideoStatistics(const char *videoId){ + return getRequestedType(videoListStats, videoId); +} + + +/** + * @brief Gets the content details of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return true, if there were no errors and the video was found + */ +bool YoutubeApi::getContentDetails(const String& videoId){ + return getRequestedType(videoListContentDetails, videoId.c_str()); } + +bool YoutubeApi::getContentDetails(const char *videoId){ + return getRequestedType(videoListContentDetails, videoId); +} + + +/** + * @brief Gets the snippet of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeApi::getSnippet(const String& videoId){ + return getRequestedType(videoListSnippet, videoId.c_str()); +} + + +bool YoutubeApi::getSnippet(const char *videoId){ + return getRequestedType(videoListSnippet, videoId); +} + + /** * @brief Parses the ISO8601 duration string into a tm time struct. * @@ -340,92 +590,6 @@ tm YoutubeApi::parseUploadDate(const char* dateTime){ } -/** - * @brief Gets the content details of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return true, if there were no errors and the video was found - */ -bool YoutubeApi::getContentDetails(const char *videoId){ - char command[150] = YTAPI_VIDEO_ENDPOINT; - char params[120]; - sprintf(params, "?part=contentDetails&id=%s&key=%s", videoId, apiKey.c_str()); - strcat(command, params); - - if (_debug) - { - Serial.println(command); - } - - bool wasSuccessful = false; - - // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = 768; - - int httpStatus = sendGetToYoutube(command); - - if (httpStatus == 200) - { - // Creating a filter to filter out - // region restrictions, content rating and metadata - DynamicJsonDocument filter(144); - - JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); - filterItems["duration"] = true; - filterItems["dimension"] = true; - filterItems["definition"] = true; - filterItems["caption"] = true; - filterItems["licensedContent"] = true; - filter["pageInfo"] = true; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); - - // check for errors and empty response - if(error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.print("No results found for video id "); - Serial.println(videoId); - } - else{ - wasSuccessful = true; - - JsonObject itemcontentDetails = doc["items"][0]["contentDetails"]; - - memcpy(contentDets.defintion, itemcontentDetails["definition"].as(), 3); - - memcpy(contentDets.dimension, itemcontentDetails["dimension"].as(), 3); - - if(strcmp("false", itemcontentDetails["caption"].as()) != 0){ - contentDets.caption = true; - } - else{ - contentDets.caption = false; - } - - contentDets.licensedContent = itemcontentDetails["licensedContent"].as(); - contentDets.duration = parseDuration(itemcontentDetails["duration"].as()); - } - - } else { - Serial.print("Unexpected HTTP Status Code: "); - Serial.println(httpStatus); - } - closeClient(); - - return wasSuccessful; -} - -bool YoutubeApi::getContentDetails(const String& videoId){ - return getContentDetails(videoId.c_str()); -} - /** * @brief Frees memory used by strings in snippet struct. Initializes it with zeros. * @@ -481,128 +645,6 @@ int YoutubeApi::allocAndCopy(char **pos, const char *data){ return 0; } -/** - * @brief Gets the snippet of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return true, if there were no errors and the video was found - */ -bool YoutubeApi::getSnippet(const char *videoId){ - char command[150] = YTAPI_VIDEO_ENDPOINT; - char params[120]; - - sprintf(params, "?part=snippet&id=%s&key=%s", videoId, apiKey.c_str()); - strcat(command, params); - - if (_debug) - { - Serial.println(command); - } - - bool wasSuccessful = false; - - // should be more, just to test - // description can be as large as 5kb, title 400 bytes - - const size_t bufferSize = 4096; - - int httpStatus = sendGetToYoutube(command); - - if (httpStatus == 200) - { - // Creating a filter to filter out - // metadata, thumbnail links, tags, localized information - DynamicJsonDocument filter(256); - - JsonObject filterItems = filter["items"][0].createNestedObject("snippet"); - filterItems["publishedAt"] = true; - filterItems["channelId"] = true; - filterItems["channelTitle"] = true; - filterItems["title"] = true; - filterItems["description"] = true; - filterItems["categoryId"] = true; - filterItems["liveBroadcastContent"] = true; - filterItems["defaultLanguage"] = true; - filterItems["defaultAudioLanguage"] = true; - filter["pageInfo"] = true; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); - - // check for errors and empty response - if(error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.print("No results found for video id "); - Serial.println(videoId); - Serial.println(doc["pageInfo"]["totalResults"].as()); - } - else{ - JsonObject itemsSnippet = doc["items"][0]["snippet"]; - - if(snip.set){ - freeSnippet(&snip); - } - int checksum = 0; - - snip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); - snip.categoryId = itemsSnippet["categoryId"].as(); - - checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); - checksum += allocAndCopy(&snip.title, itemsSnippet["title"].as()); - checksum += allocAndCopy(&snip.description, itemsSnippet["description"].as()); - checksum += allocAndCopy(&snip.channelTitle, itemsSnippet["channelTitle"].as()); - checksum += allocAndCopy(&snip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); - - // language informations appears to be optional, so it is being checked if it is in response - // if not, a placeholder will be set - if(!itemsSnippet["defaultLanguage"].as()){ - checksum += allocAndCopy(&snip.defaultLanguage, ""); - }else{ - checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); - } - - if(!itemsSnippet["defaultAudioLanguage"].as()){ - checksum += allocAndCopy(&snip.defaultAudioLanguage, ""); - }else{ - checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); - } - - if(checksum){ - // don't set snip.set flag in order to avoid false free - Serial.print("Error reading in response values. Checksum: "); - Serial.println(checksum); - snip.set = false; - wasSuccessful = false; - }else{ - snip.set = true; - wasSuccessful = true; - } - } - - } else { - Serial.print("Unexpected HTTP Status Code: "); - Serial.println(httpStatus); - } - closeClient(); - - return wasSuccessful; -} - -/** - * @brief Gets the snippet of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return wasSuccesssful true, if there were no errors and the video was found - */ -bool YoutubeApi::getSnippet(const String& videoId){ - return getSnippet(videoId.c_str()); -} void YoutubeApi::skipHeaders() { // Skip HTTP headers @@ -627,6 +669,7 @@ void YoutubeApi::skipHeaders() { } } + int YoutubeApi::getHttpStatusCode() { // Check HTTP status if(client.find("HTTP/1.1")){ @@ -637,6 +680,7 @@ int YoutubeApi::getHttpStatusCode() { return -1; } + void YoutubeApi::closeClient() { if(client.connected()) { if(_debug) { Serial.println(F("Closing client")); } diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index a77dcb8..0137c26 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -38,8 +38,17 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" +#define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" + +enum operation{ + + videoListStats, + videoListContentDetails, + videoListSnippet, + + channelListStats +}; -#define YT_VIDEO_TITLE_MAX_LENGTH 100 struct channelStatistics { long viewCount; @@ -86,16 +95,22 @@ class YoutubeApi public: YoutubeApi(const char *key, Client &client); YoutubeApi(const String& apiKey, Client& client); + int sendGetToYoutube(const char *command); int sendGetToYoutube(const String& command); - bool getChannelStatistics(const char *channelId); + bool getChannelStatistics(const String& channelId); - bool getVideoStatistics(const char *videoId); + bool getChannelStatistics(const char *channelId); + bool getVideoStatistics(const String& videoId); - bool getContentDetails(const char *videoId); + bool getVideoStatistics(const char *videoId); + bool getContentDetails(const String& videoId); - bool getSnippet(const char *videoId); + bool getContentDetails(const char *videoId); + bool getSnippet(const String& videoId); + bool getSnippet(const char *videoId); + snippet snip; channelStatistics channelStats; videoStatistics videoStats; @@ -107,9 +122,17 @@ class YoutubeApi Client &client; tm parseDuration(const char *duration); tm parseUploadDate(const char *dateTime); + void freeSnippet(snippet *s); int allocAndCopy(char **pos, const char *data); + bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); + + bool parseChannelStatistics(); + bool parseVideoStatistics(); + bool parseContentDetails(); + bool parseSnippet(); + void skipHeaders(); void closeClient(); }; From 4a3e942a62ba9adff49c391207cab0641135726b Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 21 Apr 2022 12:07:12 +0200 Subject: [PATCH 07/58] adding new example and changing config files -> new example prints all information about a video, that the library is able to fetch -> setting default baud rate to 115200 in platformio config file -> adding development files to gitignore --- .gitignore | 4 + .../VideoFullInformation.ino | 255 ++++++++++++++++++ platformio.ini | 2 + 3 files changed, 261 insertions(+) create mode 100644 examples/VideoFullInformation/VideoFullInformation.ino diff --git a/.gitignore b/.gitignore index 6675329..620acb7 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,7 @@ vs-readme.txt .gcc-flags.json .pio/ .vscode/ + +# custom ignores +src/*.ino +src/*.txt \ No newline at end of file diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoFullInformation/VideoFullInformation.ino new file mode 100644 index 0000000..6dd8d35 --- /dev/null +++ b/examples/VideoFullInformation/VideoFullInformation.ino @@ -0,0 +1,255 @@ +/******************************************************************* + Read all available YouTube Video information from the YouTube API + and print them to the serial monitor + + Compatible Boards: + * Any ESP8266 board + * Any ESP32 board + + Recommended Board: D1 Mini ESP8266 + http://s.click.aliexpress.com/e/uzFUnIe (affiliate) + + If you find what I do useful and would like to support me, + please consider becoming a sponsor on Github + https://github.com/sponsors/witnessmenow/ + + Written by Brian Lough and modified by Colum31 + YouTube: https://www.youtube.com/brianlough + Tindie: https://www.tindie.com/stores/brianlough/ + Twitter: https://twitter.com/witnessmenow + *******************************************************************/ + +// ---------------------------- +// Standard Libraries +// ---------------------------- + +#if defined(ESP8266) + #include +#elif defined(ESP32) + #include +#endif + +#include + +// ---------------------------- +// Additional Libraries - each of these will need to be installed +// ---------------------------- + +// Library for connecting to the YouTube API +// https://github.com/witnessmenow/arduino-youtube-api +// (search for "youtube" in the Arduino Library Manager) +#include + +// Library used for parsing Json from the API responses +// https://github.com/bblanchon/ArduinoJson +// (search for "Arduino Json" in the Arduino Library Manager) +#include + +//------- Replace the following! ------ +const char ssid[] = "xxxx"; // your network SSID (name) +const char password[] = "yyyy"; // your network key +#define API_KEY "zzzz" // your Google API key +//------- ---------------------- ------ + + +#define timeBetweenRequestGroup 120 * 1000 // 120 seconds, in milliseconds | time between all requests +#define timeBetweenRequests 2 * 1000 // 2 seconds, in milliseconds | time between single requests +#define videoIdLen 11 + +WiFiClientSecure client; +YoutubeApi api(API_KEY, client); + +char videoId[videoIdLen + 1]; +unsigned long startTime; + +/** + * @brief Tries to read the videoId from Serial. + * + * @return 1 on success, 0 if no data available + */ +int readVideoId(){ + + if(Serial.available() > videoIdLen - 1){ + + for(int i = 0; i < videoIdLen; i++){ + + videoId[i] = Serial.read(); + } + + videoId[videoIdLen] = '\0'; + return 1; + } + return 0; +} + +/** + * @brief Flushes the Serial input buffer. + * + */ +void flushSerialBuffer(){ + while(Serial.available()){ + Serial.read(); + } +} + + +void setup() { + Serial.begin(115200); + + // Set WiFi to 'station' mode and disconnect + // from the AP if it was previously connected + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + // Connect to the WiFi network + Serial.print("\nConnecting to WiFi: "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.println("\nWiFi connected!"); + Serial.print("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); + + #ifdef ESP8266 + // Required if you are using ESP8266 V2.5 or above + client.setInsecure(); + #endif + + // Uncomment for extra debugging info + // api._debug = true; + + flushSerialBuffer(); + Serial.print("Enter videoId: "); + + while(1){ + if(readVideoId()){ + flushSerialBuffer(); + break; + } + } + + Serial.println(videoId); +} + +void loop() { + + Serial.setTimeout(timeBetweenRequestGroup); + + // fetch and print information in videos.list:snippet + if(api.getSnippet(videoId)){ + Serial.println("\n\nsnippet"); + + Serial.print("------ Video title: "); + Serial.println(api.snip.title); + + Serial.println("------ Video description: \n"); + Serial.println(api.snip.description); + Serial.println(""); + + Serial.print("------ Uploaded by (channel title): "); + Serial.println(api.snip.channelTitle); + + Serial.print("------ Uploaded by (channel id): "); + Serial.println(api.snip.channelId); + + Serial.print("------ Published at (d.m.y h:m:s): "); + Serial.print(api.snip.publishedAt.tm_mday); + Serial.print("."); + Serial.print(api.snip.publishedAt.tm_mon); + Serial.print("."); + Serial.print(api.snip.publishedAt.tm_year + 1900); + Serial.print(" "); + Serial.print(api.snip.publishedAt.tm_hour); + Serial.print(":"); + Serial.print(api.snip.publishedAt.tm_min); + Serial.print(":"); + Serial.println(api.snip.publishedAt.tm_sec); + + Serial.print("------ Livebroadcast content: "); + Serial.println(api.snip.liveBroadcastContent); + + Serial.print("------ Category id: "); + Serial.println(api.snip.categoryId); + + Serial.print("------ Default language: "); + Serial.println(api.snip.defaultLanguage); + + + Serial.print("------ Default audio language: "); + Serial.println(api.snip.defaultAudioLanguage); + + Serial.println("-------------------------------------------------"); + } + + delay(timeBetweenRequests); + + // fetch and print information in videos.list:statistics + if(api.getVideoStatistics(videoId)){ + Serial.println("\n\nstatistics"); + + Serial.print("------ Video views: "); + Serial.println(api.videoStats.viewCount); + + Serial.print("------ Likes: "); + Serial.println(api.videoStats.likeCount); + + Serial.print("------ Comments: "); + Serial.println(api.videoStats.commentCount); + + Serial.println("-------------------------------------------------"); + } + + delay(timeBetweenRequests); + + // fetch and print information in videos.list:contentDetails + if(api.getContentDetails(videoId)){ + Serial.println("\n\ncontentDetails"); + + Serial.print("------ Video duration "); + + if(api.contentDets.duration.tm_mday != 0){ + Serial.print("(d:h:m:s): "); + Serial.print(api.contentDets.duration.tm_mday); + Serial.print(":"); + }else{ + Serial.print("(h:m:s): "); + } + + + Serial.print(api.contentDets.duration.tm_hour); + Serial.print(":"); + Serial.print(api.contentDets.duration.tm_min); + Serial.print(":"); + Serial.println(api.contentDets.duration.tm_sec); + + + Serial.print("------ Likes: "); + Serial.println(api.videoStats.likeCount); + + Serial.print("------ Comments: "); + Serial.println(api.videoStats.commentCount); + + Serial.println("-------------------------------------------------"); + } + + Serial.print("\nRefreshing in "); + Serial.print(timeBetweenRequestGroup / 1000.0); + Serial.println(" seconds..."); + Serial.print("Or set a new videoId: "); + + startTime = millis(); + flushSerialBuffer(); + + while(millis() - startTime < timeBetweenRequestGroup){ + + if(readVideoId()){; + Serial.println(videoId); + break; + } + } +} diff --git a/platformio.ini b/platformio.ini index 2bd6fb3..623fcc1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,9 +16,11 @@ platform = espressif8266 board = d1_mini framework = arduino lib_deps = ${common.lib_deps_external} +monitor_speed = 115200 [env:esp32_devkit] platform = espressif32 board = esp32doit-devkit-v1 framework = arduino lib_deps = ${common.lib_deps_external} +monitor_speed = 115200 From 7443443d6e452a53fb3c9655e52f55b2ece25595 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 22 Apr 2022 16:52:58 +0200 Subject: [PATCH 08/58] Fixing example and adjusting buffer sizes -> example code did not print correct data -> adjusted buffersizes in order to load long descriptions and content details more efficient --- .../VideoFullInformation.ino | 31 ++++++++++++++++--- src/YoutubeApi.cpp | 10 +++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoFullInformation/VideoFullInformation.ino index 6dd8d35..aed58a0 100644 --- a/examples/VideoFullInformation/VideoFullInformation.ino +++ b/examples/VideoFullInformation/VideoFullInformation.ino @@ -92,6 +92,19 @@ void flushSerialBuffer(){ } } +/** + * @brief Prints "Yes\n" if x or "No\n" if not x + * + * @param x parameter + */ +void printYesNo(bool x){ + if(x){ + Serial.println("Yes"); + }else{ + Serial.println("No"); + } +} + void setup() { Serial.begin(115200); @@ -227,12 +240,20 @@ void loop() { Serial.print(":"); Serial.println(api.contentDets.duration.tm_sec); - - Serial.print("------ Likes: "); - Serial.println(api.videoStats.likeCount); + Serial.print("------ Dimension: "); + Serial.println(api.contentDets.dimension); - Serial.print("------ Comments: "); - Serial.println(api.videoStats.commentCount); + Serial.print("------ Definition: "); + Serial.println(api.contentDets.defintion); + + Serial.print("------ Captioned: "); + printYesNo(api.contentDets.caption); + + Serial.print("------ Licensed Content: "); + printYesNo(api.contentDets.licensedContent); + + Serial.print("------ Projection: "); + Serial.println(api.contentDets.projection); Serial.println("-------------------------------------------------"); } diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index ffcc3d1..641920e 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -175,12 +175,12 @@ bool YoutubeApi::parseContentDetails(){ bool wasSuccessful = false; // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = 768; + const size_t bufferSize = 384; // Creating a filter to filter out // region restrictions, content rating and metadata - StaticJsonDocument<144> filter; + StaticJsonDocument<180> filter; JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); filterItems["duration"] = true; @@ -188,6 +188,7 @@ bool YoutubeApi::parseContentDetails(){ filterItems["definition"] = true; filterItems["caption"] = true; filterItems["licensedContent"] = true; + filterItems["projection"] = true; filter["pageInfo"] = true; // Allocate DynamicJsonDocument @@ -211,7 +212,8 @@ bool YoutubeApi::parseContentDetails(){ memcpy(contentDets.defintion, itemcontentDetails["definition"].as(), 3); memcpy(contentDets.dimension, itemcontentDetails["dimension"].as(), 3); - + strcpy(contentDets.projection, itemcontentDetails["projection"].as()); + if("false" == itemcontentDetails["caption"]){ contentDets.caption = true; } @@ -239,7 +241,7 @@ bool YoutubeApi::parseSnippet(){ // should be more, just to test // description can be as large as 5kb, title 400 bytes - const size_t bufferSize = 4096; + const size_t bufferSize = 6000; // Creating a filter to filter out // metadata, thumbnail links, tags, localized information From 154894e6d23bec509e3f410f783e68ad9f805910 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 22 Apr 2022 20:17:20 +0200 Subject: [PATCH 09/58] adding video.list:status -> adding struct and functions to fetch video.list:status -> adding status information to example -> altering doc to show which attributes are not implemented yet --- .../VideoFullInformation.ino | 26 +++- src/YoutubeApi.cpp | 114 +++++++++++++++++- src/YoutubeApi.h | 32 ++++- 3 files changed, 168 insertions(+), 4 deletions(-) diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoFullInformation/VideoFullInformation.ino index aed58a0..c70d82b 100644 --- a/examples/VideoFullInformation/VideoFullInformation.ino +++ b/examples/VideoFullInformation/VideoFullInformation.ino @@ -132,7 +132,7 @@ void setup() { // Required if you are using ESP8266 V2.5 or above client.setInsecure(); #endif - + // Uncomment for extra debugging info // api._debug = true; @@ -258,6 +258,30 @@ void loop() { Serial.println("-------------------------------------------------"); } + delay(timeBetweenRequests); + + if(api.getVideoStatus(videoId)){ + Serial.println("\n\n status"); + + Serial.print("------ upload status: "); + Serial.println(api.vStatus.uploadStatus); + + Serial.print("------ privacy status: "); + Serial.println(api.vStatus.privacyStatus); + + Serial.print("------ license: "); + Serial.println(api.vStatus.license); + + Serial.print("------ embeddable: "); + printYesNo(api.vStatus.embeddable); + + Serial.print("------ public stats viewable: "); + printYesNo(api.vStatus.publicStatsViewable); + + Serial.print("------ made for kids: "); + printYesNo(api.vStatus.madeForKids); + } + Serial.print("\nRefreshing in "); Serial.print(timeBetweenRequestGroup / 1000.0); Serial.println(" seconds..."); diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 641920e..1606760 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -26,8 +26,7 @@ // TODO // -// add video.list:status -// video.list:topicDetails +// add video.list:topicDetails #include "YoutubeApi.h" @@ -321,6 +320,75 @@ bool YoutubeApi::parseSnippet(){ } +bool YoutubeApi::parseVideoStatus(){ + + bool wasSuccessful = false; + const size_t bufferSize = 384; + + // Creating a filter to filter out + // metadata, thumbnail links, tags, localized information + + StaticJsonDocument<192> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("status"); + filterItems["uploadStatus"] = true; + filterItems["privacyStatus"] = true; + filterItems["license"] = true; + filterItems["embeddable"] = true; + filterItems["publicStatsViewable"] = true; + filterItems["madeForKids"] = true; + + JsonObject filterPageInfo = filter.createNestedObject("pageInfo"); + filterPageInfo["totalResults"] = true; + filterPageInfo["resultsPerPage"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + JsonObject itemsStatus = doc["items"][0]["status"]; + + if(vStatus.set){ + freeStatus(&vStatus); + } + + int checksum = 0; + checksum += allocAndCopy(&vStatus.uploadStatus, itemsStatus["uploadStatus"]); + checksum += allocAndCopy(&vStatus.privacyStatus, itemsStatus["privacyStatus"]); + checksum += allocAndCopy(&vStatus.license, itemsStatus["license"]); + + vStatus.embeddable = itemsStatus["embeddable"]; // true + vStatus.publicStatsViewable = itemsStatus["publicStatsViewable"]; // true + vStatus.madeForKids = itemsStatus["madeForKids"]; + + if(checksum){ + // don't set videoStatus.set flag in order to avoid false free + Serial.print("Error reading in response values. Checksum: "); + Serial.println(checksum); + vStatus.set = false; + wasSuccessful = false; + }else{ + vStatus.set = true; + wasSuccessful = true; + } + } + + closeClient(); + return wasSuccessful; +} + + /** * @brief Makes an API request for a specific endpoint and type. Calls a parsing function * to handle parsing. @@ -349,11 +417,16 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey.c_str()); break; + case videoListStatus: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey.c_str()); + break; + case channelListStats: sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey.c_str()); break; default: + Serial.println("Unknown operation"); return false; } @@ -383,6 +456,10 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { wasSuccessful = parseSnippet(); break; + case videoListStatus: + wasSuccessful = parseVideoStatus(); + break; + default: wasSuccessful = false; break; @@ -459,6 +536,23 @@ bool YoutubeApi::getSnippet(const char *videoId){ } +/** + * @brief Gets the status of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeApi::getVideoStatus(const String& videoId){ + return getRequestedType(videoListStatus, videoId.c_str()); +} + + +bool YoutubeApi::getVideoStatus(const char *videoId){ + return getRequestedType(videoListStatus, videoId); +} + + + /** * @brief Parses the ISO8601 duration string into a tm time struct. * @@ -617,6 +711,22 @@ void YoutubeApi::freeSnippet(snippet *s){ return; } + +void YoutubeApi::freeStatus(videoStatus *s){ + + if(!s->set){ + return; + } + + free(s->uploadStatus); + free(s->license); + free(s->privacyStatus); + + memset(s, 0, sizeof(videoStatus)); + + return; +} + /** * @brief Allocates memory and copies a string into it. * diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 0137c26..9393b48 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -45,6 +45,7 @@ enum operation{ videoListStats, videoListContentDetails, videoListSnippet, + videoListStatus, channelListStats }; @@ -65,6 +66,10 @@ struct contentDetails{ char defintion[3]; bool caption; bool licensedContent; +// char **regionRestriction; +// char **contentRating; + char projection[12]; +// bool hasCustomThumbnail; }; @@ -72,21 +77,40 @@ struct videoStatistics { long viewCount; long commentCount; long likeCount; +// long favourites; // long dislikeCount; + // In Memory of the old dislike count. }; +struct videoStatus{ + bool set; + char *uploadStatus; +// char *failureReason; +// char *rejectionReason; + char *privacyStatus; +// tm publishAt; + char *license; + bool embeddable; + bool publicStatsViewable; + bool madeForKids; +// bool selfDeclaredMadeForKids; +}; + struct snippet{ bool set; tm publishedAt; char *channelId; char *title; char *description; +// char **thumbnails; char *channelTitle; +// char **tags; int categoryId; char *liveBroadcastContent; char *defaultLanguage; +// char **localized; char *defaultAudioLanguage; }; @@ -110,11 +134,15 @@ class YoutubeApi bool getSnippet(const String& videoId); bool getSnippet(const char *videoId); - + + bool getVideoStatus(const String& videoId); + bool getVideoStatus(const char *videoId); + snippet snip; channelStatistics channelStats; videoStatistics videoStats; contentDetails contentDets; + videoStatus vStatus; bool _debug = false; private: @@ -124,6 +152,7 @@ class YoutubeApi tm parseUploadDate(const char *dateTime); void freeSnippet(snippet *s); + void freeStatus(videoStatus *s); int allocAndCopy(char **pos, const char *data); bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); @@ -132,6 +161,7 @@ class YoutubeApi bool parseVideoStatistics(); bool parseContentDetails(); bool parseSnippet(); + bool parseVideoStatus(); void skipHeaders(); void closeClient(); From 936846205a61196d8d67e99214d6bb3a4f65f6bb Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 22 Apr 2022 23:28:39 +0200 Subject: [PATCH 10/58] refactoring varibale names and library metadata -> adjusting keywords -> adding new features to README -> adjusting variable names to avoid conflicts in future --- README.md | 6 +- .../VideoFullInformation.ino | 100 +++++++++--------- keywords.txt | 14 ++- src/YoutubeApi.cpp | 85 ++++++++------- src/YoutubeApi.h | 27 ++--- 5 files changed, 130 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index bfc2e2b..9f36727 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,11 @@ If you don't know it, you can find your own YouTube channel ID [here](https://ww ## Supported Data Methods -Currently the only implemented method is getting channel statistics, but the library can be easily extended. Please raise an issue if there is a method you are looking for. +The library is currently able to fetch: + +- video list: snippet, status, statistics and contentDetails +- channel list: statistics + ## License diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoFullInformation/VideoFullInformation.ino index c70d82b..0fb54eb 100644 --- a/examples/VideoFullInformation/VideoFullInformation.ino +++ b/examples/VideoFullInformation/VideoFullInformation.ino @@ -154,47 +154,47 @@ void loop() { Serial.setTimeout(timeBetweenRequestGroup); // fetch and print information in videos.list:snippet - if(api.getSnippet(videoId)){ + if(api.getVideoSnippet(videoId)){ Serial.println("\n\nsnippet"); - Serial.print("------ Video title: "); - Serial.println(api.snip.title); + Serial.print("|----- Video title: "); + Serial.println(api.videoSnip.title); - Serial.println("------ Video description: \n"); - Serial.println(api.snip.description); + Serial.println("|----- Video description: \n"); + Serial.println(api.videoSnip.description); Serial.println(""); - Serial.print("------ Uploaded by (channel title): "); - Serial.println(api.snip.channelTitle); + Serial.print("|----- Uploaded by (channel title): "); + Serial.println(api.videoSnip.channelTitle); - Serial.print("------ Uploaded by (channel id): "); - Serial.println(api.snip.channelId); + Serial.print("|----- Uploaded by (channel id): "); + Serial.println(api.videoSnip.channelId); - Serial.print("------ Published at (d.m.y h:m:s): "); - Serial.print(api.snip.publishedAt.tm_mday); + Serial.print("|----- Published at (d.m.y h:m:s): "); + Serial.print(api.videoSnip.publishedAt.tm_mday); Serial.print("."); - Serial.print(api.snip.publishedAt.tm_mon); + Serial.print(api.videoSnip.publishedAt.tm_mon); Serial.print("."); - Serial.print(api.snip.publishedAt.tm_year + 1900); + Serial.print(api.videoSnip.publishedAt.tm_year + 1900); Serial.print(" "); - Serial.print(api.snip.publishedAt.tm_hour); + Serial.print(api.videoSnip.publishedAt.tm_hour); Serial.print(":"); - Serial.print(api.snip.publishedAt.tm_min); + Serial.print(api.videoSnip.publishedAt.tm_min); Serial.print(":"); - Serial.println(api.snip.publishedAt.tm_sec); + Serial.println(api.videoSnip.publishedAt.tm_sec); - Serial.print("------ Livebroadcast content: "); - Serial.println(api.snip.liveBroadcastContent); + Serial.print("|----- Livebroadcast content: "); + Serial.println(api.videoSnip.liveBroadcastContent); - Serial.print("------ Category id: "); - Serial.println(api.snip.categoryId); + Serial.print("|----- Category id: "); + Serial.println(api.videoSnip.categoryId); - Serial.print("------ Default language: "); - Serial.println(api.snip.defaultLanguage); + Serial.print("|----- Default language: "); + Serial.println(api.videoSnip.defaultLanguage); - Serial.print("------ Default audio language: "); - Serial.println(api.snip.defaultAudioLanguage); + Serial.print("|----- Default audio language: "); + Serial.println(api.videoSnip.defaultAudioLanguage); Serial.println("-------------------------------------------------"); } @@ -205,13 +205,13 @@ void loop() { if(api.getVideoStatistics(videoId)){ Serial.println("\n\nstatistics"); - Serial.print("------ Video views: "); + Serial.print("|----- Video views: "); Serial.println(api.videoStats.viewCount); - Serial.print("------ Likes: "); + Serial.print("|----- Likes: "); Serial.println(api.videoStats.likeCount); - Serial.print("------ Comments: "); + Serial.print("|----- Comments: "); Serial.println(api.videoStats.commentCount); Serial.println("-------------------------------------------------"); @@ -220,40 +220,40 @@ void loop() { delay(timeBetweenRequests); // fetch and print information in videos.list:contentDetails - if(api.getContentDetails(videoId)){ + if(api.getVideoContentDetails(videoId)){ Serial.println("\n\ncontentDetails"); - Serial.print("------ Video duration "); + Serial.print("|----- Video duration "); - if(api.contentDets.duration.tm_mday != 0){ + if(api.videoContentDets.duration.tm_mday != 0){ Serial.print("(d:h:m:s): "); - Serial.print(api.contentDets.duration.tm_mday); + Serial.print(api.videoContentDets.duration.tm_mday); Serial.print(":"); }else{ Serial.print("(h:m:s): "); } - Serial.print(api.contentDets.duration.tm_hour); + Serial.print(api.videoContentDets.duration.tm_hour); Serial.print(":"); - Serial.print(api.contentDets.duration.tm_min); + Serial.print(api.videoContentDets.duration.tm_min); Serial.print(":"); - Serial.println(api.contentDets.duration.tm_sec); + Serial.println(api.videoContentDets.duration.tm_sec); - Serial.print("------ Dimension: "); - Serial.println(api.contentDets.dimension); + Serial.print("|----- Dimension: "); + Serial.println(api.videoContentDets.dimension); - Serial.print("------ Definition: "); - Serial.println(api.contentDets.defintion); + Serial.print("|----- Definition: "); + Serial.println(api.videoContentDets.defintion); - Serial.print("------ Captioned: "); - printYesNo(api.contentDets.caption); + Serial.print("|----- Captioned: "); + printYesNo(api.videoContentDets.caption); - Serial.print("------ Licensed Content: "); - printYesNo(api.contentDets.licensedContent); + Serial.print("|----- Licensed Content: "); + printYesNo(api.videoContentDets.licensedContent); - Serial.print("------ Projection: "); - Serial.println(api.contentDets.projection); + Serial.print("|----- Projection: "); + Serial.println(api.videoContentDets.projection); Serial.println("-------------------------------------------------"); } @@ -263,22 +263,22 @@ void loop() { if(api.getVideoStatus(videoId)){ Serial.println("\n\n status"); - Serial.print("------ upload status: "); + Serial.print("|----- upload status: "); Serial.println(api.vStatus.uploadStatus); - Serial.print("------ privacy status: "); + Serial.print("|----- privacy status: "); Serial.println(api.vStatus.privacyStatus); - Serial.print("------ license: "); + Serial.print("|----- license: "); Serial.println(api.vStatus.license); - Serial.print("------ embeddable: "); + Serial.print("|----- embeddable: "); printYesNo(api.vStatus.embeddable); - Serial.print("------ public stats viewable: "); + Serial.print("|----- public stats viewable: "); printYesNo(api.vStatus.publicStatsViewable); - Serial.print("------ made for kids: "); + Serial.print("|----- made for kids: "); printYesNo(api.vStatus.madeForKids); } diff --git a/keywords.txt b/keywords.txt index 3d106e8..fc00275 100644 --- a/keywords.txt +++ b/keywords.txt @@ -10,8 +10,13 @@ YoutubeApi KEYWORD1 # API Data -channelStatistics KEYWORD1 +channelStatistics KEYWORD1 videoStatistics KEYWORD1 +videoContentDetails KEYWORD1 +videoStatistics KEYWORD1 +videoStatus KEYWORD1 +videoSnippet KEYWORD1 +operation KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -20,6 +25,10 @@ videoStatistics KEYWORD1 # API Member Functions sendGetToYoutube KEYWORD2 getChannelStatistics KEYWORD2 +getVideoStatistics KEYWORD2 +getContentDetails KEYWORD2 +getSnippet KEYWORD2 +getVideoStatus KEYWORD2 ####################################### # Instances (KEYWORD2) @@ -33,3 +42,6 @@ getChannelStatistics KEYWORD2 YTAPI_HOST LITERAL1 YTAPI_SSL_PORT LITERAL1 YTAPI_TIMEOUT LITERAL1 +YTAPI_CHANNEL_ENDPOINT LITERAL1 +YTAPI_VIDEO_ENDPOINT LITERAL1 +YTAPI_REQUEST_FORMAT LITERAL1 \ No newline at end of file diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 1606760..7e798f6 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -27,6 +27,7 @@ // TODO // // add video.list:topicDetails +// add custom error types #include "YoutubeApi.h" @@ -170,7 +171,7 @@ bool YoutubeApi::parseVideoStatistics(){ * * @return true on success, false on error */ -bool YoutubeApi::parseContentDetails(){ +bool YoutubeApi::parseVideoContentDetails(){ bool wasSuccessful = false; // Get from https://arduinojson.org/v6/assistant/ @@ -209,19 +210,19 @@ bool YoutubeApi::parseContentDetails(){ JsonObject itemcontentDetails = doc["items"][0]["contentDetails"]; - memcpy(contentDets.defintion, itemcontentDetails["definition"].as(), 3); - memcpy(contentDets.dimension, itemcontentDetails["dimension"].as(), 3); - strcpy(contentDets.projection, itemcontentDetails["projection"].as()); + memcpy(videoContentDets.defintion, itemcontentDetails["definition"].as(), 3); + memcpy(videoContentDets.dimension, itemcontentDetails["dimension"].as(), 3); + strcpy(videoContentDets.projection, itemcontentDetails["projection"].as()); if("false" == itemcontentDetails["caption"]){ - contentDets.caption = true; + videoContentDets.caption = true; } else{ - contentDets.caption = false; + videoContentDets.caption = false; } - contentDets.licensedContent = itemcontentDetails["licensedContent"].as(); - contentDets.duration = parseDuration(itemcontentDetails["duration"].as()); + videoContentDets.licensedContent = itemcontentDetails["licensedContent"].as(); + videoContentDets.duration = parseDuration(itemcontentDetails["duration"].as()); } closeClient(); @@ -234,7 +235,7 @@ bool YoutubeApi::parseContentDetails(){ * * @return true on success, false on error */ -bool YoutubeApi::parseSnippet(){ +bool YoutubeApi::parseVideoSnippet(){ bool wasSuccessful = false; @@ -275,42 +276,42 @@ bool YoutubeApi::parseSnippet(){ else{ JsonObject itemsSnippet = doc["items"][0]["snippet"]; - if(snip.set){ - freeSnippet(&snip); + if(videoSnip.set){ + freeVideoSnippet(&videoSnip); } int checksum = 0; - snip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); - snip.categoryId = itemsSnippet["categoryId"].as(); + videoSnip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); + videoSnip.categoryId = itemsSnippet["categoryId"].as(); - checksum += allocAndCopy(&snip.channelId, itemsSnippet["channelId"].as()); - checksum += allocAndCopy(&snip.title, itemsSnippet["title"].as()); - checksum += allocAndCopy(&snip.description, itemsSnippet["description"].as()); - checksum += allocAndCopy(&snip.channelTitle, itemsSnippet["channelTitle"].as()); - checksum += allocAndCopy(&snip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); + checksum += allocAndCopy(&videoSnip.channelId, itemsSnippet["channelId"].as()); + checksum += allocAndCopy(&videoSnip.title, itemsSnippet["title"].as()); + checksum += allocAndCopy(&videoSnip.description, itemsSnippet["description"].as()); + checksum += allocAndCopy(&videoSnip.channelTitle, itemsSnippet["channelTitle"].as()); + checksum += allocAndCopy(&videoSnip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); // language informations appears to be optional, so it is being checked if it is in response // if not, a placeholder will be set if(!itemsSnippet["defaultLanguage"].as()){ - checksum += allocAndCopy(&snip.defaultLanguage, ""); + checksum += allocAndCopy(&videoSnip.defaultLanguage, ""); }else{ - checksum += allocAndCopy(&snip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); + checksum += allocAndCopy(&videoSnip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); } if(!itemsSnippet["defaultAudioLanguage"].as()){ - checksum += allocAndCopy(&snip.defaultAudioLanguage, ""); + checksum += allocAndCopy(&videoSnip.defaultAudioLanguage, ""); }else{ - checksum += allocAndCopy(&snip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + checksum += allocAndCopy(&videoSnip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); } if(checksum){ // don't set snip.set flag in order to avoid false free Serial.print("Error reading in response values. Checksum: "); Serial.println(checksum); - snip.set = false; + videoSnip.set = false; wasSuccessful = false; }else{ - snip.set = true; + videoSnip.set = true; wasSuccessful = true; } } @@ -319,7 +320,11 @@ bool YoutubeApi::parseSnippet(){ return wasSuccessful; } - +/** + * @brief Parses the video status from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ bool YoutubeApi::parseVideoStatus(){ bool wasSuccessful = false; @@ -360,7 +365,7 @@ bool YoutubeApi::parseVideoStatus(){ JsonObject itemsStatus = doc["items"][0]["status"]; if(vStatus.set){ - freeStatus(&vStatus); + freeVideoStatus(&vStatus); } int checksum = 0; @@ -449,11 +454,11 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { break; case videoListContentDetails: - wasSuccessful = parseContentDetails(); + wasSuccessful = parseVideoContentDetails(); break; case videoListSnippet: - wasSuccessful = parseSnippet(); + wasSuccessful = parseVideoSnippet(); break; case videoListStatus: @@ -510,12 +515,12 @@ bool YoutubeApi::getVideoStatistics(const char *videoId){ * @param videoId videoID of the video to get the information from * @return true, if there were no errors and the video was found */ -bool YoutubeApi::getContentDetails(const String& videoId){ +bool YoutubeApi::getVideoContentDetails(const String& videoId){ return getRequestedType(videoListContentDetails, videoId.c_str()); } -bool YoutubeApi::getContentDetails(const char *videoId){ +bool YoutubeApi::getVideoContentDetails(const char *videoId){ return getRequestedType(videoListContentDetails, videoId); } @@ -526,12 +531,12 @@ bool YoutubeApi::getContentDetails(const char *videoId){ * @param videoId videoID of the video to get the information from * @return wasSuccesssful true, if there were no errors and the video was found */ -bool YoutubeApi::getSnippet(const String& videoId){ +bool YoutubeApi::getVideoSnippet(const String& videoId){ return getRequestedType(videoListSnippet, videoId.c_str()); } -bool YoutubeApi::getSnippet(const char *videoId){ +bool YoutubeApi::getVideoSnippet(const char *videoId){ return getRequestedType(videoListSnippet, videoId); } @@ -687,11 +692,11 @@ tm YoutubeApi::parseUploadDate(const char* dateTime){ /** - * @brief Frees memory used by strings in snippet struct. Initializes it with zeros. + * @brief Frees memory used by strings in videoSnippet struct. Initializes it with zeros. * - * @param s Pointer to snippet struct to free + * @param s Pointer to videoSnippet struct to free */ -void YoutubeApi::freeSnippet(snippet *s){ +void YoutubeApi::freeVideoSnippet(videoSnippet *s){ if(!s->set){ return; @@ -705,14 +710,18 @@ void YoutubeApi::freeSnippet(snippet *s){ free(s->defaultLanguage); free(s->defaultAudioLanguage); - memset(s, 0, sizeof(snippet)); + memset(s, 0, sizeof(videoSnippet)); s->set = false; return; } - -void YoutubeApi::freeStatus(videoStatus *s){ +/** + * @brief Frees memory used by strings in videoStatus struct. Initialzes it witn zeroes. + * + * @param s Pointer to videoStatus struct to free + */ +void YoutubeApi::freeVideoStatus(videoStatus *s){ if(!s->set){ return; diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 9393b48..fb7600d 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -51,6 +51,8 @@ enum operation{ }; +// not implemented data fields are commented + struct channelStatistics { long viewCount; long commentCount; /* DEPRECATED */ @@ -60,7 +62,7 @@ struct channelStatistics { }; -struct contentDetails{ +struct videoContentDetails{ tm duration; char dimension[3]; char defintion[3]; @@ -98,7 +100,7 @@ struct videoStatus{ // bool selfDeclaredMadeForKids; }; -struct snippet{ +struct videoSnippet{ bool set; tm publishedAt; char *channelId; @@ -129,19 +131,20 @@ class YoutubeApi bool getVideoStatistics(const String& videoId); bool getVideoStatistics(const char *videoId); - bool getContentDetails(const String& videoId); - bool getContentDetails(const char *videoId); + bool getVideoContentDetails(const String& videoId); + bool getVideoContentDetails(const char *videoId); - bool getSnippet(const String& videoId); - bool getSnippet(const char *videoId); + bool getVideoSnippet(const String& videoId); + bool getVideoSnippet(const char *videoId); bool getVideoStatus(const String& videoId); bool getVideoStatus(const char *videoId); - snippet snip; channelStatistics channelStats; + + videoSnippet videoSnip; videoStatistics videoStats; - contentDetails contentDets; + videoContentDetails videoContentDets; videoStatus vStatus; bool _debug = false; @@ -151,16 +154,16 @@ class YoutubeApi tm parseDuration(const char *duration); tm parseUploadDate(const char *dateTime); - void freeSnippet(snippet *s); - void freeStatus(videoStatus *s); + void freeVideoSnippet(videoSnippet *s); + void freeVideoStatus(videoStatus *s); int allocAndCopy(char **pos, const char *data); bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); bool parseChannelStatistics(); bool parseVideoStatistics(); - bool parseContentDetails(); - bool parseSnippet(); + bool parseVideoContentDetails(); + bool parseVideoSnippet(); bool parseVideoStatus(); void skipHeaders(); From 0229afe25fae737454fd09e760ba2972ef845dcb Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 11 Aug 2022 23:02:11 +0200 Subject: [PATCH 11/58] making videos a seperate class -> implementing basic functionality to interface with video class --- src/YoutubeApi.cpp | 44 --------------- src/YoutubeApi.h | 1 + src/YoutubeVideo.cpp | 124 +++++++++++++++++++++++++++++++++++++++++++ src/YoutubeVideo.h | 50 +++++++++++++++++ 4 files changed, 175 insertions(+), 44 deletions(-) create mode 100644 src/YoutubeVideo.cpp create mode 100644 src/YoutubeVideo.h diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 7e798f6..b2cdba5 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -691,50 +691,6 @@ tm YoutubeApi::parseUploadDate(const char* dateTime){ } -/** - * @brief Frees memory used by strings in videoSnippet struct. Initializes it with zeros. - * - * @param s Pointer to videoSnippet struct to free - */ -void YoutubeApi::freeVideoSnippet(videoSnippet *s){ - - if(!s->set){ - return; - } - - free(s->channelId); - free(s->title); - free(s->description); - free(s->channelTitle); - free(s->liveBroadcastContent); - free(s->defaultLanguage); - free(s->defaultAudioLanguage); - - memset(s, 0, sizeof(videoSnippet)); - s->set = false; - - return; -} - -/** - * @brief Frees memory used by strings in videoStatus struct. Initialzes it witn zeroes. - * - * @param s Pointer to videoStatus struct to free - */ -void YoutubeApi::freeVideoStatus(videoStatus *s){ - - if(!s->set){ - return; - } - - free(s->uploadStatus); - free(s->license); - free(s->privacyStatus); - - memset(s, 0, sizeof(videoStatus)); - - return; -} /** * @brief Allocates memory and copies a string into it. diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index fb7600d..7ce41d2 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -32,6 +32,7 @@ #include #include +#define YT_VIDEOID_LEN 11 #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 #define YTAPI_TIMEOUT 1500 diff --git a/src/YoutubeVideo.cpp b/src/YoutubeVideo.cpp new file mode 100644 index 0000000..bf789ec --- /dev/null +++ b/src/YoutubeVideo.cpp @@ -0,0 +1,124 @@ +#include +#include + + +YoutubeVideo::YoutubeVideo(const char *newVideoId){ + + if(videoId == NULL){ + return; + } + + setVideoId(newVideoId); +} + +void YoutubeVideo::setVideoId(const char *newVideoId){ + + strncpy(videoId, newVideoId, YT_VIDEOID_LEN); + videoId[YT_VIDEOID_LEN] = '\0'; + videoIdSet = true; +} + +void YoutubeVideo::resetInfo(){ + + if(videoSnipSet){ + freeVideoSnippet(videoSnip); + videoSnip = NULL; + videoSnipSet = false; + } + + if(vStatusSet){ + freeVideoStatus(vStatus); + vStatus = NULL; + vStatusSet = false; + } + + if(videoStatsSet){ + free(videoStats); + videoStats = NULL; + videoStatsSet = false; + } + + if(videoContentDetsSet){ + free(videoContentDets); + videoContentDets = NULL; + videoContentDetsSet = false; + } + + memcpy(videoId, 0, YT_VIDEOID_LEN + 1); + videoIdSet = false; +} + + +YoutubeVideo::~YoutubeVideo(){ + resetInfo(); +} + +/** + * @brief Frees memory used by strings in videoStatus struct. Initialzes it with zeroes. + * + * @param s Pointer to videoStatus struct to free + */ +void YoutubeVideo::freeVideoStatus(videoStatus *s){ + + if(!s->set){ + return; + } + + free(s->uploadStatus); + free(s->license); + free(s->privacyStatus); + + memset(s, 0, sizeof(videoStatus)); + + return; +} + + +const char* YoutubeVideo::getVideoId(){ + return videoId; +} + +String YoutubeVideo::getVideoIdString(){ + return String(videoId); +} + +bool YoutubeVideo::resetVideoId(const char *newVideoId){ + resetInfo(); + + if(newVideoId == NULL){ + return false; + } + + setVideoId(newVideoId); + return true; +} + +bool YoutubeVideo::resetVideoId(String& newVideoId){ + return resetVideoId(newVideoId.c_str()); +} + + +/** + * @brief Frees memory used by strings in videoSnippet struct. Initializes it with zeros. + * + * @param s Pointer to videoSnippet struct to free + */ +void YoutubeApi::freeVideoSnippet(videoSnippet *s){ + + if(!s->set){ + return; + } + + free(s->channelId); + free(s->title); + free(s->description); + free(s->channelTitle); + free(s->liveBroadcastContent); + free(s->defaultLanguage); + free(s->defaultAudioLanguage); + + memset(s, 0, sizeof(videoSnippet)); + s->set = false; + + return; +} \ No newline at end of file diff --git a/src/YoutubeVideo.h b/src/YoutubeVideo.h new file mode 100644 index 0000000..3d4e7d4 --- /dev/null +++ b/src/YoutubeVideo.h @@ -0,0 +1,50 @@ +#ifndef YoutubeVideo_h +#define YoutubeVideo_h + +#include +#include + +class YoutubeVideo{ + + public: + YoutubeVideo(const char *newVideoId); + YoutubeVideo(String& newVideoId): YoutubeVideo(newVideoId.c_str()) {}; + YoutubeVideo(); + + ~YoutubeVideo(); + + bool getVideoStatistics(); + bool getVideoSnippet(); + bool getVideoContentDetails(); + + bool resetVideoId(const char *newVideoId); + bool resetVideoId(String& newVideoId); + void resetInfo(); + const char* getVideoId(); + String getVideoIdString(); + + videoSnippet *videoSnip = NULL; + videoStatistics *videoStats = NULL; + videoContentDetails *videoContentDets = NULL; + videoStatus *vStatus = NULL; + + private: + + char videoId[YT_VIDEOID_LEN + 1]; + + bool videoIdSet = false; + bool videoSnipSet = false; + bool videoStatsSet = false; + bool videoContentDetsSet = false; + bool vStatusSet = false; + + void setVideoId(const char *newVideoId); + + void freeVideoSnippet(videoSnippet *s); + void freeVideoStatus(videoStatus *s); + +}; + + + +#endif \ No newline at end of file From ad064a52bda54945753216cefa489c2b9d437586 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 12 Aug 2022 16:36:00 +0200 Subject: [PATCH 12/58] moving library to lib and creating some unittests --- .gitignore | 3 +- .travis.yml | 27 ------- {src => lib/YoutubeApi}/YoutubeApi.cpp | 0 {src => lib/YoutubeApi}/YoutubeApi.h | 87 +------------------- lib/YoutubeTypes/YoutubeTypes.h | 92 ++++++++++++++++++++++ {src => lib/YoutubeVideo}/YoutubeVideo.cpp | 22 ++++-- {src => lib/YoutubeVideo}/YoutubeVideo.h | 14 +++- platformio.ini | 7 -- scripts/travis/platformioSingle.sh | 3 - test/test_emb/test_YoutubeVideoClass.cpp | 69 ++++++++++++++++ 10 files changed, 188 insertions(+), 136 deletions(-) delete mode 100644 .travis.yml rename {src => lib/YoutubeApi}/YoutubeApi.cpp (100%) rename {src => lib/YoutubeApi}/YoutubeApi.h (63%) create mode 100644 lib/YoutubeTypes/YoutubeTypes.h rename {src => lib/YoutubeVideo}/YoutubeVideo.cpp (90%) rename {src => lib/YoutubeVideo}/YoutubeVideo.h (86%) delete mode 100755 scripts/travis/platformioSingle.sh create mode 100644 test/test_emb/test_YoutubeVideoClass.cpp diff --git a/.gitignore b/.gitignore index 620acb7..9ab6f3c 100644 --- a/.gitignore +++ b/.gitignore @@ -369,5 +369,4 @@ vs-readme.txt .vscode/ # custom ignores -src/*.ino -src/*.txt \ No newline at end of file +src/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c7bce7e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python -python: - - "2.7" - -# Cache PlatformIO packages using Travis CI container-based infrastructure -sudo: false -cache: - directories: - - "~/.platformio" - -env: - - SCRIPT=platformioSingle EXAMPLE_NAME=ChannelStatistics EXAMPLE_FOLDER=/ BOARD=d1_mini - - SCRIPT=platformioSingle EXAMPLE_NAME=ChannelStatistics EXAMPLE_FOLDER=/ BOARD=tinypico - -install: - - pip install -U platformio - # - # Libraries from PlatformIO Library Registry: - # - # http://platformio.org/lib/show/64/ArduinoJson - - platformio lib -g install 64 - # http://platformio.org/lib/show/567/WifiManager - - platformio lib -g install 567 - # Double Reset Detector (not yet on PIO) - - platformio lib -g install https://github.com/datacute/DoubleResetDetector.git - -script: scripts/travis/$SCRIPT.sh diff --git a/src/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp similarity index 100% rename from src/YoutubeApi.cpp rename to lib/YoutubeApi/YoutubeApi.cpp diff --git a/src/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h similarity index 63% rename from src/YoutubeApi.h rename to lib/YoutubeApi/YoutubeApi.h index 7ce41d2..452dc4f 100644 --- a/src/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -28,94 +28,9 @@ #define YoutubeApi_h #include +#include "YoutubeTypes.h" #include #include -#include - -#define YT_VIDEOID_LEN 11 -#define YTAPI_HOST "www.googleapis.com" -#define YTAPI_SSL_PORT 443 -#define YTAPI_TIMEOUT 1500 - -#define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" -#define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" -#define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" - -enum operation{ - - videoListStats, - videoListContentDetails, - videoListSnippet, - videoListStatus, - - channelListStats -}; - - -// not implemented data fields are commented - -struct channelStatistics { - long viewCount; - long commentCount; /* DEPRECATED */ - long subscriberCount; - bool hiddenSubscriberCount; - long videoCount; -}; - - -struct videoContentDetails{ - tm duration; - char dimension[3]; - char defintion[3]; - bool caption; - bool licensedContent; -// char **regionRestriction; -// char **contentRating; - char projection[12]; -// bool hasCustomThumbnail; -}; - - -struct videoStatistics { - long viewCount; - long commentCount; - long likeCount; -// long favourites; -// long dislikeCount; - -// In Memory of the old dislike count. -}; - - -struct videoStatus{ - bool set; - char *uploadStatus; -// char *failureReason; -// char *rejectionReason; - char *privacyStatus; -// tm publishAt; - char *license; - bool embeddable; - bool publicStatsViewable; - bool madeForKids; -// bool selfDeclaredMadeForKids; -}; - -struct videoSnippet{ - bool set; - tm publishedAt; - char *channelId; - char *title; - char *description; -// char **thumbnails; - char *channelTitle; -// char **tags; - int categoryId; - char *liveBroadcastContent; - char *defaultLanguage; -// char **localized; - char *defaultAudioLanguage; -}; class YoutubeApi { diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h new file mode 100644 index 0000000..142ea3d --- /dev/null +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -0,0 +1,92 @@ +#ifndef YT_TYPES +#define YT_TYPES + +#include + +#define YT_VIDEOID_LEN 11 +#define YTAPI_HOST "www.googleapis.com" +#define YTAPI_SSL_PORT 443 +#define YTAPI_TIMEOUT 1500 + +#define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" +#define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" +#define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" + + +enum operation{ + + videoListStats, + videoListContentDetails, + videoListSnippet, + videoListStatus, + + channelListStats +}; + + +// not implemented data fields are commented + +struct channelStatistics { + long viewCount; + long commentCount; /* DEPRECATED */ + long subscriberCount; + bool hiddenSubscriberCount; + long videoCount; +}; + + +struct videoContentDetails{ + tm duration; + char dimension[3]; + char defintion[3]; + bool caption; + bool licensedContent; +// char **regionRestriction; +// char **contentRating; + char projection[12]; +// bool hasCustomThumbnail; +}; + + +struct videoStatistics { + long viewCount; + long commentCount; + long likeCount; +// long favourites; +// long dislikeCount; + +// In Memory of the old dislike count. +}; + + +struct videoStatus{ + bool set; + char *uploadStatus; +// char *failureReason; +// char *rejectionReason; + char *privacyStatus; +// tm publishAt; + char *license; + bool embeddable; + bool publicStatsViewable; + bool madeForKids; +// bool selfDeclaredMadeForKids; +}; + +struct videoSnippet{ + bool set; + tm publishedAt; + char *channelId; + char *title; + char *description; +// char **thumbnails; + char *channelTitle; +// char **tags; + int categoryId; + char *liveBroadcastContent; + char *defaultLanguage; +// char **localized; + char *defaultAudioLanguage; +}; + +#endif \ No newline at end of file diff --git a/src/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp similarity index 90% rename from src/YoutubeVideo.cpp rename to lib/YoutubeVideo/YoutubeVideo.cpp index bf789ec..c445155 100644 --- a/src/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -1,6 +1,4 @@ -#include -#include - +#include "YoutubeVideo.h" YoutubeVideo::YoutubeVideo(const char *newVideoId){ @@ -11,6 +9,8 @@ YoutubeVideo::YoutubeVideo(const char *newVideoId){ setVideoId(newVideoId); } +YoutubeVideo::YoutubeVideo(){} + void YoutubeVideo::setVideoId(const char *newVideoId){ strncpy(videoId, newVideoId, YT_VIDEOID_LEN); @@ -44,7 +44,7 @@ void YoutubeVideo::resetInfo(){ videoContentDetsSet = false; } - memcpy(videoId, 0, YT_VIDEOID_LEN + 1); + strncpy(videoId, "", YT_VIDEOID_LEN + 1); videoIdSet = false; } @@ -53,6 +53,7 @@ YoutubeVideo::~YoutubeVideo(){ resetInfo(); } + /** * @brief Frees memory used by strings in videoStatus struct. Initialzes it with zeroes. * @@ -74,6 +75,11 @@ void YoutubeVideo::freeVideoStatus(videoStatus *s){ } +bool YoutubeVideo::checkVideoIdSet(){ + return videoIdSet; +} + + const char* YoutubeVideo::getVideoId(){ return videoId; } @@ -83,12 +89,13 @@ String YoutubeVideo::getVideoIdString(){ } bool YoutubeVideo::resetVideoId(const char *newVideoId){ - resetInfo(); if(newVideoId == NULL){ return false; } + resetInfo(); + setVideoId(newVideoId); return true; } @@ -103,7 +110,7 @@ bool YoutubeVideo::resetVideoId(String& newVideoId){ * * @param s Pointer to videoSnippet struct to free */ -void YoutubeApi::freeVideoSnippet(videoSnippet *s){ +void YoutubeVideo::freeVideoSnippet(videoSnippet *s){ if(!s->set){ return; @@ -121,4 +128,5 @@ void YoutubeApi::freeVideoSnippet(videoSnippet *s){ s->set = false; return; -} \ No newline at end of file +} + diff --git a/src/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h similarity index 86% rename from src/YoutubeVideo.h rename to lib/YoutubeVideo/YoutubeVideo.h index 3d4e7d4..9379603 100644 --- a/src/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -1,21 +1,23 @@ #ifndef YoutubeVideo_h #define YoutubeVideo_h +#include "YoutubeTypes.h" #include -#include class YoutubeVideo{ - public: + public: + YoutubeVideo(); + YoutubeVideo(const char *newVideoId); YoutubeVideo(String& newVideoId): YoutubeVideo(newVideoId.c_str()) {}; - YoutubeVideo(); - + ~YoutubeVideo(); bool getVideoStatistics(); bool getVideoSnippet(); bool getVideoContentDetails(); + bool checkVideoIdSet(); bool resetVideoId(const char *newVideoId); bool resetVideoId(String& newVideoId); @@ -43,6 +45,10 @@ class YoutubeVideo{ void freeVideoSnippet(videoSnippet *s); void freeVideoStatus(videoStatus *s); + #ifdef UNIT_TESTING + friend void test_StringConstructor_simple() + #endif + }; diff --git a/platformio.ini b/platformio.ini index 623fcc1..1354f21 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,13 +11,6 @@ [common] lib_deps_external = ArduinoJson -[env:d1_mini] -platform = espressif8266 -board = d1_mini -framework = arduino -lib_deps = ${common.lib_deps_external} -monitor_speed = 115200 - [env:esp32_devkit] platform = espressif32 board = esp32doit-devkit-v1 diff --git a/scripts/travis/platformioSingle.sh b/scripts/travis/platformioSingle.sh deleted file mode 100755 index f375ded..0000000 --- a/scripts/travis/platformioSingle.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -eux - -platformio ci $PWD/examples/$EXAMPLE_NAME/$EXAMPLE_NAME.ino -l '.' -b $BOARD diff --git a/test/test_emb/test_YoutubeVideoClass.cpp b/test/test_emb/test_YoutubeVideoClass.cpp new file mode 100644 index 0000000..8e599fc --- /dev/null +++ b/test/test_emb/test_YoutubeVideoClass.cpp @@ -0,0 +1,69 @@ +#include +#include +#include "YoutubeVideo.h" + +#define UNIT_TESTING 1 + +void setUp(){ + +} + +void tearDown(){ + +} + +void test_emptyConstructor(){ + YoutubeVideo uut; + + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoSnip, "videoSnip pointer supposed to be NULL!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoStats, "videoStats pointer supposed to be NULL!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoContentDets, "videoContentDets pointer supposed to be NULL!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.vStatus, "vStatus pPointer supposed to be NULL!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); + +} + + +void test_constCharConstructor_simple(){ + YoutubeVideo uut("epic"); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("epic", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); +} + +void test_StringConstructor_simple(){ + String testString = "epic"; + YoutubeVideo uut(testString); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("epic", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); +} + + +void test_resetVideoId_simple(){ + YoutubeVideo uut("epic"); + + bool ret = uut.resetVideoId("oof"); + TEST_ASSERT_EQUAL(true, ret); + TEST_ASSERT_EQUAL_STRING_MESSAGE("oof", uut.getVideoId(), "VideoId did not change correctly!"); + TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); +} + + +void setup(){ + + Serial.begin(115200); + + UNITY_BEGIN(); + + RUN_TEST(test_emptyConstructor); + RUN_TEST(test_constCharConstructor_simple); + RUN_TEST(test_StringConstructor_simple); + RUN_TEST(test_resetVideoId_simple); + + UNITY_END(); +} + +void loop(){ + +} \ No newline at end of file From c225dac6157878f282253afdb90500caf4cf303c Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 12 Aug 2022 20:05:20 +0200 Subject: [PATCH 13/58] adding doc and new testcases --- lib/YoutubeVideo/YoutubeVideo.cpp | 40 +++++++-- lib/YoutubeVideo/YoutubeVideo.h | 2 +- test/test_emb/test_YoutubeVideoClass.cpp | 102 ++++++++++++++++++----- 3 files changed, 115 insertions(+), 29 deletions(-) diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index c445155..2ea6bcf 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -11,13 +11,30 @@ YoutubeVideo::YoutubeVideo(const char *newVideoId){ YoutubeVideo::YoutubeVideo(){} -void YoutubeVideo::setVideoId(const char *newVideoId){ +/** + * @brief Sets the new videoId. Sets videoIdSet on success + * + * @param newVideoId new videoId to set + * @return true If the id is valid (len = YT_VIDEOID_LEN) + * @return false If the id is not valid or a null pointer. + */ +bool YoutubeVideo::setVideoId(const char *newVideoId){ + + if(!newVideoId || strlen(newVideoId) != YT_VIDEOID_LEN){ + return false; + } strncpy(videoId, newVideoId, YT_VIDEOID_LEN); videoId[YT_VIDEOID_LEN] = '\0'; videoIdSet = true; + + return true; } +/** + * @brief Deletes all information from object. + * + */ void YoutubeVideo::resetInfo(){ if(videoSnipSet){ @@ -74,7 +91,11 @@ void YoutubeVideo::freeVideoStatus(videoStatus *s){ return; } - +/** + * @brief Get function to check if videoId is set. + * + * @return bool value of videoIdSet + */ bool YoutubeVideo::checkVideoIdSet(){ return videoIdSet; } @@ -88,16 +109,21 @@ String YoutubeVideo::getVideoIdString(){ return String(videoId); } +/** + * @brief Resets the videoId and all information in the object. + * Even if the id is not valid, all information gets deleted. + * + * @param newVideoId new video id to set + * @return bool true on success, false if setting the id fails + */ bool YoutubeVideo::resetVideoId(const char *newVideoId){ + resetInfo(); + if(newVideoId == NULL){ return false; } - - resetInfo(); - - setVideoId(newVideoId); - return true; + return setVideoId(newVideoId); } bool YoutubeVideo::resetVideoId(String& newVideoId){ diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index 9379603..b11cc68 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -40,7 +40,7 @@ class YoutubeVideo{ bool videoContentDetsSet = false; bool vStatusSet = false; - void setVideoId(const char *newVideoId); + bool setVideoId(const char *newVideoId); void freeVideoSnippet(videoSnippet *s); void freeVideoStatus(videoStatus *s); diff --git a/test/test_emb/test_YoutubeVideoClass.cpp b/test/test_emb/test_YoutubeVideoClass.cpp index 8e599fc..796d72f 100644 --- a/test/test_emb/test_YoutubeVideoClass.cpp +++ b/test/test_emb/test_YoutubeVideoClass.cpp @@ -4,15 +4,21 @@ #define UNIT_TESTING 1 -void setUp(){ +const char *validIdChar = "12345678901"; +const char *invalidIdChar = "123"; +String validIdString = "12345678901"; +String invalidIdString = "123"; +void setUp() +{ } -void tearDown(){ - +void tearDown() +{ } -void test_emptyConstructor(){ +void test_emptyConstructor() +{ YoutubeVideo uut; TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoSnip, "videoSnip pointer supposed to be NULL!"); @@ -20,37 +26,84 @@ void test_emptyConstructor(){ TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoContentDets, "videoContentDets pointer supposed to be NULL!"); TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.vStatus, "vStatus pPointer supposed to be NULL!"); TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); - } +void test_constCharConstructor_simple() +{ + YoutubeVideo uut(validIdChar); -void test_constCharConstructor_simple(){ - YoutubeVideo uut("epic"); - - TEST_ASSERT_EQUAL_STRING_MESSAGE("epic", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); } -void test_StringConstructor_simple(){ - String testString = "epic"; - YoutubeVideo uut(testString); +void test_constCharConstructor_rejectId() +{ + YoutubeVideo uut(invalidIdChar); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); +} + +void test_StringConstructor_simple() +{ + YoutubeVideo uut(validIdString); - TEST_ASSERT_EQUAL_STRING_MESSAGE("epic", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); } +void test_StringConstructor_rejectId() +{ + YoutubeVideo uut(invalidIdString); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Did not return right videoId!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); +} + +void test_resetVideoIdConstChar_simple() +{ + YoutubeVideo uut(validIdChar); + + bool ret = uut.resetVideoId("10987654321"); + TEST_ASSERT_EQUAL(true, ret); + TEST_ASSERT_EQUAL_STRING_MESSAGE("10987654321", uut.getVideoId(), "VideoId did not change correctly!"); + TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); +} -void test_resetVideoId_simple(){ - YoutubeVideo uut("epic"); +void test_resetVideoIdConstChar_videoId_notSet() +{ + YoutubeVideo uut; - bool ret = uut.resetVideoId("oof"); + bool ret = uut.resetVideoId(validIdChar); TEST_ASSERT_EQUAL(true, ret); - TEST_ASSERT_EQUAL_STRING_MESSAGE("oof", uut.getVideoId(), "VideoId did not change correctly!"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, uut.getVideoId(), "VideoId did not change correctly!"); TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); } +void test_resetVideoIdConstChar_videoId_toLong() +{ + YoutubeVideo uut(validIdChar); + + const char videoId[15] = "01234567891011"; -void setup(){ + bool ret = uut.resetVideoId(videoId); + TEST_ASSERT_EQUAL(false, ret); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "VideoId did not change correctly!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); +} + +void test_resetVideoIdConstChar_videoId_toShort() +{ + YoutubeVideo uut(validIdChar); + + bool ret = uut.resetVideoId(invalidIdChar); + TEST_ASSERT_EQUAL(false, ret); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "VideoId did not change correctly!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); +} + +void setup() +{ Serial.begin(115200); @@ -58,12 +111,19 @@ void setup(){ RUN_TEST(test_emptyConstructor); RUN_TEST(test_constCharConstructor_simple); + RUN_TEST(test_constCharConstructor_rejectId); + RUN_TEST(test_StringConstructor_simple); - RUN_TEST(test_resetVideoId_simple); + RUN_TEST(test_resetVideoIdConstChar_simple); + RUN_TEST(test_StringConstructor_rejectId); + + RUN_TEST(test_resetVideoIdConstChar_videoId_notSet); + RUN_TEST(test_resetVideoIdConstChar_videoId_toLong); + RUN_TEST(test_resetVideoIdConstChar_videoId_toShort); UNITY_END(); } -void loop(){ - +void loop() +{ } \ No newline at end of file From d82ba8b9b3f1ad53a03a607fa3b96da3e37633e7 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 13 Aug 2022 12:00:26 +0200 Subject: [PATCH 14/58] adding new testcases --- lib/YoutubeVideo/YoutubeVideo.h | 2 +- test/test_emb/test_YoutubeVideoClass.cpp | 47 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index b11cc68..5a1a11b 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -32,7 +32,7 @@ class YoutubeVideo{ private: - char videoId[YT_VIDEOID_LEN + 1]; + char videoId[YT_VIDEOID_LEN + 1] = ""; bool videoIdSet = false; bool videoSnipSet = false; diff --git a/test/test_emb/test_YoutubeVideoClass.cpp b/test/test_emb/test_YoutubeVideoClass.cpp index 796d72f..017b1d3 100644 --- a/test/test_emb/test_YoutubeVideoClass.cpp +++ b/test/test_emb/test_YoutubeVideoClass.cpp @@ -9,14 +9,6 @@ const char *invalidIdChar = "123"; String validIdString = "12345678901"; String invalidIdString = "123"; -void setUp() -{ -} - -void tearDown() -{ -} - void test_emptyConstructor() { YoutubeVideo uut; @@ -84,22 +76,48 @@ void test_resetVideoIdConstChar_videoId_toLong() { YoutubeVideo uut(validIdChar); - const char videoId[15] = "01234567891011"; + const char videoId[13] = "012345678910"; bool ret = uut.resetVideoId(videoId); TEST_ASSERT_EQUAL(false, ret); TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "VideoId did not change correctly!"); - TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); } void test_resetVideoIdConstChar_videoId_toShort() { YoutubeVideo uut(validIdChar); - bool ret = uut.resetVideoId(invalidIdChar); + const char videoId[11] = "0123456789"; + bool ret = uut.resetVideoId(videoId); TEST_ASSERT_EQUAL(false, ret); TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "VideoId did not change correctly!"); - TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); +} + +void test_getVideoIdConstChar_videoId_notSet(){ + YoutubeVideo uut; + + const char* vidId = uut.getVideoId(); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", vidId, "Expected a empty string, as video id has not been set yet!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); +} + +void test_getVideoIdConstChar_videoId_set(){ + YoutubeVideo uut(validIdChar); + + const char* vidId = uut.getVideoId(); + TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, vidId, "Did not return correct string"); + TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); +} + +void test_resetInfo_afterConstruct(){ + YoutubeVideo uut(validIdChar); + uut.resetInfo(); + + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); + } void setup() @@ -121,6 +139,11 @@ void setup() RUN_TEST(test_resetVideoIdConstChar_videoId_toLong); RUN_TEST(test_resetVideoIdConstChar_videoId_toShort); + RUN_TEST(test_getVideoIdConstChar_videoId_notSet); + RUN_TEST(test_getVideoIdConstChar_videoId_set); + + RUN_TEST(test_resetInfo_afterConstruct); + UNITY_END(); } From 040c6f807104e8611d66e43dad6d8a679cc11694 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 13 Aug 2022 20:30:32 +0200 Subject: [PATCH 15/58] moving functions from api to video class and new tests --- lib/YoutubeApi/YoutubeApi.cpp | 65 ++++++++++++++----- lib/YoutubeApi/YoutubeApi.h | 14 ++-- lib/YoutubeTypes/YoutubeTypes.h | 2 +- lib/YoutubeVideo/YoutubeVideo.cpp | 61 ++++++++++++++++- lib/YoutubeVideo/YoutubeVideo.h | 11 +++- test/test_apiClass/test_YoutubeApiClass.cpp | 31 +++++++++ .../test_YoutubeVideoClass.cpp | 18 ++--- 7 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 test/test_apiClass/test_YoutubeApiClass.cpp rename test/{test_emb => test_videoClass}/test_YoutubeVideoClass.cpp (92%) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index b2cdba5..bf2beae 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -30,14 +30,18 @@ // add custom error types #include "YoutubeApi.h" +#include -YoutubeApi::YoutubeApi(const char* key, Client &client) - : apiKey(key), client(client) -{} +char YoutubeApi::apiKey[YTAPI_KEY_LEN + 1] = ""; +YoutubeApi::YoutubeApi(const char* key, Client &client) : client(client) +{ + strncpy(apiKey, key, YTAPI_KEY_LEN); + apiKey[YTAPI_KEY_LEN] = '\0'; +} -YoutubeApi::YoutubeApi(const String &apiKey, Client &client) - : YoutubeApi(apiKey.c_str(), client) // passing the key as c-string to force a copy +YoutubeApi::YoutubeApi(const String &key, Client &newClient) + : YoutubeApi(key.c_str(), newClient) // passing the key as c-string to force a copy {} @@ -411,30 +415,30 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { switch (op) { case videoListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey.c_str()); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey); break; case videoListContentDetails: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey.c_str()); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey); break; case videoListSnippet: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey.c_str()); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey); break; case videoListStatus: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey.c_str()); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey); break; case channelListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey.c_str()); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey); break; default: Serial.println("Unknown operation"); return false; } - + if (_debug){ Serial.println(command); } @@ -476,6 +480,39 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { return wasSuccessful; } +bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { + + switch (mode) + { + case videoListStats: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey); + break; + + case videoListContentDetails: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey); + break; + + case videoListSnippet: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey); + break; + + case videoListStatus: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey); + break; + + case channelListStats: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey); + break; + + default: + Serial.println("Unknown operation"); + return false; + } + + return true; +} + + /** * @brief Gets the statistics of a specific channel. Stores them in the calling object. @@ -738,11 +775,6 @@ void YoutubeApi::skipHeaders() { { char c = 0; client.readBytes(&c, 1); - if (_debug) - { - Serial.print("Tossing an unexpected character: "); - Serial.println(c); - } } } @@ -760,7 +792,6 @@ int YoutubeApi::getHttpStatusCode() { void YoutubeApi::closeClient() { if(client.connected()) { - if(_debug) { Serial.println(F("Closing client")); } client.stop(); } } diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index 452dc4f..d47dfc8 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -35,8 +35,10 @@ class YoutubeApi { public: - YoutubeApi(const char *key, Client &client); - YoutubeApi(const String& apiKey, Client& client); + YoutubeApi(const char *key, Client &newClient); + YoutubeApi(const String& key, Client& newClient); + + static bool createRequestString(int mode, char *command, const char *id); int sendGetToYoutube(const char *command); int sendGetToYoutube(const String& command); @@ -63,10 +65,13 @@ class YoutubeApi videoContentDetails videoContentDets; videoStatus vStatus; bool _debug = false; + Client &client; + + void closeClient(); private: - const String apiKey; - Client &client; + static char apiKey[YTAPI_KEY_LEN + 1]; + // Client &client; tm parseDuration(const char *duration); tm parseUploadDate(const char *dateTime); @@ -83,7 +88,6 @@ class YoutubeApi bool parseVideoStatus(); void skipHeaders(); - void closeClient(); }; #endif diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 142ea3d..65ee2ca 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -11,7 +11,7 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" #define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" - +#define YTAPI_KEY_LEN 30 enum operation{ diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index 2ea6bcf..1bc3856 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -1,15 +1,15 @@ #include "YoutubeVideo.h" -YoutubeVideo::YoutubeVideo(const char *newVideoId){ +YoutubeVideo::YoutubeVideo(const char *newVideoId, YoutubeApi *obj){ if(videoId == NULL){ return; } - + apiObj = obj; setVideoId(newVideoId); } -YoutubeVideo::YoutubeVideo(){} +YoutubeVideo::YoutubeVideo(): apiObj(){} /** * @brief Sets the new videoId. Sets videoIdSet on success @@ -156,3 +156,58 @@ void YoutubeVideo::freeVideoSnippet(videoSnippet *s){ return; } +bool YoutubeVideo::parseVideoStatistics(){ + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(2) + + 2*JSON_OBJECT_SIZE(4) + + JSON_OBJECT_SIZE(5) + + 330; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + + if (error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + + videoStatistics *newStats = (videoStatistics*) malloc(sizeof(videoStatistics)); + + JsonObject itemStatistics = doc["items"][0]["statistics"]; + + newStats->viewCount = itemStatistics["viewCount"].as(); + newStats->likeCount = itemStatistics["likeCount"].as(); + newStats->commentCount= itemStatistics["commentCount"].as(); + + videoStats = newStats; + wasSuccessful = true; + } + + apiObj->closeClient(); + + return wasSuccessful; +} + +bool YoutubeVideo::getVideoStatistics(){ + + char command[150]; + YoutubeApi::createRequestString(videoListStats, command, videoId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseVideoStatistics(); + } + + return false; +} diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index 5a1a11b..d995812 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -2,6 +2,9 @@ #define YoutubeVideo_h #include "YoutubeTypes.h" +#include "YoutubeApi.h" +#include +#include #include class YoutubeVideo{ @@ -9,8 +12,8 @@ class YoutubeVideo{ public: YoutubeVideo(); - YoutubeVideo(const char *newVideoId); - YoutubeVideo(String& newVideoId): YoutubeVideo(newVideoId.c_str()) {}; + YoutubeVideo(const char *newVideoId, YoutubeApi *apiObj); + YoutubeVideo(String& newVideoId, YoutubeApi *apiObj): YoutubeVideo(newVideoId.c_str(), apiObj) {}; ~YoutubeVideo(); @@ -40,6 +43,10 @@ class YoutubeVideo{ bool videoContentDetsSet = false; bool vStatusSet = false; + YoutubeApi *apiObj; + + bool parseVideoStatistics(); + bool setVideoId(const char *newVideoId); void freeVideoSnippet(videoSnippet *s); diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp new file mode 100644 index 0000000..7b48d70 --- /dev/null +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "YoutubeApi.h" +#include + + +void test_createRequestString_videoStats_simple(){ + WiFiClientSecure client; + YoutubeApi uut("123", client); + + char uutCommand[150]; + const char *expectedRes = "/youtube/v3/videos?part=statistics&id=456&key=123"; + YoutubeApi::createRequestString(videoListStats, uutCommand, "456"); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + +void setup() +{ + + Serial.begin(115200); + + UNITY_BEGIN(); + RUN_TEST(test_createRequestString_videoStats_simple); + UNITY_END(); +} + +void loop() +{ +} \ No newline at end of file diff --git a/test/test_emb/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp similarity index 92% rename from test/test_emb/test_YoutubeVideoClass.cpp rename to test/test_videoClass/test_YoutubeVideoClass.cpp index 017b1d3..f779ce9 100644 --- a/test/test_emb/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -22,7 +22,7 @@ void test_emptyConstructor() void test_constCharConstructor_simple() { - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); @@ -30,7 +30,7 @@ void test_constCharConstructor_simple() void test_constCharConstructor_rejectId() { - YoutubeVideo uut(invalidIdChar); + YoutubeVideo uut(invalidIdChar, NULL); TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); @@ -38,7 +38,7 @@ void test_constCharConstructor_rejectId() void test_StringConstructor_simple() { - YoutubeVideo uut(validIdString); + YoutubeVideo uut(validIdString, NULL); TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(true, uut.checkVideoIdSet(), "videoId should be set"); @@ -46,7 +46,7 @@ void test_StringConstructor_simple() void test_StringConstructor_rejectId() { - YoutubeVideo uut(invalidIdString); + YoutubeVideo uut(invalidIdString, NULL); TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Did not return right videoId!"); TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should be set"); @@ -54,7 +54,7 @@ void test_StringConstructor_rejectId() void test_resetVideoIdConstChar_simple() { - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); bool ret = uut.resetVideoId("10987654321"); TEST_ASSERT_EQUAL(true, ret); @@ -74,7 +74,7 @@ void test_resetVideoIdConstChar_videoId_notSet() void test_resetVideoIdConstChar_videoId_toLong() { - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); const char videoId[13] = "012345678910"; @@ -86,7 +86,7 @@ void test_resetVideoIdConstChar_videoId_toLong() void test_resetVideoIdConstChar_videoId_toShort() { - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); const char videoId[11] = "0123456789"; bool ret = uut.resetVideoId(videoId); @@ -104,7 +104,7 @@ void test_getVideoIdConstChar_videoId_notSet(){ } void test_getVideoIdConstChar_videoId_set(){ - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); const char* vidId = uut.getVideoId(); TEST_ASSERT_EQUAL_STRING_MESSAGE(validIdChar, vidId, "Did not return correct string"); @@ -112,7 +112,7 @@ void test_getVideoIdConstChar_videoId_set(){ } void test_resetInfo_afterConstruct(){ - YoutubeVideo uut(validIdChar); + YoutubeVideo uut(validIdChar, NULL); uut.resetInfo(); TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); From 3ac50a7e3b2cb96db766dfe94ebc2b02b36f883d Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 00:54:21 +0200 Subject: [PATCH 16/58] fixing size reserved for api key --- lib/YoutubeTypes/YoutubeTypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 65ee2ca..a02c9e3 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -11,7 +11,7 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" #define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" -#define YTAPI_KEY_LEN 30 +#define YTAPI_KEY_LEN 45 enum operation{ From b49551b827cfb13dc55f7bb7f3abc7304b022a9c Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 01:04:29 +0200 Subject: [PATCH 17/58] removing duplicate functions from YoutubeApi --- lib/YoutubeApi/YoutubeApi.cpp | 64 ------------------------------- lib/YoutubeApi/YoutubeApi.h | 4 -- lib/YoutubeVideo/YoutubeVideo.cpp | 6 +++ 3 files changed, 6 insertions(+), 68 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index bf2beae..3106a9d 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -126,50 +126,6 @@ bool YoutubeApi::parseChannelStatistics() { } -/** - * @brief Parses the video statistics from caller client. Stores information in calling object. - * - * @return true on success, false on error - */ -bool YoutubeApi::parseVideoStatistics(){ - - bool wasSuccessful = false; - - // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = JSON_ARRAY_SIZE(1) - + JSON_OBJECT_SIZE(2) - + 2*JSON_OBJECT_SIZE(4) - + JSON_OBJECT_SIZE(5) - + 330; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client); - - if (error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.println("No results found for video id "); - } - else{ - JsonObject itemStatistics = doc["items"][0]["statistics"]; - - videoStats.viewCount = itemStatistics["viewCount"].as(); - videoStats.likeCount = itemStatistics["likeCount"].as(); - videoStats.commentCount= itemStatistics["commentCount"].as(); - - wasSuccessful = true; - } - - closeClient(); - return wasSuccessful; -} - - /** * @brief Parses the video content details from caller client. Stores information in calling object. * @@ -452,10 +408,6 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { case channelListStats: wasSuccessful = parseChannelStatistics(); break; - - case videoListStats: - wasSuccessful = parseVideoStatistics(); - break; case videoListContentDetails: wasSuccessful = parseVideoContentDetails(); @@ -530,22 +482,6 @@ bool YoutubeApi::getChannelStatistics(const char *channelId) { } -/** - * @brief Gets the statistics of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return true, if there were no errors and the video was found - */ -bool YoutubeApi::getVideoStatistics(const String& videoId){ - return getRequestedType(videoListStats, videoId.c_str()); -} - - -bool YoutubeApi::getVideoStatistics(const char *videoId){ - return getRequestedType(videoListStats, videoId); -} - - /** * @brief Gets the content details of a specific video. Stores them in the calling object. * diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index d47dfc8..beabce1 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -46,9 +46,6 @@ class YoutubeApi bool getChannelStatistics(const String& channelId); bool getChannelStatistics(const char *channelId); - bool getVideoStatistics(const String& videoId); - bool getVideoStatistics(const char *videoId); - bool getVideoContentDetails(const String& videoId); bool getVideoContentDetails(const char *videoId); @@ -82,7 +79,6 @@ class YoutubeApi int getHttpStatusCode(); bool parseChannelStatistics(); - bool parseVideoStatistics(); bool parseVideoContentDetails(); bool parseVideoSnippet(); bool parseVideoStatus(); diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index 1bc3856..e067ddc 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -156,6 +156,12 @@ void YoutubeVideo::freeVideoSnippet(videoSnippet *s){ return; } + +/** + * @brief Parses the video statistics from client in YoutubeApi object. Stores information in calling object. + * + * @return true on success, false on error + */ bool YoutubeVideo::parseVideoStatistics(){ bool wasSuccessful = false; From a156114cf7c953adb34e1964719ca4a3884796e2 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 02:04:23 +0200 Subject: [PATCH 18/58] adding new testcases --- .gitignore | 3 +- lib/YoutubeVideo/YoutubeVideo.cpp | 1 + test/test_apiClass/test_YoutubeApiClass.cpp | 27 ++++++++ .../test_YoutubeVideoClass.cpp | 65 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9ab6f3c..d40a532 100644 --- a/.gitignore +++ b/.gitignore @@ -369,4 +369,5 @@ vs-readme.txt .vscode/ # custom ignores -src/* \ No newline at end of file +src/* +lib/secrets/* \ No newline at end of file diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index e067ddc..ffb4c4d 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -198,6 +198,7 @@ bool YoutubeVideo::parseVideoStatistics(){ videoStats = newStats; wasSuccessful = true; + videoStatsSet = true; } apiObj->closeClient(); diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 7b48d70..14dc118 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -1,6 +1,7 @@ #include #include #include "YoutubeApi.h" +#include "YoutubeTypes.h" #include @@ -15,6 +16,30 @@ void test_createRequestString_videoStats_simple(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_videoStats_length40(){ + WiFiClientSecure client; + char Length40ApiKey[41] = "1234567890123456789012345678901234567890"; + YoutubeApi uut(Length40ApiKey, client); + + char uutCommand[150]; + const char *expectedRes = "/youtube/v3/videos?part=statistics&id=456&key=1234567890123456789012345678901234567890"; + YoutubeApi::createRequestString(videoListStats, uutCommand, "456"); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + +void test_createRequestString_videoStats_length46(){ + WiFiClientSecure client; + char length41ApiKey[47] = "1234567890123456789012345678901234567890123456"; + YoutubeApi uut(length41ApiKey, client); + + char uutCommand[150]; + const char *expectedRes = "/youtube/v3/videos?part=statistics&id=456&key=123456789012345678901234567890123456789012345"; // should be cut off + YoutubeApi::createRequestString(videoListStats, uutCommand, "456"); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + void setup() { @@ -23,6 +48,8 @@ void setup() UNITY_BEGIN(); RUN_TEST(test_createRequestString_videoStats_simple); + RUN_TEST(test_createRequestString_videoStats_length40); + RUN_TEST(test_createRequestString_videoStats_length46); UNITY_END(); } diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index f779ce9..9c69518 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -2,6 +2,10 @@ #include #include "YoutubeVideo.h" +#include +#include +#include "secrets.h" // API key and wifi password are defined in here + #define UNIT_TESTING 1 const char *validIdChar = "12345678901"; @@ -120,6 +124,62 @@ void test_resetInfo_afterConstruct(){ } +bool establishInternetConnection(){ + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + WiFi.begin(SSID, WIFI_PASSWORD); + delay(2000); + + if(!WiFi.status() != WL_CONNECTED){ + return false; + } + + return false; +} + + +void test_getVideoStats_simple(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoStatistics(); + + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoStats, "There should be a videoStatistics object set!"); + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); +} + +void test_getVideoStats_simple_reset(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoStatistics(); + + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoStats, "There should be a videoStatistics object set!"); + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + + uut.resetInfo(); + + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoStats, "Videostats should have been reset!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); +} + void setup() { @@ -144,6 +204,11 @@ void setup() RUN_TEST(test_resetInfo_afterConstruct); + establishInternetConnection(); + RUN_TEST(test_getVideoStats_simple); + delay(250); + RUN_TEST(test_getVideoStats_simple_reset); + UNITY_END(); } From 83f2e7580bb030134bf09c6bfb14812bc5dcbe16 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 14:57:40 +0200 Subject: [PATCH 19/58] -moving videoSnippet functionality to YoutubeVideo -adding new testcases --- lib/YoutubeApi/YoutubeApi.cpp | 128 ++------------ lib/YoutubeApi/YoutubeApi.h | 9 +- lib/YoutubeVideo/YoutubeVideo.cpp | 163 +++++++++++++++++- lib/YoutubeVideo/YoutubeVideo.h | 8 + test/test_apiClass/test_YoutubeApiClass.cpp | 55 ++++++ .../test_YoutubeVideoClass.cpp | 70 +++++++- 6 files changed, 303 insertions(+), 130 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 3106a9d..60f6b08 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -189,97 +189,6 @@ bool YoutubeApi::parseVideoContentDetails(){ return wasSuccessful; } - -/** - * @brief Parses the video snippet from caller client. Stores information in calling object. - * - * @return true on success, false on error - */ -bool YoutubeApi::parseVideoSnippet(){ - - bool wasSuccessful = false; - - // should be more, just to test - // description can be as large as 5kb, title 400 bytes - const size_t bufferSize = 6000; - - // Creating a filter to filter out - // metadata, thumbnail links, tags, localized information - StaticJsonDocument<256> filter; - - JsonObject filterItems = filter["items"][0].createNestedObject("snippet"); - filterItems["publishedAt"] = true; - filterItems["channelId"] = true; - filterItems["channelTitle"] = true; - filterItems["title"] = true; - filterItems["description"] = true; - filterItems["categoryId"] = true; - filterItems["liveBroadcastContent"] = true; - filterItems["defaultLanguage"] = true; - filterItems["defaultAudioLanguage"] = true; - filter["pageInfo"] = true; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); - - // check for errors and empty response - if(error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.println("No results found for video id "); - } - else{ - JsonObject itemsSnippet = doc["items"][0]["snippet"]; - - if(videoSnip.set){ - freeVideoSnippet(&videoSnip); - } - int checksum = 0; - - videoSnip.publishedAt = parseUploadDate(itemsSnippet["publishedAt"]); - videoSnip.categoryId = itemsSnippet["categoryId"].as(); - - checksum += allocAndCopy(&videoSnip.channelId, itemsSnippet["channelId"].as()); - checksum += allocAndCopy(&videoSnip.title, itemsSnippet["title"].as()); - checksum += allocAndCopy(&videoSnip.description, itemsSnippet["description"].as()); - checksum += allocAndCopy(&videoSnip.channelTitle, itemsSnippet["channelTitle"].as()); - checksum += allocAndCopy(&videoSnip.liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); - - // language informations appears to be optional, so it is being checked if it is in response - // if not, a placeholder will be set - if(!itemsSnippet["defaultLanguage"].as()){ - checksum += allocAndCopy(&videoSnip.defaultLanguage, ""); - }else{ - checksum += allocAndCopy(&videoSnip.defaultLanguage, itemsSnippet["defaultLanguage"].as()); - } - - if(!itemsSnippet["defaultAudioLanguage"].as()){ - checksum += allocAndCopy(&videoSnip.defaultAudioLanguage, ""); - }else{ - checksum += allocAndCopy(&videoSnip.defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); - } - - if(checksum){ - // don't set snip.set flag in order to avoid false free - Serial.print("Error reading in response values. Checksum: "); - Serial.println(checksum); - videoSnip.set = false; - wasSuccessful = false; - }else{ - videoSnip.set = true; - wasSuccessful = true; - } - } - - closeClient(); - return wasSuccessful; -} - /** * @brief Parses the video status from caller client. Stores information in calling object. * @@ -413,10 +322,6 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { wasSuccessful = parseVideoContentDetails(); break; - case videoListSnippet: - wasSuccessful = parseVideoSnippet(); - break; - case videoListStatus: wasSuccessful = parseVideoStatus(); break; @@ -498,22 +403,6 @@ bool YoutubeApi::getVideoContentDetails(const char *videoId){ } -/** - * @brief Gets the snippet of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return wasSuccesssful true, if there were no errors and the video was found - */ -bool YoutubeApi::getVideoSnippet(const String& videoId){ - return getRequestedType(videoListSnippet, videoId.c_str()); -} - - -bool YoutubeApi::getVideoSnippet(const char *videoId){ - return getRequestedType(videoListSnippet, videoId); -} - - /** * @brief Gets the status of a specific video. Stores them in the calling object. * @@ -679,18 +568,23 @@ int YoutubeApi::allocAndCopy(char **pos, const char *data){ return 1; } - size_t size = strlen(data) + 1; - char *space = (char*) malloc(size); + if(!pos){ + Serial.println("pos is a NULL pointer!"); + return 1; + } + + size_t sizeOfData = strlen(data) + 1; + char *allocatedMemory = (char*) malloc(sizeOfData); - if(!space){ + if(!allocatedMemory){ Serial.println("malloc returned NULL pointer!"); return 1; } - *pos = space; + *pos = allocatedMemory; - memcpy(space, data, size); - space[size - 1] = '\0'; + memcpy(allocatedMemory, data, sizeOfData); + allocatedMemory[sizeOfData - 1] = '\0'; return 0; } diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index beabce1..dcb0deb 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -48,13 +48,13 @@ class YoutubeApi bool getVideoContentDetails(const String& videoId); bool getVideoContentDetails(const char *videoId); - - bool getVideoSnippet(const String& videoId); - bool getVideoSnippet(const char *videoId); bool getVideoStatus(const String& videoId); bool getVideoStatus(const char *videoId); + static int allocAndCopy(char **pos, const char *data); + static tm parseUploadDate(const char *dateTime); + channelStatistics channelStats; videoSnippet videoSnip; @@ -70,17 +70,14 @@ class YoutubeApi static char apiKey[YTAPI_KEY_LEN + 1]; // Client &client; tm parseDuration(const char *duration); - tm parseUploadDate(const char *dateTime); void freeVideoSnippet(videoSnippet *s); void freeVideoStatus(videoStatus *s); - int allocAndCopy(char **pos, const char *data); bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); bool parseChannelStatistics(); bool parseVideoContentDetails(); - bool parseVideoSnippet(); bool parseVideoStatus(); void skipHeaders(); diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index ffb4c4d..e4bdd8e 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -32,7 +32,7 @@ bool YoutubeVideo::setVideoId(const char *newVideoId){ } /** - * @brief Deletes all information from object. + * @brief Deletes all information from object, except the YoutubeApi object. * */ void YoutubeVideo::resetInfo(){ @@ -92,19 +92,60 @@ void YoutubeVideo::freeVideoStatus(videoStatus *s){ } /** - * @brief Get function to check if videoId is set. + * @brief Getter function to check if videoId is set. * - * @return bool value of videoIdSet + * @return true if it is set, false otherwise */ bool YoutubeVideo::checkVideoIdSet(){ return videoIdSet; } +/** + * @brief Getter function to check if videoSnip is set. + * + * @return true if it is set, false otherwise + */ +bool YoutubeVideo::checkVideoSnippetSet() {return videoSnipSet;}; + +/** + * @brief Getter function to check if videoStats is set. + * + * @return true if it is set, false otherwise + */ +bool YoutubeVideo::checkVideoStatisticsSet(){return videoStatsSet;}; + +/** + * @brief Getter function to check if videoContentDets is set. + * + * @return true if it is set, false otherwise + */ +bool YoutubeVideo::checkVideoContentDetailsSet(){return videoContentDetsSet;}; + +/** + * @brief Getter function to check if vStatus is set. + * + * @return true if it is set, false otherwise + */ +bool YoutubeVideo::checkVideoStatusSet(){return vStatusSet;}; +/** + * @brief Getter function to get the value of the current videoId. + * + * @return returns a pointer to the videoId. Might be NULL. + */ const char* YoutubeVideo::getVideoId(){ return videoId; } +YoutubeApi* YoutubeVideo::getYoutubeApiObj(){ + return apiObj; +} + +/** + * @brief Getter function to get the value of the current videoId as String. + * + * @return returns a pointer to the videoId. Might be NULL. + */ String YoutubeVideo::getVideoIdString(){ return String(videoId); } @@ -188,6 +229,8 @@ bool YoutubeVideo::parseVideoStatistics(){ } else{ + //TODO check if videoStat already set + videoStatistics *newStats = (videoStatistics*) malloc(sizeof(videoStatistics)); JsonObject itemStatistics = doc["items"][0]["statistics"]; @@ -218,3 +261,117 @@ bool YoutubeVideo::getVideoStatistics(){ return false; } + +/** + * @brief Gets the snippet of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeVideo::getVideoSnippet(){ + + char command[150]; + YoutubeApi::createRequestString(videoListSnippet, command, videoId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseVideoSnippet(); + } + + return false; +} + +/** + * @brief Parses the video snippet from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeVideo::parseVideoSnippet(){ + + bool wasSuccessful = false; + + // should be more, just to test + // description can be as large as 5kb, title 400 bytes + const size_t bufferSize = 6000; + + // Creating a filter to filter out + // metadata, thumbnail links, tags, localized information + StaticJsonDocument<256> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("snippet"); + filterItems["publishedAt"] = true; + filterItems["channelId"] = true; + filterItems["channelTitle"] = true; + filterItems["title"] = true; + filterItems["description"] = true; + filterItems["categoryId"] = true; + filterItems["liveBroadcastContent"] = true; + filterItems["defaultLanguage"] = true; + filterItems["defaultAudioLanguage"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + JsonObject itemsSnippet = doc["items"][0]["snippet"]; + + if(videoSnipSet){ + freeVideoSnippet(videoSnip); + } + + videoSnippet *newSnippet = (videoSnippet*) malloc(sizeof(videoSnippet)); + + int checksum = 0; + + newSnippet->publishedAt = YoutubeApi::parseUploadDate(itemsSnippet["publishedAt"]); + newSnippet->categoryId = itemsSnippet["categoryId"].as(); + + checksum += YoutubeApi::allocAndCopy(&newSnippet->channelId, itemsSnippet["channelId"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->title, itemsSnippet["title"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->description, itemsSnippet["description"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->channelTitle, itemsSnippet["channelTitle"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); + + // language informations appears to be optional, so it is being checked if it is in response + // if not, a placeholder will be set + if(!itemsSnippet["defaultLanguage"].as()){ + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultLanguage, ""); + }else{ + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultLanguage, itemsSnippet["defaultLanguage"].as()); + } + + if(!itemsSnippet["defaultAudioLanguage"].as()){ + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultAudioLanguage, ""); + }else{ + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + } + + if(checksum){ + // don't set snip.set flag in order to avoid false free + Serial.print("Error reading in response values. Checksum: "); + Serial.println(checksum); + videoSnipSet = false; + wasSuccessful = false; + }else{ + videoSnipSet = true; + videoSnip = newSnippet; + wasSuccessful = true; + } + } + + apiObj->closeClient(); + delay(20); + return wasSuccessful; +} diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index d995812..a43b93c 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -20,12 +20,19 @@ class YoutubeVideo{ bool getVideoStatistics(); bool getVideoSnippet(); bool getVideoContentDetails(); + bool checkVideoIdSet(); + bool checkVideoSnippetSet(); + bool checkVideoStatisticsSet(); + bool checkVideoContentDetailsSet(); + bool checkVideoStatusSet(); bool resetVideoId(const char *newVideoId); bool resetVideoId(String& newVideoId); void resetInfo(); + const char* getVideoId(); + YoutubeApi* getYoutubeApiObj(); String getVideoIdString(); videoSnippet *videoSnip = NULL; @@ -46,6 +53,7 @@ class YoutubeVideo{ YoutubeApi *apiObj; bool parseVideoStatistics(); + bool parseVideoSnippet(); bool setVideoId(const char *newVideoId); diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 14dc118..b0ee4bf 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -1,5 +1,6 @@ #include #include +#include "secrets.h" #include "YoutubeApi.h" #include "YoutubeTypes.h" #include @@ -40,6 +41,54 @@ void test_createRequestString_videoStats_length46(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_videoSnip_length40(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/videos?part=snippet&id=%s&key=%s", TEST_VID_ID, API_KEY); + YoutubeApi::createRequestString(videoListSnippet, uutCommand, TEST_VID_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + +void test_allocAndCopy_pos_NULL(){ + + const char someData[] = "testdata"; + + int ret = YoutubeApi::allocAndCopy(NULL, someData); + + TEST_ASSERT_EQUAL_MESSAGE(1, ret, "There should be an error copying to NULL :p!"); + +} + +void test_allocAndCopy_data_NULL(){ + + char *destinationOfCopy; + + int ret = YoutubeApi::allocAndCopy(&destinationOfCopy, NULL); + + TEST_ASSERT_EQUAL_MESSAGE(1, ret, "There should be an error copying from NULL :p!"); + +} + +void test_allocAndCopy_simple(){ + + char *destinationOfCopy; + const char someData[] = "testdata"; + + int ret = YoutubeApi::allocAndCopy(&destinationOfCopy, someData); + + TEST_ASSERT_EQUAL_MESSAGE(0, ret, "There should not be an error!"); + if(destinationOfCopy == NULL){ + TEST_FAIL_MESSAGE("Data destination has become a NULL pointer!"); + } + TEST_ASSERT_EQUAL_STRING_MESSAGE(someData, destinationOfCopy, "Data should match!"); + + free(destinationOfCopy); +} + void setup() { @@ -50,6 +99,12 @@ void setup() RUN_TEST(test_createRequestString_videoStats_simple); RUN_TEST(test_createRequestString_videoStats_length40); RUN_TEST(test_createRequestString_videoStats_length46); + RUN_TEST(test_createRequestString_videoSnip_length40); + + RUN_TEST(test_allocAndCopy_pos_NULL); + RUN_TEST(test_allocAndCopy_data_NULL); + RUN_TEST(test_allocAndCopy_simple); + UNITY_END(); } diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 9c69518..ea5d45d 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -124,12 +124,24 @@ void test_resetInfo_afterConstruct(){ } + +void test_resetInfo_keepYoutubeApi_obj(){ + WiFiClientSecure client; + YoutubeApi apiObject(API_KEY, client); + YoutubeApi *pointerToObj = &apiObject; + YoutubeVideo uut(validIdChar, pointerToObj); + + uut.resetInfo(); + + TEST_ASSERT_EQUAL_MESSAGE(pointerToObj, uut.getYoutubeApiObj(), "YoutubeApi object should remain the same!"); +} + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100); - WiFi.begin(SSID, WIFI_PASSWORD); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); delay(2000); if(!WiFi.status() != WL_CONNECTED){ @@ -153,8 +165,9 @@ void test_getVideoStats_simple(){ YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatistics(); - TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoStats, "There should be a videoStatistics object set!"); TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoStats, "There should be a videoStatistics object set!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoStatisticsSet(), "Video statistics flag should be set!"); } void test_getVideoStats_simple_reset(){ @@ -175,11 +188,54 @@ void test_getVideoStats_simple_reset(){ uut.resetInfo(); + TEST_ASSERT_FALSE_MESSAGE(uut.checkVideoStatisticsSet(), "Video statistics flag should not be set!"); TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoStats, "Videostats should have been reset!"); TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); } + +void test_getVideoSnippet_simple(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoSnippet(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoSnippetSet(), "Video snippet flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoSnip, "There should be a snippet object set!"); +} + +void test_getVideoSnippet_simple_reset(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoSnippet(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoSnippetSet(), "Video snippet flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoSnip, "There should be a snippet object set!"); + + uut.resetInfo(); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkVideoSnippetSet(), "Video snippet flag should not be set!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoSnip, "Videosnippet should have been reset!"); +} + void setup() { @@ -203,12 +259,18 @@ void setup() RUN_TEST(test_getVideoIdConstChar_videoId_set); RUN_TEST(test_resetInfo_afterConstruct); + RUN_TEST(test_resetInfo_keepYoutubeApi_obj); + establishInternetConnection(); RUN_TEST(test_getVideoStats_simple); - delay(250); + delay(100); RUN_TEST(test_getVideoStats_simple_reset); - + delay(100); + RUN_TEST(test_getVideoSnippet_simple); + delay(100); + RUN_TEST(test_getVideoSnippet_simple_reset); + UNITY_END(); } From 7f470fa1dff2efbc4153d82e37ca4587f4a0f618 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:40:32 +0200 Subject: [PATCH 20/58] changing variable type to address popular videos --- lib/YoutubeTypes/YoutubeTypes.h | 6 +++--- lib/YoutubeVideo/YoutubeVideo.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index a02c9e3..ee03b44 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -49,9 +49,9 @@ struct videoContentDetails{ struct videoStatistics { - long viewCount; - long commentCount; - long likeCount; + unsigned long long int viewCount; // required for popular videos. (Baby Shark would else overflow xD) + unsigned long commentCount; + unsigned long likeCount; // long favourites; // long dislikeCount; diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index e4bdd8e..2f38073 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -235,7 +235,7 @@ bool YoutubeVideo::parseVideoStatistics(){ JsonObject itemStatistics = doc["items"][0]["statistics"]; - newStats->viewCount = itemStatistics["viewCount"].as(); + newStats->viewCount = itemStatistics["viewCount"].as(); newStats->likeCount = itemStatistics["likeCount"].as(); newStats->commentCount= itemStatistics["commentCount"].as(); From ad7d843e9725164f022293d5d7a07ad5e52ef8ee Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 16:30:21 +0200 Subject: [PATCH 21/58] changing long variable types to inttypes --- lib/YoutubeTypes/YoutubeTypes.h | 7 ++++--- lib/YoutubeVideo/YoutubeVideo.cpp | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index ee03b44..e960c12 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -2,6 +2,7 @@ #define YT_TYPES #include +#include #define YT_VIDEOID_LEN 11 #define YTAPI_HOST "www.googleapis.com" @@ -49,9 +50,9 @@ struct videoContentDetails{ struct videoStatistics { - unsigned long long int viewCount; // required for popular videos. (Baby Shark would else overflow xD) - unsigned long commentCount; - unsigned long likeCount; + uint64_t viewCount; // required for popular videos. (Baby Shark would else overflow xD) + uint32_t commentCount; + uint32_t likeCount; // long favourites; // long dislikeCount; diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index 2f38073..fa1571c 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -235,9 +235,9 @@ bool YoutubeVideo::parseVideoStatistics(){ JsonObject itemStatistics = doc["items"][0]["statistics"]; - newStats->viewCount = itemStatistics["viewCount"].as(); - newStats->likeCount = itemStatistics["likeCount"].as(); - newStats->commentCount= itemStatistics["commentCount"].as(); + newStats->viewCount = itemStatistics["viewCount"].as(); + newStats->likeCount = itemStatistics["likeCount"].as(); + newStats->commentCount= itemStatistics["commentCount"].as(); videoStats = newStats; wasSuccessful = true; From de8d7b0e70f1ee52bbc27ddc679c6338af0d7d17 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 17:08:05 +0200 Subject: [PATCH 22/58] fixing memory leaks --- lib/YoutubeVideo/YoutubeVideo.cpp | 86 ++++++++++--------- lib/YoutubeVideo/YoutubeVideo.h | 4 +- .../test_YoutubeVideoClass.cpp | 1 - 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index fa1571c..76715c3 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -72,23 +72,29 @@ YoutubeVideo::~YoutubeVideo(){ /** - * @brief Frees memory used by strings in videoStatus struct. Initialzes it with zeroes. + * @brief Frees memory used by strings in videoStatus struct and the struct itself. * * @param s Pointer to videoStatus struct to free + * + * @return true on success, false on error */ -void YoutubeVideo::freeVideoStatus(videoStatus *s){ +bool YoutubeVideo::freeVideoStatus(videoStatus *s){ + + if(!s){ + return false; + } if(!s->set){ - return; + return false; } free(s->uploadStatus); free(s->license); free(s->privacyStatus); - memset(s, 0, sizeof(videoStatus)); + free(s); - return; + return true; } /** @@ -173,14 +179,19 @@ bool YoutubeVideo::resetVideoId(String& newVideoId){ /** - * @brief Frees memory used by strings in videoSnippet struct. Initializes it with zeros. + * @brief Frees memory used by strings in videoSnippet struct and the struct itself. * * @param s Pointer to videoSnippet struct to free + * @return true on success, false on error */ -void YoutubeVideo::freeVideoSnippet(videoSnippet *s){ +bool YoutubeVideo::freeVideoSnippet(videoSnippet *s){ + + if(!s){ + return false; + } if(!s->set){ - return; + return false; } free(s->channelId); @@ -191,10 +202,9 @@ void YoutubeVideo::freeVideoSnippet(videoSnippet *s){ free(s->defaultLanguage); free(s->defaultAudioLanguage); - memset(s, 0, sizeof(videoSnippet)); - s->set = false; + free(s); - return; + return true; } @@ -214,10 +224,7 @@ bool YoutubeVideo::parseVideoStatistics(){ + JSON_OBJECT_SIZE(5) + 330; - // Allocate DynamicJsonDocument DynamicJsonDocument doc(bufferSize); - - // Parse JSON object DeserializationError error = deserializeJson(doc, apiObj->client); if (error){ @@ -228,20 +235,15 @@ bool YoutubeVideo::parseVideoStatistics(){ Serial.println("No results found for video id "); } else{ - - //TODO check if videoStat already set - videoStatistics *newStats = (videoStatistics*) malloc(sizeof(videoStatistics)); - JsonObject itemStatistics = doc["items"][0]["statistics"]; - - newStats->viewCount = itemStatistics["viewCount"].as(); - newStats->likeCount = itemStatistics["likeCount"].as(); - newStats->commentCount= itemStatistics["commentCount"].as(); + newStats->viewCount = doc["items"][0]["statistics"]["viewCount"].as(); + newStats->likeCount = doc["items"][0]["statistics"]["likeCount"].as(); + newStats->commentCount= doc["items"][0]["statistics"]["commentCount"].as(); videoStats = newStats; + videoStatsSet = true; wasSuccessful = true; - videoStatsSet = true; } apiObj->closeClient(); @@ -251,6 +253,10 @@ bool YoutubeVideo::parseVideoStatistics(){ bool YoutubeVideo::getVideoStatistics(){ + if(checkVideoStatisticsSet()){ + free(videoStats); + } + char command[150]; YoutubeApi::createRequestString(videoListStats, command, videoId); int httpStatus = apiObj->sendGetToYoutube(command); @@ -270,6 +276,12 @@ bool YoutubeVideo::getVideoStatistics(){ */ bool YoutubeVideo::getVideoSnippet(){ + if(checkVideoSnippetSet()){ + if(!freeVideoSnippet(videoSnip)){ + return false; + } + } + char command[150]; YoutubeApi::createRequestString(videoListSnippet, command, videoId); int httpStatus = apiObj->sendGetToYoutube(command); @@ -325,37 +337,31 @@ bool YoutubeVideo::parseVideoSnippet(){ Serial.println("No results found for video id "); } else{ - JsonObject itemsSnippet = doc["items"][0]["snippet"]; - - if(videoSnipSet){ - freeVideoSnippet(videoSnip); - } - videoSnippet *newSnippet = (videoSnippet*) malloc(sizeof(videoSnippet)); int checksum = 0; - newSnippet->publishedAt = YoutubeApi::parseUploadDate(itemsSnippet["publishedAt"]); - newSnippet->categoryId = itemsSnippet["categoryId"].as(); + newSnippet->publishedAt = YoutubeApi::parseUploadDate(doc["items"][0]["snippet"]["publishedAt"]); + newSnippet->categoryId = doc["items"][0]["snippet"]["categoryId"].as(); - checksum += YoutubeApi::allocAndCopy(&newSnippet->channelId, itemsSnippet["channelId"].as()); - checksum += YoutubeApi::allocAndCopy(&newSnippet->title, itemsSnippet["title"].as()); - checksum += YoutubeApi::allocAndCopy(&newSnippet->description, itemsSnippet["description"].as()); - checksum += YoutubeApi::allocAndCopy(&newSnippet->channelTitle, itemsSnippet["channelTitle"].as()); - checksum += YoutubeApi::allocAndCopy(&newSnippet->liveBroadcastContent, itemsSnippet["liveBroadcastContent"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->channelId, doc["items"][0]["snippet"]["channelId"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->title, doc["items"][0]["snippet"]["title"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->description, doc["items"][0]["snippet"]["description"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->channelTitle, doc["items"][0]["snippet"]["channelTitle"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->liveBroadcastContent, doc["items"][0]["snippet"]["liveBroadcastContent"].as()); // language informations appears to be optional, so it is being checked if it is in response // if not, a placeholder will be set - if(!itemsSnippet["defaultLanguage"].as()){ + if(!doc["items"][0]["snippet"]["defaultLanguage"].as()){ checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultLanguage, ""); }else{ - checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultLanguage, itemsSnippet["defaultLanguage"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultLanguage, doc["items"][0]["snippet"]["defaultLanguage"].as()); } - if(!itemsSnippet["defaultAudioLanguage"].as()){ + if(!doc["items"][0]["snippet"]["defaultAudioLanguage"].as()){ checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultAudioLanguage, ""); }else{ - checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultAudioLanguage, itemsSnippet["defaultAudioLanguage"].as()); + checksum += YoutubeApi::allocAndCopy(&newSnippet->defaultAudioLanguage, doc["items"][0]["snippet"]["defaultAudioLanguage"].as()); } if(checksum){ diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index a43b93c..5182431 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -57,8 +57,8 @@ class YoutubeVideo{ bool setVideoId(const char *newVideoId); - void freeVideoSnippet(videoSnippet *s); - void freeVideoStatus(videoStatus *s); + bool freeVideoSnippet(videoSnippet *s); + bool freeVideoStatus(videoStatus *s); #ifdef UNIT_TESTING friend void test_StringConstructor_simple() diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index ea5d45d..2530990 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -1,7 +1,6 @@ #include #include #include "YoutubeVideo.h" - #include #include #include "secrets.h" // API key and wifi password are defined in here From dee25dba9acaa9404e9d41bc19d991ef9fae07e2 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 14 Aug 2022 22:59:13 +0200 Subject: [PATCH 23/58] moving video status functionality to Video Class --- lib/YoutubeApi/YoutubeApi.cpp | 96 ------------------- lib/YoutubeApi/YoutubeApi.h | 13 +-- lib/YoutubeVideo/YoutubeVideo.cpp | 95 ++++++++++++++++++ lib/YoutubeVideo/YoutubeVideo.h | 2 + .../test_YoutubeVideoClass.cpp | 48 ++++++++++ 5 files changed, 146 insertions(+), 108 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 60f6b08..7c17109 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -189,80 +189,6 @@ bool YoutubeApi::parseVideoContentDetails(){ return wasSuccessful; } -/** - * @brief Parses the video status from caller client. Stores information in calling object. - * - * @return true on success, false on error - */ -bool YoutubeApi::parseVideoStatus(){ - - bool wasSuccessful = false; - const size_t bufferSize = 384; - - // Creating a filter to filter out - // metadata, thumbnail links, tags, localized information - - StaticJsonDocument<192> filter; - - JsonObject filterItems = filter["items"][0].createNestedObject("status"); - filterItems["uploadStatus"] = true; - filterItems["privacyStatus"] = true; - filterItems["license"] = true; - filterItems["embeddable"] = true; - filterItems["publicStatsViewable"] = true; - filterItems["madeForKids"] = true; - - JsonObject filterPageInfo = filter.createNestedObject("pageInfo"); - filterPageInfo["totalResults"] = true; - filterPageInfo["resultsPerPage"] = true; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); - - // check for errors and empty response - if(error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.println("No results found for video id "); - } - else{ - JsonObject itemsStatus = doc["items"][0]["status"]; - - if(vStatus.set){ - freeVideoStatus(&vStatus); - } - - int checksum = 0; - checksum += allocAndCopy(&vStatus.uploadStatus, itemsStatus["uploadStatus"]); - checksum += allocAndCopy(&vStatus.privacyStatus, itemsStatus["privacyStatus"]); - checksum += allocAndCopy(&vStatus.license, itemsStatus["license"]); - - vStatus.embeddable = itemsStatus["embeddable"]; // true - vStatus.publicStatsViewable = itemsStatus["publicStatsViewable"]; // true - vStatus.madeForKids = itemsStatus["madeForKids"]; - - if(checksum){ - // don't set videoStatus.set flag in order to avoid false free - Serial.print("Error reading in response values. Checksum: "); - Serial.println(checksum); - vStatus.set = false; - wasSuccessful = false; - }else{ - vStatus.set = true; - wasSuccessful = true; - } - } - - closeClient(); - return wasSuccessful; -} - - /** * @brief Makes an API request for a specific endpoint and type. Calls a parsing function * to handle parsing. @@ -321,10 +247,6 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { case videoListContentDetails: wasSuccessful = parseVideoContentDetails(); break; - - case videoListStatus: - wasSuccessful = parseVideoStatus(); - break; default: wasSuccessful = false; @@ -402,24 +324,6 @@ bool YoutubeApi::getVideoContentDetails(const char *videoId){ return getRequestedType(videoListContentDetails, videoId); } - -/** - * @brief Gets the status of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return wasSuccesssful true, if there were no errors and the video was found - */ -bool YoutubeApi::getVideoStatus(const String& videoId){ - return getRequestedType(videoListStatus, videoId.c_str()); -} - - -bool YoutubeApi::getVideoStatus(const char *videoId){ - return getRequestedType(videoListStatus, videoId); -} - - - /** * @brief Parses the ISO8601 duration string into a tm time struct. * diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index dcb0deb..76f1698 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -49,18 +49,12 @@ class YoutubeApi bool getVideoContentDetails(const String& videoId); bool getVideoContentDetails(const char *videoId); - bool getVideoStatus(const String& videoId); - bool getVideoStatus(const char *videoId); - static int allocAndCopy(char **pos, const char *data); static tm parseUploadDate(const char *dateTime); channelStatistics channelStats; - - videoSnippet videoSnip; - videoStatistics videoStats; videoContentDetails videoContentDets; - videoStatus vStatus; + bool _debug = false; Client &client; @@ -68,17 +62,12 @@ class YoutubeApi private: static char apiKey[YTAPI_KEY_LEN + 1]; - // Client &client; tm parseDuration(const char *duration); - - void freeVideoSnippet(videoSnippet *s); - void freeVideoStatus(videoStatus *s); bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); bool parseChannelStatistics(); bool parseVideoContentDetails(); - bool parseVideoStatus(); void skipHeaders(); }; diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index 76715c3..c756367 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -381,3 +381,98 @@ bool YoutubeVideo::parseVideoSnippet(){ delay(20); return wasSuccessful; } + +/** + * @brief Gets the status of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return wasSuccesssful true, if there were no errors and the video was found + */ +bool YoutubeVideo::getVideoStatus(){ + if(checkVideoStatusSet()){ + if(!freeVideoStatus(vStatus)){ + return false; + } + } + + char command[150]; + YoutubeApi::createRequestString(videoListStatus, command, videoId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseVideoStatus(); + } + return false; +} + + +/** + * @brief Parses the video status from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeVideo::parseVideoStatus(){ + + bool wasSuccessful = false; + const size_t bufferSize = 384; + + // Creating a filter to filter out + // metadata, thumbnail links, tags, localized information + + StaticJsonDocument<192> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("status"); + filterItems["uploadStatus"] = true; + filterItems["privacyStatus"] = true; + filterItems["license"] = true; + filterItems["embeddable"] = true; + filterItems["publicStatsViewable"] = true; + filterItems["madeForKids"] = true; + + JsonObject filterPageInfo = filter.createNestedObject("pageInfo"); + filterPageInfo["totalResults"] = true; + filterPageInfo["resultsPerPage"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + JsonObject itemsStatus = doc["items"][0]["status"]; + + videoStatus *newVideoStatus = (videoStatus*) malloc(sizeof(videoStatus)); + + int checksum = 0; + checksum += YoutubeApi::allocAndCopy(&newVideoStatus->uploadStatus, itemsStatus["uploadStatus"]); + checksum += YoutubeApi::allocAndCopy(&newVideoStatus->privacyStatus, itemsStatus["privacyStatus"]); + checksum += YoutubeApi::allocAndCopy(&newVideoStatus->license, itemsStatus["license"]); + + newVideoStatus->embeddable = itemsStatus["embeddable"]; // true + newVideoStatus->publicStatsViewable = itemsStatus["publicStatsViewable"]; // true + newVideoStatus->madeForKids = itemsStatus["madeForKids"]; + + if(checksum){ + // don't set videoStatus.set flag in order to avoid false free + Serial.print("Error reading in response values. Checksum: "); + Serial.println(checksum); + wasSuccessful = false; + }else{ + vStatusSet = true; + vStatus = newVideoStatus; + wasSuccessful = true; + } + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index 5182431..134df63 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -20,6 +20,7 @@ class YoutubeVideo{ bool getVideoStatistics(); bool getVideoSnippet(); bool getVideoContentDetails(); + bool getVideoStatus(); bool checkVideoIdSet(); bool checkVideoSnippetSet(); @@ -54,6 +55,7 @@ class YoutubeVideo{ bool parseVideoStatistics(); bool parseVideoSnippet(); + bool parseVideoStatus(); bool setVideoId(const char *newVideoId); diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 2530990..5c23de8 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -235,6 +235,50 @@ void test_getVideoSnippet_simple_reset(){ TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoSnip, "Videosnippet should have been reset!"); } + +void test_getVideoStatus_simple(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoStatus(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoStatusSet(), "Video status flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.vStatus, "There should be a videoStatus object set!"); +} + +void test_getVideoStatus_simple_reset(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoStatus(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoStatusSet(), "Video status flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.vStatus, "There should be a videoStatus object set!"); + + uut.resetInfo(); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkVideoStatusSet(), "Video status flag should not be set!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.vStatus, "There should not be a videoStatus object set!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); +} + void setup() { @@ -269,6 +313,10 @@ void setup() RUN_TEST(test_getVideoSnippet_simple); delay(100); RUN_TEST(test_getVideoSnippet_simple_reset); + delay(100); + RUN_TEST(test_getVideoStatus_simple); + delay(100); + RUN_TEST(test_getVideoStatus_simple_reset); UNITY_END(); } From c7196bd4a6144e84defd86558aa1d213407074ee Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 16 Aug 2022 13:27:42 +0200 Subject: [PATCH 24/58] moving contentDetails functionality to video class --- lib/YoutubeApi/YoutubeApi.cpp | 85 +-------------------------- lib/YoutubeApi/YoutubeApi.h | 7 +-- lib/YoutubeVideo/YoutubeVideo.cpp | 97 +++++++++++++++++++++++++++++++ lib/YoutubeVideo/YoutubeVideo.h | 1 + 4 files changed, 100 insertions(+), 90 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 7c17109..1e416f5 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -126,69 +126,6 @@ bool YoutubeApi::parseChannelStatistics() { } -/** - * @brief Parses the video content details from caller client. Stores information in calling object. - * - * @return true on success, false on error - */ -bool YoutubeApi::parseVideoContentDetails(){ - bool wasSuccessful = false; - - // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = 384; - - - // Creating a filter to filter out - // region restrictions, content rating and metadata - StaticJsonDocument<180> filter; - - JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); - filterItems["duration"] = true; - filterItems["dimension"] = true; - filterItems["definition"] = true; - filterItems["caption"] = true; - filterItems["licensedContent"] = true; - filterItems["projection"] = true; - filter["pageInfo"] = true; - - // Allocate DynamicJsonDocument - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client, DeserializationOption::Filter(filter)); - - // check for errors and empty response - if(error){ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - else if(doc["pageInfo"]["totalResults"].as() == 0){ - Serial.println("No results found for video id "); - } - else{ - wasSuccessful = true; - - JsonObject itemcontentDetails = doc["items"][0]["contentDetails"]; - - memcpy(videoContentDets.defintion, itemcontentDetails["definition"].as(), 3); - memcpy(videoContentDets.dimension, itemcontentDetails["dimension"].as(), 3); - strcpy(videoContentDets.projection, itemcontentDetails["projection"].as()); - - if("false" == itemcontentDetails["caption"]){ - videoContentDets.caption = true; - } - else{ - videoContentDets.caption = false; - } - - videoContentDets.licensedContent = itemcontentDetails["licensedContent"].as(); - videoContentDets.duration = parseDuration(itemcontentDetails["duration"].as()); - } - - closeClient(); - return wasSuccessful; -} - /** * @brief Makes an API request for a specific endpoint and type. Calls a parsing function * to handle parsing. @@ -243,11 +180,7 @@ bool YoutubeApi::getRequestedType(int op, const char *id) { case channelListStats: wasSuccessful = parseChannelStatistics(); break; - - case videoListContentDetails: - wasSuccessful = parseVideoContentDetails(); - break; - + default: wasSuccessful = false; break; @@ -308,22 +241,6 @@ bool YoutubeApi::getChannelStatistics(const char *channelId) { return getRequestedType(channelListStats, channelId); } - -/** - * @brief Gets the content details of a specific video. Stores them in the calling object. - * - * @param videoId videoID of the video to get the information from - * @return true, if there were no errors and the video was found - */ -bool YoutubeApi::getVideoContentDetails(const String& videoId){ - return getRequestedType(videoListContentDetails, videoId.c_str()); -} - - -bool YoutubeApi::getVideoContentDetails(const char *videoId){ - return getRequestedType(videoListContentDetails, videoId); -} - /** * @brief Parses the ISO8601 duration string into a tm time struct. * diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index 76f1698..2de5687 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -46,14 +46,11 @@ class YoutubeApi bool getChannelStatistics(const String& channelId); bool getChannelStatistics(const char *channelId); - bool getVideoContentDetails(const String& videoId); - bool getVideoContentDetails(const char *videoId); - static int allocAndCopy(char **pos, const char *data); static tm parseUploadDate(const char *dateTime); + static tm parseDuration(const char *duration); channelStatistics channelStats; - videoContentDetails videoContentDets; bool _debug = false; Client &client; @@ -62,12 +59,10 @@ class YoutubeApi private: static char apiKey[YTAPI_KEY_LEN + 1]; - tm parseDuration(const char *duration); bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); bool parseChannelStatistics(); - bool parseVideoContentDetails(); void skipHeaders(); }; diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index c756367..29cc2b3 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -255,6 +255,8 @@ bool YoutubeVideo::getVideoStatistics(){ if(checkVideoStatisticsSet()){ free(videoStats); + videoStatsSet = false; + videoStats = NULL; } char command[150]; @@ -280,6 +282,9 @@ bool YoutubeVideo::getVideoSnippet(){ if(!freeVideoSnippet(videoSnip)){ return false; } + + videoSnipSet = false; + videoSnip = NULL; } char command[150]; @@ -393,6 +398,8 @@ bool YoutubeVideo::getVideoStatus(){ if(!freeVideoStatus(vStatus)){ return false; } + vStatusSet = false; + vStatus = NULL; } char command[150]; @@ -476,3 +483,93 @@ bool YoutubeVideo::parseVideoStatus(){ apiObj->closeClient(); return wasSuccessful; } + +/** + * @brief Gets the content details of a specific video. Stores them in the calling object. + * + * @param videoId videoID of the video to get the information from + * @return true, if there were no errors and the video was found + */ +bool YoutubeVideo::getVideoContentDetails(){ + if(videoContentDetsSet){ + free(videoContentDets); + videoContentDets = NULL; + videoContentDetsSet = false; + } + + char command[150]; + YoutubeApi::createRequestString(videoListContentDetails, command, videoId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseVideoContentDetails(); + } + return false; +} + + +/** + * @brief Parses the video content details from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeVideo::parseVideoContentDetails(){ + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 384; + + + // Creating a filter to filter out + // region restrictions, content rating and metadata + StaticJsonDocument<180> filter; + + JsonObject filterItems = filter["items"][0].createNestedObject("contentDetails"); + filterItems["duration"] = true; + filterItems["dimension"] = true; + filterItems["definition"] = true; + filterItems["caption"] = true; + filterItems["licensedContent"] = true; + filterItems["projection"] = true; + filter["pageInfo"] = true; + + // Allocate DynamicJsonDocument + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client, DeserializationOption::Filter(filter)); + + // check for errors and empty response + if(error){ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + else if(doc["pageInfo"]["totalResults"].as() == 0){ + Serial.println("No results found for video id "); + } + else{ + videoContentDetails *newContentDetails = (videoContentDetails*) malloc(sizeof(videoContentDetails)); + + memcpy(newContentDetails ->defintion, doc["items"][0]["contentDetails"]["definition"].as(), 3); + memcpy(newContentDetails ->dimension, doc["items"][0]["contentDetails"]["dimension"].as(), 3); + strcpy(newContentDetails ->projection, doc["items"][0]["contentDetails"]["projection"].as()); + + if("false" == doc["items"][0]["contentDetails"]["caption"]){ + newContentDetails ->caption = true; + } + else{ + newContentDetails->caption = false; + } + + newContentDetails ->licensedContent = doc["items"][0]["contentDetails"]["licensedContent"].as(); + newContentDetails ->duration = YoutubeApi::parseDuration(doc["items"][0]["contentDetails"]["duration"].as()); + + videoContentDets = newContentDetails; + videoContentDetsSet = true; + + wasSuccessful = true; + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index 134df63..e91ea4c 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -56,6 +56,7 @@ class YoutubeVideo{ bool parseVideoStatistics(); bool parseVideoSnippet(); bool parseVideoStatus(); + bool parseVideoContentDetails(); bool setVideoId(const char *newVideoId); From d58a66e0ddfc09e3a7fd090c71ce07742ecd9516 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 16 Aug 2022 18:27:43 +0200 Subject: [PATCH 25/58] adding test for videoContentDetails --- .../test_YoutubeVideoClass.cpp | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 5c23de8..272dadb 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -279,6 +279,50 @@ void test_getVideoStatus_simple_reset(){ TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); } + +void test_getVideoContentDetails_simple(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoContentDetails(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoContentDetailsSet(), "Video content details flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoContentDets, "There should be a content details object set!"); +} + +void test_getVideoContentDetails_simple_reset(){ + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + client.setInsecure(); + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + YoutubeVideo uut("USKD3vPD6ZA", &apiObj); + bool ret = uut.getVideoContentDetails(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch video info!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkVideoContentDetailsSet(), "Video content details flag should be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.videoContentDets, "There should be a content details object set!"); + + uut.resetInfo(); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkVideoContentDetailsSet(), "Video content details flag should not be set!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.videoContentDets, "There should not be a content details object set!"); + TEST_ASSERT_EQUAL_MESSAGE(false, uut.checkVideoIdSet(), "videoId should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getVideoId(), "Expected a empty string, as video id has not been set yet!"); +} + void setup() { @@ -317,6 +361,10 @@ void setup() RUN_TEST(test_getVideoStatus_simple); delay(100); RUN_TEST(test_getVideoStatus_simple_reset); + delay(100); + RUN_TEST(test_getVideoContentDetails_simple); + delay(100); + RUN_TEST(test_getVideoContentDetails_simple_reset); UNITY_END(); } From 9ee517d152d331ac82b8d22ce7fd36a9fa1a9c09 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 19 Aug 2022 22:52:56 +0200 Subject: [PATCH 26/58] internet connection is now established in severaly tries when testing --- test/test_videoClass/test_YoutubeVideoClass.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 272dadb..9ca67bd 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -6,6 +6,7 @@ #include "secrets.h" // API key and wifi password are defined in here #define UNIT_TESTING 1 +#define MAX_WIFI_RETRIES 10 const char *validIdChar = "12345678901"; const char *invalidIdChar = "123"; @@ -141,12 +142,13 @@ bool establishInternetConnection(){ delay(100); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - delay(2000); - if(!WiFi.status() != WL_CONNECTED){ - return false; + for(int tryWIFI = 1; tryWIFI < MAX_WIFI_RETRIES; tryWIFI++){ + if(WiFi.status() == WL_CONNECTED){ + return true; + } + delay(1000); } - return false; } From 529436cd87bd594bcbe7c878469f197f5ec17b88 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 19 Aug 2022 23:34:58 +0200 Subject: [PATCH 27/58] spliting channel functions into seperate class --- lib/YoutubeChannel/YoutubeChannel.cpp | 45 +++++++++++++++++++++++++++ lib/YoutubeChannel/YoutubeChannel.h | 30 ++++++++++++++++++ lib/YoutubeTypes/YoutubeTypes.h | 2 ++ lib/YoutubeVideo/YoutubeVideo.cpp | 2 +- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 lib/YoutubeChannel/YoutubeChannel.cpp create mode 100644 lib/YoutubeChannel/YoutubeChannel.h diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp new file mode 100644 index 0000000..34bbd67 --- /dev/null +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -0,0 +1,45 @@ +#include "YoutubeChannel.h" + +YoutubeChannel::YoutubeChannel(const char *newChannelId, YoutubeApi *newApiObj){ + if(newApiObj){ + apiObj = newApiObj; + } + + setChannelId(newChannelId); +} + + +void YoutubeChannel::setChannelId(const char *newChannelId){ + + if(!newChannelId || strlen(newChannelId) != YT_CHANNELID_LEN){ + return; + } + + strncpy(channelId, newChannelId, YT_CHANNELID_LEN); + channelId[YT_CHANNELID_LEN] = '\0'; + channelIdSet = true; +} + + +const char* YoutubeChannel::getChannelId(){ + return channelId; +} + +YoutubeApi* YoutubeChannel::getYoututbeApiObj(){ + return apiObj; +} + +void YoutubeChannel::freeChannelStats(){ + if(channelStatsSet && channelStats){ + free(channelStats); + } +} + + +void YoutubeChannel::resetInfo(){ + freeChannelStats(); +} + +YoutubeChannel::~YoutubeChannel(){ + resetInfo(); +} \ No newline at end of file diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h new file mode 100644 index 0000000..9d8ad8f --- /dev/null +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -0,0 +1,30 @@ +#include "YoutubeApi.h" +#include "YoutubeTypes.h" + + +class YoutubeChannel{ + + public: + YoutubeChannel(); + YoutubeChannel(const char *newChannelId, YoutubeApi *newApiObj); + YoutubeChannel(String& newChannelId, YoutubeApi *newApiObj): YoutubeChannel(newChannelId.c_str(), newApiObj) {}; + + ~YoutubeChannel(); + + channelStatistics *channelStats = NULL; + bool checkChannelStatsSet(); + YoutubeApi* getYoututbeApiObj(); + const char *getChannelId(); + void resetInfo(); + + private: + + char channelId[YT_CHANNELID_LEN + 1] = ""; + YoutubeApi *apiObj = NULL; + + bool channelIdSet = false; + bool channelStatsSet = false; + + void freeChannelStats(); + void setChannelId(const char *newChannelId); +}; \ No newline at end of file diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index e960c12..df40523 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -5,6 +5,8 @@ #include #define YT_VIDEOID_LEN 11 +#define YT_CHANNELID_LEN 24 + #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 #define YTAPI_TIMEOUT 1500 diff --git a/lib/YoutubeVideo/YoutubeVideo.cpp b/lib/YoutubeVideo/YoutubeVideo.cpp index 29cc2b3..6e41371 100644 --- a/lib/YoutubeVideo/YoutubeVideo.cpp +++ b/lib/YoutubeVideo/YoutubeVideo.cpp @@ -2,7 +2,7 @@ YoutubeVideo::YoutubeVideo(const char *newVideoId, YoutubeApi *obj){ - if(videoId == NULL){ + if(newVideoId == NULL){ return; } apiObj = obj; From 829490f0106c6c73805114965c93511448f31de1 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 20 Aug 2022 12:01:59 +0200 Subject: [PATCH 28/58] adding tests --- lib/YoutubeChannel/YoutubeChannel.cpp | 7 ++ lib/YoutubeChannel/YoutubeChannel.h | 3 +- .../test_YoutubeChannelClass.cpp | 81 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 test/test_channelClass/test_YoutubeChannelClass.cpp diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index 34bbd67..302a150 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -36,8 +36,15 @@ void YoutubeChannel::freeChannelStats(){ } +bool YoutubeChannel::checkChannelIdSet(){ return channelIdSet; } + +bool YoutubeChannel::checkChannelStatsSet(){return channelStatsSet;} + void YoutubeChannel::resetInfo(){ freeChannelStats(); + + strncpy(channelId, "", YT_CHANNELID_LEN + 1); + channelIdSet = false; } YoutubeChannel::~YoutubeChannel(){ diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h index 9d8ad8f..1a046e7 100644 --- a/lib/YoutubeChannel/YoutubeChannel.h +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -5,7 +5,7 @@ class YoutubeChannel{ public: - YoutubeChannel(); + YoutubeChannel(YoutubeApi *newApiObj): YoutubeChannel(NULL, newApiObj){}; YoutubeChannel(const char *newChannelId, YoutubeApi *newApiObj); YoutubeChannel(String& newChannelId, YoutubeApi *newApiObj): YoutubeChannel(newChannelId.c_str(), newApiObj) {}; @@ -13,6 +13,7 @@ class YoutubeChannel{ channelStatistics *channelStats = NULL; bool checkChannelStatsSet(); + bool checkChannelIdSet(); YoutubeApi* getYoututbeApiObj(); const char *getChannelId(); void resetInfo(); diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp new file mode 100644 index 0000000..7c65d72 --- /dev/null +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -0,0 +1,81 @@ +#include +#include +#include "YoutubeChannel.h" +#include + +#define validChannelId "123456789012345678901234" +#define tooLongChannelId "1234567890123456789012345" +#define tooShortChannelId "12345678901234567890123" + + +void test_onlyApiConstructor(){ + WiFiClientSecure client; + YoutubeApi apiDummy("", client); + YoutubeChannel uut(&apiDummy); + + TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "Expected vaild apiObject!"); + TEST_ASSERT_FALSE_MESSAGE(uut.checkChannelIdSet(), "Channel id flag should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getChannelId(), "Channel id should be empty"); +} + +void test_charConstructor_validId(){ + WiFiClientSecure client; + YoutubeApi apiDummy("", client); + YoutubeChannel uut(validChannelId, &apiDummy); + + TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "Expected vaild apiObject!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelIdSet(), "Channel id flag should be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(validChannelId, uut.getChannelId(), "Channel id should be empty"); +} + +void test_charConstructor_tooLongId(){ + WiFiClientSecure client; + YoutubeApi apiDummy("", client); + YoutubeChannel uut(tooLongChannelId, &apiDummy); + + TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "Expected vaild apiObject!"); + TEST_ASSERT_FALSE_MESSAGE(uut.checkChannelIdSet(), "Channel id flag should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getChannelId(), "Channel id should be empty"); +} + +void test_charConstructor_tooShortId(){ + WiFiClientSecure client; + YoutubeApi apiDummy("", client); + YoutubeChannel uut(tooShortChannelId, &apiDummy); + + TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "Expected vaild apiObject!"); + TEST_ASSERT_FALSE_MESSAGE(uut.checkChannelIdSet(), "Channel id flag should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getChannelId(), "Channel id should be empty"); +} + +void test_resetInfo(){ + WiFiClientSecure client; + YoutubeApi apiDummy("", client); + YoutubeChannel uut(validChannelId, &apiDummy); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(validChannelId, uut.getChannelId(), "Channel id not correct!"); + + uut.resetInfo(); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkChannelIdSet(), "channel id flag should not be set"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uut.getChannelId(), "channel id should be empty"); + TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "api object pointer should stay"); +} + + +void setup(){ + Serial.begin(115200); + UNITY_BEGIN(); + + RUN_TEST(test_onlyApiConstructor); + + RUN_TEST(test_charConstructor_validId); + RUN_TEST(test_charConstructor_tooShortId); + RUN_TEST(test_charConstructor_tooLongId); + + RUN_TEST(test_resetInfo); + + UNITY_END(); +} + +void loop(){} \ No newline at end of file From 069025bc2d51ae4568613fe0026c889adfa275f6 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 20 Aug 2022 14:50:24 +0200 Subject: [PATCH 29/58] moving channelStatisitcs functionality into channel class --- lib/YoutubeApi/YoutubeApi.cpp | 123 ------------------ lib/YoutubeApi/YoutubeApi.h | 8 -- lib/YoutubeChannel/YoutubeChannel.cpp | 65 ++++++++- lib/YoutubeChannel/YoutubeChannel.h | 3 + lib/YoutubeTypes/YoutubeTypes.h | 8 +- test/test_apiClass/test_YoutubeApiClass.cpp | 15 +++ .../test_YoutubeChannelClass.cpp | 75 ++++++++++- .../test_YoutubeVideoClass.cpp | 2 +- 8 files changed, 159 insertions(+), 140 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 1e416f5..c8a11a3 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -85,113 +85,6 @@ int YoutubeApi::sendGetToYoutube(const String& command) { return sendGetToYoutube(command.c_str()); } - -/** - * @brief Parses the channel statistics from caller client. Stores information in calling object. - * - * @return true on success, false on error - */ -bool YoutubeApi::parseChannelStatistics() { - - bool wasSuccessful = false; - - // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = JSON_ARRAY_SIZE(1) - + JSON_OBJECT_SIZE(2) - + 2*JSON_OBJECT_SIZE(4) - + JSON_OBJECT_SIZE(5) - + 330; - DynamicJsonDocument doc(bufferSize); - - // Parse JSON object - DeserializationError error = deserializeJson(doc, client); - if (!error){ - JsonObject itemStatistics = doc["items"][0]["statistics"]; - - channelStats.viewCount = itemStatistics["viewCount"].as(); - channelStats.subscriberCount = itemStatistics["subscriberCount"].as(); - channelStats.commentCount = itemStatistics["commentCount"].as(); - channelStats.hiddenSubscriberCount = itemStatistics["hiddenSubscriberCount"].as(); - channelStats.videoCount = itemStatistics["videoCount"].as(); - - wasSuccessful = true; - } - else{ - Serial.print(F("deserializeJson() failed with code ")); - Serial.println(error.c_str()); - } - - closeClient(); - return wasSuccessful; -} - - -/** - * @brief Makes an API request for a specific endpoint and type. Calls a parsing function - * to handle parsing. - * - * @param op API request type to make - * @param id video or channel id - * @return Returns parsing function return value, or false in case of an unexpected HTTP status code. - */ -bool YoutubeApi::getRequestedType(int op, const char *id) { - - char command[150]; - bool wasSuccessful = false; - int httpStatus; - - switch (op) - { - case videoListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey); - break; - - case videoListContentDetails: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey); - break; - - case videoListSnippet: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey); - break; - - case videoListStatus: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey); - break; - - case channelListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey); - break; - - default: - Serial.println("Unknown operation"); - return false; - } - - if (_debug){ - Serial.println(command); - } - - httpStatus = sendGetToYoutube(command); - - if (httpStatus == 200) - { - switch(op) - { - case channelListStats: - wasSuccessful = parseChannelStatistics(); - break; - - default: - wasSuccessful = false; - break; - } - } else { - Serial.print("Unexpected HTTP Status Code: "); - Serial.println(httpStatus); - } - return wasSuccessful; -} - bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { switch (mode) @@ -225,22 +118,6 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { } - -/** - * @brief Gets the statistics of a specific channel. Stores them in the calling object. - * - * @param channelId channelID of the channel to get the information from - * @return true, if there were no errors and the channel was found - */ -bool YoutubeApi::getChannelStatistics(const String& channelId) { - return getRequestedType(channelListStats, channelId.c_str()); -} - - -bool YoutubeApi::getChannelStatistics(const char *channelId) { - return getRequestedType(channelListStats, channelId); -} - /** * @brief Parses the ISO8601 duration string into a tm time struct. * diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index 2de5687..9dfdc26 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -43,14 +43,9 @@ class YoutubeApi int sendGetToYoutube(const char *command); int sendGetToYoutube(const String& command); - bool getChannelStatistics(const String& channelId); - bool getChannelStatistics(const char *channelId); - static int allocAndCopy(char **pos, const char *data); static tm parseUploadDate(const char *dateTime); static tm parseDuration(const char *duration); - - channelStatistics channelStats; bool _debug = false; Client &client; @@ -59,11 +54,8 @@ class YoutubeApi private: static char apiKey[YTAPI_KEY_LEN + 1]; - bool getRequestedType(int op, const char *channelId); int getHttpStatusCode(); - bool parseChannelStatistics(); - void skipHeaders(); }; diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index 302a150..e53e610 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -32,6 +32,8 @@ YoutubeApi* YoutubeChannel::getYoututbeApiObj(){ void YoutubeChannel::freeChannelStats(){ if(channelStatsSet && channelStats){ free(channelStats); + channelStats = NULL; + channelStatsSet = false; } } @@ -49,4 +51,65 @@ void YoutubeChannel::resetInfo(){ YoutubeChannel::~YoutubeChannel(){ resetInfo(); -} \ No newline at end of file +} + +bool YoutubeChannel::getChannelStatistics(){ + if(channelStatsSet){ + freeChannelStats(); + } + + char command[150]; + YoutubeApi::createRequestString(channelListStats, command, channelId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseChannelStatistics(); + } + + return false; +} + + +/** + * @brief Parses the channel statistics from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeChannel::parseChannelStatistics() { + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = JSON_ARRAY_SIZE(1) + + JSON_OBJECT_SIZE(2) + + 2*JSON_OBJECT_SIZE(4) + + JSON_OBJECT_SIZE(5) + + 330; + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + channelStatistics *newChannelStats = (channelStatistics*) malloc(sizeof(channelStatistics)); + + JsonObject itemStatistics = doc["items"][0]["statistics"]; + + newChannelStats->viewCount = itemStatistics["viewCount"].as(); + newChannelStats->subscriberCount = itemStatistics["subscriberCount"].as(); + newChannelStats->hiddenSubscriberCount = itemStatistics["hiddenSubscriberCount"].as(); + newChannelStats->videoCount = itemStatistics["videoCount"].as(); + + channelStats = newChannelStats; + channelStatsSet = true; + + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h index 1a046e7..39d844f 100644 --- a/lib/YoutubeChannel/YoutubeChannel.h +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -18,6 +18,8 @@ class YoutubeChannel{ const char *getChannelId(); void resetInfo(); + bool getChannelStatistics(); + private: char channelId[YT_CHANNELID_LEN + 1] = ""; @@ -28,4 +30,5 @@ class YoutubeChannel{ void freeChannelStats(); void setChannelId(const char *newChannelId); + bool parseChannelStatistics(); }; \ No newline at end of file diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index df40523..89249ab 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -30,11 +30,11 @@ enum operation{ // not implemented data fields are commented struct channelStatistics { - long viewCount; - long commentCount; /* DEPRECATED */ - long subscriberCount; + uint64_t viewCount; +// long commentCount; /* DEPRECATED */ + uint64_t subscriberCount; bool hiddenSubscriberCount; - long videoCount; + uint32_t videoCount; }; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index b0ee4bf..1dbc5b3 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -53,6 +53,19 @@ void test_createRequestString_videoSnip_length40(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_channelStatistics_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/channels?part=statistics&id=%s&key=%s", TEST_CHANNEL_ID, API_KEY); + YoutubeApi::createRequestString(channelListStats, uutCommand, TEST_CHANNEL_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + void test_allocAndCopy_pos_NULL(){ const char someData[] = "testdata"; @@ -101,6 +114,8 @@ void setup() RUN_TEST(test_createRequestString_videoStats_length46); RUN_TEST(test_createRequestString_videoSnip_length40); + RUN_TEST(test_createRequestString_channelStatistics_simple); + RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); RUN_TEST(test_allocAndCopy_simple); diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index 7c65d72..fa6ff9e 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -1,8 +1,11 @@ #include #include #include "YoutubeChannel.h" +#include "secrets.h" #include +#define MAX_WIFI_RETRIES 10 + #define validChannelId "123456789012345678901234" #define tooLongChannelId "1234567890123456789012345" #define tooShortChannelId "12345678901234567890123" @@ -21,11 +24,11 @@ void test_onlyApiConstructor(){ void test_charConstructor_validId(){ WiFiClientSecure client; YoutubeApi apiDummy("", client); - YoutubeChannel uut(validChannelId, &apiDummy); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiDummy); TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "Expected vaild apiObject!"); TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelIdSet(), "Channel id flag should be set"); - TEST_ASSERT_EQUAL_STRING_MESSAGE(validChannelId, uut.getChannelId(), "Channel id should be empty"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(TEST_CHANNEL_ID, uut.getChannelId(), "Channel id wrong!"); } void test_charConstructor_tooLongId(){ @@ -62,6 +65,67 @@ void test_resetInfo(){ TEST_ASSERT_EQUAL_MESSAGE(&apiDummy, uut.getYoututbeApiObj(), "api object pointer should stay"); } +bool establishInternetConnection(){ + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + for(int tryWIFI = 1; tryWIFI < MAX_WIFI_RETRIES; tryWIFI++){ + if(WiFi.status() == WL_CONNECTED){ + return true; + } + delay(1000); + } + return false; +} + +void test_getChannelStatistics_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); + + client.setInsecure(); + + bool ret = uut.getChannelStatistics(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelStatsSet(), "Expected the channel statistics flag to be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelStats, "Expected a channelStatistics object to be set!"); +} + +void test_getChannelStatistics_simple_reset(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); + + client.setInsecure(); + + bool ret = uut.getChannelStatistics(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelStatsSet(), "Expected the channel statistics flag to be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelStats, "Expected a channelStatistics object to be set!"); + + uut.resetInfo(); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkChannelStatsSet(), "Expected the channel statistics flag to not be set!"); + TEST_ASSERT_EQUAL_MESSAGE(NULL, uut.channelStats, "Expected the channelStatistics object to not be set!"); + +} + + void setup(){ Serial.begin(115200); @@ -75,7 +139,12 @@ void setup(){ RUN_TEST(test_resetInfo); + establishInternetConnection(); + + RUN_TEST(test_getChannelStatistics_simple); + RUN_TEST(test_getChannelStatistics_simple_reset); + UNITY_END(); } -void loop(){} \ No newline at end of file +void loop(){} diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 9ca67bd..065ac08 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -373,4 +373,4 @@ void setup() void loop() { -} \ No newline at end of file +} From df9f8b5c2967a5d911b2b468a98f79e51d899b75 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 20 Aug 2022 16:02:23 +0200 Subject: [PATCH 30/58] adding documentation to functions --- lib/YoutubeChannel/YoutubeChannel.cpp | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index e53e610..44a9a57 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -8,7 +8,11 @@ YoutubeChannel::YoutubeChannel(const char *newChannelId, YoutubeApi *newApiObj){ setChannelId(newChannelId); } - +/** + * @brief Private function to set the channel id. + * + * @param newChannelId new channel id o set + */ void YoutubeChannel::setChannelId(const char *newChannelId){ if(!newChannelId || strlen(newChannelId) != YT_CHANNELID_LEN){ @@ -20,15 +24,28 @@ void YoutubeChannel::setChannelId(const char *newChannelId){ channelIdSet = true; } - +/** + * @brief Returns the stored channel id. + * + * @return const char* currently stored channel id. If none is stored, returns an empty string + */ const char* YoutubeChannel::getChannelId(){ return channelId; } +/** + * @brief Returns the current YoutubeApi object used to fetch requests. + * + * @return YoutubeApi* pointer to currrent YoutubeApi object + */ YoutubeApi* YoutubeChannel::getYoututbeApiObj(){ return apiObj; } +/** + * @brief Deletes channel statistics and resets the flag. + * + */ void YoutubeChannel::freeChannelStats(){ if(channelStatsSet && channelStats){ free(channelStats); @@ -38,10 +55,24 @@ void YoutubeChannel::freeChannelStats(){ } +/** + * @brief Returns the flag indicating if channel id is currently set (and valid). + * + * @return boolean value of the flag + */ bool YoutubeChannel::checkChannelIdSet(){ return channelIdSet; } +/** + * @brief Returns the flag indicating if channel statistics object is currently set (and valid). + * + * @return boolean value of the flag + */ bool YoutubeChannel::checkChannelStatsSet(){return channelStatsSet;} +/** + * @brief Resets all information of the YoutubeChannel object, except the YoutubeApi object. + * + */ void YoutubeChannel::resetInfo(){ freeChannelStats(); @@ -53,6 +84,11 @@ YoutubeChannel::~YoutubeChannel(){ resetInfo(); } +/** + * @brief Fetches channel statistics of the set channel id. + * + * @return true on success, otherwise false + */ bool YoutubeChannel::getChannelStatistics(){ if(channelStatsSet){ freeChannelStats(); From 2fd9408bec4e83608a5d13fb08d7d3d870fa7dab Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 16 Sep 2022 22:51:15 +0200 Subject: [PATCH 31/58] it is now possible to fetch channel snippet -> channel name and description -> channel country and creation date -> adding tests --- lib/YoutubeApi/YoutubeApi.cpp | 5 + lib/YoutubeChannel/YoutubeChannel.cpp | 103 ++++++++++++++++++ lib/YoutubeChannel/YoutubeChannel.h | 9 ++ lib/YoutubeTypes/YoutubeTypes.h | 11 +- test/test_apiClass/test_YoutubeApiClass.cpp | 14 +++ .../test_YoutubeChannelClass.cpp | 21 ++++ .../test_YoutubeVideoClass.cpp | 1 + 7 files changed, 163 insertions(+), 1 deletion(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index c8a11a3..bd0a502 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -108,6 +108,10 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { case channelListStats: sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey); break; + + case channelListSnippet: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "snippet", id, apiKey); + break; default: Serial.println("Unknown operation"); @@ -320,6 +324,7 @@ int YoutubeApi::getHttpStatusCode() { void YoutubeApi::closeClient() { if(client.connected()) { + client.flush(); client.stop(); } } diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index 44a9a57..cef516d 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -69,12 +69,20 @@ bool YoutubeChannel::checkChannelIdSet(){ return channelIdSet; } */ bool YoutubeChannel::checkChannelStatsSet(){return channelStatsSet;} +/** + * @brief Returns the flag indicating if channel snippet object is currently set (and valid). + * + * @return boolean value of the flag + */ +bool YoutubeChannel::checkChannelSnipSet(){return channelSnipSet;}; + /** * @brief Resets all information of the YoutubeChannel object, except the YoutubeApi object. * */ void YoutubeChannel::resetInfo(){ freeChannelStats(); + freeChannelSnippet(); strncpy(channelId, "", YT_CHANNELID_LEN + 1); channelIdSet = false; @@ -149,3 +157,98 @@ bool YoutubeChannel::parseChannelStatistics() { apiObj->closeClient(); return wasSuccessful; } + + + +/** + * @brief Fetches channel statistics of the set channel id. + * + * @return true on success, otherwise false + */ +bool YoutubeChannel::getChannelSnippet(){ + if(channelSnipSet){ + freeChannelSnippet(); + } + + char command[150]; + YoutubeApi::createRequestString(channelListSnippet, command, channelId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseChannelSnippet(); + } + + return false; +} + + +/** + * @brief Parses the channel statistics from caller client. Stores information in calling object. + * + * @return true on success, false on error + */ +bool YoutubeChannel::parseChannelSnippet() { + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 2000; + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + channelSnippet *newChannelSnippet = (channelSnippet*) malloc(sizeof(channelSnippet)); + + JsonObject itemSnippet = doc["items"][0]["snippet"]; + + uint8_t errorCode = 0; + + errorCode += YoutubeApi::allocAndCopy(&newChannelSnippet->title, itemSnippet["title"].as()); + errorCode += YoutubeApi::allocAndCopy(&newChannelSnippet->description, itemSnippet["description"].as()); + errorCode += YoutubeApi::allocAndCopy(&newChannelSnippet->country, itemSnippet["country"].as()); + + newChannelSnippet->publishedAt = YoutubeApi::parseUploadDate(itemSnippet["publishedAt"].as()); + + if(errorCode){ + Serial.print("Error code: "); + Serial.print(errorCode); + } + + channelSnip = newChannelSnippet; + channelSnipSet = true; + + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} + + +/** + * @brief Frees the channel snippet struct of the object. + * + */ +void YoutubeChannel::freeChannelSnippet(){ + + if(!channelSnip){ + channelSnipSet = false; + return; + } + + free(channelSnip->title); + free(channelSnip->description); + free(channelSnip->country); + + free(channelSnip); + + channelSnip = NULL; + channelSnipSet = false; +} + diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h index 39d844f..3eb791d 100644 --- a/lib/YoutubeChannel/YoutubeChannel.h +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -12,13 +12,18 @@ class YoutubeChannel{ ~YoutubeChannel(); channelStatistics *channelStats = NULL; + channelSnippet *channelSnip = NULL; + bool checkChannelStatsSet(); + bool checkChannelSnipSet(); bool checkChannelIdSet(); + YoutubeApi* getYoututbeApiObj(); const char *getChannelId(); void resetInfo(); bool getChannelStatistics(); + bool getChannelSnippet(); private: @@ -27,8 +32,12 @@ class YoutubeChannel{ bool channelIdSet = false; bool channelStatsSet = false; + bool channelSnipSet = false; void freeChannelStats(); + void freeChannelSnippet(); void setChannelId(const char *newChannelId); + bool parseChannelStatistics(); + bool parseChannelSnippet(); }; \ No newline at end of file diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 89249ab..3bdf084 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -23,7 +23,8 @@ enum operation{ videoListSnippet, videoListStatus, - channelListStats + channelListStats, + channelListSnippet }; @@ -37,6 +38,14 @@ struct channelStatistics { uint32_t videoCount; }; +struct channelSnippet { + char *title; + char *description; + // char *customUrl; + tm publishedAt; + //char **thumbnails; + char *country; +}; struct videoContentDetails{ tm duration; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 1dbc5b3..64b5a31 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -65,6 +65,19 @@ void test_createRequestString_channelStatistics_simple(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_channelSnippet_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/channels?part=snippet&id=%s&key=%s", TEST_CHANNEL_ID, API_KEY); + YoutubeApi::createRequestString(channelListSnippet, uutCommand, TEST_CHANNEL_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + void test_allocAndCopy_pos_NULL(){ @@ -115,6 +128,7 @@ void setup() RUN_TEST(test_createRequestString_videoSnip_length40); RUN_TEST(test_createRequestString_channelStatistics_simple); + RUN_TEST(test_createRequestString_channelSnippet_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index fa6ff9e..eccd0ca 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -125,6 +125,25 @@ void test_getChannelStatistics_simple_reset(){ } +void test_getChannelSnippet_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); + + client.setInsecure(); + + bool ret = uut.getChannelSnippet(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelSnipSet(), "Expected the channel statistics flag to be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelSnip, "Expected a channelSnip to be set!"); +} + void setup(){ @@ -144,6 +163,8 @@ void setup(){ RUN_TEST(test_getChannelStatistics_simple); RUN_TEST(test_getChannelStatistics_simple_reset); + RUN_TEST(test_getChannelSnippet_simple); + UNITY_END(); } diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index 065ac08..c2410ae 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -144,6 +144,7 @@ bool establishInternetConnection(){ WiFi.begin(WIFI_SSID, WIFI_PASSWORD); for(int tryWIFI = 1; tryWIFI < MAX_WIFI_RETRIES; tryWIFI++){ + if(WiFi.status() == WL_CONNECTED){ return true; } From 993e448a2b602bf8697fa91f4a6a37d4116eef3c Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 30 Sep 2022 13:46:57 +0200 Subject: [PATCH 32/58] implementing channelContentDetails struct -> liked and uploaded videos as playlist link --- lib/YoutubeChannel/YoutubeChannel.cpp | 17 +++++++++++++++++ lib/YoutubeChannel/YoutubeChannel.h | 5 +++++ lib/YoutubeTypes/YoutubeTypes.h | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index cef516d..72de1bf 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -54,6 +54,23 @@ void YoutubeChannel::freeChannelStats(){ } } +/** + * @brief Frees channelContentDetails struct of object. + * + */ +void YoutubeChannel::freeChannelContentDetails(){ + if(!channelContentDetailsSet){ + return; + } + + free(channelContentDets->relatedPlaylistsLikes); + free(channelContentDets->relatedPlaylistsUploads); + free(channelContentDets); + + channelContentDets = NULL; + channelContentDetailsSet = false; +} + /** * @brief Returns the flag indicating if channel id is currently set (and valid). diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h index 3eb791d..386707d 100644 --- a/lib/YoutubeChannel/YoutubeChannel.h +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -13,10 +13,12 @@ class YoutubeChannel{ channelStatistics *channelStats = NULL; channelSnippet *channelSnip = NULL; + channelContentDetails *channelContentDets = NULL; bool checkChannelStatsSet(); bool checkChannelSnipSet(); bool checkChannelIdSet(); + bool checkChannelContentDetailsSet(); YoutubeApi* getYoututbeApiObj(); const char *getChannelId(); @@ -33,9 +35,12 @@ class YoutubeChannel{ bool channelIdSet = false; bool channelStatsSet = false; bool channelSnipSet = false; + bool channelContentDetailsSet = false; void freeChannelStats(); void freeChannelSnippet(); + void freeChannelContentDetails(); + void setChannelId(const char *newChannelId); bool parseChannelStatistics(); diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 3bdf084..03e9f3e 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -47,6 +47,12 @@ struct channelSnippet { char *country; }; +struct channelContentDetails{ + + char* relatedPlaylistsLikes; + char* relatedPlaylistsUploads; +}; + struct videoContentDetails{ tm duration; char dimension[3]; From 4482dfb47003f40927f791e83b81673909d07bd7 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:54:48 +0200 Subject: [PATCH 33/58] adding content details functionality --- lib/YoutubeApi/YoutubeApi.cpp | 4 ++ lib/YoutubeChannel/YoutubeChannel.cpp | 70 +++++++++++++++++++ lib/YoutubeChannel/YoutubeChannel.h | 2 + lib/YoutubeTypes/YoutubeTypes.h | 3 +- test/test_apiClass/test_YoutubeApiClass.cpp | 12 ++++ .../test_YoutubeChannelClass.cpp | 25 +++++++ 6 files changed, 115 insertions(+), 1 deletion(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index bd0a502..002957b 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -113,6 +113,10 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "snippet", id, apiKey); break; + case channelListContentDetails: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "contentDetails", id, apiKey); + break; + default: Serial.println("Unknown operation"); return false; diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index 72de1bf..fe769d1 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -93,6 +93,14 @@ bool YoutubeChannel::checkChannelStatsSet(){return channelStatsSet;} */ bool YoutubeChannel::checkChannelSnipSet(){return channelSnipSet;}; + +/** + * @brief Returns the flag indicating if channel content details object is currently set (and valid). + * + * @return boolean value of the flag + */ +bool YoutubeChannel::checkChannelContentDetailsSet(){return channelContentDetailsSet;} + /** * @brief Resets all information of the YoutubeChannel object, except the YoutubeApi object. * @@ -100,6 +108,7 @@ bool YoutubeChannel::checkChannelSnipSet(){return channelSnipSet;}; void YoutubeChannel::resetInfo(){ freeChannelStats(); freeChannelSnippet(); + freeChannelContentDetails(); strncpy(channelId, "", YT_CHANNELID_LEN + 1); channelIdSet = false; @@ -269,3 +278,64 @@ void YoutubeChannel::freeChannelSnippet(){ channelSnipSet = false; } +/** + * @brief Fetches channel contentDetails of the set channel id. + * + * @return true on success, false on error + */ +bool YoutubeChannel::getChannelContentDetails(){ + if(channelContentDetailsSet){ + freeChannelContentDetails(); + } + + char command[150]; + YoutubeApi::createRequestString(channelListContentDetails, command, channelId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parseChannelContentDetails(); + } + + return false; +} + +bool YoutubeChannel::parseChannelContentDetails(){ + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 600; + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + channelContentDetails *newChannelContentDetails = (channelContentDetails*) malloc(sizeof(channelContentDetails)); + + JsonObject itemContentDetails = doc["items"][0]["contentDetails"]["relatedPlaylists"]; + + uint8_t errorCode = 0; + + errorCode += YoutubeApi::allocAndCopy(&newChannelContentDetails->relatedPlaylistsLikes, itemContentDetails["likes"].as()); + errorCode += YoutubeApi::allocAndCopy(&newChannelContentDetails->relatedPlaylistsUploads, itemContentDetails["uploads"].as()); + + if(errorCode){ + Serial.print("Error code: "); + Serial.print(errorCode); + } + + channelContentDets = newChannelContentDetails; + channelContentDetailsSet = true; + + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} + diff --git a/lib/YoutubeChannel/YoutubeChannel.h b/lib/YoutubeChannel/YoutubeChannel.h index 386707d..ac95bb1 100644 --- a/lib/YoutubeChannel/YoutubeChannel.h +++ b/lib/YoutubeChannel/YoutubeChannel.h @@ -26,6 +26,7 @@ class YoutubeChannel{ bool getChannelStatistics(); bool getChannelSnippet(); + bool getChannelContentDetails(); private: @@ -45,4 +46,5 @@ class YoutubeChannel{ bool parseChannelStatistics(); bool parseChannelSnippet(); + bool parseChannelContentDetails(); }; \ No newline at end of file diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 03e9f3e..8c87d9f 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -24,7 +24,8 @@ enum operation{ videoListStatus, channelListStats, - channelListSnippet + channelListSnippet, + channelListContentDetails }; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 64b5a31..4a10747 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -77,6 +77,17 @@ void test_createRequestString_channelSnippet_simple(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_channelContentDetails_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/channels?part=contentDetails&id=%s&key=%s", TEST_CHANNEL_ID, API_KEY); + YoutubeApi::createRequestString(channelListContentDetails, uutCommand, TEST_CHANNEL_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} void test_allocAndCopy_pos_NULL(){ @@ -129,6 +140,7 @@ void setup() RUN_TEST(test_createRequestString_channelStatistics_simple); RUN_TEST(test_createRequestString_channelSnippet_simple); + RUN_TEST(test_createRequestString_channelContentDetails_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index eccd0ca..0cd4a6c 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -144,6 +144,29 @@ void test_getChannelSnippet_simple(){ TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelSnip, "Expected a channelSnip to be set!"); } +void test_getChannelContentDetails_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure client; + YoutubeApi apiObj(API_KEY, client); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); + + client.setInsecure(); + + bool ret = uut.getChannelContentDetails(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkChannelContentDetailsSet(), "Expected the channel contentDetails flag to be set!"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelContentDets, "Expected a contentDetails to be set!"); + + // checking for valid strings (not NULL) + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelContentDets->relatedPlaylistsLikes, "Expected a valid string"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(NULL, uut.channelContentDets->relatedPlaylistsUploads, "Expected a valid string"); +} + void setup(){ @@ -165,6 +188,8 @@ void setup(){ RUN_TEST(test_getChannelSnippet_simple); + RUN_TEST(test_getChannelContentDetails_simple); + UNITY_END(); } From d05de133b5c47858040486a3ab6bc3b410bc4f4d Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 3 Oct 2022 23:04:26 +0200 Subject: [PATCH 34/58] rewrting video information example --- .../VideoFullInformation.ino | 122 ++++++++++-------- lib/YoutubeChannel/YoutubeChannel.cpp | 1 - 2 files changed, 65 insertions(+), 58 deletions(-) diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoFullInformation/VideoFullInformation.ino index 0fb54eb..b766625 100644 --- a/examples/VideoFullInformation/VideoFullInformation.ino +++ b/examples/VideoFullInformation/VideoFullInformation.ino @@ -38,7 +38,8 @@ // Library for connecting to the YouTube API // https://github.com/witnessmenow/arduino-youtube-api // (search for "youtube" in the Arduino Library Manager) -#include +#include "YoutubeApi.h" +#include "YoutubeVideo.h" // Library used for parsing Json from the API responses // https://github.com/bblanchon/ArduinoJson @@ -46,12 +47,13 @@ #include //------- Replace the following! ------ -const char ssid[] = "xxxx"; // your network SSID (name) -const char password[] = "yyyy"; // your network key +#define WIFI_SSID "xxxx" // your network SSID (name) +#define WIFI_PASSWORD "yyyy" // your network key #define API_KEY "zzzz" // your Google API key //------- ---------------------- ------ + #define timeBetweenRequestGroup 120 * 1000 // 120 seconds, in milliseconds | time between all requests #define timeBetweenRequests 2 * 1000 // 2 seconds, in milliseconds | time between single requests #define videoIdLen 11 @@ -117,8 +119,8 @@ void setup() { // Connect to the WiFi network Serial.print("\nConnecting to WiFi: "); - Serial.println(ssid); - WiFi.begin(ssid, password); + Serial.println(WIFI_SSID); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); @@ -132,6 +134,8 @@ void setup() { // Required if you are using ESP8266 V2.5 or above client.setInsecure(); #endif + + client.setInsecure(); // Uncomment for extra debugging info // api._debug = true; @@ -151,137 +155,141 @@ void setup() { void loop() { + YoutubeVideo vid(videoId, &api); Serial.setTimeout(timeBetweenRequestGroup); + + // fetch and print information in videos.list:statistics + if(vid.getVideoStatistics()){ + Serial.println("\n\nstatistics"); + + Serial.print("|----- Video views: "); + Serial.println(vid.videoStats->viewCount); + + Serial.print("|----- Likes: "); + Serial.println(vid.videoStats->likeCount); + + Serial.print("|----- Comments: "); + Serial.println(vid.videoStats->commentCount); + + Serial.println("-------------------------------------------------"); + } + + + // fetch and print information in videos.list:snippet - if(api.getVideoSnippet(videoId)){ + if(vid.getVideoSnippet()){ Serial.println("\n\nsnippet"); Serial.print("|----- Video title: "); - Serial.println(api.videoSnip.title); + Serial.println(vid.videoSnip->title); Serial.println("|----- Video description: \n"); - Serial.println(api.videoSnip.description); + Serial.println(vid.videoSnip->description); Serial.println(""); Serial.print("|----- Uploaded by (channel title): "); - Serial.println(api.videoSnip.channelTitle); + Serial.println(vid.videoSnip->channelTitle); Serial.print("|----- Uploaded by (channel id): "); - Serial.println(api.videoSnip.channelId); + Serial.println(vid.videoSnip->channelId); Serial.print("|----- Published at (d.m.y h:m:s): "); - Serial.print(api.videoSnip.publishedAt.tm_mday); + Serial.print(vid.videoSnip->publishedAt.tm_mday); Serial.print("."); - Serial.print(api.videoSnip.publishedAt.tm_mon); + Serial.print(vid.videoSnip->publishedAt.tm_mon); Serial.print("."); - Serial.print(api.videoSnip.publishedAt.tm_year + 1900); + Serial.print(vid.videoSnip->publishedAt.tm_year + 1900); Serial.print(" "); - Serial.print(api.videoSnip.publishedAt.tm_hour); + Serial.print(vid.videoSnip->publishedAt.tm_hour); Serial.print(":"); - Serial.print(api.videoSnip.publishedAt.tm_min); + Serial.print(vid.videoSnip->publishedAt.tm_min); Serial.print(":"); - Serial.println(api.videoSnip.publishedAt.tm_sec); + Serial.println(vid.videoSnip->publishedAt.tm_sec); Serial.print("|----- Livebroadcast content: "); - Serial.println(api.videoSnip.liveBroadcastContent); + Serial.println(vid.videoSnip->liveBroadcastContent); Serial.print("|----- Category id: "); - Serial.println(api.videoSnip.categoryId); + Serial.println(vid.videoSnip->categoryId); Serial.print("|----- Default language: "); - Serial.println(api.videoSnip.defaultLanguage); + Serial.println(vid.videoSnip->defaultLanguage); Serial.print("|----- Default audio language: "); - Serial.println(api.videoSnip.defaultAudioLanguage); + Serial.println(vid.videoSnip->defaultAudioLanguage); Serial.println("-------------------------------------------------"); } - delay(timeBetweenRequests); - - // fetch and print information in videos.list:statistics - if(api.getVideoStatistics(videoId)){ - Serial.println("\n\nstatistics"); - - Serial.print("|----- Video views: "); - Serial.println(api.videoStats.viewCount); - - Serial.print("|----- Likes: "); - Serial.println(api.videoStats.likeCount); - - Serial.print("|----- Comments: "); - Serial.println(api.videoStats.commentCount); - - Serial.println("-------------------------------------------------"); - } - delay(timeBetweenRequests); // fetch and print information in videos.list:contentDetails - if(api.getVideoContentDetails(videoId)){ + if(vid.getVideoContentDetails()){ Serial.println("\n\ncontentDetails"); Serial.print("|----- Video duration "); - if(api.videoContentDets.duration.tm_mday != 0){ + if(vid.videoContentDets->duration.tm_mday != 0){ Serial.print("(d:h:m:s): "); - Serial.print(api.videoContentDets.duration.tm_mday); + Serial.print(vid.videoContentDets->duration.tm_mday); Serial.print(":"); }else{ Serial.print("(h:m:s): "); } - Serial.print(api.videoContentDets.duration.tm_hour); + Serial.print(vid.videoContentDets->duration.tm_hour); Serial.print(":"); - Serial.print(api.videoContentDets.duration.tm_min); + Serial.print(vid.videoContentDets->duration.tm_min); Serial.print(":"); - Serial.println(api.videoContentDets.duration.tm_sec); + Serial.println(vid.videoContentDets->duration.tm_sec); Serial.print("|----- Dimension: "); - Serial.println(api.videoContentDets.dimension); + Serial.println(vid.videoContentDets->dimension); Serial.print("|----- Definition: "); - Serial.println(api.videoContentDets.defintion); + Serial.println(vid.videoContentDets->defintion); Serial.print("|----- Captioned: "); - printYesNo(api.videoContentDets.caption); + printYesNo(vid.videoContentDets->caption); Serial.print("|----- Licensed Content: "); - printYesNo(api.videoContentDets.licensedContent); + printYesNo(vid.videoContentDets->licensedContent); Serial.print("|----- Projection: "); - Serial.println(api.videoContentDets.projection); + Serial.println(vid.videoContentDets->projection); Serial.println("-------------------------------------------------"); } - + delay(timeBetweenRequests); - if(api.getVideoStatus(videoId)){ + if(vid.getVideoStatus()){ Serial.println("\n\n status"); Serial.print("|----- upload status: "); - Serial.println(api.vStatus.uploadStatus); + Serial.println(vid.vStatus->uploadStatus); Serial.print("|----- privacy status: "); - Serial.println(api.vStatus.privacyStatus); + Serial.println(vid.vStatus->privacyStatus); Serial.print("|----- license: "); - Serial.println(api.vStatus.license); + Serial.println(vid.vStatus->license); Serial.print("|----- embeddable: "); - printYesNo(api.vStatus.embeddable); + printYesNo(vid.vStatus->embeddable); Serial.print("|----- public stats viewable: "); - printYesNo(api.vStatus.publicStatsViewable); + printYesNo(vid.vStatus->publicStatsViewable); Serial.print("|----- made for kids: "); - printYesNo(api.vStatus.madeForKids); + printYesNo(vid.vStatus->madeForKids); } + + Serial.print("\nRefreshing in "); Serial.print(timeBetweenRequestGroup / 1000.0); Serial.println(" seconds..."); diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index fe769d1..d2f9a5b 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -338,4 +338,3 @@ bool YoutubeChannel::parseChannelContentDetails(){ apiObj->closeClient(); return wasSuccessful; } - From 4a6b2f8409626693e2efcb5b72d229da9df3f792 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 6 Oct 2022 00:42:14 +0200 Subject: [PATCH 35/58] rewriting example and adding extra error checking --- .../ChannelStatistics/ChannelStatistics.ino | 169 ++++++++++++++++-- lib/YoutubeApi/YoutubeApi.cpp | 10 +- lib/YoutubeApi/YoutubeApi.h | 2 + lib/YoutubeChannel/YoutubeChannel.cpp | 41 +++-- 4 files changed, 194 insertions(+), 28 deletions(-) diff --git a/examples/ChannelStatistics/ChannelStatistics.ino b/examples/ChannelStatistics/ChannelStatistics.ino index ac1107c..8aedbfe 100644 --- a/examples/ChannelStatistics/ChannelStatistics.ino +++ b/examples/ChannelStatistics/ChannelStatistics.ino @@ -38,7 +38,8 @@ // Library for connecting to the YouTube API // https://github.com/witnessmenow/arduino-youtube-api // (search for "youtube" in the Arduino Library Manager) -#include +#include "YoutubeApi.h" +#include "YoutubeChannel.h" // Library used for parsing Json from the API responses // https://github.com/bblanchon/ArduinoJson @@ -52,14 +53,64 @@ const char password[] = "yyyy"; // your network key #define CHANNEL_ID "UCezJOfu7OtqGzd5xrP3q6WA" // part of the channel url //------- ---------------------- ------ +#define channelIdLen 24 + +#define timeBetweenRequestGroup 120 * 1000 // 120 seconds, in milliseconds | time between all requests +#define timeBetweenRequests 2 * 1000 // 2 seconds, in milliseconds | time between single requests + WiFiClientSecure client; YoutubeApi api(API_KEY, client); -unsigned long timeBetweenRequests = 60 * 1000; // 60 seconds, in milliseconds +char videoId[channelIdLen + 1]; +unsigned long startTime; + +/** + * @brief Tries to read the videoId from Serial. + * + * @return 1 on success, 0 if no data available + */ +int readVideoId(){ + + if(Serial.available() > channelIdLen - 1){ + + for(int i = 0; i < channelIdLen; i++){ + + videoId[i] = Serial.read(); + } + + videoId[channelIdLen] = '\0'; + return 1; + } + return 0; +} + +/** + * @brief Flushes the Serial input buffer. + * + */ +void flushSerialBuffer(){ + while(Serial.available()){ + Serial.read(); + } +} + +/** + * @brief Prints "Yes\n" if x or "No\n" if not x + * + * @param x parameter + */ +void printYesNo(bool x){ + if(x){ + Serial.println("Yes"); + }else{ + Serial.println("No"); + } +} + void setup() { Serial.begin(115200); - + // Set WiFi to 'station' mode and disconnect // from the AP if it was previously connected WiFi.mode(WIFI_STA); @@ -83,29 +134,115 @@ void setup() { // Required if you are using ESP8266 V2.5 or above client.setInsecure(); #endif - + + client.setInsecure(); + // Uncomment for extra debugging info // api._debug = true; + + flushSerialBuffer(); + Serial.print("Enter channelId: "); + + while(1){ + if(readVideoId()){ + flushSerialBuffer(); + break; + } + } + + Serial.println(videoId); } void loop() { - if(api.getChannelStatistics(CHANNEL_ID)) { - Serial.println("\n---------Stats---------"); - Serial.print("Subscriber Count: "); - Serial.println(api.channelStats.subscriberCount); + Serial.setTimeout(timeBetweenRequestGroup); + + YoutubeChannel channel(videoId, &api); - Serial.print("View Count: "); - Serial.println(api.channelStats.viewCount); + // fetch and print information in channel.list:snippet + if(channel.getChannelSnippet()){ + Serial.println("\n\nsnippet"); - Serial.print("Video Count: "); - Serial.println(api.channelStats.videoCount); + channelSnippet *fetchedSnip = channel.channelSnip; - // Probably not needed :) - //Serial.print("hiddenSubscriberCount: "); - //Serial.println(api.channelStats.hiddenSubscriberCount); - Serial.println("------------------------"); + Serial.print("|----- Channel title: "); + Serial.println(fetchedSnip->title); + + Serial.print("|----- Channel description: "); + Serial.println(fetchedSnip->description); + + Serial.print("|----- Channel country: "); + Serial.println(fetchedSnip->country); + + tm channelCreation = fetchedSnip->publishedAt; + + Serial.print("|----- Channel creation (d.m.y h:m:s): "); + Serial.print(channelCreation.tm_mday); + Serial.print("."); + Serial.print(channelCreation.tm_mon); + Serial.print("."); + Serial.print(channelCreation.tm_year + 1900); + Serial.print(" "); + Serial.print(channelCreation.tm_hour); + Serial.print(":"); + Serial.print(channelCreation.tm_min); + Serial.print(":"); + Serial.println(channelCreation.tm_sec); + + Serial.println("-------------------------------------------------"); + } + + if(channel.getChannelStatistics()){ + Serial.println("\n\nstatistics"); + + channelStatistics *fetchedStats = channel.channelStats; + + Serial.print("|----- Channel views: "); + Serial.println(fetchedStats->viewCount); + + Serial.print("|----- Channel subscriber count hidden? "); + printYesNo(fetchedStats->hiddenSubscriberCount); + + Serial.print("|----- Channel subscribers: "); + Serial.println(fetchedStats->subscriberCount); + + Serial.print("|----- Channel video count: "); + Serial.println(fetchedStats->videoCount); + + Serial.println("-------------------------------------------------"); } + delay(timeBetweenRequests); + + // fetch and print information in channel.list:contentDetails + if(channel.getChannelContentDetails()){ + Serial.println("\n\ncontent details"); + + channelContentDetails *fetchedDetails = channel.channelContentDets; + + Serial.print("|----- Liked videos playlist id: "); + Serial.println(fetchedDetails->relatedPlaylistsLikes); + + Serial.print("|----- Uploaded videos playlist id: "); + Serial.println(fetchedDetails->relatedPlaylistsUploads); + + Serial.println("-------------------------------------------------"); + } + + Serial.print("\nRefreshing in "); + Serial.print(timeBetweenRequestGroup / 1000.0); + Serial.println(" seconds..."); + Serial.print("Or set a new channelId: "); + + startTime = millis(); + flushSerialBuffer(); + + while(millis() - startTime < timeBetweenRequestGroup){ + + if(readVideoId()){; + Serial.println(videoId); + break; + } + } } diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 002957b..974a3a3 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -30,7 +30,6 @@ // add custom error types #include "YoutubeApi.h" -#include char YoutubeApi::apiKey[YTAPI_KEY_LEN + 1] = ""; @@ -325,6 +324,15 @@ int YoutubeApi::getHttpStatusCode() { return -1; } +bool YoutubeApi::checkEmptyResponse(DynamicJsonDocument response){ + + if(response["pageInfo"]["totalResults"].as() == 0){ + return true; + } + + return false; +} + void YoutubeApi::closeClient() { if(client.connected()) { diff --git a/lib/YoutubeApi/YoutubeApi.h b/lib/YoutubeApi/YoutubeApi.h index 9dfdc26..d411968 100644 --- a/lib/YoutubeApi/YoutubeApi.h +++ b/lib/YoutubeApi/YoutubeApi.h @@ -30,6 +30,7 @@ #include #include "YoutubeTypes.h" #include +#include #include class YoutubeApi @@ -46,6 +47,7 @@ class YoutubeApi static int allocAndCopy(char **pos, const char *data); static tm parseUploadDate(const char *dateTime); static tm parseDuration(const char *duration); + static bool checkEmptyResponse(DynamicJsonDocument response); bool _debug = false; Client &client; diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index d2f9a5b..71fbdab 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -161,6 +161,12 @@ bool YoutubeChannel::parseChannelStatistics() { DeserializationError error = deserializeJson(doc, apiObj->client); if (!error){ + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find channel id!"); + apiObj->closeClient(); + return wasSuccessful; + } + channelStatistics *newChannelStats = (channelStatistics*) malloc(sizeof(channelStatistics)); JsonObject itemStatistics = doc["items"][0]["statistics"]; @@ -225,6 +231,12 @@ bool YoutubeChannel::parseChannelSnippet() { DeserializationError error = deserializeJson(doc, apiObj->client); if (!error){ + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find channel id!"); + apiObj->closeClient(); + return wasSuccessful; + } + channelSnippet *newChannelSnippet = (channelSnippet*) malloc(sizeof(channelSnippet)); JsonObject itemSnippet = doc["items"][0]["snippet"]; @@ -240,12 +252,13 @@ bool YoutubeChannel::parseChannelSnippet() { if(errorCode){ Serial.print("Error code: "); Serial.print(errorCode); - } - - channelSnip = newChannelSnippet; - channelSnipSet = true; + }else{ + channelSnip = newChannelSnippet; + channelSnipSet = true; - wasSuccessful = true; + wasSuccessful = true; + } + } else{ Serial.print(F("deserializeJson() failed with code ")); @@ -311,6 +324,12 @@ bool YoutubeChannel::parseChannelContentDetails(){ DeserializationError error = deserializeJson(doc, apiObj->client); if (!error){ + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find channel id!"); + apiObj->closeClient(); + return wasSuccessful; + } + channelContentDetails *newChannelContentDetails = (channelContentDetails*) malloc(sizeof(channelContentDetails)); JsonObject itemContentDetails = doc["items"][0]["contentDetails"]["relatedPlaylists"]; @@ -323,13 +342,13 @@ bool YoutubeChannel::parseChannelContentDetails(){ if(errorCode){ Serial.print("Error code: "); Serial.print(errorCode); - } - - channelContentDets = newChannelContentDetails; - channelContentDetailsSet = true; + }else{ + channelContentDets = newChannelContentDetails; + channelContentDetailsSet = true; - wasSuccessful = true; - } + wasSuccessful = true; + } + } else{ Serial.print(F("deserializeJson() failed with code ")); Serial.println(error.c_str()); From 19643d773fe3cdf26cb8bd0d940b5e4bfe2db239 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 6 Oct 2022 00:46:32 +0200 Subject: [PATCH 36/58] Renaming examples --- .../ChannelInformation.ino} | 2 +- .../VideoInformation.ino} | 0 examples/VideoStatistics/VideoStatistics.ino | 107 ------------------ 3 files changed, 1 insertion(+), 108 deletions(-) rename examples/{ChannelStatistics/ChannelStatistics.ino => ChannelInformation/ChannelInformation.ino} (99%) rename examples/{VideoFullInformation/VideoFullInformation.ino => VideoInformation/VideoInformation.ino} (100%) delete mode 100644 examples/VideoStatistics/VideoStatistics.ino diff --git a/examples/ChannelStatistics/ChannelStatistics.ino b/examples/ChannelInformation/ChannelInformation.ino similarity index 99% rename from examples/ChannelStatistics/ChannelStatistics.ino rename to examples/ChannelInformation/ChannelInformation.ino index 8aedbfe..410d7c1 100644 --- a/examples/ChannelStatistics/ChannelStatistics.ino +++ b/examples/ChannelInformation/ChannelInformation.ino @@ -1,5 +1,5 @@ /******************************************************************* - Read YouTube Channel statistics from the YouTube API + Read YouTube Channel information from the YouTube API and print them to the serial monitor Compatible Boards: diff --git a/examples/VideoFullInformation/VideoFullInformation.ino b/examples/VideoInformation/VideoInformation.ino similarity index 100% rename from examples/VideoFullInformation/VideoFullInformation.ino rename to examples/VideoInformation/VideoInformation.ino diff --git a/examples/VideoStatistics/VideoStatistics.ino b/examples/VideoStatistics/VideoStatistics.ino deleted file mode 100644 index deb2485..0000000 --- a/examples/VideoStatistics/VideoStatistics.ino +++ /dev/null @@ -1,107 +0,0 @@ -/******************************************************************* - Read YouTube Video statistics from the YouTube API - and print them to the serial monitor - - Compatible Boards: - * Any ESP8266 board - * Any ESP32 board - - Recommended Board: D1 Mini ESP8266 - http://s.click.aliexpress.com/e/uzFUnIe (affiliate) - - If you find what I do useful and would like to support me, - please consider becoming a sponsor on Github - https://github.com/sponsors/witnessmenow/ - - Written by Brian Lough and modified by Colum31 - YouTube: https://www.youtube.com/brianlough - Tindie: https://www.tindie.com/stores/brianlough/ - Twitter: https://twitter.com/witnessmenow - *******************************************************************/ - -// ---------------------------- -// Standard Libraries -// ---------------------------- - -#if defined(ESP8266) - #include -#elif defined(ESP32) - #include -#endif - -#include - -// ---------------------------- -// Additional Libraries - each of these will need to be installed -// ---------------------------- - -// Library for connecting to the YouTube API -// https://github.com/witnessmenow/arduino-youtube-api -// (search for "youtube" in the Arduino Library Manager) -#include - -// Library used for parsing Json from the API responses -// https://github.com/bblanchon/ArduinoJson -// (search for "Arduino Json" in the Arduino Library Manager) -#include - -//------- Replace the following! ------ -const char ssid[] = "xxx"; // your network SSID (name) -const char password[] = "yyyy"; // your network key -#define API_KEY "zzzz" // your Google API key -#define VIDEO_ID "dQw4w9WgXcQ" // part of the video url -//------- ---------------------- ------ - -WiFiClientSecure client; -YoutubeApi api(API_KEY, client); - -unsigned long timeBetweenRequests = 60 * 1000; // 60 seconds, in milliseconds - -void setup() { - Serial.begin(115200); - - // Set WiFi to 'station' mode and disconnect - // from the AP if it was previously connected - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - delay(100); - - // Connect to the WiFi network - Serial.print("\nConnecting to WiFi: "); - Serial.println(ssid); - WiFi.begin(ssid, password); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); - } - Serial.println("\nWiFi connected!"); - Serial.print("IP address: "); - IPAddress ip = WiFi.localIP(); - Serial.println(ip); - - #ifdef ESP8266 - // Required if you are using ESP8266 V2.5 or above - client.setInsecure(); - #endif - - // Uncomment for extra debugging info - // api._debug = true; -} - -void loop() { - if(api.getVideoStatistics(VIDEO_ID)){ - Serial.println("\n--------Video Stats---------"); - - Serial.print("Video views: "); - Serial.println(api.videoStats.viewCount); - - Serial.print("Likes: "); - Serial.println(api.videoStats.likeCount); - - Serial.print("Comments: "); - Serial.println(api.videoStats.commentCount); - - Serial.println("------------------------"); - } - delay(timeBetweenRequests); -} From d32b83dafea962369b5d39db2ed1f46882039e11 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:29:51 +0200 Subject: [PATCH 37/58] changing readme --- README.md | 13 +++++++------ examples/VideoInformation/VideoInformation.ino | 1 - lib/YoutubeVideo/YoutubeVideo.h | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9f36727..0ec0b6a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Arduino YouTube API Library -[![arduino-library-badge](https://www.ardu-badge.com/badge/YoutubeApi.svg?)](https://www.ardu-badge.com/YoutubeApi) [![Build Status](https://travis-ci.org/witnessmenow/arduino-youtube-api.svg?branch=master)](https://travis-ci.org/witnessmenow/arduino-youtube-api) A wrapper for the [YouTube API](https://developers.google.com/youtube/v3/docs/) for Arduino. Works on both ESP8266 and ESP32. -![Imgur](http://i.imgur.com/FmXyW4E.png) +![Imgur](https://imgur.com/JaZR9m9) +![Imgur](https://imgur.com/a/Xrq55X2) ### Support what I do! @@ -42,9 +42,11 @@ then once you're connected to WiFi you can start requesting data from the API: #define CHANNEL_ID "UCezJOfu7OtqGzd5xrP3q6WA" - if(api.getChannelStatistics(CHANNEL_ID)) { + YoutubeChannel channel(CHANNEL_ID, &api); + + if(channel.getChannelStatistics()) { Serial.print("Subscriber Count: "); - Serial.println(api.channelStats.subscriberCount); + Serial.println(channel.channelStats->subscriberCount); } If you don't know it, you can find your own YouTube channel ID [here](https://www.youtube.com/account_advanced). See [the examples](examples) for more details on how to use the library. @@ -54,8 +56,7 @@ If you don't know it, you can find your own YouTube channel ID [here](https://ww The library is currently able to fetch: - video list: snippet, status, statistics and contentDetails -- channel list: statistics - +- channel list: snippet, statistics and content details ## License diff --git a/examples/VideoInformation/VideoInformation.ino b/examples/VideoInformation/VideoInformation.ino index b766625..bad028b 100644 --- a/examples/VideoInformation/VideoInformation.ino +++ b/examples/VideoInformation/VideoInformation.ino @@ -38,7 +38,6 @@ // Library for connecting to the YouTube API // https://github.com/witnessmenow/arduino-youtube-api // (search for "youtube" in the Arduino Library Manager) -#include "YoutubeApi.h" #include "YoutubeVideo.h" // Library used for parsing Json from the API responses diff --git a/lib/YoutubeVideo/YoutubeVideo.h b/lib/YoutubeVideo/YoutubeVideo.h index e91ea4c..1efe41d 100644 --- a/lib/YoutubeVideo/YoutubeVideo.h +++ b/lib/YoutubeVideo/YoutubeVideo.h @@ -3,6 +3,7 @@ #include "YoutubeTypes.h" #include "YoutubeApi.h" + #include #include #include From f7c683a14ff917f452820454a2c3e1eae1176de9 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:35:42 +0200 Subject: [PATCH 38/58] fixing imgur links in readme --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ec0b6a..c3dd82f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,13 @@ A wrapper for the [YouTube API](https://developers.google.com/youtube/v3/docs/) for Arduino. Works on both ESP8266 and ESP32. -![Imgur](https://imgur.com/JaZR9m9) -![Imgur](https://imgur.com/a/Xrq55X2) +### Example fetching channel information: + +![Imgur](https://i.imgur.com/JaZR9m9.png) + +### Example fetching video information: + +![Imgur](https://i.imgur.com/hTbtVvg.png) ### Support what I do! From 1cf6f1ddc8cbb5a3618c9a9d15ebbe922d10eb57 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 8 Oct 2022 17:07:26 +0200 Subject: [PATCH 39/58] initial commit on playlist branch --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 1 + lib/YoutubePlaylist/YoutubePlaylist.h | 1 + 2 files changed, 2 insertions(+) create mode 100644 lib/YoutubePlaylist/YoutubePlaylist.cpp create mode 100644 lib/YoutubePlaylist/YoutubePlaylist.h diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp new file mode 100644 index 0000000..9d95d9d --- /dev/null +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -0,0 +1 @@ +#include "YoutubePlaylist.h" \ No newline at end of file diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h new file mode 100644 index 0000000..39b6dbf --- /dev/null +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -0,0 +1 @@ +#include "YoutubeApi.h" \ No newline at end of file From ffe898572eb9cad39bc9b93c282b0f1d095710a9 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 8 Oct 2022 18:03:26 +0200 Subject: [PATCH 40/58] implementing constructor/destructor and new types --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 15 ++++++++++++++- lib/YoutubePlaylist/YoutubePlaylist.h | 22 +++++++++++++++++++++- lib/YoutubeTypes/YoutubeTypes.h | 24 +++++++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 9d95d9d..94bc40a 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -1 +1,14 @@ -#include "YoutubePlaylist.h" \ No newline at end of file +#include "YoutubePlaylist.h" + + +YoutubePlaylist::YoutubePlaylist(YoutubeApi *newApiObj, const char *newPlaylistId){ + strncpy(playlistId, newPlaylistId, YT_PLAYLISTID_LEN); + playlistId[YT_PLAYLISTID_LEN] = '\0'; + + apiObj = newApiObj; +} + + +YoutubePlaylist::~YoutubePlaylist(){ + +} \ No newline at end of file diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 39b6dbf..62454ea 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -1 +1,21 @@ -#include "YoutubeApi.h" \ No newline at end of file +#include "YoutubeApi.h" +#include "YoutubeTypes.h" + + +class YoutubePlaylist{ + + public: + YoutubePlaylist(YoutubeApi *newApiObj, const char *newPlaylistId); + YoutubePlaylist(YoutubeApi *newApiObj, String& newPlaylistId): YoutubePlaylist(newApiObj, newPlaylistId.c_str()) {}; + + ~YoutubePlaylist(); + + private: + + char playlistId[YT_PLAYLISTID_LEN + 1]; + YoutubeApi *apiObj; + + playlistSnippet snip; + playlistContentDetails contentDets; + playlistStatus status; +}; diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 8c87d9f..b08ed7d 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -6,6 +6,7 @@ #define YT_VIDEOID_LEN 11 #define YT_CHANNELID_LEN 24 +#define YT_PLAYLISTID_LEN 24 #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 @@ -29,7 +30,28 @@ enum operation{ }; -// not implemented data fields are commented +// not implemented data fields are commented out + +struct playlistContentDetails{ + uint32_t itemCount; +}; + +struct playlistSnippet{ + tm publishedAt; + char *channelId; + char *title; + char *description; +// char **thumbnails; + char *channelTitle; + char *defaultLanguage; +// char **localized; + +}; + +struct playlistStatus{ + char *privacyStatus; +}; + struct channelStatistics { uint64_t viewCount; From 7571572baef1cd9db4b5ad93fde59336150f0033 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 8 Oct 2022 22:39:02 +0200 Subject: [PATCH 41/58] adding cleanup functions --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 61 ++++++++++++++++++++++++- lib/YoutubePlaylist/YoutubePlaylist.h | 14 ++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 94bc40a..097ceb2 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -11,4 +11,63 @@ YoutubePlaylist::YoutubePlaylist(YoutubeApi *newApiObj, const char *newPlaylistI YoutubePlaylist::~YoutubePlaylist(){ -} \ No newline at end of file + freePlaylistSnippet(); + freePlaylistContentDetails(); + freePlaylistStatus(); +} + + +/** + * @brief Frees playlistSnippet struct of object and resets flag and pointer. + * + */ +void YoutubePlaylist::freePlaylistSnippet(){ + + if(!snipSet){ + return; + } + + free(snip->channelId); + free(snip->title); + free(snip->description); + free(snip->channelTitle); + free(snip->defaultLanguage); + + free(snip); + snipSet = false; + snip = NULL; +} + + +/** + * @brief Frees playlistContentDetails struct of object and resets flag and pointer. + * + */ +void YoutubePlaylist::freePlaylistContentDetails(){ + + if(!contentDetsSet){ + return; + } + + free(contentDets); + contentDetsSet = false; + contentDets = NULL; +} + + +/** + * @brief Frees playlistStatus struct of object and resets flag and pointer. + * + */ +void YoutubePlaylist::freePlaylistStatus(){ + + if(!statusSet){ + return; + } + + free(status->privacyStatus); + + free(status); + statusSet = false; + status = NULL; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 62454ea..27a2ae7 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -15,7 +15,15 @@ class YoutubePlaylist{ char playlistId[YT_PLAYLISTID_LEN + 1]; YoutubeApi *apiObj; - playlistSnippet snip; - playlistContentDetails contentDets; - playlistStatus status; + playlistSnippet *snip = NULL; + playlistContentDetails *contentDets = NULL; + playlistStatus *status = NULL; + + bool snipSet = false; + bool contentDetsSet = false; + bool statusSet = false; + + void freePlaylistSnippet(); + void freePlaylistContentDetails(); + void freePlaylistStatus(); }; From 8f06009498895ab8d59994d32aad04ae32bdbe43 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 9 Oct 2022 00:07:47 +0200 Subject: [PATCH 42/58] adding helper functions and flags --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 32 ++++++++++++++++ lib/YoutubePlaylist/YoutubePlaylist.h | 14 +++++-- .../test_YoutubePlaylistClass.cpp | 37 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 test/test_playlistClass/test_YoutubePlaylistClass.cpp diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 097ceb2..72f5cdd 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -71,3 +71,35 @@ void YoutubePlaylist::freePlaylistStatus(){ statusSet = false; status = NULL; } + + +/** + * @brief Returns the value of the playlistStatusSet flag, indicating a valid object. + * + * @return Value of flag. + */ +bool YoutubePlaylist::checkPlaylistStatusSet(){return statusSet;} + + +/** + * @brief Returns the value of the playlistSnipSet flag, indicating a valid object. + * + * @return Value of flag. + */ +bool YoutubePlaylist::checkPlaylistSnipSet(){return snipSet;} + + +/** + * @brief Returns the value of the playlistContentDets flag, indicating a valid object. + * + * @return Value of flag. + */ +bool YoutubePlaylist::checkPlaylistContentDetsSet(){return contentDetsSet;} + + +/** + * @brief Returns the currently set playlist id. + * + * @return const char* playlistId + */ +const char* YoutubePlaylist::getPlaylistId(){return playlistId;} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 27a2ae7..dd71402 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -9,15 +9,21 @@ class YoutubePlaylist{ YoutubePlaylist(YoutubeApi *newApiObj, String& newPlaylistId): YoutubePlaylist(newApiObj, newPlaylistId.c_str()) {}; ~YoutubePlaylist(); - - private: - char playlistId[YT_PLAYLISTID_LEN + 1]; - YoutubeApi *apiObj; + bool checkPlaylistStatusSet(); + bool checkPlaylistSnipSet(); + bool checkPlaylistContentDetsSet(); + + const char* getPlaylistId(); playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; playlistStatus *status = NULL; + + private: + + char playlistId[YT_PLAYLISTID_LEN + 1]; + YoutubeApi *apiObj; bool snipSet = false; bool contentDetsSet = false; diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp new file mode 100644 index 0000000..85effeb --- /dev/null +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -0,0 +1,37 @@ +#include +#include +#include "secrets.h" +#include "YoutubePlaylist.h" +#include "YoutubeTypes.h" +#include + + +void test_constructDestruct_simple(){ + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); + + TEST_ASSERT_FALSE_MESSAGE(uut.checkPlaylistContentDetsSet(), "Should be false in initial state!"); + TEST_ASSERT_FALSE_MESSAGE(uut.checkPlaylistStatusSet(), "Should be false in initial state!"); + TEST_ASSERT_FALSE_MESSAGE(uut.checkPlaylistSnipSet(), "Should be false in initial state!"); + + TEST_ASSERT_NULL_MESSAGE(uut.snip, "Should be NULL in initial state!"); + TEST_ASSERT_NULL_MESSAGE(uut.contentDets, "Should be NULL in initial state!"); + TEST_ASSERT_NULL_MESSAGE(uut.status, "Should be NULL in initial state!"); +} + + +void setup(){ + + Serial.begin(115200); + + UNITY_BEGIN(); + + RUN_TEST(test_constructDestruct_simple); + + UNITY_END(); +} + +void loop(){} \ No newline at end of file From 4b2734ad569e591d12607f4440bfbd0fd8702418 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 9 Oct 2022 00:09:29 +0200 Subject: [PATCH 43/58] refactoring YoutubeChannel test and error checking --- lib/YoutubeChannel/YoutubeChannel.cpp | 4 +++- test/test_channelClass/test_YoutubeChannelClass.cpp | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/YoutubeChannel/YoutubeChannel.cpp b/lib/YoutubeChannel/YoutubeChannel.cpp index 71fbdab..287b490 100644 --- a/lib/YoutubeChannel/YoutubeChannel.cpp +++ b/lib/YoutubeChannel/YoutubeChannel.cpp @@ -15,7 +15,9 @@ YoutubeChannel::YoutubeChannel(const char *newChannelId, YoutubeApi *newApiObj){ */ void YoutubeChannel::setChannelId(const char *newChannelId){ - if(!newChannelId || strlen(newChannelId) != YT_CHANNELID_LEN){ + // TODO: When an invalid channel id is being rejected, it should block certain actions + // like fetching data etc. + if(!newChannelId || strlen(newChannelId) != YT_CHANNELID_LEN || newChannelId[0] != 'U' || newChannelId[1] != 'C'){ return; } diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index 0cd4a6c..13cbef9 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -6,7 +6,6 @@ #define MAX_WIFI_RETRIES 10 -#define validChannelId "123456789012345678901234" #define tooLongChannelId "1234567890123456789012345" #define tooShortChannelId "12345678901234567890123" @@ -54,9 +53,9 @@ void test_charConstructor_tooShortId(){ void test_resetInfo(){ WiFiClientSecure client; YoutubeApi apiDummy("", client); - YoutubeChannel uut(validChannelId, &apiDummy); + YoutubeChannel uut(TEST_CHANNEL_ID, &apiDummy); - TEST_ASSERT_EQUAL_STRING_MESSAGE(validChannelId, uut.getChannelId(), "Channel id not correct!"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(TEST_CHANNEL_ID, uut.getChannelId(), "Channel id not correct!"); uut.resetInfo(); From d64e860f8290b0a81604c5cd9e7123dcbff18d11 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 9 Oct 2022 00:12:39 +0200 Subject: [PATCH 44/58] adding url generation for playlist:status -> a new test case checks url generation -> refactoring url generation -> adding documentation --- lib/YoutubeApi/YoutubeApi.cpp | 27 +++++++++++++++------ lib/YoutubeTypes/YoutubeTypes.h | 12 ++++++++- test/test_apiClass/test_YoutubeApiClass.cpp | 13 ++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 974a3a3..ac93be9 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -84,36 +84,49 @@ int YoutubeApi::sendGetToYoutube(const String& command) { return sendGetToYoutube(command.c_str()); } + +/** + * @brief Creates a url-string to request data from. + * + * @param mode The type of request to make a string for. (operation enum) + * @param command The destination of the string. + * @param id The id of the resource. + * @return true on success, false on error + */ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { switch (mode) { case videoListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "statistics", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, YTAPI_PART_STATISTICS, id, apiKey); break; case videoListContentDetails: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "contentDetails", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); break; case videoListSnippet: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "snippet", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, YTAPI_PART_SNIPPET, id, apiKey); break; case videoListStatus: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, "status", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_VIDEO_ENDPOINT, YTAPI_PART_STATUS, id, apiKey); break; case channelListStats: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "statistics", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, YTAPI_PART_STATISTICS, id, apiKey); break; case channelListSnippet: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "snippet", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, YTAPI_PART_SNIPPET, id, apiKey); break; case channelListContentDetails: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, "contentDetails", id, apiKey); + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_CHANNEL_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); + break; + + case playlistListStatus: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_STATUS, id, apiKey); break; default: diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index b08ed7d..34ed68d 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -14,7 +14,15 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" +#define YTAPI_PLAYLIST_ENDPOINT "/youtube/v3/playlists" + #define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" + +#define YTAPI_PART_STATISTICS "statistics" +#define YTAPI_PART_CONTENTDETAILS "contentDetails" +#define YTAPI_PART_SNIPPET "snippet" +#define YTAPI_PART_STATUS "status" + #define YTAPI_KEY_LEN 45 enum operation{ @@ -26,7 +34,9 @@ enum operation{ channelListStats, channelListSnippet, - channelListContentDetails + channelListContentDetails, + + playlistListStatus }; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 4a10747..5653eeb 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -89,6 +89,18 @@ void test_createRequestString_channelContentDetails_simple(){ TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); } +void test_createRequestString_playlistStatus_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/playlists?part=status&id=%s&key=%s", TEST_PLAYLIST_ID, API_KEY); + YoutubeApi::createRequestString(playlistListStatus, uutCommand, TEST_PLAYLIST_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + void test_allocAndCopy_pos_NULL(){ @@ -141,6 +153,7 @@ void setup() RUN_TEST(test_createRequestString_channelStatistics_simple); RUN_TEST(test_createRequestString_channelSnippet_simple); RUN_TEST(test_createRequestString_channelContentDetails_simple); + RUN_TEST(test_createRequestString_playlistStatus_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); From 48948cfb332197a56b7df2a5e6ebdbf7da9aab26 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 9 Oct 2022 18:07:51 +0200 Subject: [PATCH 45/58] adding fetching of playlist status and tests --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 68 +++++++++++++++++++ lib/YoutubePlaylist/YoutubePlaylist.h | 4 ++ .../test_YoutubePlaylistClass.cpp | 40 +++++++++++ 3 files changed, 112 insertions(+) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 72f5cdd..cbf80bd 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -103,3 +103,71 @@ bool YoutubePlaylist::checkPlaylistContentDetsSet(){return contentDetsSet;} * @return const char* playlistId */ const char* YoutubePlaylist::getPlaylistId(){return playlistId;} + +/** + * @brief Fetches playlist status of the set playlist id. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::getPlaylistStatus(){ + + freePlaylistStatus(); + + char command[150]; + YoutubeApi::createRequestString(playlistListStatus, command, playlistId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parsePlaylistStatus(); + } + + return false; +} + +/** + * @brief Parses the response of the api request to retrieve the playlistStatus. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::parsePlaylistStatus(){ + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 512; + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find playlistId!"); + apiObj->closeClient(); + return wasSuccessful; + } + + playlistStatus *newplaylistStatus = (playlistStatus*) malloc(sizeof(playlistStatus)); + + uint8_t errorCode = 0; + + errorCode += YoutubeApi::allocAndCopy(&newplaylistStatus->privacyStatus, doc["items"][0]["status"]["privacyStatus"].as()); + + if(errorCode){ + Serial.print("Error code: "); + Serial.print(errorCode); + }else{ + status = newplaylistStatus; + statusSet = true; + + wasSuccessful = true; + } + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index dd71402..8cdcbc9 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -16,6 +16,8 @@ class YoutubePlaylist{ const char* getPlaylistId(); + bool getPlaylistStatus(); + playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; playlistStatus *status = NULL; @@ -32,4 +34,6 @@ class YoutubePlaylist{ void freePlaylistSnippet(); void freePlaylistContentDetails(); void freePlaylistStatus(); + + bool parsePlaylistStatus(); }; diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 85effeb..23ae2be 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -6,6 +6,8 @@ #include +#define MAX_WIFI_RETRIES 10 + void test_constructDestruct_simple(){ WiFiClientSecure dummyClient; @@ -22,6 +24,42 @@ void test_constructDestruct_simple(){ TEST_ASSERT_NULL_MESSAGE(uut.status, "Should be NULL in initial state!"); } +void test_getPlaylistStatus_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistStatus(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist status!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.status, "Expected a valid playlistStatus object to be set!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.status->privacyStatus, "Expected a valid string to be set!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkPlaylistStatusSet(), "Expected the playlistStatus flag to be set!"); +} + +bool establishInternetConnection(){ + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + for(int tryWIFI = 1; tryWIFI < MAX_WIFI_RETRIES; tryWIFI++){ + if(WiFi.status() == WL_CONNECTED){ + return true; + } + delay(1000); + } + return false; +} void setup(){ @@ -30,6 +68,8 @@ void setup(){ UNITY_BEGIN(); RUN_TEST(test_constructDestruct_simple); + establishInternetConnection(); + RUN_TEST(test_getPlaylistStatus_simple); UNITY_END(); } From 5cf8528493d9e04e590722667fa0a57261acc744 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sun, 9 Oct 2022 22:08:14 +0200 Subject: [PATCH 46/58] adding fetching of playlists:contentDetails --- lib/YoutubeApi/YoutubeApi.cpp | 4 ++ lib/YoutubePlaylist/YoutubePlaylist.cpp | 61 +++++++++++++++++++ lib/YoutubePlaylist/YoutubePlaylist.h | 2 + lib/YoutubeTypes/YoutubeTypes.h | 3 +- test/test_apiClass/test_YoutubeApiClass.cpp | 14 +++++ .../test_YoutubePlaylistClass.cpp | 22 +++++++ 6 files changed, 105 insertions(+), 1 deletion(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index ac93be9..9de76d6 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -128,6 +128,10 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { case playlistListStatus: sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_STATUS, id, apiKey); break; + + case playlistListContentDetails: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); + break; default: Serial.println("Unknown operation"); diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index cbf80bd..a9123aa 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -171,3 +171,64 @@ bool YoutubePlaylist::parsePlaylistStatus(){ apiObj->closeClient(); return wasSuccessful; } + + +/** + * @brief Fetches playlist content details of the set playlist id. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::getPlaylistContentDetails(){ + + freePlaylistContentDetails(); + + char command[150]; + YoutubeApi::createRequestString(playlistListContentDetails, command, playlistId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parsePlaylistContentDetails(); + } + + return false; +} + +/** + * @brief Parses the response of the api request to retrieve the playlist content details. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::parsePlaylistContentDetails(){ + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 512; // recommended 384, but it throwed errors + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find playlistId!"); + apiObj->closeClient(); + return wasSuccessful; + } + + playlistContentDetails *newPlaylistContentDetails = (playlistContentDetails*) malloc(sizeof(playlistContentDetails)); + + newPlaylistContentDetails->itemCount = doc["items"][0]["contentDetails"]["itemCount"].as(); + + contentDets = newPlaylistContentDetails; + contentDetsSet = true; + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 8cdcbc9..592bf69 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -17,6 +17,7 @@ class YoutubePlaylist{ const char* getPlaylistId(); bool getPlaylistStatus(); + bool getPlaylistContentDetails(); playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; @@ -36,4 +37,5 @@ class YoutubePlaylist{ void freePlaylistStatus(); bool parsePlaylistStatus(); + bool parsePlaylistContentDetails(); }; diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 34ed68d..523607e 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -36,7 +36,8 @@ enum operation{ channelListSnippet, channelListContentDetails, - playlistListStatus + playlistListStatus, + playlistListContentDetails }; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 5653eeb..1051fc8 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -102,6 +102,19 @@ void test_createRequestString_playlistStatus_simple(){ } +void test_createRequestString_playlistContentDetails_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/playlists?part=contentDetails&id=%s&key=%s", TEST_PLAYLIST_ID, API_KEY); + YoutubeApi::createRequestString(playlistListContentDetails, uutCommand, TEST_PLAYLIST_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + void test_allocAndCopy_pos_NULL(){ const char someData[] = "testdata"; @@ -154,6 +167,7 @@ void setup() RUN_TEST(test_createRequestString_channelSnippet_simple); RUN_TEST(test_createRequestString_channelContentDetails_simple); RUN_TEST(test_createRequestString_playlistStatus_simple); + RUN_TEST(test_createRequestString_playlistContentDetails_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 23ae2be..799573c 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -45,6 +45,27 @@ void test_getPlaylistStatus_simple(){ TEST_ASSERT_TRUE_MESSAGE(uut.checkPlaylistStatusSet(), "Expected the playlistStatus flag to be set!"); } + +void test_getPlaylistContentDetails_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistContentDetails(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist content details!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.contentDets, "Expected a valid playlistContentDetails object to be set!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkPlaylistContentDetsSet(), "Expected the playlistContentDetails flag to be set!"); +} + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); @@ -70,6 +91,7 @@ void setup(){ RUN_TEST(test_constructDestruct_simple); establishInternetConnection(); RUN_TEST(test_getPlaylistStatus_simple); + RUN_TEST(test_getPlaylistContentDetails_simple); UNITY_END(); } From f75d34c57b6da612565249d094de5dd698e88f11 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 10 Oct 2022 23:06:20 +0200 Subject: [PATCH 47/58] adding fetching of playlists:snippet --- lib/YoutubeApi/YoutubeApi.cpp | 4 + lib/YoutubePlaylist/YoutubePlaylist.cpp | 75 ++++++++++++++++++- lib/YoutubePlaylist/YoutubePlaylist.h | 2 + lib/YoutubeTypes/YoutubeTypes.h | 7 +- test/test_apiClass/test_YoutubeApiClass.cpp | 14 ++++ .../test_YoutubePlaylistClass.cpp | 28 +++++++ 6 files changed, 126 insertions(+), 4 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 9de76d6..eb61970 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -132,6 +132,10 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { case playlistListContentDetails: sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); break; + + case playlistListSnippet: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_SNIPPET, id, apiKey); + break; default: Serial.println("Unknown operation"); diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index a9123aa..62e1c60 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -31,7 +31,6 @@ void YoutubePlaylist::freePlaylistSnippet(){ free(snip->title); free(snip->description); free(snip->channelTitle); - free(snip->defaultLanguage); free(snip); snipSet = false; @@ -232,3 +231,77 @@ bool YoutubePlaylist::parsePlaylistContentDetails(){ apiObj->closeClient(); return wasSuccessful; } + + + +/** + * @brief Fetches playlist snippet of the set playlist id. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::getPlaylistSnippet(){ + + freePlaylistSnippet(); + + char command[150]; + YoutubeApi::createRequestString(playlistListSnippet, command, playlistId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parsePlaylistSnippet(); + } + + return false; +} + +/** + * @brief Parses the response of the api request to retrieve the playlist content details. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::parsePlaylistSnippet(){ + + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 1600; // is just enough for upload playlists. It might not work for user made playlists. + // 1600 Bytes is way too large. #TODO: implement filtering to reduce allocated space + DynamicJsonDocument doc(bufferSize); + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client); + if (!error){ + + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find playlistId!"); + apiObj->closeClient(); + return wasSuccessful; + } + + playlistSnippet *newPlaylistSnippet = (playlistSnippet*) malloc(sizeof(playlistSnippet)); + + uint8_t errorCode = 0; + + errorCode += YoutubeApi::allocAndCopy(&newPlaylistSnippet->channelId, doc["items"][0]["snippet"]["channelId"].as()); + errorCode += YoutubeApi::allocAndCopy(&newPlaylistSnippet->title, doc["items"][0]["snippet"]["title"].as()); + errorCode += YoutubeApi::allocAndCopy(&newPlaylistSnippet->description, doc["items"][0]["snippet"]["description"].as()); + + errorCode += YoutubeApi::allocAndCopy(&newPlaylistSnippet->channelTitle, doc["items"][0]["snippet"]["channelTitle"].as()); + + char *ret = strncpy(newPlaylistSnippet->defaultLanguage, doc["items"][0]["snippet"]["defaultLanguage"].as(), 3); + newPlaylistSnippet->defaultLanguage[3] = '\0'; + + newPlaylistSnippet->publishedAt = YoutubeApi::parseUploadDate(doc["items"][0]["snippet"]["publishedAt"].as()); + + snip = newPlaylistSnippet; + snipSet = true; + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 592bf69..f726152 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -18,6 +18,7 @@ class YoutubePlaylist{ bool getPlaylistStatus(); bool getPlaylistContentDetails(); + bool getPlaylistSnippet(); playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; @@ -38,4 +39,5 @@ class YoutubePlaylist{ bool parsePlaylistStatus(); bool parsePlaylistContentDetails(); + bool parsePlaylistSnippet(); }; diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 523607e..e8d671d 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -37,7 +37,8 @@ enum operation{ channelListContentDetails, playlistListStatus, - playlistListContentDetails + playlistListContentDetails, + playlistListSnippet }; @@ -54,7 +55,7 @@ struct playlistSnippet{ char *description; // char **thumbnails; char *channelTitle; - char *defaultLanguage; + char defaultLanguage[4]; // char **localized; }; @@ -141,4 +142,4 @@ struct videoSnippet{ char *defaultAudioLanguage; }; -#endif \ No newline at end of file +#endif diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 1051fc8..5ea7aa5 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -115,6 +115,19 @@ void test_createRequestString_playlistContentDetails_simple(){ } +void test_createRequestString_playlistSnippet_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/playlists?part=snippet&id=%s&key=%s", TEST_PLAYLIST_ID, API_KEY); + YoutubeApi::createRequestString(playlistListSnippet, uutCommand, TEST_PLAYLIST_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + void test_allocAndCopy_pos_NULL(){ const char someData[] = "testdata"; @@ -168,6 +181,7 @@ void setup() RUN_TEST(test_createRequestString_channelContentDetails_simple); RUN_TEST(test_createRequestString_playlistStatus_simple); RUN_TEST(test_createRequestString_playlistContentDetails_simple); + RUN_TEST(test_createRequestString_playlistSnippet_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 799573c..2e38f8a 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -66,6 +66,33 @@ void test_getPlaylistContentDetails_simple(){ TEST_ASSERT_TRUE_MESSAGE(uut.checkPlaylistContentDetsSet(), "Expected the playlistContentDetails flag to be set!"); } + +void test_getPlaylistSnippet_simple(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistSnippet(); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist snippet!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.snip, "Expected a valid playlist snippet object to be set!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkPlaylistSnipSet(), "Expected the pplaylist snippet flag to be set!"); + + TEST_ASSERT_NOT_NULL_MESSAGE(uut.snip->channelId, "Expected a valid channel id string to be set!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.snip->title, "Expected a valid title string to be set!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.snip->description, "Expected a description string to be set!"); + TEST_ASSERT_NOT_NULL_MESSAGE(uut.snip->channelTitle, "Expected a valid channel title stringt to be set!"); +} + + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); @@ -92,6 +119,7 @@ void setup(){ establishInternetConnection(); RUN_TEST(test_getPlaylistStatus_simple); RUN_TEST(test_getPlaylistContentDetails_simple); + RUN_TEST(test_getPlaylistSnippet_simple); UNITY_END(); } From 8b53c66021b46af457ab2dc7015cd389a712a45c Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 11 Oct 2022 01:10:26 +0200 Subject: [PATCH 48/58] creating structs for playlist items --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 16 ++++++++++++++++ lib/YoutubePlaylist/YoutubePlaylist.h | 9 ++++++++- lib/YoutubeTypes/YoutubeTypes.h | 20 +++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 62e1c60..ece810c 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -72,6 +72,22 @@ void YoutubePlaylist::freePlaylistStatus(){ } +/** + * @brief Frees playlistItemsConfiguration struct of object and resets flag and pointer. + * + */ +void YoutubePlaylist::freePlaylistItemsConfig(){ + + if(!itemsConfigSet){ + return; + } + + free(playlistItemsConfig); + itemsConfigSet = false; + playlistItemsConfig = NULL; +} + + /** * @brief Returns the value of the playlistStatusSet flag, indicating a valid object. * diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index f726152..cb3543c 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -23,19 +23,26 @@ class YoutubePlaylist{ playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; playlistStatus *status = NULL; + + // "caches" a page of playlistItems + playlistItemsContentDetails itemsContentDets[YT_PLAYLIST_ITEM_RESULTS_PER_PAGE]; private: char playlistId[YT_PLAYLISTID_LEN + 1]; - YoutubeApi *apiObj; + YoutubeApi *apiObj = NULL; + playlistItemsConfiguration *playlistItemsConfig = NULL; bool snipSet = false; bool contentDetsSet = false; bool statusSet = false; + bool itemsConfigSet = false; + bool itemsContentDetsSet = false; void freePlaylistSnippet(); void freePlaylistContentDetails(); void freePlaylistStatus(); + void freePlaylistItemsConfig(); bool parsePlaylistStatus(); bool parsePlaylistContentDetails(); diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index e8d671d..60f46c5 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -7,6 +7,8 @@ #define YT_VIDEOID_LEN 11 #define YT_CHANNELID_LEN 24 #define YT_PLAYLISTID_LEN 24 +#define YT_PLALIST_ITEMS_PAGE_TOKEN_LEN 14 +#define YT_PLAYLIST_ITEM_RESULTS_PER_PAGE 5 #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 @@ -44,6 +46,22 @@ enum operation{ // not implemented data fields are commented out +struct playlistItemsConfiguration{ + uint16_t totalResults; +// uint8_t resultsPerPage; should be YT_PLAYLIST_ITEM_RESULTS_PER_PAGE + + uint16_t currentPage; + char currentPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1] = ""; + + char nextPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1] = ""; + char previousPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1] = ""; +}; + +struct playlistItemsContentDetails{ + char videoId[YT_VIDEOID_LEN + 1] = ""; + tm videoPublishedAt; +}; + struct playlistContentDetails{ uint32_t itemCount; }; @@ -102,7 +120,7 @@ struct videoContentDetails{ struct videoStatistics { - uint64_t viewCount; // required for popular videos. (Baby Shark would else overflow xD) + uint64_t viewCount; // uint64_t required for popular videos. (Baby Shark would else overflow xD) uint32_t commentCount; uint32_t likeCount; // long favourites; From c2394d5821003ca4e3ffeef0735cd552691404bc Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:37:44 +0200 Subject: [PATCH 49/58] adding fetching of playlistItems:contentDetails -> untested and not yet implemented --- lib/YoutubeApi/YoutubeApi.cpp | 4 + lib/YoutubePlaylist/YoutubePlaylist.cpp | 112 +++++++++++++++++++++++- lib/YoutubePlaylist/YoutubePlaylist.h | 3 + lib/YoutubeTypes/YoutubeTypes.h | 7 +- 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index eb61970..9c4196c 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -136,6 +136,10 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { case playlistListSnippet: sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ENDPOINT, YTAPI_PART_SNIPPET, id, apiKey); break; + + case playlistItemsListContentDetails: + sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ITEMS_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); + break; default: Serial.println("Unknown operation"); diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index ece810c..6d437b0 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -14,6 +14,7 @@ YoutubePlaylist::~YoutubePlaylist(){ freePlaylistSnippet(); freePlaylistContentDetails(); freePlaylistStatus(); + freePlaylistItemsConfig(); } @@ -249,7 +250,6 @@ bool YoutubePlaylist::parsePlaylistContentDetails(){ } - /** * @brief Fetches playlist snippet of the set playlist id. * @@ -270,6 +270,7 @@ bool YoutubePlaylist::getPlaylistSnippet(){ return false; } + /** * @brief Parses the response of the api request to retrieve the playlist content details. * @@ -321,3 +322,112 @@ bool YoutubePlaylist::parsePlaylistSnippet(){ apiObj->closeClient(); return wasSuccessful; } + + +/** + * @brief Gets the the first page of playlistItems. Initialises the playlistItemsConfig and the first page of data. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::getPlaylistItemsInitialConfig(){ + + if(playlistItemsConfig || itemsConfigSet){ + Serial.println("playlistItemsConfig is not supposed to be set already!"); + return false; + } + + // unlike other fetching methods, the config is only set once and then modified + playlistItemsConfiguration *newConfig = (playlistItemsConfiguration*) malloc(sizeof(playlistItemsConfiguration)); + playlistItemsConfig = newConfig; + itemsConfigSet = true; + + char command[150]; + YoutubeApi::createRequestString(playlistItemsListContentDetails, command, playlistId); + int httpStatus = apiObj->sendGetToYoutube(command); + + if(httpStatus == 200){ + return parsePlaylistItemsContentDetails(); + } + + return false; +} + + +/** + * @brief Parses a page of playlistItems:contentDetails. It also modifies values of playlistItemsConfig. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::parsePlaylistItemsContentDetails(){ + bool wasSuccessful = false; + + // Get from https://arduinojson.org/v6/assistant/ + const size_t bufferSize = 600; + + DynamicJsonDocument doc(bufferSize); + + StaticJsonDocument<48> filter; + filter["nextPageToken"] = true; + filter["prevPageToken"] = true; + filter["items"][0]["contentDetails"] = true; + filter["pageInfo"] = true; + + // Parse JSON object + DeserializationError error = deserializeJson(doc, apiObj->client, DeserializationOption::Filter(filter)); + if (!error){ + + if(YoutubeApi::checkEmptyResponse(doc)){ + Serial.println("Could not find playlistId!"); + apiObj->closeClient(); + return wasSuccessful; + } + + uint8_t pos = 0; + + for (JsonObject item : doc["items"].as()) { + + strcpy(itemsContentDets[pos].videoId ,item["contentDetails"]["videoId"]); + itemsContentDets[pos].videoPublishedAt = YoutubeApi::parseUploadDate(item["contentDetails"]["videoPublishedAt"]); + + pos++; + } + + playlistItemsConfig->currentPageLastValidPos = pos - 1; + + // if page not full, fill in with dummy data + if(pos != YT_PLAYLIST_ITEM_RESULTS_PER_PAGE - 1){ + for(int i = pos; i < YT_PLAYLIST_ITEM_RESULTS_PER_PAGE; i++){ + strcpy(itemsContentDets[pos].videoId ,""); + itemsContentDets[pos].videoPublishedAt = YoutubeApi::parseUploadDate("1970-01-01T00:00:00Z"); + } + } + + if(doc["nextPageToken"].as()){ + strcpy(playlistItemsConfig->nextPageToken, doc["nextPageToken"].as()); + }else{ + strcpy(playlistItemsConfig->nextPageToken, ""); + } + + if(doc["prevPageToken"].as()){ + strcpy(playlistItemsConfig->previousPageToken, doc["prevPageToken"].as()); + }else{ + strcpy(playlistItemsConfig->previousPageToken, ""); + } + + playlistItemsConfig->totalResults = doc["pageInfo"]["totalResults"]; + + if(doc["pageInfo"]["resultsPerPage"] != YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + Serial.println("WARNING: Unexpected resultsPerPage!"); + } + + itemsContentDetsSet = true; + wasSuccessful = true; + } + else{ + Serial.print(F("deserializeJson() failed with code ")); + Serial.println(error.c_str()); + } + + apiObj->closeClient(); + return wasSuccessful; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index cb3543c..e1e3777 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -47,4 +47,7 @@ class YoutubePlaylist{ bool parsePlaylistStatus(); bool parsePlaylistContentDetails(); bool parsePlaylistSnippet(); + + bool getPlaylistItemsInitialConfig(); + bool parsePlaylistItemsContentDetails(); }; diff --git a/lib/YoutubeTypes/YoutubeTypes.h b/lib/YoutubeTypes/YoutubeTypes.h index 60f46c5..bee8381 100644 --- a/lib/YoutubeTypes/YoutubeTypes.h +++ b/lib/YoutubeTypes/YoutubeTypes.h @@ -17,8 +17,10 @@ #define YTAPI_CHANNEL_ENDPOINT "/youtube/v3/channels" #define YTAPI_VIDEO_ENDPOINT "/youtube/v3/videos" #define YTAPI_PLAYLIST_ENDPOINT "/youtube/v3/playlists" +#define YTAPI_PLAYLIST_ITEMS_ENDPOINT "/youtube/v3/playlistItems" #define YTAPI_REQUEST_FORMAT "%s?part=%s&id=%s&key=%s" +#define YTAPI_PLAYLIST_ITEMS_REQUEST_FORMAT "%s?part=%s&playlistId=%s&key=%s" #define YTAPI_PART_STATISTICS "statistics" #define YTAPI_PART_CONTENTDETAILS "contentDetails" @@ -40,7 +42,9 @@ enum operation{ playlistListStatus, playlistListContentDetails, - playlistListSnippet + playlistListSnippet, + + playlistItemsListContentDetails }; @@ -51,6 +55,7 @@ struct playlistItemsConfiguration{ // uint8_t resultsPerPage; should be YT_PLAYLIST_ITEM_RESULTS_PER_PAGE uint16_t currentPage; + uint8_t currentPageLastValidPos; // last valid data entry on page char currentPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1] = ""; char nextPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1] = ""; From 79da5863db2ef9b25071d024fc42abba99687512 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:46:02 +0200 Subject: [PATCH 50/58] implementing and testing fetching of playlistItems -> can fetch first page of playlistItems --- lib/YoutubeApi/YoutubeApi.cpp | 2 +- lib/YoutubePlaylist/YoutubePlaylist.cpp | 93 +++++++++++++++++-- lib/YoutubePlaylist/YoutubePlaylist.h | 13 ++- test/test_apiClass/test_YoutubeApiClass.cpp | 14 +++ .../test_YoutubePlaylistClass.cpp | 83 +++++++++++++++++ 5 files changed, 194 insertions(+), 11 deletions(-) diff --git a/lib/YoutubeApi/YoutubeApi.cpp b/lib/YoutubeApi/YoutubeApi.cpp index 9c4196c..0d8c9ba 100644 --- a/lib/YoutubeApi/YoutubeApi.cpp +++ b/lib/YoutubeApi/YoutubeApi.cpp @@ -138,7 +138,7 @@ bool YoutubeApi::createRequestString(int mode, char* command, const char *id) { break; case playlistItemsListContentDetails: - sprintf(command, YTAPI_REQUEST_FORMAT, YTAPI_PLAYLIST_ITEMS_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); + sprintf(command, YTAPI_PLAYLIST_ITEMS_REQUEST_FORMAT, YTAPI_PLAYLIST_ITEMS_ENDPOINT, YTAPI_PART_CONTENTDETAILS, id, apiKey); break; default: diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 6d437b0..91a34c9 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -106,13 +106,29 @@ bool YoutubePlaylist::checkPlaylistSnipSet(){return snipSet;} /** - * @brief Returns the value of the playlistContentDets flag, indicating a valid object. + * @brief Returns the value of the itemsConfigSet flag, indicating a valid object. * * @return Value of flag. */ bool YoutubePlaylist::checkPlaylistContentDetsSet(){return contentDetsSet;} +/** + * @brief Returns the value of the itemsConfigSet flag, indicating a valid connfiguration. + * + * @return Value of flag. + */ +bool YoutubePlaylist::checkItemsConfigSet(){return itemsConfigSet;} + + +/** + * @brief Returns the value of the itemsContentDetsSet flag, indicating a valid object. + * + * @return Value of flag. + */ +bool YoutubePlaylist::checkItemsContentDetsSet(){return itemsContentDetsSet;} + + /** * @brief Returns the currently set playlist id. * @@ -339,6 +355,7 @@ bool YoutubePlaylist::getPlaylistItemsInitialConfig(){ // unlike other fetching methods, the config is only set once and then modified playlistItemsConfiguration *newConfig = (playlistItemsConfiguration*) malloc(sizeof(playlistItemsConfiguration)); playlistItemsConfig = newConfig; + playlistItemsConfig->currentPage = 0; itemsConfigSet = true; char command[150]; @@ -347,6 +364,7 @@ bool YoutubePlaylist::getPlaylistItemsInitialConfig(){ if(httpStatus == 200){ return parsePlaylistItemsContentDetails(); + } return false; @@ -362,18 +380,22 @@ bool YoutubePlaylist::parsePlaylistItemsContentDetails(){ bool wasSuccessful = false; // Get from https://arduinojson.org/v6/assistant/ - const size_t bufferSize = 600; - + const size_t bufferSize = 2048; DynamicJsonDocument doc(bufferSize); +/* + Can not get the filter to work - for now. + It appears the optional parameters (nextPageToken and prevPageToken) break the filter. + StaticJsonDocument<48> filter; + filter["nextPageToken"] = true; filter["prevPageToken"] = true; filter["items"][0]["contentDetails"] = true; filter["pageInfo"] = true; - +*/ // Parse JSON object - DeserializationError error = deserializeJson(doc, apiObj->client, DeserializationOption::Filter(filter)); + DeserializationError error = deserializeJson(doc, apiObj->client); if (!error){ if(YoutubeApi::checkEmptyResponse(doc)){ @@ -391,14 +413,14 @@ bool YoutubePlaylist::parsePlaylistItemsContentDetails(){ pos++; } - + playlistItemsConfig->currentPageLastValidPos = pos - 1; // if page not full, fill in with dummy data if(pos != YT_PLAYLIST_ITEM_RESULTS_PER_PAGE - 1){ for(int i = pos; i < YT_PLAYLIST_ITEM_RESULTS_PER_PAGE; i++){ - strcpy(itemsContentDets[pos].videoId ,""); - itemsContentDets[pos].videoPublishedAt = YoutubeApi::parseUploadDate("1970-01-01T00:00:00Z"); + strcpy(itemsContentDets[i].videoId ,""); + itemsContentDets[i].videoPublishedAt = YoutubeApi::parseUploadDate("1970-01-01T00:00:00Z"); } } @@ -431,3 +453,58 @@ bool YoutubePlaylist::parsePlaylistItemsContentDetails(){ apiObj->closeClient(); return wasSuccessful; } + + +bool YoutubePlaylist::getPlaylistItemsPage(int pageNum){ + + if(pageNum < 0){ + Serial.println("Page number must be greater than zero!"); + return false; + } + + if(!playlistItemsConfig || !itemsContentDetsSet){ + bool ret = getPlaylistItemsInitialConfig(); + + if(!ret){return ret;} + + } + + if(pageNum == playlistItemsConfig->currentPage){ + return true; + } + + int diff = pageNum - playlistItemsConfig->currentPage; + + // TODO: add skiping logic => sometimes it is faster to skip to the start and traversed from there + // TODO: when traversing playlist, contentDetails dont need to be parsed + + while(diff != 0){ + bool ret; + + if(diff > 0){ + ret = getNextPlaylistItemsPage(); + diff--; + }else{ + ret = getPreviousPlaylistItemsPage(); + diff++; + } + + if(!ret){ + Serial.println("Error traversing!"); + return ret; + } + } + return true; +} + + +bool YoutubePlaylist::getPreviousPlaylistItemsPage(){ + Serial.println("getPreviousPlaylistItemsPage() not yet implemented!"); + return false; +} + + +bool YoutubePlaylist::getNextPlaylistItemsPage(){ + Serial.println("getNextPlaylistItemsPage() not yet implemented!"); + return false; +} diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index e1e3777..faa71af 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -14,12 +14,20 @@ class YoutubePlaylist{ bool checkPlaylistSnipSet(); bool checkPlaylistContentDetsSet(); + bool checkItemsConfigSet(); + bool checkItemsContentDetsSet(); + const char* getPlaylistId(); bool getPlaylistStatus(); bool getPlaylistContentDetails(); bool getPlaylistSnippet(); + bool getPlaylistItemsPage(int pageNum); + + bool getNextPlaylistItemsPage(); + bool getPreviousPlaylistItemsPage(); + playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; playlistStatus *status = NULL; @@ -27,11 +35,11 @@ class YoutubePlaylist{ // "caches" a page of playlistItems playlistItemsContentDetails itemsContentDets[YT_PLAYLIST_ITEM_RESULTS_PER_PAGE]; + playlistItemsConfiguration *playlistItemsConfig = NULL; private: char playlistId[YT_PLAYLISTID_LEN + 1]; YoutubeApi *apiObj = NULL; - playlistItemsConfiguration *playlistItemsConfig = NULL; bool snipSet = false; bool contentDetsSet = false; @@ -49,5 +57,6 @@ class YoutubePlaylist{ bool parsePlaylistSnippet(); bool getPlaylistItemsInitialConfig(); - bool parsePlaylistItemsContentDetails(); + + bool parsePlaylistItemsContentDetails(); }; diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index 5ea7aa5..b6354f9 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -128,6 +128,19 @@ void test_createRequestString_playlistSnippet_simple(){ } +void test_createRequestString_playlistItemsContentDetails_simple(){ + WiFiClientSecure client; + YoutubeApi uut(API_KEY, client); + + char uutCommand[150]; + char expectedRes[150]; + sprintf(expectedRes, "/youtube/v3/playlistItems?part=contentDetails&playlistId=%s&key=%s", TEST_PLAYLIST_ID, API_KEY); + YoutubeApi::createRequestString(playlistItemsListContentDetails, uutCommand, TEST_PLAYLIST_ID); + + TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedRes, uutCommand, "The request string is not correct!"); +} + + void test_allocAndCopy_pos_NULL(){ const char someData[] = "testdata"; @@ -182,6 +195,7 @@ void setup() RUN_TEST(test_createRequestString_playlistStatus_simple); RUN_TEST(test_createRequestString_playlistContentDetails_simple); RUN_TEST(test_createRequestString_playlistSnippet_simple); + RUN_TEST(test_createRequestString_playlistItemsContentDetails_simple); RUN_TEST(test_allocAndCopy_pos_NULL); RUN_TEST(test_allocAndCopy_data_NULL); diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 2e38f8a..59766cd 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -8,6 +8,8 @@ #define MAX_WIFI_RETRIES 10 +char playlistId[YT_PLAYLISTID_LEN + 1]; + void test_constructDestruct_simple(){ WiFiClientSecure dummyClient; @@ -93,6 +95,79 @@ void test_getPlaylistSnippet_simple(){ } +/** + * @brief Checks if the given playlistItem is valid, or a "filler" set with default values + * + * @param c playlistItem to check + * @return returns true, if default values were detected. Otherwise returns false + */ +bool checkForDefaultPlaylistItemsContentDetails_value(playlistItemsContentDetails *c){ + + if(!strcmp(c->videoId, "") && c->videoPublishedAt.tm_year == 70){ + return true; + } + + return false; +} + + +void test_getPlaylistItems_firstPage(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, playlistId); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistItemsPage(0); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get first PlaylistItemsPage"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected the configuration flag to be set!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsContentDetsSet(), "Expected the data flag to be set!"); + + TEST_ASSERT_MESSAGE(uut.playlistItemsConfig->currentPage == 0, "Expected to be on first page"); + + playlistItemsConfiguration *uutConfig = uut.playlistItemsConfig; + + + if(uutConfig->totalResults < YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uutConfig->nextPageToken, "Did not expect a next page token to be set, when results fit into one page!"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", uutConfig->previousPageToken, "Did not expect a previous page token to be set, when feetching first page!"); + + TEST_ASSERT_EQUAL_UINT8_MESSAGE(uutConfig->totalResults - 1, uutConfig->currentPageLastValidPos, "Expected amount of total results correlating with lastValidPage position!"); + + }else{ // full page + + if(uutConfig->totalResults == YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + TEST_ASSERT_MESSAGE(strcmp(uutConfig->nextPageToken, "") == 0, "Expected no next page token, as all results fit into one page!"); // Couldnt test this case. + }else{ + TEST_ASSERT_MESSAGE(strcmp(uutConfig->nextPageToken, "") != 0, "Expected a next page token to be set!"); + } + + TEST_ASSERT_EQUAL_UINT8_MESSAGE(YT_PLAYLIST_ITEM_RESULTS_PER_PAGE - 1, uutConfig->currentPageLastValidPos, "Expected page to be filed!"); + } + + // Testing if default values are set + for(int i = 0; i < YT_PLAYLIST_ITEM_RESULTS_PER_PAGE; i++){ + + if(i > uutConfig->currentPageLastValidPos){ + ret = checkForDefaultPlaylistItemsContentDetails_value(&uut.itemsContentDets[i]); + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected a default value!"); + }else{ + ret = checkForDefaultPlaylistItemsContentDetails_value(&uut.itemsContentDets[i]); + TEST_ASSERT_EQUAL_MESSAGE(strlen(uut.itemsContentDets[i].videoId), YT_VIDEOID_LEN, "Expected other length of videoId string!"); + TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(104, uut.itemsContentDets[i].videoPublishedAt.tm_year, "Video upload date should be after 2004"); + TEST_ASSERT_FALSE_MESSAGE(ret, "Did not expect filler values on full page!"); + } + } +} + + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); @@ -121,6 +196,14 @@ void setup(){ RUN_TEST(test_getPlaylistContentDetails_simple); RUN_TEST(test_getPlaylistSnippet_simple); + strcpy(playlistId, TEST_PLAYLIST_ID_FEW_UPLOADS); + RUN_TEST(test_getPlaylistItems_firstPage); + + Serial.println("Testing now with an playlistId, with more than YT_PLAYLIST_ITEM_RESULTS_PER_PAGE items"); + + strcpy(playlistId, TEST_PLAYLIST_ID_MANY_UPLOADS ); + RUN_TEST(test_getPlaylistItems_firstPage); + UNITY_END(); } From eb6804fdfaa23a891fcca06da3fb776e6ff98c29 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 13 Oct 2022 14:49:38 +0200 Subject: [PATCH 51/58] adding helper function getYoutubeApiObj() --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 9 +++++++++ lib/YoutubePlaylist/YoutubePlaylist.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 91a34c9..0732b65 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -136,6 +136,15 @@ bool YoutubePlaylist::checkItemsContentDetsSet(){return itemsContentDetsSet;} */ const char* YoutubePlaylist::getPlaylistId(){return playlistId;} + +/** + * @brief Returns the YoutubeApi object of the object. + * + * @return pointer to YoutubeApi object + */ +YoutubeApi* YoutubePlaylist::getYoutubeApiObj(){return apiObj;} + + /** * @brief Fetches playlist status of the set playlist id. * diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index faa71af..5b539ac 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -28,6 +28,8 @@ class YoutubePlaylist{ bool getNextPlaylistItemsPage(); bool getPreviousPlaylistItemsPage(); + YoutubeApi* getYoutubeApiObj(); + playlistSnippet *snip = NULL; playlistContentDetails *contentDets = NULL; playlistStatus *status = NULL; From 023b86fc4ff7a64fbb1f669648acc2eb707a60d8 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 13 Oct 2022 21:12:51 +0200 Subject: [PATCH 52/58] implementing fetching of following pages --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 63 +++++++++++++------ lib/YoutubePlaylist/YoutubePlaylist.h | 9 ++- .../test_YoutubePlaylistClass.cpp | 37 ++++++++++- 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 0732b65..1b81fe5 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -350,25 +350,33 @@ bool YoutubePlaylist::parsePlaylistSnippet(){ /** - * @brief Gets the the first page of playlistItems. Initialises the playlistItemsConfig and the first page of data. + * @brief Creates and initiliazes an playlistItems configuration object * - * @return true on success, false on error */ -bool YoutubePlaylist::getPlaylistItemsInitialConfig(){ - - if(playlistItemsConfig || itemsConfigSet){ - Serial.println("playlistItemsConfig is not supposed to be set already!"); - return false; - } - - // unlike other fetching methods, the config is only set once and then modified +void YoutubePlaylist::setConfig(){ playlistItemsConfiguration *newConfig = (playlistItemsConfiguration*) malloc(sizeof(playlistItemsConfiguration)); playlistItemsConfig = newConfig; playlistItemsConfig->currentPage = 0; itemsConfigSet = true; +} - char command[150]; - YoutubeApi::createRequestString(playlistItemsListContentDetails, command, playlistId); +/** + * @brief Gets a page of playlistItems. A page token can be passed. + * + * @return true on success, false on error + */ +bool YoutubePlaylist::getPlaylistItemsContentDetails(bool usePageToken, char *pageToken){ + + char command[180]; + char tokenAndPlaylistId[50]; + + if(usePageToken){ + sprintf(tokenAndPlaylistId, "%s&pageToken=%s", playlistId, pageToken); + }else{ + strcpy(tokenAndPlaylistId, playlistId); + } + + YoutubeApi::createRequestString(playlistItemsListContentDetails, command, tokenAndPlaylistId); int httpStatus = apiObj->sendGetToYoutube(command); if(httpStatus == 200){ @@ -472,20 +480,23 @@ bool YoutubePlaylist::getPlaylistItemsPage(int pageNum){ } if(!playlistItemsConfig || !itemsContentDetsSet){ - bool ret = getPlaylistItemsInitialConfig(); + // if it is the first time, the object fetches thee page, get the first page first + setConfig(); + bool ret = getPlaylistItemsContentDetails(false, ""); if(!ret){return ret;} - } - if(pageNum == playlistItemsConfig->currentPage){ - return true; + //check if page exists + if((int) ceil(((float) playlistItemsConfig->totalResults) / YT_PLAYLIST_ITEM_RESULTS_PER_PAGE) < pageNum){ + Serial.println("Page number too large!"); + return false; } int diff = pageNum - playlistItemsConfig->currentPage; // TODO: add skiping logic => sometimes it is faster to skip to the start and traversed from there - // TODO: when traversing playlist, contentDetails dont need to be parsed + // TODO: when traversing playlist, contentDetails dont need to be parsed or fetched while(diff != 0){ bool ret; @@ -514,6 +525,20 @@ bool YoutubePlaylist::getPreviousPlaylistItemsPage(){ bool YoutubePlaylist::getNextPlaylistItemsPage(){ - Serial.println("getNextPlaylistItemsPage() not yet implemented!"); - return false; + + if(strcmp("", playlistItemsConfig->nextPageToken) == 0){ + Serial.print("There is no next page!"); + return false; + } + + char nextPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1]; + strcpy(nextPageToken, playlistItemsConfig->nextPageToken); + + bool ret = getPlaylistItemsContentDetails(true, nextPageToken); + if(!ret){return ret;} + + strcpy(playlistItemsConfig->currentPageToken, nextPageToken); + playlistItemsConfig->currentPage++; + + return true; } diff --git a/lib/YoutubePlaylist/YoutubePlaylist.h b/lib/YoutubePlaylist/YoutubePlaylist.h index 5b539ac..46d42d6 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.h +++ b/lib/YoutubePlaylist/YoutubePlaylist.h @@ -25,8 +25,6 @@ class YoutubePlaylist{ bool getPlaylistItemsPage(int pageNum); - bool getNextPlaylistItemsPage(); - bool getPreviousPlaylistItemsPage(); YoutubeApi* getYoutubeApiObj(); @@ -58,7 +56,12 @@ class YoutubePlaylist{ bool parsePlaylistContentDetails(); bool parsePlaylistSnippet(); - bool getPlaylistItemsInitialConfig(); + void setConfig(); + + bool getPlaylistItemsContentDetails(bool usePageToken, char *pageToken); + + bool getNextPlaylistItemsPage(); + bool getPreviousPlaylistItemsPage(); bool parsePlaylistItemsContentDetails(); }; diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 59766cd..749ddb3 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -113,7 +113,7 @@ bool checkForDefaultPlaylistItemsContentDetails_value(playlistItemsContentDetail void test_getPlaylistItems_firstPage(){ - if(WiFi.status() != WL_CONNECTED){ + if(WiFi.status() != WL_CONNECTED){ TEST_IGNORE_MESSAGE("Could not establish internet connection!"); } @@ -167,6 +167,39 @@ void test_getPlaylistItems_firstPage(){ } } +void test_getPlaylistItems_secondPage(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, playlistId); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistItemsPage(1); + + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected playlistItemsConfig to be set and configured!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsContentDetsSet(), "Expected a page to be set and configured!"); + + playlistItemsConfiguration *uutConfig = uut.playlistItemsConfig; + + if(uutConfig->totalResults > YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch second page, as there are enough items!"); + }else{ + TEST_ASSERT_FALSE_MESSAGE(ret, "Should not be able to fetch second page, as there not enough items!"); + return; + } + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(1, uut.playlistItemsConfig->currentPage, "Expected to be on second page"); + + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->currentPageToken) != 0, "Expected the current page token to be set, when fetching second page!"); + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->previousPageToken) != 0, "Expected a previous page token to be set, when fetching second page!"); +} + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); @@ -198,11 +231,13 @@ void setup(){ strcpy(playlistId, TEST_PLAYLIST_ID_FEW_UPLOADS); RUN_TEST(test_getPlaylistItems_firstPage); + RUN_TEST(test_getPlaylistItems_secondPage); Serial.println("Testing now with an playlistId, with more than YT_PLAYLIST_ITEM_RESULTS_PER_PAGE items"); strcpy(playlistId, TEST_PLAYLIST_ID_MANY_UPLOADS ); RUN_TEST(test_getPlaylistItems_firstPage); + RUN_TEST(test_getPlaylistItems_secondPage); UNITY_END(); } From e794867d73e10dff6eae4a4400f417b6cf6ebf04 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Thu, 20 Oct 2022 22:54:46 +0200 Subject: [PATCH 53/58] implementing fetching previous page and a simple test --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 18 ++++++++- .../test_YoutubePlaylistClass.cpp | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 1b81fe5..b43f93b 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -519,8 +519,22 @@ bool YoutubePlaylist::getPlaylistItemsPage(int pageNum){ bool YoutubePlaylist::getPreviousPlaylistItemsPage(){ - Serial.println("getPreviousPlaylistItemsPage() not yet implemented!"); - return false; + + if(strcmp("", playlistItemsConfig->previousPageToken) == 0){ + Serial.print("There is no previous page!"); + return false; + } + + char prevPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1]; + strcpy(prevPageToken, playlistItemsConfig->previousPageToken); + + bool ret = getPlaylistItemsContentDetails(true, prevPageToken); + if(!ret){return ret;} + + strcpy(playlistItemsConfig->currentPageToken, prevPageToken); + playlistItemsConfig->currentPage--; + + return true; } diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 749ddb3..5510399 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -201,6 +201,45 @@ void test_getPlaylistItems_secondPage(){ } +void test_getPlaylistItems_FirstSecondFirstPage(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, playlistId); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistItemsPage(1); + + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected playlistItemsConfig to be set and configured!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsContentDetsSet(), "Expected a page to be set and configured!"); + + playlistItemsConfiguration *uutConfig = uut.playlistItemsConfig; + + if(uutConfig->totalResults > YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch second page, as there are enough items!"); + }else{ + TEST_ASSERT_FALSE_MESSAGE(ret, "Should not be able to fetch second page, as there not enough items!"); + return; + } + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(1, uut.playlistItemsConfig->currentPage, "Expected to be on second page"); + + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->currentPageToken) != 0, "Expected the current page token to be set, when fetching second page!"); + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->previousPageToken) != 0, "Expected a previous page token to be set, when fetching second page!"); + + ret = uut.getPlaylistItemsPage(0); + + TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to fetch first page!"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, uut.playlistItemsConfig->currentPage, "Expected to be on first page!"); +} + + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); @@ -238,6 +277,7 @@ void setup(){ strcpy(playlistId, TEST_PLAYLIST_ID_MANY_UPLOADS ); RUN_TEST(test_getPlaylistItems_firstPage); RUN_TEST(test_getPlaylistItems_secondPage); + RUN_TEST(test_getPlaylistItems_FirstSecondFirstPage); UNITY_END(); } From a2ea175a318c5cb4f8042d36e3d46589a77e29bc Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Sat, 27 May 2023 01:58:54 +0200 Subject: [PATCH 54/58] Implemented fetching previous playlist pages --- lib/YoutubePlaylist/YoutubePlaylist.cpp | 17 ++++++- .../test_YoutubePlaylistClass.cpp | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index 1b81fe5..5cdc8da 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -519,8 +519,21 @@ bool YoutubePlaylist::getPlaylistItemsPage(int pageNum){ bool YoutubePlaylist::getPreviousPlaylistItemsPage(){ - Serial.println("getPreviousPlaylistItemsPage() not yet implemented!"); - return false; + if(strcmp("", playlistItemsConfig->previousPageToken) == 0){ + Serial.print("There is no previous page!"); + return false; + } + + char previousPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1]; + strcpy(previousPageToken, playlistItemsConfig->previousPageToken); + + bool ret = getPlaylistItemsContentDetails(true, previousPageToken); + if(!ret){return ret;} + + strcpy(playlistItemsConfig->currentPageToken, previousPageToken); + playlistItemsConfig->currentPage--; + + return true; } diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 749ddb3..0b186a4 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -201,6 +201,52 @@ void test_getPlaylistItems_secondPage(){ } +void test_getPlaylistItems_first_second_first_page(){ + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + + WiFiClientSecure dummyClient; + YoutubeApi dummyApi(API_KEY, dummyClient); + + YoutubePlaylist uut(&dummyApi, playlistId); + + dummyClient.setInsecure(); + + bool ret = uut.getPlaylistItemsPage(1); + + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected playlistItemsConfig to be set and configured!"); + TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsContentDetsSet(), "Expected a page to be set and configured!"); + + playlistItemsConfiguration *uutConfig = uut.playlistItemsConfig; + + if(uutConfig->totalResults > YT_PLAYLIST_ITEM_RESULTS_PER_PAGE){ + TEST_ASSERT_TRUE_MESSAGE(ret, "Should be able to fetch second page, as there are enough items!"); + }else{ + TEST_ASSERT_FALSE_MESSAGE(ret, "Should not be able to fetch second page, as there not enough items!"); + return; + } + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(1, uut.playlistItemsConfig->currentPage, "Expected to be on second page"); + + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->currentPageToken) != 0, "Expected the current page token to be set, when fetching second page!"); + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->previousPageToken) != 0, "Expected a previous page token to be set, when fetching second page!"); + + char secondPageToken[YT_PLALIST_ITEMS_PAGE_TOKEN_LEN + 1]; + strcpy(secondPageToken, uut.playlistItemsConfig->currentPageToken); + + ret = uut.getPlaylistItemsPage(0); + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, uut.playlistItemsConfig->currentPage, "Expected to be on first page"); + TEST_ASSERT_MESSAGE(strcmp(secondPageToken, uutConfig->currentPageToken) != 0, "Current token should not match token of second page!"); + TEST_ASSERT_MESSAGE(strcmp(secondPageToken, uutConfig->nextPageToken) == 0, "Next token should match token of second page!"); + TEST_ASSERT_MESSAGE(strcmp("", uutConfig->currentPageToken) != 0, "Expected the current page token to be set, when fetching second page!"); + + + +} + + bool establishInternetConnection(){ WiFi.mode(WIFI_STA); WiFi.disconnect(); @@ -238,6 +284,7 @@ void setup(){ strcpy(playlistId, TEST_PLAYLIST_ID_MANY_UPLOADS ); RUN_TEST(test_getPlaylistItems_firstPage); RUN_TEST(test_getPlaylistItems_secondPage); + RUN_TEST(test_getPlaylistItems_first_second_first_page); UNITY_END(); } From 405597ee2de69fa6b27c5e7f8cb5fcb12113246f Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 29 May 2023 23:59:56 +0200 Subject: [PATCH 55/58] adding example for YoutubePlaylist --- .../PlaylistInformation.ino | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 examples/PlaylistInformation/PlaylistInformation.ino diff --git a/examples/PlaylistInformation/PlaylistInformation.ino b/examples/PlaylistInformation/PlaylistInformation.ino new file mode 100644 index 0000000..4c7afb4 --- /dev/null +++ b/examples/PlaylistInformation/PlaylistInformation.ino @@ -0,0 +1,320 @@ +/******************************************************************* + Read YouTube Channel information from the YouTube API + and print them to the serial monitor + + Compatible Boards: + * Any ESP8266 board + * Any ESP32 board + + Recommended Board: D1 Mini ESP8266 + http://s.click.aliexpress.com/e/uzFUnIe (affiliate) + + If you find what I do useful and would like to support me, + please consider becoming a sponsor on Github + https://github.com/sponsors/witnessmenow/ + + Written by Brian Lough + YouTube: https://www.youtube.com/brianlough + Tindie: https://www.tindie.com/stores/brianlough/ + Twitter: https://twitter.com/witnessmenow + *******************************************************************/ + +// ---------------------------- +// Standard Libraries +// ---------------------------- + +#if defined(ESP8266) + #include +#elif defined(ESP32) + #include +#endif + +#include + +// ---------------------------- +// Additional Libraries - each of these will need to be installed +// ---------------------------- + +// Library for connecting to the YouTube API +// https://github.com/witnessmenow/arduino-youtube-api +// (search for "youtube" in the Arduino Library Manager) +#include "YoutubeApi.h" +#include "YoutubePlaylist.h" +#include "YoutubeVideo.h" + +// Library used for parsing Json from the API responses +// https://github.com/bblanchon/ArduinoJson +// (search for "Arduino Json" in the Arduino Library Manager) +#include + +//------- Replace the following! ------ +const char ssid[] = "xxx"; // your network SSID (name) +const char password[] = "yyyy"; // your network key +#define API_KEY "zzzz" // your Google API key +//------------------------------------- + +#define playlistIdLen 24 + +#define timeBetweenRequestGroup 120 * 1000 // 120 seconds, in milliseconds | time between all requests +#define timeBetweenRequests 2 * 1000 // 2 seconds, in milliseconds | time between single requests + +WiFiClientSecure client; +YoutubeApi api(API_KEY, client); + +char playlistId[playlistIdLen + 1]; +unsigned long startTime; + +/** + * @brief Tries to read the playlistId from Serial. + * + * @return 1 on success, 0 if no data available + */ +int readPlaylist(){ + + if(Serial.available() > playlistIdLen - 1){ + + for(int i = 0; i < playlistIdLen; i++){ + + playlistId[i] = Serial.read(); + } + + playlistId[playlistIdLen] = '\0'; + return 1; + } + return 0; +} + +/** + * @brief Flushes the Serial input buffer. + * + */ +void flushSerialBuffer(){ + while(Serial.available()){ + Serial.read(); + } +} + +/** + * @brief Prints "Yes\n" if x or "No\n" if not x + * + * @param x parameter + */ +void printYesNo(bool x){ + if(x){ + Serial.println("Yes"); + }else{ + Serial.println("No"); + } +} + +void printTimestamp(tm t){ + Serial.print(t.tm_mday); + Serial.print("."); + Serial.print(t.tm_mon); + Serial.print("."); + Serial.print(t.tm_year + 1900); + Serial.print(" "); + Serial.print(t.tm_hour); + Serial.print(":"); + Serial.print(t.tm_min); + Serial.print(":"); + Serial.println(t.tm_sec); +} + +void printPlaylistItemsPage(YoutubePlaylist *plist){ + + playlistItemsConfiguration *config = plist->playlistItemsConfig; + + Serial.print("-------------------[Page "); + Serial.print(config->currentPage + 1); + Serial.print(" of "); + Serial.print((int) ceil(((float) config->totalResults) / YT_PLAYLIST_ITEM_RESULTS_PER_PAGE)); + Serial.print(" | Entries: "); + Serial.print(config->currentPageLastValidPos + 1); + Serial.println(" ]-------------------"); + + Serial.print("|----- previous Page token: "); + if(strcmp("", config->previousPageToken)){ + Serial.println(config->previousPageToken); + }else{ + Serial.println("[none]"); + } + + for(int entry = 0; entry < config->currentPageLastValidPos + 1; entry++){ + + YoutubeVideo vid(plist->itemsContentDets[entry].videoId, plist->getYoutubeApiObj()); + vid.getVideoSnippet(); + + Serial.print("\n|----- Entry: "); + Serial.println(entry + 1); + + Serial.print("|----- video id: "); + Serial.print(plist->itemsContentDets[entry].videoId); + + if(vid.checkVideoSnippetSet()){ + Serial.print("\t\t(\""); + Serial.print(vid.videoSnip->title); + Serial.println("\")"); + }else{ + Serial.print("\n"); + } + + Serial.print("|----- published at: "); + printTimestamp(plist->itemsContentDets[entry].videoPublishedAt); + } + + + Serial.print("\n|----- next Page token: "); + if(strcmp("", config->nextPageToken)){ + Serial.println(config->nextPageToken); + }else{ + Serial.println("[none]"); + } + + Serial.println("-------------------------------------------------"); +} + + +void setup() { + Serial.begin(115200); + + // Set WiFi to 'station' mode and disconnect + // from the AP if it was previously connected + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + // Connect to the WiFi network + Serial.print("\nConnecting to WiFi: "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.println("\nWiFi connected!"); + Serial.print("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); + + #ifdef ESP8266 + // Required if you are using ESP8266 V2.5 or above + client.setInsecure(); + #endif + + client.setInsecure(); + + // Uncomment for extra debugging info + // api._debug = true; + + flushSerialBuffer(); + Serial.print("Enter playlistId: "); + + while(1){ + if(readPlaylist()){ + flushSerialBuffer(); + break; + } + } + + Serial.println(playlistId); +} + +void loop() { + + Serial.setTimeout(timeBetweenRequestGroup); + + YoutubePlaylist playlist(&api, playlistId); + + // fetch and print information in channel.list:snippet + if(playlist.getPlaylistStatus()){ + Serial.println("\n\nstatus"); + + playlistStatus *status = playlist.status; + Serial.print("|----- privacy status: "); + Serial.println(status->privacyStatus); + + Serial.println("-------------------------------------------------"); + } + + + if(playlist.getPlaylistContentDetails()){ + Serial.println("\n\ncontentDetails"); + + playlistContentDetails *contentDetails = playlist.contentDets; + Serial.print("|----- item count: "); + Serial.println(contentDetails->itemCount); + + Serial.println("-------------------------------------------------"); + } + + + if(playlist.getPlaylistSnippet()){ + Serial.println("\n\nsnippet"); + + + playlistSnippet *snippet = playlist.snip; + + tm playlistCreation = snippet->publishedAt; + + Serial.print("|----- playlist creation (d.m.y h:m:s): "); + Serial.print(playlistCreation.tm_mday); + Serial.print("."); + Serial.print(playlistCreation.tm_mon); + Serial.print("."); + Serial.print(playlistCreation.tm_year + 1900); + Serial.print(" "); + Serial.print(playlistCreation.tm_hour); + Serial.print(":"); + Serial.print(playlistCreation.tm_min); + Serial.print(":"); + Serial.println(playlistCreation.tm_sec); + + Serial.print("|----- channel id: "); + Serial.println(snippet->channelId); + + Serial.print("|----- title: "); + Serial.println(snippet->title); + + Serial.print("|----- description: "); + Serial.println(snippet->description); + + Serial.print("|----- channel title: "); + Serial.println(snippet->channelTitle); + + Serial.print("|----- default language: "); + Serial.println(snippet->defaultLanguage); + + Serial.println("-------------------------------------------------"); + } + + Serial.println("\n"); + + int page = 0; + + do{ + + if(playlist.getPlaylistItemsPage(page)){ + printPlaylistItemsPage(&playlist); + } + + page++; + + }while(page < (int) ceil(((float) playlist.playlistItemsConfig->totalResults) / YT_PLAYLIST_ITEM_RESULTS_PER_PAGE)); + + Serial.print("\nRefreshing in "); + Serial.print(timeBetweenRequestGroup / 1000.0); + Serial.println(" seconds..."); + Serial.print("Or set a new playlistId: "); + + startTime = millis(); + flushSerialBuffer(); + + while(millis() - startTime < timeBetweenRequestGroup){ + + if(readPlaylist()){; + Serial.println(playlistId); + break; + } + } +} From 05e7d6706633b01a00935658a4c030a9a87d9153 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Fri, 2 Jun 2023 00:49:50 +0200 Subject: [PATCH 56/58] Using YoutubePlaylist in YoutubeChannel example --- .../ChannelInformation/ChannelInformation.ino | 33 ++++++++++++++++++- lib/YoutubePlaylist/YoutubePlaylist.cpp | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/ChannelInformation/ChannelInformation.ino b/examples/ChannelInformation/ChannelInformation.ino index 410d7c1..99b6c1c 100644 --- a/examples/ChannelInformation/ChannelInformation.ino +++ b/examples/ChannelInformation/ChannelInformation.ino @@ -40,6 +40,8 @@ // (search for "youtube" in the Arduino Library Manager) #include "YoutubeApi.h" #include "YoutubeChannel.h" +#include "YoutubePlaylist.h" +#include "YoutubeVideo.h" // Library used for parsing Json from the API responses // https://github.com/bblanchon/ArduinoJson @@ -230,6 +232,35 @@ void loop() { Serial.println("-------------------------------------------------"); } + if(channel.checkChannelContentDetailsSet() && channel.checkChannelSnipSet()){ + Serial.print("\n\nFetching last five videos of "); + Serial.println(channel.channelSnip->title); + + YoutubePlaylist recentVideos = YoutubePlaylist(&api, channel.channelContentDets->relatedPlaylistsUploads); + recentVideos.getPlaylistItemsPage(0); + + playlistItemsContentDetails *page = recentVideos.itemsContentDets; + + for(int i = 0; i < YT_PLAYLIST_ITEM_RESULTS_PER_PAGE; i++){ + char *videoId = page[i].videoId; + + if(!strcmp("", videoId)){ + break; + } + + YoutubeVideo vid = YoutubeVideo(videoId, &api); + if(vid.getVideoSnippet() && vid.getVideoStatistics()){ + Serial.print("videoId: "); + Serial.print(videoId); + Serial.print(" | \""); + Serial.print(vid.videoSnip->title); + Serial.print("\" | Views: "); + Serial.println(vid.videoStats->viewCount); + } + } + } + + Serial.print("\nRefreshing in "); Serial.print(timeBetweenRequestGroup / 1000.0); Serial.println(" seconds..."); @@ -244,5 +275,5 @@ void loop() { Serial.println(videoId); break; } - } + } } diff --git a/lib/YoutubePlaylist/YoutubePlaylist.cpp b/lib/YoutubePlaylist/YoutubePlaylist.cpp index b43f93b..310d68c 100644 --- a/lib/YoutubePlaylist/YoutubePlaylist.cpp +++ b/lib/YoutubePlaylist/YoutubePlaylist.cpp @@ -482,7 +482,7 @@ bool YoutubePlaylist::getPlaylistItemsPage(int pageNum){ if(!playlistItemsConfig || !itemsContentDetsSet){ // if it is the first time, the object fetches thee page, get the first page first setConfig(); - bool ret = getPlaylistItemsContentDetails(false, ""); + bool ret = getPlaylistItemsContentDetails(false, NULL); if(!ret){return ret;} } From 2d1f28df35f799d8678aecec1439becee25f4c7e Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 5 Jun 2023 22:46:13 +0200 Subject: [PATCH 57/58] putting internet connection check into setup --- .../test_YoutubeChannelClass.cpp | 27 ++++-------- .../test_YoutubePlaylistClass.cpp | 29 ++++--------- .../test_YoutubeVideoClass.cpp | 41 ++++--------------- 3 files changed, 26 insertions(+), 71 deletions(-) diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index 13cbef9..73de044 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -9,6 +9,13 @@ #define tooLongChannelId "1234567890123456789012345" #define tooShortChannelId "12345678901234567890123" +void setUp(){ + + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } + +} void test_onlyApiConstructor(){ WiFiClientSecure client; @@ -82,10 +89,6 @@ bool establishInternetConnection(){ void test_getChannelStatistics_simple(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); @@ -101,10 +104,6 @@ void test_getChannelStatistics_simple(){ void test_getChannelStatistics_simple_reset(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); @@ -126,10 +125,6 @@ void test_getChannelStatistics_simple_reset(){ void test_getChannelSnippet_simple(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); @@ -145,10 +140,6 @@ void test_getChannelSnippet_simple(){ void test_getChannelContentDetails_simple(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); @@ -172,6 +163,8 @@ void setup(){ Serial.begin(115200); UNITY_BEGIN(); + establishInternetConnection(); + RUN_TEST(test_onlyApiConstructor); RUN_TEST(test_charConstructor_validId); @@ -180,8 +173,6 @@ void setup(){ RUN_TEST(test_resetInfo); - establishInternetConnection(); - RUN_TEST(test_getChannelStatistics_simple); RUN_TEST(test_getChannelStatistics_simple_reset); diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 0b186a4..715bc04 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -10,6 +10,13 @@ char playlistId[YT_PLAYLISTID_LEN + 1]; +void setUp(){ + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } +} + + void test_constructDestruct_simple(){ WiFiClientSecure dummyClient; @@ -50,10 +57,6 @@ void test_getPlaylistStatus_simple(){ void test_getPlaylistContentDetails_simple(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure dummyClient; YoutubeApi dummyApi(API_KEY, dummyClient); @@ -71,10 +74,6 @@ void test_getPlaylistContentDetails_simple(){ void test_getPlaylistSnippet_simple(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure dummyClient; YoutubeApi dummyApi(API_KEY, dummyClient); @@ -113,10 +112,6 @@ bool checkForDefaultPlaylistItemsContentDetails_value(playlistItemsContentDetail void test_getPlaylistItems_firstPage(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure dummyClient; YoutubeApi dummyApi(API_KEY, dummyClient); @@ -169,10 +164,6 @@ void test_getPlaylistItems_firstPage(){ void test_getPlaylistItems_secondPage(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - WiFiClientSecure dummyClient; YoutubeApi dummyApi(API_KEY, dummyClient); @@ -202,9 +193,6 @@ void test_getPlaylistItems_secondPage(){ void test_getPlaylistItems_first_second_first_page(){ - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } WiFiClientSecure dummyClient; YoutubeApi dummyApi(API_KEY, dummyClient); @@ -269,8 +257,9 @@ void setup(){ UNITY_BEGIN(); - RUN_TEST(test_constructDestruct_simple); establishInternetConnection(); + + RUN_TEST(test_constructDestruct_simple); RUN_TEST(test_getPlaylistStatus_simple); RUN_TEST(test_getPlaylistContentDetails_simple); RUN_TEST(test_getPlaylistSnippet_simple); diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index c2410ae..cb0eec6 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -13,6 +13,12 @@ const char *invalidIdChar = "123"; String validIdString = "12345678901"; String invalidIdString = "123"; +void setUp(){ + if(WiFi.status() != WL_CONNECTED){ + TEST_IGNORE_MESSAGE("Could not establish internet connection!"); + } +} + void test_emptyConstructor() { YoutubeVideo uut; @@ -160,10 +166,6 @@ void test_getVideoStats_simple(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatistics(); @@ -178,10 +180,6 @@ void test_getVideoStats_simple_reset(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatistics(); @@ -203,10 +201,6 @@ void test_getVideoSnippet_simple(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoSnippet(); @@ -221,10 +215,6 @@ void test_getVideoSnippet_simple_reset(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoSnippet(); @@ -245,10 +235,6 @@ void test_getVideoStatus_simple(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatus(); @@ -263,10 +249,6 @@ void test_getVideoStatus_simple_reset(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatus(); @@ -289,10 +271,6 @@ void test_getVideoContentDetails_simple(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoContentDetails(); @@ -307,10 +285,6 @@ void test_getVideoContentDetails_simple_reset(){ YoutubeApi apiObj(API_KEY, client); client.setInsecure(); - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoContentDetails(); @@ -333,6 +307,8 @@ void setup() UNITY_BEGIN(); + establishInternetConnection(); + RUN_TEST(test_emptyConstructor); RUN_TEST(test_constCharConstructor_simple); RUN_TEST(test_constCharConstructor_rejectId); @@ -352,7 +328,6 @@ void setup() RUN_TEST(test_resetInfo_keepYoutubeApi_obj); - establishInternetConnection(); RUN_TEST(test_getVideoStats_simple); delay(100); RUN_TEST(test_getVideoStats_simple_reset); From 0409256122b08f0bfb6e23824bd128190c2e7d70 Mon Sep 17 00:00:00 2001 From: Colum31 <19858862+Colum31@users.noreply.github.com> Date: Mon, 5 Jun 2023 23:36:59 +0200 Subject: [PATCH 58/58] Making client a global var --- test/test_apiClass/test_YoutubeApiClass.cpp | 17 +++--- .../test_YoutubeChannelClass.cpp | 27 ++-------- .../test_YoutubePlaylistClass.cpp | 54 +++++-------------- .../test_YoutubeVideoClass.cpp | 41 +++----------- 4 files changed, 31 insertions(+), 108 deletions(-) diff --git a/test/test_apiClass/test_YoutubeApiClass.cpp b/test/test_apiClass/test_YoutubeApiClass.cpp index b6354f9..e94ae78 100644 --- a/test/test_apiClass/test_YoutubeApiClass.cpp +++ b/test/test_apiClass/test_YoutubeApiClass.cpp @@ -5,9 +5,14 @@ #include "YoutubeTypes.h" #include +WiFiClientSecure client; + +void setUp(){ + client = WiFiClientSecure(); + client.setInsecure(); +} void test_createRequestString_videoStats_simple(){ - WiFiClientSecure client; YoutubeApi uut("123", client); char uutCommand[150]; @@ -18,7 +23,6 @@ void test_createRequestString_videoStats_simple(){ } void test_createRequestString_videoStats_length40(){ - WiFiClientSecure client; char Length40ApiKey[41] = "1234567890123456789012345678901234567890"; YoutubeApi uut(Length40ApiKey, client); @@ -30,7 +34,6 @@ void test_createRequestString_videoStats_length40(){ } void test_createRequestString_videoStats_length46(){ - WiFiClientSecure client; char length41ApiKey[47] = "1234567890123456789012345678901234567890123456"; YoutubeApi uut(length41ApiKey, client); @@ -42,7 +45,6 @@ void test_createRequestString_videoStats_length46(){ } void test_createRequestString_videoSnip_length40(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -54,7 +56,6 @@ void test_createRequestString_videoSnip_length40(){ } void test_createRequestString_channelStatistics_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -66,7 +67,6 @@ void test_createRequestString_channelStatistics_simple(){ } void test_createRequestString_channelSnippet_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -78,7 +78,6 @@ void test_createRequestString_channelSnippet_simple(){ } void test_createRequestString_channelContentDetails_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -90,7 +89,6 @@ void test_createRequestString_channelContentDetails_simple(){ } void test_createRequestString_playlistStatus_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -103,7 +101,6 @@ void test_createRequestString_playlistStatus_simple(){ void test_createRequestString_playlistContentDetails_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -116,7 +113,6 @@ void test_createRequestString_playlistContentDetails_simple(){ void test_createRequestString_playlistSnippet_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; @@ -129,7 +125,6 @@ void test_createRequestString_playlistSnippet_simple(){ void test_createRequestString_playlistItemsContentDetails_simple(){ - WiFiClientSecure client; YoutubeApi uut(API_KEY, client); char uutCommand[150]; diff --git a/test/test_channelClass/test_YoutubeChannelClass.cpp b/test/test_channelClass/test_YoutubeChannelClass.cpp index 73de044..61ee94c 100644 --- a/test/test_channelClass/test_YoutubeChannelClass.cpp +++ b/test/test_channelClass/test_YoutubeChannelClass.cpp @@ -9,16 +9,19 @@ #define tooLongChannelId "1234567890123456789012345" #define tooShortChannelId "12345678901234567890123" +WiFiClientSecure client; + void setUp(){ if(WiFi.status() != WL_CONNECTED){ TEST_IGNORE_MESSAGE("Could not establish internet connection!"); } + client = WiFiClientSecure(); + client.setInsecure(); } void test_onlyApiConstructor(){ - WiFiClientSecure client; YoutubeApi apiDummy("", client); YoutubeChannel uut(&apiDummy); @@ -28,7 +31,6 @@ void test_onlyApiConstructor(){ } void test_charConstructor_validId(){ - WiFiClientSecure client; YoutubeApi apiDummy("", client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiDummy); @@ -38,7 +40,6 @@ void test_charConstructor_validId(){ } void test_charConstructor_tooLongId(){ - WiFiClientSecure client; YoutubeApi apiDummy("", client); YoutubeChannel uut(tooLongChannelId, &apiDummy); @@ -48,7 +49,6 @@ void test_charConstructor_tooLongId(){ } void test_charConstructor_tooShortId(){ - WiFiClientSecure client; YoutubeApi apiDummy("", client); YoutubeChannel uut(tooShortChannelId, &apiDummy); @@ -58,7 +58,6 @@ void test_charConstructor_tooShortId(){ } void test_resetInfo(){ - WiFiClientSecure client; YoutubeApi apiDummy("", client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiDummy); @@ -88,13 +87,9 @@ bool establishInternetConnection(){ } void test_getChannelStatistics_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); - client.setInsecure(); - bool ret = uut.getChannelStatistics(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); @@ -103,13 +98,9 @@ void test_getChannelStatistics_simple(){ } void test_getChannelStatistics_simple_reset(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); - client.setInsecure(); - bool ret = uut.getChannelStatistics(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); @@ -124,13 +115,9 @@ void test_getChannelStatistics_simple_reset(){ } void test_getChannelSnippet_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); - client.setInsecure(); - bool ret = uut.getChannelSnippet(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); @@ -139,13 +126,9 @@ void test_getChannelSnippet_simple(){ } void test_getChannelContentDetails_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); YoutubeChannel uut(TEST_CHANNEL_ID, &apiObj); - - client.setInsecure(); - + bool ret = uut.getChannelContentDetails(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to receive valid response!"); diff --git a/test/test_playlistClass/test_YoutubePlaylistClass.cpp b/test/test_playlistClass/test_YoutubePlaylistClass.cpp index 715bc04..2434347 100644 --- a/test/test_playlistClass/test_YoutubePlaylistClass.cpp +++ b/test/test_playlistClass/test_YoutubePlaylistClass.cpp @@ -10,10 +10,16 @@ char playlistId[YT_PLAYLISTID_LEN + 1]; +WiFiClientSecure client; + void setUp(){ - if(WiFi.status() != WL_CONNECTED){ + + if(WiFi.status() != WL_CONNECTED){ TEST_IGNORE_MESSAGE("Could not establish internet connection!"); } + + client = WiFiClientSecure(); + client.setInsecure(); } @@ -34,18 +40,9 @@ void test_constructDestruct_simple(){ } void test_getPlaylistStatus_simple(){ - - if(WiFi.status() != WL_CONNECTED){ - TEST_IGNORE_MESSAGE("Could not establish internet connection!"); - } - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistStatus(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist status!"); @@ -56,14 +53,9 @@ void test_getPlaylistStatus_simple(){ void test_getPlaylistContentDetails_simple(){ - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistContentDetails(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist content details!"); @@ -73,14 +65,9 @@ void test_getPlaylistContentDetails_simple(){ void test_getPlaylistSnippet_simple(){ - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, TEST_PLAYLIST_ID); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistSnippet(); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get a playlist snippet!"); @@ -111,14 +98,9 @@ bool checkForDefaultPlaylistItemsContentDetails_value(playlistItemsContentDetail void test_getPlaylistItems_firstPage(){ - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, playlistId); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistItemsPage(0); TEST_ASSERT_TRUE_MESSAGE(ret, "Expected to be able to get first PlaylistItemsPage"); @@ -163,14 +145,9 @@ void test_getPlaylistItems_firstPage(){ } void test_getPlaylistItems_secondPage(){ - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, playlistId); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistItemsPage(1); TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected playlistItemsConfig to be set and configured!"); @@ -193,14 +170,9 @@ void test_getPlaylistItems_secondPage(){ void test_getPlaylistItems_first_second_first_page(){ - - WiFiClientSecure dummyClient; - YoutubeApi dummyApi(API_KEY, dummyClient); - + YoutubeApi dummyApi(API_KEY, client); YoutubePlaylist uut(&dummyApi, playlistId); - dummyClient.setInsecure(); - bool ret = uut.getPlaylistItemsPage(1); TEST_ASSERT_TRUE_MESSAGE(uut.checkItemsConfigSet(), "Expected playlistItemsConfig to be set and configured!"); diff --git a/test/test_videoClass/test_YoutubeVideoClass.cpp b/test/test_videoClass/test_YoutubeVideoClass.cpp index cb0eec6..195fccc 100644 --- a/test/test_videoClass/test_YoutubeVideoClass.cpp +++ b/test/test_videoClass/test_YoutubeVideoClass.cpp @@ -13,10 +13,16 @@ const char *invalidIdChar = "123"; String validIdString = "12345678901"; String invalidIdString = "123"; +WiFiClientSecure client; + void setUp(){ - if(WiFi.status() != WL_CONNECTED){ + + if(WiFi.status() != WL_CONNECTED){ TEST_IGNORE_MESSAGE("Could not establish internet connection!"); } + + client = WiFiClientSecure(); + client.setInsecure(); } void test_emptyConstructor() @@ -132,7 +138,6 @@ void test_resetInfo_afterConstruct(){ void test_resetInfo_keepYoutubeApi_obj(){ - WiFiClientSecure client; YoutubeApi apiObject(API_KEY, client); YoutubeApi *pointerToObj = &apiObject; YoutubeVideo uut(validIdChar, pointerToObj); @@ -161,11 +166,7 @@ bool establishInternetConnection(){ void test_getVideoStats_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatistics(); @@ -175,11 +176,7 @@ void test_getVideoStats_simple(){ } void test_getVideoStats_simple_reset(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatistics(); @@ -196,11 +193,7 @@ void test_getVideoStats_simple_reset(){ void test_getVideoSnippet_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoSnippet(); @@ -210,11 +203,7 @@ void test_getVideoSnippet_simple(){ } void test_getVideoSnippet_simple_reset(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoSnippet(); @@ -230,11 +219,7 @@ void test_getVideoSnippet_simple_reset(){ void test_getVideoStatus_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatus(); @@ -244,11 +229,7 @@ void test_getVideoStatus_simple(){ } void test_getVideoStatus_simple_reset(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoStatus(); @@ -266,11 +247,7 @@ void test_getVideoStatus_simple_reset(){ void test_getVideoContentDetails_simple(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoContentDetails(); @@ -280,11 +257,7 @@ void test_getVideoContentDetails_simple(){ } void test_getVideoContentDetails_simple_reset(){ - - WiFiClientSecure client; YoutubeApi apiObj(API_KEY, client); - client.setInsecure(); - YoutubeVideo uut("USKD3vPD6ZA", &apiObj); bool ret = uut.getVideoContentDetails();