From a37fbc729f0999bab0cef0e886547fa5927a7512 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:07:16 +0200 Subject: [PATCH 01/10] Initial commit --- library.properties | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/library.properties b/library.properties index af778d5..406bcf8 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,8 @@ -name=YoutubeApi +name=YoutubeApiOAuth2 version=1.0.0 -author=Brian Lough -maintainer=Brian Lough -sentence=A wrapper for the YouTube API for Arduino (supports ESP8266 & WiFi101 boards) -paragraph=Use this library to get YouTube channel statistics +author=marcfon +sentence=See the original library https://github.com/witnessmenow/arduino-youtube-api for documentation. +paragraph=Use this library to get YouTube channel statistics using OAuth2 category=Communication -url=https://github.com/witnessmenow/arduino-youtube-api +url=https://github.com/marcfon/arduino-youtube-api architectures=* From 44cdddadabc333ada158f242fd9dcc976f83c588 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:08:11 +0200 Subject: [PATCH 02/10] Initial commit --- README.md | 36 +------ src/YoutubeApi.cpp | 248 ++++++++++++++++++++++++++++++++++++--------- src/YoutubeApi.h | 28 +++-- 3 files changed, 224 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 65a4b40..671d143 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,3 @@ # arduino-youtube-api -A wrapper for the [YouTube API](https://developers.google.com/youtube/v3/docs/) for Arduino (works on ESP8266) - -Currently the only implemented method is getting the channel statistics but it can be easily extended. Please raise an issue if there is a method you are looking for. - -![Imgur](http://i.imgur.com/FmXyW4E.png) - -## Getting a Google Apps API key (Required!) - -* Create an application [here](https://console.developers.google.com) -* On the API Manager section, go to "Credentials" and create a new API key -* Enable your application to communicate the YouTube Api [here](https://console.developers.google.com/apis/api/youtube) -* Make sure the following URL works for you in your browser (Change the key at the end!): -https://www.googleapis.com/youtube/v3/channels?part=statistics&id=UCu7_D0o48KbfhpEohoP7YSQ&key=PutYourNewlyGeneratedKeyHere - -## Installing - -The downloaded code can be included as a new library into the IDE selecting the menu: - - Sketch / include Library / Add .Zip library - -You also have to install the ArduinoJson library written by [Benoît Blanchon](https://github.com/bblanchon). Search for it on the Arduino Library manager or get it from [here](https://github.com/bblanchon/ArduinoJson). - -Include YoutubeApi in your project: - - #include - -and pass it a Bot token and a SSL Client (See the examples for more details) - - #define API_KEY "XXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - // WiFiSSLClient client; //For 101 boards - WiFiClientSecure client; //For ESP8266 boards - YoutubeApi bot(API_KEY, client); - -*NOTE:* This library has not been tested with the 101 boards as I do not have a compatible board. If you can help please let me know! +A fork of the [arduino-youtube-api](https://github.com/witnessmenow/arduino-youtube-api) +that uses OAuth2 instead of the YouTube API key. diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index c400e61..6590bbd 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -21,72 +21,149 @@ #include "YoutubeApi.h" -YoutubeApi::YoutubeApi(String apiKey, Client &client) { - _apiKey = apiKey; +YoutubeApi::YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client) { + _clientId = clientId; + _clientSecret = clientSecret; + oAuth2Token.refreshToken = refreshToken; + oAuth2Token.expiresIn = 0; + oAuth2Token.lastRefreshedAt = 0; this->client = &client; } String YoutubeApi::sendGetToYoutube(String command) { - String headers=""; - String body=""; + String headers = ""; + String body = ""; bool finishedHeaders = false; bool currentLineIsBlank = true; unsigned long now; bool avail; - // Connect with youtube api over ssl - if (client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { - // Serial.println(".... connected to server"); - String a=""; - char c; - int ch_count=0; - client->println("GET "+command+"&key="+_apiKey); - now=millis(); - avail=false; - while (millis() - now < YTAPI_TIMEOUT) { - while (client->available()) { - - // Allow body to be parsed before finishing - avail = finishedHeaders; - char c = client->read(); - //Serial.write(c); - - if(!finishedHeaders){ - if (currentLineIsBlank && c == '\n') { - finishedHeaders = true; - } - else{ - headers = headers + c; + if(getAccessToken()) { + // Connect with youtube api over ssl + if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { + // Serial.println(".... connected to server"); + String a = ""; + char c; + int ch_count = 0; + command = "https://" YTAPI_HOST + command + "&access_token=" + oAuth2Token.accessToken; + // Serial.println(command); + client->println("GET " + command); + now = millis(); + avail = false; + while(millis() - now < YTAPI_TIMEOUT) { + while(client->available()) { + // Allow body to be parsed before finishing + avail = finishedHeaders; + char c = client->read(); + //Serial.write(c); + + if(!finishedHeaders) { + if(currentLineIsBlank && c == '\n') { + finishedHeaders = true; + } + else { + headers = headers + c; + } + } else { + if(ch_count < maxMessageLength) { + body = body+c; + ch_count++; + } } - } else { - if (ch_count < maxMessageLength) { - body=body+c; - ch_count++; + + if(c == '\n') { + currentLineIsBlank = true; + } else if(c != '\r') { + currentLineIsBlank = false; } } - - if (c == '\n') { - currentLineIsBlank = true; - }else if (c != '\r') { - currentLineIsBlank = false; + if(avail) { + // Serial.println("Body:"); + // Serial.println(body); + // Serial.println("END"); + break; } } - if (avail) { - //Serial.println("Body:"); - //Serial.println(body); - //Serial.println("END"); - break; - } } + return body; } + return "Failed to retrieve valid access token."; +} + +/** + * This method sends a HTTP POST to the YouTube API host and is used for + * instance to refresh the OAuth2 access token. + * + * Reference: + * http://playground.arduino.cc/Code/WebClient + **/ +String YoutubeApi::sendPostToYouTube(String page, String postData) { + // Serial.println("Doing POST request!"); + if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { + // Serial.println("Connected to server!"); + client->println("POST " + page + " HTTP/1.1"); + client->println("Host: " YTAPI_HOST); + client->println("Content-Type: application/x-www-form-urlencoded"); + client->println("Connection: close"); + client->print("Content-Length: "); + client->println(postData.length()); + client->println(); + client->println(postData); + } + String headers = ""; + String body = ""; + bool finishedHeaders = false; + bool currentLineIsBlank = true; + unsigned long now; + bool avail; + String a = ""; + char c; + int ch_count = 0; + now = millis(); + avail = false; + + while(millis() - now < YTAPI_TIMEOUT) { + while(client->available()) { + // Allow body to be parsed before finishing + avail = finishedHeaders; + char c = client->read(); + + if(!finishedHeaders) { + if(currentLineIsBlank && c == '\n') { + finishedHeaders = true; + } + else { + headers = headers + c; + } + } else { + if(ch_count < maxMessageLength) { + body = body+c; + ch_count++; + } + } + if(c == '\n') { + currentLineIsBlank = true; + } else if(c != '\r') { + currentLineIsBlank = false; + } + } + if(avail) { + // Serial.println("Body:"); + // Serial.println(body); + // Serial.println("END"); + break; + } + } return body; } -bool YoutubeApi::getChannelStatistics(String channelId){ - String command="https://" YTAPI_HOST "/youtube/v3/channels?part=statistics&id="+channelId; //If you can't find it(for example if you have a custom url) look here: https://www.youtube.com/account_advanced - //Serial.println(command); - String response = sendGetToYoutube(command); //recieve reply from youtube +/** + * https://developers.google.com/youtube/v3/docs/channels/list + */ +bool YoutubeApi::getChannelStatistics(String channelId) { + String command = "/youtube/v3/channels?part=statistics&id=" + channelId; //If you can't find it(for example if you have a custom url) look here: https://www.youtube.com/account_advanced + String response = sendGetToYoutube(command); //receive reply from youtube DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(response); if(root.success()) { @@ -106,6 +183,83 @@ bool YoutubeApi::getChannelStatistics(String channelId){ return true; } } - return false; } + +/** + * This method returns the 3 most recent subscribers to the channel. + * + * TODO This method should eventually be implemented as getRecentSubscribers() + * with all the available YouTube API parameters. + * + * The pageToken is the String for the next (or previous) set of subscribers. + * + * Return the next page string token if succesfull otherwise the error + * message that was returned by the YouTube API + * + * References + * https://www.googleapis.com/youtube/v3/subscriptions + * https://developers.google.com/youtube/v3/docs/subscriptions/list + */ +String YoutubeApi::getMyRecentSubscribers(String pageToken) { + String command = "/youtube/v3/subscriptions?part=subscriberSnippet&myRecentSubscribers=true&maxResults=3&pageToken=" + pageToken; + String response = sendGetToYoutube(command); + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(response); + if(root.success()) { + if (root.containsKey("items")) { + for (int i = 0; i < root["items"].size(); i++) { + String subscriber = root["items"][i]["subscriberSnippet"]["title"]; + myRecentSubscribers[i] = subscriber; + // Serial.println(subscriber); + } + return root["nextPageToken"]; + } + } + String code = root["error"]["code"]; + String message = root["error"]["message"]; + return "error," + code + "," + message; +} + +String YoutubeApi::getMyRecentSubscribers() { + return getMyRecentSubscribers(""); +} + +/** + * The access token is only valid for 3600 seconds (by default) before it needs + * to be refreshed. + */ +bool YoutubeApi::getAccessToken() { + unsigned long currentTime = millis(); + unsigned long tokenExpiresAt = oAuth2Token.lastRefreshedAt + (oAuth2Token.expiresIn * 1000); + + String page = "/oauth2/v4/token"; // https://developers.google.com/youtube/v3/guides/auth/devices + String myClientId(_clientId); // TODO Is there a better way to turn char[] into a String? + String myClientSecret(_clientSecret); + String postData = "client_id=" + myClientId + "&client_secret=" + myClientSecret + "&refresh_token=" + oAuth2Token.refreshToken + "&grant_type=refresh_token"; + + // Only get a new access token if the old one is expired + if(currentTime > tokenExpiresAt) { + String response = sendPostToYouTube(page, postData); + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(response); + if(root.success()) { + if(!root.containsKey("error")) { + String accessToken = root["access_token"]; + String tokenType = root["token_type"]; + int expiresIn = root["expires_in"]; + + oAuth2Token.accessToken = accessToken; + oAuth2Token.tokenType = tokenType; + oAuth2Token.expiresIn = expiresIn; + oAuth2Token.lastRefreshedAt = millis(); + + return true; + } + root.printTo(Serial); + } + return false; + } + // Serial.println("Don't need to refresh the token. Current access token is still valid!"); + return true; +} diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 207e5e2..9a2f550 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -28,10 +28,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define YTAPI_HOST "www.googleapis.com" #define YTAPI_SSL_PORT 443 -#define YTAPI_TIMEOUT 1500 +#define YTAPI_TIMEOUT 2000 - -struct channelStatistics{ +struct channelStatistics { long viewCount; long commentCount; long subscriberCount; @@ -39,19 +38,34 @@ struct channelStatistics{ long videoCount; }; +struct token { + String accessToken; + String tokenType; + int expiresIn; + String refreshToken; + unsigned long lastRefreshedAt; +}; + class YoutubeApi { public: - YoutubeApi (String apiKey, Client &client); - String sendGetToYoutube(String command); + YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client); bool getChannelStatistics(String channelId); + String getMyRecentSubscribers(); + String getMyRecentSubscribers(String pageToken); channelStatistics channelStats; + String myRecentSubscribers[3]; // Fixed number, above 4 seems to crash private: - String _apiKey; + String _clientId; + String _clientSecret; Client *client; - const int maxMessageLength = 1000; + token oAuth2Token; + const int maxMessageLength = 5000; bool checkForOkResponse(String response); + String sendGetToYoutube(String command); + String sendPostToYouTube(String page, String postData); + bool getAccessToken(); }; #endif From c20d1dd5c53804c3589200b8d2756b159056c408 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:09:36 +0200 Subject: [PATCH 03/10] Initial commit --- src/YoutubeApi.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 6590bbd..9495e67 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -211,7 +211,6 @@ String YoutubeApi::getMyRecentSubscribers(String pageToken) { for (int i = 0; i < root["items"].size(); i++) { String subscriber = root["items"][i]["subscriberSnippet"]["title"]; myRecentSubscribers[i] = subscriber; - // Serial.println(subscriber); } return root["nextPageToken"]; } From 9bdc7b9d7f2374cc1e818df10cb367f4269f4fdd Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:10:22 +0200 Subject: [PATCH 04/10] Code cleanup --- src/YoutubeApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 9495e67..6651ca1 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -21,7 +21,7 @@ #include "YoutubeApi.h" -YoutubeApi::YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client) { +YoutubeApi::YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client) { _clientId = clientId; _clientSecret = clientSecret; oAuth2Token.refreshToken = refreshToken; From cec35405947080d0990773b36e9fb7e45a066e97 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:23:03 +0200 Subject: [PATCH 05/10] More code cleanups --- src/YoutubeApi.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index 6651ca1..bbb7237 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -41,12 +41,10 @@ String YoutubeApi::sendGetToYoutube(String command) { if(getAccessToken()) { // Connect with youtube api over ssl if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { - // Serial.println(".... connected to server"); String a = ""; char c; int ch_count = 0; command = "https://" YTAPI_HOST + command + "&access_token=" + oAuth2Token.accessToken; - // Serial.println(command); client->println("GET " + command); now = millis(); avail = false; @@ -55,7 +53,6 @@ String YoutubeApi::sendGetToYoutube(String command) { // Allow body to be parsed before finishing avail = finishedHeaders; char c = client->read(); - //Serial.write(c); if(!finishedHeaders) { if(currentLineIsBlank && c == '\n') { @@ -98,9 +95,7 @@ String YoutubeApi::sendGetToYoutube(String command) { * http://playground.arduino.cc/Code/WebClient **/ String YoutubeApi::sendPostToYouTube(String page, String postData) { - // Serial.println("Doing POST request!"); if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { - // Serial.println("Connected to server!"); client->println("POST " + page + " HTTP/1.1"); client->println("Host: " YTAPI_HOST); client->println("Content-Type: application/x-www-form-urlencoded"); From f8f9a3e7c3776899e79f94137e409c3148c2b0b6 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 14:37:46 +0200 Subject: [PATCH 06/10] Added example (which also includes ArduinoOTA) --- .../MyRecentSubscribers.ino | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino diff --git a/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino new file mode 100644 index 0000000..c869db5 --- /dev/null +++ b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino @@ -0,0 +1,215 @@ +/******************************************************************* + * Read YouTube Channel statistics from the YouTube API * + * This sketch uses the WiFiManager Library for configuraiton * + * * + * By Brian Lough * + * https://www.youtube.com/channel/UCezJOfu7OtqGzd5xrP3q6WA * + *******************************************************************/ + +#include +#include +#include + +// For storing configurations +#include "FS.h" +#include + +// WiFiManager Libraries +#include //Local DNS Server used for redirecting all rs to the configuration portal +#include //Local WebServer used to serve the configuration portal +#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic + +// OTA Stuff +#include +#include +#include + +const int resetConfigPin = D8; //When high will reset the wifi manager config + +char channelId[30] = ""; +char clientId[80] = ""; +char clientSecret[120] = ""; +char refreshToken[80] = ""; + +WiFiClientSecure client; +YoutubeApi *api; + +unsigned long api_mtbs = 5000; //mean time between api requests +unsigned long api_lasttime; //last time api request has been done + +long subs = 0; + +// flag for saving data +bool shouldSaveConfig = false; + +void saveConfigCallback(); +bool loadConfig(); +bool saveConfig(); +void forceConfigMode(); + +//callback notifying us of the need to save config +void saveConfigCallback() { + Serial.println("Should save config"); + shouldSaveConfig = true; +} + +void setup() { + + Serial.begin(115200); + + if (!SPIFFS.begin()) { + Serial.println("Failed to mount FS"); + return; + } + + pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output + digitalWrite(LED_BUILTIN, LOW); // Turn the LED on (Note that LOW is the voltage level + loadConfig(); + + WiFiManager wifiManager; + wifiManager.setSaveConfigCallback(saveConfigCallback); + + // Adding an additional config on the WIFI manager webpage for the API Key and Channel ID + WiFiManagerParameter customChannelId("channelId", "Channel ID", channelId, 35); + WiFiManagerParameter customClientId("clientId", "Client ID", clientId, 85); + WiFiManagerParameter customClientSecret("clientSecret", "Client secret", clientSecret, 125); + WiFiManagerParameter customRefreshToken("refreshToken", "Refresh token", refreshToken, 85); + wifiManager.addParameter(&customChannelId); + wifiManager.addParameter(&customClientId); + wifiManager.addParameter(&customClientSecret); + wifiManager.addParameter(&customRefreshToken); + + // If it fails to connect it will create a YouTube-Counter access point + wifiManager.autoConnect("YouTube-Counter", "supersecret"); + + strcpy(channelId, customChannelId.getValue()); + strcpy(clientId, customClientId.getValue()); + strcpy(clientSecret, customClientSecret.getValue()); + strcpy(refreshToken, customRefreshToken.getValue()); + + if (shouldSaveConfig) { + saveConfig(); + } + + digitalWrite(LED_BUILTIN, HIGH); // Turn the LED off by making the voltage HIGH + // Force Config mode if there is no API key + if(strcmp(clientId, "") > 0 || strcmp(clientSecret, "") > 0 || strcmp(refreshToken, "") > 0) { + Serial.println("Init YouTube API"); + api = new YoutubeApi(clientId, clientSecret, refreshToken, client); + } else { + Serial.println("Forcing Config Mode"); + forceConfigMode(); + } + + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.begin(); + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); +} + +bool loadConfig() { + File configFile = SPIFFS.open("/config.json", "r"); + if (!configFile) { + Serial.println("Failed to open config file"); + return false; + } + + size_t size = configFile.size(); + if (size > 1024) { + Serial.println("Config file size is too large"); + return false; + } + + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(buf.get()); + + if (!json.success()) { + Serial.println("Failed to parse config file"); + return false; + } + + strcpy(channelId, json["channelId"]); + strcpy(clientId, json["clientId"]); + strcpy(clientSecret, json["clientSecret"]); + strcpy(refreshToken, json["refreshToken"]); + return true; +} + +bool saveConfig() { + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + json["channelId"] = channelId; + json["clientId"] = clientId; + json["clientSecret"] = clientSecret; + json["refreshToken"] = refreshToken; + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println("Failed to open config file for writing"); + return false; + } + + json.printTo(configFile); + return true; +} + +void forceConfigMode() { + Serial.println("Reset"); + WiFi.disconnect(); + Serial.println("Dq"); + delay(500); + ESP.restart(); + delay(5000); +} + +void loop() { + if(digitalRead(resetConfigPin) == HIGH) { + forceConfigMode(); + } + + if (millis() - api_lasttime > api_mtbs) { + String result = api->getMyRecentSubscribers(); // Returns the nextPageToken or the error message + Serial.println("--My Recent Subscribers--"); + for (int i = 0; i < sizeof(api->myRecentSubscribers)/sizeof(String); i++) { + Serial.println(api->myRecentSubscribers[i]); + } + Serial.print("Result (nextPageToken or error): "); + Serial.println(result); + Serial.println("------------------------"); + + api_lasttime = millis(); + } + ArduinoOTA.handle(); // enables OTA, don't remove +} From 6d3e78512c434e55ede2b37de0f8f64058585935 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 22 Apr 2017 16:38:55 +0200 Subject: [PATCH 07/10] Added examples and did some code refactoring --- README.md | 4 +- .../MyRecentSubscribers.ino | 180 ++------------- .../MyRecentSubscribersWifiManager.ino | 181 +++++++++++++++ .../MyRecentSubscribersWithOTA.ino | 213 ++++++++++++++++++ src/YoutubeApi.cpp | 54 +---- src/YoutubeApi.h | 1 + 6 files changed, 419 insertions(+), 214 deletions(-) create mode 100644 examples/ESP8266/MyRecentSubscribersWifiManager/MyRecentSubscribersWifiManager.ino create mode 100644 examples/ESP8266/MyRecentSubscribersWithOTA/MyRecentSubscribersWithOTA.ino diff --git a/README.md b/README.md index 671d143..499d872 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # arduino-youtube-api -A fork of the [arduino-youtube-api](https://github.com/witnessmenow/arduino-youtube-api) -that uses OAuth2 instead of the YouTube API key. +A fork of the [arduino youtube api](https://github.com/witnessmenow/arduino-youtube-api) +library. This fork uses OAuth2 instead of the YouTube API key. diff --git a/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino index c869db5..0f8185c 100644 --- a/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino +++ b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino @@ -10,22 +10,9 @@ #include #include -// For storing configurations -#include "FS.h" -#include - -// WiFiManager Libraries -#include //Local DNS Server used for redirecting all rs to the configuration portal -#include //Local WebServer used to serve the configuration portal -#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic - -// OTA Stuff -#include -#include -#include - -const int resetConfigPin = D8; //When high will reset the wifi manager config +#include // This Sketch doesn't technically need this, but the library does so it must be installed. +//------- Replace the following! ------ char channelId[30] = ""; char clientId[80] = ""; char clientSecret[120] = ""; @@ -37,96 +24,24 @@ YoutubeApi *api; unsigned long api_mtbs = 5000; //mean time between api requests unsigned long api_lasttime; //last time api request has been done -long subs = 0; - -// flag for saving data -bool shouldSaveConfig = false; - -void saveConfigCallback(); -bool loadConfig(); -bool saveConfig(); -void forceConfigMode(); - -//callback notifying us of the need to save config -void saveConfigCallback() { - Serial.println("Should save config"); - shouldSaveConfig = true; -} - void setup() { - Serial.begin(115200); - if (!SPIFFS.begin()) { - Serial.println("Failed to mount FS"); - return; - } - - pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output - digitalWrite(LED_BUILTIN, LOW); // Turn the LED on (Note that LOW is the voltage level - loadConfig(); - - WiFiManager wifiManager; - wifiManager.setSaveConfigCallback(saveConfigCallback); - - // Adding an additional config on the WIFI manager webpage for the API Key and Channel ID - WiFiManagerParameter customChannelId("channelId", "Channel ID", channelId, 35); - WiFiManagerParameter customClientId("clientId", "Client ID", clientId, 85); - WiFiManagerParameter customClientSecret("clientSecret", "Client secret", clientSecret, 125); - WiFiManagerParameter customRefreshToken("refreshToken", "Refresh token", refreshToken, 85); - wifiManager.addParameter(&customChannelId); - wifiManager.addParameter(&customClientId); - wifiManager.addParameter(&customClientSecret); - wifiManager.addParameter(&customRefreshToken); - - // If it fails to connect it will create a YouTube-Counter access point - wifiManager.autoConnect("YouTube-Counter", "supersecret"); - - strcpy(channelId, customChannelId.getValue()); - strcpy(clientId, customClientId.getValue()); - strcpy(clientSecret, customClientSecret.getValue()); - strcpy(refreshToken, customRefreshToken.getValue()); - - if (shouldSaveConfig) { - saveConfig(); - } - - digitalWrite(LED_BUILTIN, HIGH); // Turn the LED off by making the voltage HIGH - // Force Config mode if there is no API key - if(strcmp(clientId, "") > 0 || strcmp(clientSecret, "") > 0 || strcmp(refreshToken, "") > 0) { - Serial.println("Init YouTube API"); - api = new YoutubeApi(clientId, clientSecret, refreshToken, client); - } else { - Serial.println("Forcing Config Mode"); - forceConfigMode(); + // Set WiFi to station mode and disconnect from an AP if it was Previously + // connected + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(100); + + // Attempt to connect to Wifi network: + Serial.print("Connecting Wifi: "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); } - ArduinoOTA.onStart([]() { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) - type = "sketch"; - else // U_SPIFFS - type = "filesystem"; - - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() - Serial.println("Start updating " + type); - }); - ArduinoOTA.onEnd([]() { - Serial.println("\nEnd"); - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }); - ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - ArduinoOTA.begin(); - Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); @@ -134,71 +49,7 @@ void setup() { Serial.println(ip); } -bool loadConfig() { - File configFile = SPIFFS.open("/config.json", "r"); - if (!configFile) { - Serial.println("Failed to open config file"); - return false; - } - - size_t size = configFile.size(); - if (size > 1024) { - Serial.println("Config file size is too large"); - return false; - } - - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - - StaticJsonBuffer<200> jsonBuffer; - JsonObject& json = jsonBuffer.parseObject(buf.get()); - - if (!json.success()) { - Serial.println("Failed to parse config file"); - return false; - } - - strcpy(channelId, json["channelId"]); - strcpy(clientId, json["clientId"]); - strcpy(clientSecret, json["clientSecret"]); - strcpy(refreshToken, json["refreshToken"]); - return true; -} - -bool saveConfig() { - StaticJsonBuffer<200> jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); - json["channelId"] = channelId; - json["clientId"] = clientId; - json["clientSecret"] = clientSecret; - json["refreshToken"] = refreshToken; - - File configFile = SPIFFS.open("/config.json", "w"); - if (!configFile) { - Serial.println("Failed to open config file for writing"); - return false; - } - - json.printTo(configFile); - return true; -} - -void forceConfigMode() { - Serial.println("Reset"); - WiFi.disconnect(); - Serial.println("Dq"); - delay(500); - ESP.restart(); - delay(5000); -} - void loop() { - if(digitalRead(resetConfigPin) == HIGH) { - forceConfigMode(); - } - if (millis() - api_lasttime > api_mtbs) { String result = api->getMyRecentSubscribers(); // Returns the nextPageToken or the error message Serial.println("--My Recent Subscribers--"); @@ -211,5 +62,4 @@ void loop() { api_lasttime = millis(); } - ArduinoOTA.handle(); // enables OTA, don't remove } diff --git a/examples/ESP8266/MyRecentSubscribersWifiManager/MyRecentSubscribersWifiManager.ino b/examples/ESP8266/MyRecentSubscribersWifiManager/MyRecentSubscribersWifiManager.ino new file mode 100644 index 0000000..19bd196 --- /dev/null +++ b/examples/ESP8266/MyRecentSubscribersWifiManager/MyRecentSubscribersWifiManager.ino @@ -0,0 +1,181 @@ +/******************************************************************* + * Read YouTube Channel statistics from the YouTube API * + * This sketch uses the WiFiManager Library for configuraiton * + * * + * By Brian Lough * + * https://www.youtube.com/channel/UCezJOfu7OtqGzd5xrP3q6WA * + *******************************************************************/ + +#include +#include +#include + +// For storing configurations +#include "FS.h" +#include + +// WiFiManager Libraries +#include //Local DNS Server used for redirecting all rs to the configuration portal +#include //Local WebServer used to serve the configuration portal +#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic + +const int resetConfigPin = D8; //When high will reset the wifi manager config + +char channelId[30] = ""; +char clientId[80] = ""; +char clientSecret[120] = ""; +char refreshToken[80] = ""; + +WiFiClientSecure client; +YoutubeApi *api; + +unsigned long api_mtbs = 5000; //mean time between api requests +unsigned long api_lasttime; //last time api request has been done + +// flag for saving data +bool shouldSaveConfig = false; + +void saveConfigCallback(); +bool loadConfig(); +bool saveConfig(); +void forceConfigMode(); + +//callback notifying us of the need to save config +void saveConfigCallback() { + Serial.println("Should save config"); + shouldSaveConfig = true; +} + +void setup() { + + Serial.begin(115200); + + if (!SPIFFS.begin()) { + Serial.println("Failed to mount FS"); + return; + } + + pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output + digitalWrite(LED_BUILTIN, LOW); // Turn the LED on (Note that LOW is the voltage level + loadConfig(); + + WiFiManager wifiManager; + wifiManager.setSaveConfigCallback(saveConfigCallback); + + // Adding an additional config on the WIFI manager webpage for the API Key and Channel ID + WiFiManagerParameter customChannelId("channelId", "Channel ID", channelId, 35); + WiFiManagerParameter customClientId("clientId", "Client ID", clientId, 85); + WiFiManagerParameter customClientSecret("clientSecret", "Client secret", clientSecret, 125); + WiFiManagerParameter customRefreshToken("refreshToken", "Refresh token", refreshToken, 85); + wifiManager.addParameter(&customChannelId); + wifiManager.addParameter(&customClientId); + wifiManager.addParameter(&customClientSecret); + wifiManager.addParameter(&customRefreshToken); + + // If it fails to connect it will create a YouTube-Counter access point + wifiManager.autoConnect("YouTube-Counter", "supersecret"); + + strcpy(channelId, customChannelId.getValue()); + strcpy(clientId, customClientId.getValue()); + strcpy(clientSecret, customClientSecret.getValue()); + strcpy(refreshToken, customRefreshToken.getValue()); + + if (shouldSaveConfig) { + saveConfig(); + } + + digitalWrite(LED_BUILTIN, HIGH); // Turn the LED off by making the voltage HIGH + // Force Config mode if there is no API key + if(strcmp(clientId, "") > 0 || strcmp(clientSecret, "") > 0 || strcmp(refreshToken, "") > 0) { + Serial.println("Init YouTube API"); + api = new YoutubeApi(clientId, clientSecret, refreshToken, client); + } else { + Serial.println("Forcing Config Mode"); + forceConfigMode(); + } + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); +} + +bool loadConfig() { + File configFile = SPIFFS.open("/config.json", "r"); + if (!configFile) { + Serial.println("Failed to open config file"); + return false; + } + + size_t size = configFile.size(); + if (size > 1024) { + Serial.println("Config file size is too large"); + return false; + } + + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(buf.get()); + + if (!json.success()) { + Serial.println("Failed to parse config file"); + return false; + } + + strcpy(channelId, json["channelId"]); + strcpy(clientId, json["clientId"]); + strcpy(clientSecret, json["clientSecret"]); + strcpy(refreshToken, json["refreshToken"]); + return true; +} + +bool saveConfig() { + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + json["channelId"] = channelId; + json["clientId"] = clientId; + json["clientSecret"] = clientSecret; + json["refreshToken"] = refreshToken; + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println("Failed to open config file for writing"); + return false; + } + + json.printTo(configFile); + return true; +} + +void forceConfigMode() { + Serial.println("Reset"); + WiFi.disconnect(); + Serial.println("Dq"); + delay(500); + ESP.restart(); + delay(5000); +} + +void loop() { + if(digitalRead(resetConfigPin) == HIGH) { + forceConfigMode(); + } + + if (millis() - api_lasttime > api_mtbs) { + String result = api->getMyRecentSubscribers(); // Returns the nextPageToken or the error message + Serial.println("--My Recent Subscribers--"); + for (int i = 0; i < sizeof(api->myRecentSubscribers)/sizeof(String); i++) { + Serial.println(api->myRecentSubscribers[i]); + } + Serial.print("Result (nextPageToken or error): "); + Serial.println(result); + Serial.println("------------------------"); + + api_lasttime = millis(); + } +} diff --git a/examples/ESP8266/MyRecentSubscribersWithOTA/MyRecentSubscribersWithOTA.ino b/examples/ESP8266/MyRecentSubscribersWithOTA/MyRecentSubscribersWithOTA.ino new file mode 100644 index 0000000..db4e067 --- /dev/null +++ b/examples/ESP8266/MyRecentSubscribersWithOTA/MyRecentSubscribersWithOTA.ino @@ -0,0 +1,213 @@ +/******************************************************************* + * Read YouTube Channel statistics from the YouTube API * + * This sketch uses the WiFiManager Library for configuraiton * + * * + * By Brian Lough * + * https://www.youtube.com/channel/UCezJOfu7OtqGzd5xrP3q6WA * + *******************************************************************/ + +#include +#include +#include + +// For storing configurations +#include "FS.h" +#include + +// WiFiManager Libraries +#include //Local DNS Server used for redirecting all rs to the configuration portal +#include //Local WebServer used to serve the configuration portal +#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic + +// OTA Stuff +#include +#include +#include + +const int resetConfigPin = D8; //When high will reset the wifi manager config + +char channelId[30] = ""; +char clientId[80] = ""; +char clientSecret[120] = ""; +char refreshToken[80] = ""; + +WiFiClientSecure client; +YoutubeApi *api; + +unsigned long api_mtbs = 5000; //mean time between api requests +unsigned long api_lasttime; //last time api request has been done + +// flag for saving data +bool shouldSaveConfig = false; + +void saveConfigCallback(); +bool loadConfig(); +bool saveConfig(); +void forceConfigMode(); + +//callback notifying us of the need to save config +void saveConfigCallback() { + Serial.println("Should save config"); + shouldSaveConfig = true; +} + +void setup() { + + Serial.begin(115200); + + if (!SPIFFS.begin()) { + Serial.println("Failed to mount FS"); + return; + } + + pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output + digitalWrite(LED_BUILTIN, LOW); // Turn the LED on (Note that LOW is the voltage level + loadConfig(); + + WiFiManager wifiManager; + wifiManager.setSaveConfigCallback(saveConfigCallback); + + // Adding an additional config on the WIFI manager webpage for the API Key and Channel ID + WiFiManagerParameter customChannelId("channelId", "Channel ID", channelId, 35); + WiFiManagerParameter customClientId("clientId", "Client ID", clientId, 85); + WiFiManagerParameter customClientSecret("clientSecret", "Client secret", clientSecret, 125); + WiFiManagerParameter customRefreshToken("refreshToken", "Refresh token", refreshToken, 85); + wifiManager.addParameter(&customChannelId); + wifiManager.addParameter(&customClientId); + wifiManager.addParameter(&customClientSecret); + wifiManager.addParameter(&customRefreshToken); + + // If it fails to connect it will create a YouTube-Counter access point + wifiManager.autoConnect("YouTube-Counter", "supersecret"); + + strcpy(channelId, customChannelId.getValue()); + strcpy(clientId, customClientId.getValue()); + strcpy(clientSecret, customClientSecret.getValue()); + strcpy(refreshToken, customRefreshToken.getValue()); + + if (shouldSaveConfig) { + saveConfig(); + } + + digitalWrite(LED_BUILTIN, HIGH); // Turn the LED off by making the voltage HIGH + // Force Config mode if there is no API key + if(strcmp(clientId, "") > 0 || strcmp(clientSecret, "") > 0 || strcmp(refreshToken, "") > 0) { + Serial.println("Init YouTube API"); + api = new YoutubeApi(clientId, clientSecret, refreshToken, client); + } else { + Serial.println("Forcing Config Mode"); + forceConfigMode(); + } + + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.begin(); + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + IPAddress ip = WiFi.localIP(); + Serial.println(ip); +} + +bool loadConfig() { + File configFile = SPIFFS.open("/config.json", "r"); + if (!configFile) { + Serial.println("Failed to open config file"); + return false; + } + + size_t size = configFile.size(); + if (size > 1024) { + Serial.println("Config file size is too large"); + return false; + } + + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(buf.get()); + + if (!json.success()) { + Serial.println("Failed to parse config file"); + return false; + } + + strcpy(channelId, json["channelId"]); + strcpy(clientId, json["clientId"]); + strcpy(clientSecret, json["clientSecret"]); + strcpy(refreshToken, json["refreshToken"]); + return true; +} + +bool saveConfig() { + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + json["channelId"] = channelId; + json["clientId"] = clientId; + json["clientSecret"] = clientSecret; + json["refreshToken"] = refreshToken; + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println("Failed to open config file for writing"); + return false; + } + + json.printTo(configFile); + return true; +} + +void forceConfigMode() { + Serial.println("Reset"); + WiFi.disconnect(); + Serial.println("Dq"); + delay(500); + ESP.restart(); + delay(5000); +} + +void loop() { + if(digitalRead(resetConfigPin) == HIGH) { + forceConfigMode(); + } + + if (millis() - api_lasttime > api_mtbs) { + String result = api->getMyRecentSubscribers(); // Returns the nextPageToken or the error message + Serial.println("--My Recent Subscribers--"); + for (int i = 0; i < sizeof(api->myRecentSubscribers)/sizeof(String); i++) { + Serial.println(api->myRecentSubscribers[i]); + } + Serial.print("Result (nextPageToken or error): "); + Serial.println(result); + Serial.println("------------------------"); + + api_lasttime = millis(); + } + ArduinoOTA.handle(); // enables OTA, don't remove +} diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index bbb7237..edf0348 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -31,58 +31,14 @@ YoutubeApi::YoutubeApi(String clientId, String clientSecret, String refreshToken } String YoutubeApi::sendGetToYoutube(String command) { - String headers = ""; - String body = ""; - bool finishedHeaders = false; - bool currentLineIsBlank = true; - unsigned long now; - bool avail; - if(getAccessToken()) { // Connect with youtube api over ssl if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { - String a = ""; - char c; - int ch_count = 0; command = "https://" YTAPI_HOST + command + "&access_token=" + oAuth2Token.accessToken; client->println("GET " + command); - now = millis(); - avail = false; - while(millis() - now < YTAPI_TIMEOUT) { - while(client->available()) { - // Allow body to be parsed before finishing - avail = finishedHeaders; - char c = client->read(); - - if(!finishedHeaders) { - if(currentLineIsBlank && c == '\n') { - finishedHeaders = true; - } - else { - headers = headers + c; - } - } else { - if(ch_count < maxMessageLength) { - body = body+c; - ch_count++; - } - } - - if(c == '\n') { - currentLineIsBlank = true; - } else if(c != '\r') { - currentLineIsBlank = false; - } - } - if(avail) { - // Serial.println("Body:"); - // Serial.println(body); - // Serial.println("END"); - break; - } - } + return readRequestResponse(); } - return body; + return "Failed to connect to YouTube."; } return "Failed to retrieve valid access token."; } @@ -104,9 +60,13 @@ String YoutubeApi::sendPostToYouTube(String page, String postData) { client->println(postData.length()); client->println(); client->println(postData); + return readRequestResponse(); } + return "Failed to connect to YouTube."; +} - String headers = ""; +String YoutubeApi::readRequestResponse() { + String headers = ""; String body = ""; bool finishedHeaders = false; bool currentLineIsBlank = true; diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index 9a2f550..d4fb9a2 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -65,6 +65,7 @@ class YoutubeApi bool checkForOkResponse(String response); String sendGetToYoutube(String command); String sendPostToYouTube(String page, String postData); + String readRequestResponse(); bool getAccessToken(); }; From 17f091f9c948757cf66ed449fca177efec970199 Mon Sep 17 00:00:00 2001 From: marcfon Date: Sat, 13 May 2017 20:38:51 +0200 Subject: [PATCH 08/10] Wrapped some Strings in the F() macro but eventually we should remove all the Strings from code. --- src/YoutubeApi.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index edf0348..b9f9342 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -38,9 +38,9 @@ String YoutubeApi::sendGetToYoutube(String command) { client->println("GET " + command); return readRequestResponse(); } - return "Failed to connect to YouTube."; + return F("Failed to connect to YouTube."); } - return "Failed to retrieve valid access token."; + return F("Failed to retrieve valid access token."); } /** @@ -54,20 +54,20 @@ String YoutubeApi::sendPostToYouTube(String page, String postData) { if(client->connect(YTAPI_HOST, YTAPI_SSL_PORT)) { client->println("POST " + page + " HTTP/1.1"); client->println("Host: " YTAPI_HOST); - client->println("Content-Type: application/x-www-form-urlencoded"); - client->println("Connection: close"); - client->print("Content-Length: "); + client->println(F("Content-Type: application/x-www-form-urlencoded")); + client->println(F("Connection: close")); + client->print(F("Content-Length: ")); client->println(postData.length()); client->println(); client->println(postData); return readRequestResponse(); } - return "Failed to connect to YouTube."; + return F("Failed to connect to YouTube."); } String YoutubeApi::readRequestResponse() { - String headers = ""; - String body = ""; + String headers = ""; // FIXME using a string is not a very good idea + String body = ""; // FIXME using a string is not a very good idea bool finishedHeaders = false; bool currentLineIsBlank = true; unsigned long now; From 751b137e3c63ec48d2984db11796d016ea998ab2 Mon Sep 17 00:00:00 2001 From: marcfon Date: Wed, 28 Jun 2017 09:49:20 +0200 Subject: [PATCH 09/10] Some minor refactoring --- .../MyRecentSubscribers.ino | 2 ++ src/YoutubeApi.cpp | 21 ++++++++++++------- src/YoutubeApi.h | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino index 0f8185c..b5c587a 100644 --- a/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino +++ b/examples/ESP8266/MyRecentSubscribers/MyRecentSubscribers.ino @@ -47,6 +47,8 @@ void setup() { Serial.println("IP address: "); IPAddress ip = WiFi.localIP(); Serial.println(ip); + + api = new YoutubeApi(client, clientId, clientSecret, refreshToken); } void loop() { diff --git a/src/YoutubeApi.cpp b/src/YoutubeApi.cpp index b9f9342..f842335 100644 --- a/src/YoutubeApi.cpp +++ b/src/YoutubeApi.cpp @@ -21,13 +21,13 @@ #include "YoutubeApi.h" -YoutubeApi::YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client) { +YoutubeApi::YoutubeApi(Client &client, String clientId, String clientSecret, String refreshToken) { + this->client = &client; _clientId = clientId; _clientSecret = clientSecret; oAuth2Token.refreshToken = refreshToken; oAuth2Token.expiresIn = 0; oAuth2Token.lastRefreshedAt = 0; - this->client = &client; } String YoutubeApi::sendGetToYoutube(String command) { @@ -66,8 +66,8 @@ String YoutubeApi::sendPostToYouTube(String page, String postData) { } String YoutubeApi::readRequestResponse() { - String headers = ""; // FIXME using a string is not a very good idea - String body = ""; // FIXME using a string is not a very good idea + String headers = ""; + String body = ""; bool finishedHeaders = false; bool currentLineIsBlank = true; unsigned long now; @@ -93,7 +93,7 @@ String YoutubeApi::readRequestResponse() { } } else { if(ch_count < maxMessageLength) { - body = body+c; + body = body + c; ch_count++; } } @@ -142,10 +142,10 @@ bool YoutubeApi::getChannelStatistics(String channelId) { } /** - * This method returns the 3 most recent subscribers to the channel. + * This method returns the most recent subscribers to the channel. * * TODO This method should eventually be implemented as getRecentSubscribers() - * with all the available YouTube API parameters. + * with all the available parameters documented in the YouTube API. * * The pageToken is the String for the next (or previous) set of subscribers. * @@ -157,7 +157,12 @@ bool YoutubeApi::getChannelStatistics(String channelId) { * https://developers.google.com/youtube/v3/docs/subscriptions/list */ String YoutubeApi::getMyRecentSubscribers(String pageToken) { - String command = "/youtube/v3/subscriptions?part=subscriberSnippet&myRecentSubscribers=true&maxResults=3&pageToken=" + pageToken; + // FIXME there is a bug in the youtube api that seems to ignore the pageToken + // and just returns the first page https://issuetracker.google.com/issues/35176305 + + // We specify which fields should be returned which minimizes the size of the JSON response + // and saves valuable memory space + String command = "/youtube/v3/subscriptions?part=subscriberSnippet&myRecentSubscribers=true&maxResults=" + String(sizeof(myRecentSubscribers)/sizeof(String)) + "&fields=items%2FsubscriberSnippet%2Ftitle%2CnextPageToken%2CprevPageToken&prevPageToken=" + pageToken; String response = sendGetToYoutube(command); DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(response); diff --git a/src/YoutubeApi.h b/src/YoutubeApi.h index d4fb9a2..ba65036 100644 --- a/src/YoutubeApi.h +++ b/src/YoutubeApi.h @@ -49,12 +49,12 @@ struct token { class YoutubeApi { public: - YoutubeApi(String clientId, String clientSecret, String refreshToken, Client &client); + YoutubeApi(Client &client, String clientId, String clientSecret, String refreshToken); bool getChannelStatistics(String channelId); String getMyRecentSubscribers(); String getMyRecentSubscribers(String pageToken); channelStatistics channelStats; - String myRecentSubscribers[3]; // Fixed number, above 4 seems to crash + String myRecentSubscribers[5]; // Fixed number for now, shouldn't go above 50 private: String _clientId; From 9aaa3abaa502d6cfad0a3a25bd28a7d78d7b56cc Mon Sep 17 00:00:00 2001 From: marcfon Date: Wed, 28 Jun 2017 09:51:09 +0200 Subject: [PATCH 10/10] Reverted to the original README --- README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 499d872..65a4b40 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # arduino-youtube-api -A fork of the [arduino youtube api](https://github.com/witnessmenow/arduino-youtube-api) -library. This fork uses OAuth2 instead of the YouTube API key. +A wrapper for the [YouTube API](https://developers.google.com/youtube/v3/docs/) for Arduino (works on ESP8266) + +Currently the only implemented method is getting the channel statistics but it can be easily extended. Please raise an issue if there is a method you are looking for. + +![Imgur](http://i.imgur.com/FmXyW4E.png) + +## Getting a Google Apps API key (Required!) + +* Create an application [here](https://console.developers.google.com) +* On the API Manager section, go to "Credentials" and create a new API key +* Enable your application to communicate the YouTube Api [here](https://console.developers.google.com/apis/api/youtube) +* Make sure the following URL works for you in your browser (Change the key at the end!): +https://www.googleapis.com/youtube/v3/channels?part=statistics&id=UCu7_D0o48KbfhpEohoP7YSQ&key=PutYourNewlyGeneratedKeyHere + +## Installing + +The downloaded code can be included as a new library into the IDE selecting the menu: + + Sketch / include Library / Add .Zip library + +You also have to install the ArduinoJson library written by [Benoît Blanchon](https://github.com/bblanchon). Search for it on the Arduino Library manager or get it from [here](https://github.com/bblanchon/ArduinoJson). + +Include YoutubeApi in your project: + + #include + +and pass it a Bot token and a SSL Client (See the examples for more details) + + #define API_KEY "XXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + // WiFiSSLClient client; //For 101 boards + WiFiClientSecure client; //For ESP8266 boards + YoutubeApi bot(API_KEY, client); + +*NOTE:* This library has not been tested with the 101 boards as I do not have a compatible board. If you can help please let me know!