incorporate api results into item properties

This commit is contained in:
frank 2021-05-11 16:50:28 -04:00
parent 8645486078
commit edc6d8946d
7 changed files with 295 additions and 100 deletions

View File

@ -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 $@

View File

@ -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"
}
}

@ -1 +1 @@
Subproject commit a8948bca73450be6ec0820f07a6703e245fd1edc
Subproject commit 4ece64442fc12be150be217d71dab9afd2d2f8e4

85
src/Item.cpp Normal file
View File

@ -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<SDL_Texture>(texture, Item::destroy_texture));
}
const std::vector<std::shared_ptr<SDL_Texture>>& 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);
}

32
src/Item.hpp Normal file
View File

@ -0,0 +1,32 @@
#ifndef Item_h_
#define Item_h_
#include <vector>
#include <string>
#include <memory>
#include <SDL.h>
class Item
{
private:
std::vector<std::shared_ptr<SDL_Texture>> 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<std::shared_ptr<SDL_Texture>>& 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

View File

@ -30,7 +30,123 @@ void Pudding::load_sdl_context()
super::load_sdl_context();
}
void Pudding::curl_get_bytes(std::string url, std::vector<std::uint8_t>* 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<std::string>(),
"x-app-key: " + get_configuration()["api"]["nutronix-app-key"].get<std::string>()
});
// 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<std::string>& headers)
{
std::vector<std::uint8_t> 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<std::uint8_t>& storage, const std::vector<std::string>& headers)
{
CURL *curl;
CURLcode result;
@ -45,11 +161,21 @@ void Pudding::curl_get_bytes(std::string url, std::vector<std::uint8_t>* 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<std::uint8_t> 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<std::string>().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<std::uint8_t>* 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<std::uint8_t> 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<std::string>() + "_";
}
if (food_json["product"].value("product_name", "") != "")
{
prefix += food_json["product"]["product_name"].get<std::string>() + "_";
}
}
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<std::uint8_t>* storage)
size_t Pudding::curl_write_response(std::uint8_t* buffer, size_t size, size_t count, std::vector<std::uint8_t>* 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<std::uint8_t> 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();
}

View File

@ -3,29 +3,27 @@
#include <string>
#include <iostream>
#include <curl/curl.h>
#include <type_traits>
#include <filesystem>
#include <curl/curl.h>
#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<Item> items;
Pudding();
void load_sdl_context();
nlohmann::json food_json_from_barcode(std::string);
void curl_get_bytes(std::string url, std::vector<std::uint8_t>*, std::string = "");
static size_t store_curl_response(std::uint8_t*, size_t, size_t, std::vector<std::uint8_t>*);
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<std::string>& = {});
void curl_get_bytes(const std::string& url, std::vector<std::uint8_t>&, const std::vector<std::string>& = {});
static size_t curl_write_response(std::uint8_t*, size_t, size_t, std::vector<std::uint8_t>*);
SDL_Texture* texture_from_image_url(const std::string&);
void update();
std::string get_class_name() { return "Pudding"; }