diff --git a/Makefile b/Makefile index f834f59..ce67b4a 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ $(SFW_SRC_DIR)Box.o : $(addprefix $(SFW_SRC_DIR),extension.hpp Segment.hpp) $(SFW_SRC_DIR)Segment.o : $(addprefix $(SFW_SRC_DIR),extension.hpp Box.hpp) $(SFW_SRC_DIR)Pixels.o : $(addprefix $(SFW_SRC_DIR),Box.hpp extension.hpp) $(SFW_SRC_DIR)Audio.o : $(addprefix $(SFW_SRC_DIR),Node.hpp Display.hpp Configuration.hpp Box.hpp filesystem.hpp extension.hpp) -$(SRC_DIR)Pudding.o : $(SFW_H_FILES) +$(SRC_DIR)Pudding.o : $(addprefix $(SRC_DIR), Item.hpp) $(SFW_H_FILES) %.o : %.cpp %.hpp $(CPPC) $(CPP_FLAGS) $< -c -o $@ diff --git a/config.json b/config.json index 3ffcce9..812b493 100644 --- a/config.json +++ b/config.json @@ -7,14 +7,6 @@ "debug": false, "render driver": "opengl" }, - "path": - { - "screenshots": "local/screenshots", - "video": "local/video" - }, - "gamepad": - { - }, "keys": { "print-video-memory-size": ["CTRL", "v"], @@ -22,6 +14,8 @@ }, "recording": { + "screenshot-directory": "local/screenshots", + "video-directory": "local/video", "enabled": true, "write-mp4": false, "video-frame-length": 33.333, @@ -32,10 +26,26 @@ "any-key-ignore-commands": ["up", "right", "down", "left"], "suppress-any-key-on-mods": true }, + "logging": + { + "enabled": true, + "output-directory": "local", + "debug": true + }, "scan": { "json-save": true, "json-save-directory": "local/scans", - "barcode": "02266600" + "barcode": "015700057605" + }, + "api": + { + "user-agent": "Pudding making game for http://shampoo.ooo", + "nutronix-app-id": "ea0f2e7e", + "nutronix-app-key": "39218dde526dd3349daa028deda518ae", + "edamam-app-id": "c23b139f", + "edamam-app-key": "c54cf8c997534caf7ee92b1ccc7d95a3", + "best-buy-api-key": "cFshpD7C2LtMq07GqlBVpYtY", + "giantbomb-api-key": "91a395231f4e1fd9f9ba8840c52a61cda343cd70" } } diff --git a/lib/sfw b/lib/sfw index a8948bc..4ece644 160000 --- a/lib/sfw +++ b/lib/sfw @@ -1 +1 @@ -Subproject commit a8948bca73450be6ec0820f07a6703e245fd1edc +Subproject commit 4ece64442fc12be150be217d71dab9afd2d2f8e4 diff --git a/src/Item.cpp b/src/Item.cpp new file mode 100644 index 0000000..850cea4 --- /dev/null +++ b/src/Item.cpp @@ -0,0 +1,85 @@ +#include "Item.hpp" + +void Item::set_text_property(const std::string& value, std::string& property, const std::string& property_name) +{ + if (property == "") + { + if (value != "") + { + property = value; + SDL_Log("set %s to %s in %s", property_name.c_str(), property.c_str(), get_full_name().c_str()); + } + else + { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "empty string passed, not setting %s in %s", + property_name.c_str(), get_full_name().c_str()); + } + } + else + { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%s already set to %s in %s, not setting", + property_name.c_str(), property.c_str(), get_full_name().c_str()); + } +} + +void Item::destroy_texture(SDL_Texture* texture) +{ + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "destroying texture %p", texture); + SDL_DestroyTexture(texture); +} + +void Item::add_image_texture(SDL_Texture* texture) +{ + image_textures.push_back(std::shared_ptr(texture, Item::destroy_texture)); +} + +const std::vector>& Item::get_image_textures() const +{ + return image_textures; +} + +void Item::set_brand_name(const std::string& name) +{ + set_text_property(name, brand_name, "brand name"); +} + +const std::string& Item::get_brand_name() const +{ + return brand_name; +} + +void Item::set_product_name(const std::string& name) +{ + set_text_property(name, product_name, "product name"); +} + +const std::string& Item::get_product_name() const +{ + return product_name; +} + +void Item::set_upc(const std::string& upc) +{ + set_text_property(upc, this->upc, "UPC"); +} + +const std::string& Item::get_upc() const +{ + return upc; +} + +std::string Item::get_full_name() const +{ + std::string name = get_brand_name(); + if (name != "") + { + name += " "; + } + name += get_product_name(); + return name; +} + +Item::~Item() +{ + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "destroying item %p", this); +} diff --git a/src/Item.hpp b/src/Item.hpp new file mode 100644 index 0000000..79f3875 --- /dev/null +++ b/src/Item.hpp @@ -0,0 +1,32 @@ +#ifndef Item_h_ +#define Item_h_ + +#include +#include +#include +#include + +class Item +{ + +private: + std::vector> image_textures; + std::string brand_name = "", product_name = "", upc = ""; + void set_text_property(const std::string&, std::string&, const std::string&); + static void destroy_texture(SDL_Texture*); + +public: + void add_image_texture(SDL_Texture*); + const std::vector>& get_image_textures() const; + void set_brand_name(const std::string&); + const std::string& get_brand_name() const; + void set_product_name(const std::string&); + const std::string& get_product_name() const; + void set_upc(const std::string&); + const std::string& get_upc() const; + std::string get_full_name() const; + ~Item(); + +}; + +#endif diff --git a/src/Pudding.cpp b/src/Pudding.cpp index 04e5439..fe82437 100644 --- a/src/Pudding.cpp +++ b/src/Pudding.cpp @@ -30,7 +30,123 @@ void Pudding::load_sdl_context() super::load_sdl_context(); } -void Pudding::curl_get_bytes(std::string url, std::vector* storage, std::string user_agent) +void Pudding::add_item(const std::string& barcode) +{ + Item item; + item.set_upc(barcode); + incorporate_open_food_api(item); + incorporate_nutronix_api(item); + items.push_back(item); +} + +/* Look for item upc in the Open Food API, and use result to fill out item properties if found + */ +void Pudding::incorporate_open_food_api(Item& item) +{ + SDL_Log("checking Open Food API"); + nlohmann::json json = json_from_url(OPEN_FOOD_API_URL + item.get_upc()); + // test that should determine if an Open Food API response is not empty + if (json.value("status", 0) && json.contains("product")) + { + if (json["product"].value("image_url", "") != "") + { + std::string url = json["product"]["image_url"]; + SDL_Texture* texture = texture_from_image_url(url); + if (texture != nullptr) + { + item.add_image_texture(texture); + } + } + item.set_brand_name(json["product"].value("brands", "")); + item.set_product_name(json["product"].value("product_name", "")); + save_item_json(json, item, "Open_Food_API"); + } + else + { + SDL_Log("no results from Open Food API"); + } +} + +/* Look for item upc in the Nutronix API, and use result to fill out item properties if found + */ +void Pudding::incorporate_nutronix_api(Item& item) +{ + SDL_Log("checking Nutronix API"); + // Nutronix requires API keys in headers for validation + nlohmann::json json = json_from_url( + NUTRONIX_API_URL + item.get_upc(), { + "x-app-id: " + get_configuration()["api"]["nutronix-app-id"].get(), + "x-app-key: " + get_configuration()["api"]["nutronix-app-key"].get() + }); + // test that should determine if a Nutronix response is not empty + if (!(json.contains("message") && json["message"] == NUTRONIX_NOT_FOUND)) + { + nlohmann::json food = json["foods"][0]; + if (food.contains("photo") && food["photo"].value("thumb", "") != "") + { + std::string url = food["photo"]["thumb"]; + SDL_Log("adding image listed in Nutronix API at %s", url.c_str()); + SDL_Texture* texture = texture_from_image_url(url); + if (texture != nullptr) + { + item.add_image_texture(texture); + } + } + item.set_brand_name(food.value("brand_name", "")); + item.set_product_name(food.value("food_name", "")); + save_item_json(json, item, "Nutronix_API"); + } + else + { + SDL_Log("no results from Nutronix API"); + } +} + +/* Write submitted JSON to file, creating parent directories if necessary, and using item and + * api_name to determine file name prefix + */ +void Pudding::save_item_json(const nlohmann::json& json, const Item& item, const std::string& api_name) +{ + if (get_configuration()["scan"]["json-save"]) + { + fs::path path = get_configuration()["scan"]["json-save-directory"]; + if (!fs::exists(path)) + { + fs::create_directories(path); + } + std::string prefix = api_name; + if (item.get_full_name() != "") + { + prefix += "_" + item.get_full_name(); + } + else + { + prefix += "_Unknown"; + } + std::replace_if(prefix.begin(), prefix.end(), [](char c) { return !std::isalnum(c); }, '_'); + path /= prefix + "_" + item.get_upc() + ".json"; + std::ofstream out(path); + out << std::setw(4) << json << std::endl; + SDL_Log("Saved JSON to %s", path.c_str()); + } + else + { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "not saving JSON, saving disabled by configuration"); + } +} + +nlohmann::json Pudding::json_from_url(const std::string& url, const std::vector& headers) +{ + std::vector storage; + curl_get_bytes(url, storage, headers); + nlohmann::json json = nlohmann::json::parse(storage); + std::stringstream json_formatted; + json_formatted << std::setw(4) << json << std::endl; + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%s", json_formatted.str().c_str()); + return json; +} + +void Pudding::curl_get_bytes(const std::string& url, std::vector& storage, const std::vector& headers) { CURL *curl; CURLcode result; @@ -45,11 +161,21 @@ void Pudding::curl_get_bytes(std::string url, std::vector* storage if (curl) { curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Pudding::store_curl_response); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Pudding::curl_write_response); std::vector food_barcode_response; - curl_easy_setopt(curl, CURLOPT_WRITEDATA, storage); - curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &storage); + curl_easy_setopt(curl, CURLOPT_USERAGENT, get_configuration()["api"]["user-agent"].get().c_str()); + struct curl_slist* list = nullptr; + if (headers.size() > 0) + { + for (const std::string& header : headers) + { + list = curl_slist_append(list, header.c_str()); + } + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); result = curl_easy_perform(curl); + curl_slist_free_all(list); if (result != CURLE_OK) { std::cout << "curl request failed " << curl_easy_strerror(result) << std::endl; @@ -64,61 +190,18 @@ void Pudding::curl_get_bytes(std::string url, std::vector* storage curl_global_cleanup(); } -nlohmann::json Pudding::food_json_from_barcode(std::string barcode) -{ - std::string url = "https://world.openfoodfacts.org/api/v0/product/" + barcode; - std::vector storage; - curl_get_bytes(url, &storage, USER_AGENT); - nlohmann::json food_json; - food_json = nlohmann::json::parse(storage); - if (get_configuration()["scan"]["json-save"] && !is_food_json_empty(food_json)) - { - fs::path path = get_configuration()["scan"]["json-save-directory"]; - if (!fs::exists(path)) - { - fs::create_directories(path); - } - std::string prefix = ""; - if (food_json.contains("product")) - { - if (food_json["product"].value("brands", "") != "") - { - prefix += food_json["product"]["brands"].get() + "_"; - } - if (food_json["product"].value("product_name", "") != "") - { - prefix += food_json["product"]["product_name"].get() + "_"; - } - } - if (prefix == "") - { - prefix = "Unknown_"; - } - std::replace_if(prefix.begin(), prefix.end(), [](char c) { return !std::isalnum(c); }, '_'); - path /= prefix + barcode + ".json"; - std::ofstream out(path); - out << std::setw(4) << food_json << std::endl; - SDL_Log("Saved JSON to %s", path.c_str()); - } - return food_json; -} - -size_t Pudding::store_curl_response(std::uint8_t* buffer, size_t size, size_t count, std::vector* storage) +size_t Pudding::curl_write_response(std::uint8_t* buffer, size_t size, size_t count, std::vector* storage) { size_t total_size = size * count; storage->insert(storage->end(), buffer, buffer + total_size); return total_size; } -bool Pudding::is_food_json_empty(nlohmann::json food_json) -{ - return !food_json.value("status", 0) || !food_json.contains("product"); -} - -SDL_Texture* Pudding::texture_from_image_url(std::string url) +SDL_Texture* Pudding::texture_from_image_url(const std::string& url) { + SDL_Log("looking up image at %s", url.c_str()); std::vector storage; - curl_get_bytes(url, &storage, USER_AGENT); + curl_get_bytes(url, storage); if (!storage.empty()) { SDL_RWops* rw = SDL_RWFromConstMem(storage.data(), storage.size()); @@ -130,26 +213,6 @@ SDL_Texture* Pudding::texture_from_image_url(std::string url) } } -void Pudding::refresh() -{ - nlohmann::json food_json = food_json_from_barcode(get_configuration()["scan"]["barcode"]); - if (!is_food_json_empty(food_json)) - { - if (food_json["product"].value("image_url", "") != "") - { - std::string url = food_json["product"]["image_url"]; - SDL_Texture* texture = texture_from_image_url(url); - SDL_Log("looking up image at %s", url.c_str()); - if (texture != nullptr) - { - image.unload(); - image.add_frames(texture); - image.add_wrap(true, true); - } - } - } -} - void Pudding::update() { get_root()->configuration.load("config.json"); @@ -157,8 +220,13 @@ void Pudding::update() if (current_barcode != get_configuration()["scan"]["barcode"]) { current_barcode = get_configuration()["scan"]["barcode"]; - refresh(); + add_item(current_barcode); + } + if (items.size() > 0) + { + if (items.front().get_image_textures().size() > 0) + { + SDL_RenderCopyF(get_renderer(), items.front().get_image_textures().front().get(), nullptr, nullptr); + } } - image.move_weighted(glm::vec2(1.681, 1)); - image.update(); } diff --git a/src/Pudding.hpp b/src/Pudding.hpp index 65e097d..d9255b2 100644 --- a/src/Pudding.hpp +++ b/src/Pudding.hpp @@ -3,29 +3,27 @@ #include #include -#include +#include #include +#include +#include "SDL.h" #include "json/json.hpp" #include "glm/vec2.hpp" #include "Game.hpp" #include "Sprite.hpp" #include "Color.hpp" #include "extension.hpp" +#include "Item.hpp" struct Pudding : Game { - const std::string USER_AGENT = "Work in progress game for http://shampoo.ooo"; const std::string OPEN_FOOD_API_URL = "https://world.openfoodfacts.org/api/v0/product/"; const std::string NUTRONIX_API_URL = "https://trackapi.nutritionix.com/v2/search/item?upc="; - const std::string NUTRONIX_APP_ID = "ea0f2e7e"; - const std::string NUTRONIX_APP_KEY = "39218dde526dd3349daa028deda518ae"; - const std::string BARCODE_MONSTER_API_URL = "https://barcode.monster/api/"; const std::string EDAMAM_API_URL = "https://api.edamam.com/api/food-database/v2/parser?upc=&app_id=&app_key="; - const std::string EDAMAM_APP_ID = "c23b139f"; - const std::string EDAMAM_APP_KEY = "c54cf8c997534caf7ee92b1ccc7d95a3"; + const std::string BARCODE_MONSTER_API_URL = "https://barcode.monster/api/"; const std::string BEST_BUY_API_URL = "https://api.bestbuy.com/v1/products(upc=)?format=json&apiKey="; - const std::string BEST_BUY_API_KEY = "cFshpD7C2LtMq07GqlBVpYtY"; + const std::string NUTRONIX_NOT_FOUND = "resource not found"; /* * images, ingredients, protein weight, nutrition grade, popularity, "serving unit", item name, brand name, keywords, @@ -33,17 +31,19 @@ struct Pudding : Game */ typedef Game super; - Sprite image = Sprite(this); std::string current_barcode; + std::vector items; Pudding(); void load_sdl_context(); - nlohmann::json food_json_from_barcode(std::string); - void curl_get_bytes(std::string url, std::vector*, std::string = ""); - static size_t store_curl_response(std::uint8_t*, size_t, size_t, std::vector*); - bool is_food_json_empty(nlohmann::json); - SDL_Texture* texture_from_image_url(std::string); - void refresh(); + void add_item(const std::string&); + void incorporate_open_food_api(Item&); + void incorporate_nutronix_api(Item&); + void save_item_json(const nlohmann::json&, const Item&, const std::string&); + nlohmann::json json_from_url(const std::string&, const std::vector& = {}); + void curl_get_bytes(const std::string& url, std::vector&, const std::vector& = {}); + static size_t curl_write_response(std::uint8_t*, size_t, size_t, std::vector*); + SDL_Texture* texture_from_image_url(const std::string&); void update(); std::string get_class_name() { return "Pudding"; }