Large code cleanup and code documentation update, mostly related to the scraper. Only cosmetic changes in this commit.

This commit is contained in:
Leon Styhre 2020-05-26 18:34:33 +02:00
parent da946279de
commit fd7da08bf9
30 changed files with 1153 additions and 935 deletions

View file

@ -1,3 +1,9 @@
//
// AsyncReqComponent.cpp
//
// Deprecated, not in use any longer?
//
#include "components/AsyncReqComponent.h" #include "components/AsyncReqComponent.h"
#include "renderers/Renderer.h" #include "renderers/Renderer.h"

View file

@ -1,3 +1,9 @@
//
// AsyncReqComponent.h
//
// Deprecated, not in use any longer?
//
#pragma once #pragma once
#ifndef ES_APP_COMPONENTS_ASYNC_REQ_COMPONENT_H #ifndef ES_APP_COMPONENTS_ASYNC_REQ_COMPONENT_H
#define ES_APP_COMPONENTS_ASYNC_REQ_COMPONENT_H #define ES_APP_COMPONENTS_ASYNC_REQ_COMPONENT_H

View file

@ -1,3 +1,15 @@
//
// ScraperSearchComponent.cpp
//
// User interface component for the scraper where the user is able to see an overview
// of the game being scraped and an option to override the game search string.
// Used by both single-game scraping from the GuiMetaDataEd menu as well as
// to resolve scraping conflicts when run from GuiScraperStart.
//
// This component is called from GuiGameScraper for single-game scraping and
// from GuiScraperMulti for multi-game scraping.
//
#include "components/ScraperSearchComponent.h" #include "components/ScraperSearchComponent.h"
#include "components/ComponentList.h" #include "components/ComponentList.h"
@ -14,32 +26,39 @@
#include "Log.h" #include "Log.h"
#include "Window.h" #include "Window.h"
ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) : GuiComponent(window), ScraperSearchComponent::ScraperSearchComponent(
mGrid(window, Vector2i(4, 3)), mBusyAnim(window), Window* window,
mSearchType(type) SearchType type)
: GuiComponent(window),
mGrid(window, Vector2i(4, 3)),
mBusyAnim(window),
mSearchType(type)
{ {
addChild(&mGrid); addChild(&mGrid);
mBlockAccept = false; mBlockAccept = false;
// left spacer (empty component, needed for borders) // Left spacer (empty component, needed for borders).
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 0),
false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
// selected result name // Selected result name.
mResultName = std::make_shared<TextComponent>(mWindow, "Result name", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); mResultName = std::make_shared<TextComponent>(mWindow, "Result name",
Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
// selected result thumbnail // Selected result thumbnail.
mResultThumbnail = std::make_shared<ImageComponent>(mWindow); mResultThumbnail = std::make_shared<ImageComponent>(mWindow);
mGrid.setEntry(mResultThumbnail, Vector2i(1, 1), false, false, Vector2i(1, 1)); mGrid.setEntry(mResultThumbnail, Vector2i(1, 1), false, false, Vector2i(1, 1));
// selected result desc + container // Selected result description and container.
mDescContainer = std::make_shared<ScrollableContainer>(mWindow); mDescContainer = std::make_shared<ScrollableContainer>(mWindow);
mResultDesc = std::make_shared<TextComponent>(mWindow, "Result desc", Font::get(FONT_SIZE_SMALL), 0x777777FF); mResultDesc = std::make_shared<TextComponent>(mWindow, "Result desc",
Font::get(FONT_SIZE_SMALL), 0x777777FF);
mDescContainer->addChild(mResultDesc.get()); mDescContainer->addChild(mResultDesc.get());
mDescContainer->setAutoScroll(true); mDescContainer->setAutoScroll(true);
// metadata // Metadata.
auto font = Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder auto font = Font::get(FONT_SIZE_SMALL); // Placeholder, gets replaced in onSizeChanged().
const unsigned int mdColor = 0x777777FF; const unsigned int mdColor = 0x777777FF;
const unsigned int mdLblColor = 0x666666FF; const unsigned int mdLblColor = 0x666666FF;
mMD_Rating = std::make_shared<RatingComponent>(mWindow); mMD_Rating = std::make_shared<RatingComponent>(mWindow);
@ -50,17 +69,23 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type)
mMD_Genre = std::make_shared<TextComponent>(mWindow, "", font, mdColor); mMD_Genre = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Players = std::make_shared<TextComponent>(mWindow, "", font, mdColor); mMD_Players = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate)); (mWindow, "RATING:", font, mdLblColor), mMD_Rating, false));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer)); mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher)); (mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "GENRE:", font, mdLblColor), mMD_Genre)); mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players)); (mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
(mWindow, "GENRE:", font, mdLblColor), mMD_Genre));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players));
mMD_Grid = std::make_shared<ComponentGrid>(mWindow, Vector2i(2, (int)mMD_Pairs.size()*2 - 1)); mMD_Grid = std::make_shared<ComponentGrid>(mWindow,
Vector2i(2, (int)mMD_Pairs.size()*2 - 1));
unsigned int i = 0; unsigned int i = 0;
for(auto it = mMD_Pairs.cbegin(); it != mMD_Pairs.cend(); it++) for (auto it = mMD_Pairs.cbegin(); it != mMD_Pairs.cend(); it++) {
{
mMD_Grid->setEntry(it->first, Vector2i(0, i), false, true); mMD_Grid->setEntry(it->first, Vector2i(0, i), false, true);
mMD_Grid->setEntry(it->second, Vector2i(1, i), false, it->resize); mMD_Grid->setEntry(it->second, Vector2i(1, i), false, it->resize);
i += 2; i += 2;
@ -68,9 +93,10 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type)
mGrid.setEntry(mMD_Grid, Vector2i(2, 1), false, false); mGrid.setEntry(mMD_Grid, Vector2i(2, 1), false, false);
// result list // Result list.
mResultList = std::make_shared<ComponentList>(mWindow); mResultList = std::make_shared<ComponentList>(mWindow);
mResultList->setCursorChangedCallback([this](CursorState state) { if(state == CURSOR_STOPPED) updateInfoPane(); }); mResultList->setCursorChangedCallback([this](CursorState state) {
if (state == CURSOR_STOPPED) updateInfoPane(); });
updateViewStyle(); updateViewStyle();
} }
@ -79,46 +105,49 @@ void ScraperSearchComponent::onSizeChanged()
{ {
mGrid.setSize(mSize); mGrid.setSize(mSize);
if(mSize.x() == 0 || mSize.y() == 0) if (mSize.x() == 0 || mSize.y() == 0)
return; return;
// column widths // Column widths.
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
mGrid.setColWidthPerc(0, 0.02f); // looks better when this is higher in auto mode mGrid.setColWidthPerc(0, 0.02f); // Looks better when this is higher in auto mode.
else else
mGrid.setColWidthPerc(0, 0.01f); mGrid.setColWidthPerc(0, 0.01f);
mGrid.setColWidthPerc(1, 0.25f); mGrid.setColWidthPerc(1, 0.25f);
mGrid.setColWidthPerc(2, 0.25f); mGrid.setColWidthPerc(2, 0.25f);
// row heights // Row heights.
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) // show name if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) // Show name.
mGrid.setRowHeightPerc(0, (mResultName->getFont()->getHeight() * 1.6f) / mGrid.getSize().y()); // result name mGrid.setRowHeightPerc(0, (mResultName->getFont()->getHeight() * 1.6f) /
mGrid.getSize().y()); // Result name.
else else
mGrid.setRowHeightPerc(0, 0.0825f); // hide name but do padding mGrid.setRowHeightPerc(0, 0.0825f); // Hide name but do padding.
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
{
mGrid.setRowHeightPerc(2, 0.2f); mGrid.setRowHeightPerc(2, 0.2f);
}else{ else
mGrid.setRowHeightPerc(1, 0.505f); mGrid.setRowHeightPerc(1, 0.505f);
}
const float boxartCellScale = 0.9f; const float boxartCellScale = 0.9f;
// limit thumbnail size using setMaxHeight - we do this instead of letting mGrid call setSize because it maintains the aspect ratio // Limit thumbnail size using setMaxHeight - we do this instead of letting mGrid
// we also pad a little so it doesn't rub up against the metadata labels // call setSize because it maintains the aspect ratio.
// We also pad a little so it doesn't rub up against the metadata labels.
mResultThumbnail->setMaxSize(mGrid.getColWidth(1) * boxartCellScale, mGrid.getRowHeight(1)); mResultThumbnail->setMaxSize(mGrid.getColWidth(1) * boxartCellScale, mGrid.getRowHeight(1));
// metadata // Metadata.
resizeMetadata(); resizeMetadata();
if(mSearchType != ALWAYS_ACCEPT_FIRST_RESULT) if (mSearchType != ALWAYS_ACCEPT_FIRST_RESULT)
mDescContainer->setSize(mGrid.getColWidth(1)*boxartCellScale + mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3); mDescContainer->setSize(mGrid.getColWidth(1)*boxartCellScale +
mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3);
else else
mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, mResultDesc->getFont()->getHeight() * 8); mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale,
mResultDesc->getFont()->getHeight() * 8);
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container // Make description text wrap at edge of container.
mResultDesc->setSize(mDescContainer->getSize().x(), 0);
mGrid.onSizeChanged(); mGrid.onSizeChanged();
@ -128,28 +157,25 @@ void ScraperSearchComponent::onSizeChanged()
void ScraperSearchComponent::resizeMetadata() void ScraperSearchComponent::resizeMetadata()
{ {
mMD_Grid->setSize(mGrid.getColWidth(2), mGrid.getRowHeight(1)); mMD_Grid->setSize(mGrid.getColWidth(2), mGrid.getRowHeight(1));
if(mMD_Grid->getSize().y() > mMD_Pairs.size()) if (mMD_Grid->getSize().y() > mMD_Pairs.size()) {
{
const int fontHeight = (int)(mMD_Grid->getSize().y() / mMD_Pairs.size() * 0.8f); const int fontHeight = (int)(mMD_Grid->getSize().y() / mMD_Pairs.size() * 0.8f);
auto fontLbl = Font::get(fontHeight, FONT_PATH_REGULAR); auto fontLbl = Font::get(fontHeight, FONT_PATH_REGULAR);
auto fontComp = Font::get(fontHeight, FONT_PATH_LIGHT); auto fontComp = Font::get(fontHeight, FONT_PATH_LIGHT);
// update label fonts // Update label fonts.
float maxLblWidth = 0; float maxLblWidth = 0;
for(auto it = mMD_Pairs.cbegin(); it != mMD_Pairs.cend(); it++) for (auto it = mMD_Pairs.cbegin(); it != mMD_Pairs.cend(); it++) {
{
it->first->setFont(fontLbl); it->first->setFont(fontLbl);
it->first->setSize(0, 0); it->first->setSize(0, 0);
if(it->first->getSize().x() > maxLblWidth) if (it->first->getSize().x() > maxLblWidth)
maxLblWidth = it->first->getSize().x() + 6; maxLblWidth = it->first->getSize().x() + 6;
} }
for(unsigned int i = 0; i < mMD_Pairs.size(); i++) for (unsigned int i = 0; i < mMD_Pairs.size(); i++)
{ mMD_Grid->setRowHeightPerc(i*2, (fontLbl->getLetterHeight() + 2) /
mMD_Grid->setRowHeightPerc(i*2, (fontLbl->getLetterHeight() + 2) / mMD_Grid->getSize().y()); mMD_Grid->getSize().y());
}
// update component fonts // Update component fonts.
mMD_ReleaseDate->setFont(fontComp); mMD_ReleaseDate->setFont(fontComp);
mMD_Developer->setFont(fontComp); mMD_Developer->setFont(fontComp);
mMD_Publisher->setFont(fontComp); mMD_Publisher->setFont(fontComp);
@ -158,44 +184,52 @@ void ScraperSearchComponent::resizeMetadata()
mMD_Grid->setColWidthPerc(0, maxLblWidth / mMD_Grid->getSize().x()); mMD_Grid->setColWidthPerc(0, maxLblWidth / mMD_Grid->getSize().x());
// rating is manually sized // Rating is manually sized.
mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f); mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f);
mMD_Grid->onSizeChanged(); mMD_Grid->onSizeChanged();
// make result font follow label font // Make result font follow label font.
mResultDesc->setFont(Font::get(fontHeight, FONT_PATH_REGULAR)); mResultDesc->setFont(Font::get(fontHeight, FONT_PATH_REGULAR));
} }
} }
void ScraperSearchComponent::updateViewStyle() void ScraperSearchComponent::updateViewStyle()
{ {
// unlink description and result list and result name // Unlink description, result list and result name.
mGrid.removeEntry(mResultName); mGrid.removeEntry(mResultName);
mGrid.removeEntry(mResultDesc); mGrid.removeEntry(mResultDesc);
mGrid.removeEntry(mResultList); mGrid.removeEntry(mResultList);
// add them back depending on search type // Add them back depending on search type.
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) {
{ // Show name.
// show name mGrid.setEntry(mResultName, Vector2i(1, 0), false, true, Vector2i(2, 1),
mGrid.setEntry(mResultName, Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP); GridFlags::BORDER_TOP);
// need a border on the bottom left // Need a border on the bottom left.
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 2), false, false, Vector2i(3, 1), GridFlags::BORDER_BOTTOM); mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 2),
false, false, Vector2i(3, 1), GridFlags::BORDER_BOTTOM);
// show description on the right // Show description on the right.
mGrid.setEntry(mDescContainer, Vector2i(3, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); mGrid.setEntry(mDescContainer, Vector2i(3, 0), false, false, Vector2i(1, 3),
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
}else{ // Make description text wrap at edge of container.
// fake row where name would be mResultDesc->setSize(mDescContainer->getSize().x(), 0);
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP); }
else {
// Fake row where name would be.
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(1, 0),
false, true, Vector2i(2, 1), GridFlags::BORDER_TOP);
// show result list on the right // Show result list on the right.
mGrid.setEntry(mResultList, Vector2i(3, 0), true, true, Vector2i(1, 3), GridFlags::BORDER_LEFT | GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); mGrid.setEntry(mResultList, Vector2i(3, 0), true, true, Vector2i(1, 3),
GridFlags::BORDER_LEFT | GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
// show description under image/info // Show description under image/info.
mGrid.setEntry(mDescContainer, Vector2i(1, 2), false, false, Vector2i(2, 1), GridFlags::BORDER_BOTTOM); mGrid.setEntry(mDescContainer, Vector2i(1, 2), false, false, Vector2i(2, 1),
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container GridFlags::BORDER_BOTTOM);
// Make description text wrap at edge of container.
mResultDesc->setSize(mDescContainer->getSize().x(), 0);
} }
} }
@ -229,31 +263,31 @@ void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>
auto font = Font::get(FONT_SIZE_MEDIUM); auto font = Font::get(FONT_SIZE_MEDIUM);
unsigned int color = 0x777777FF; unsigned int color = 0x777777FF;
if(results.empty()) if (results.empty()) {
{ // Check if the scraper used is still valid.
// Check if the scraper used is still valid if (!isValidConfiguredScraper()) {
if (!isValidConfiguredScraper()) mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper "
{ "is no longer available.\nPlease change the scraping source in the settings."),
mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper is no longer available.\nPlease change the scraping source in the settings."),
"FINISH", mSkipCallback)); "FINISH", mSkipCallback));
} }
else else {
{
ComponentListRow row; ComponentListRow row;
row.addElement(std::make_shared<TextComponent>(mWindow, "NO GAMES FOUND - SKIP", font, color), true); row.addElement(std::make_shared<TextComponent>(mWindow, "NO GAMES FOUND - SKIP",
font, color), true);
if(mSkipCallback) if (mSkipCallback)
row.makeAcceptInputHandler(mSkipCallback); row.makeAcceptInputHandler(mSkipCallback);
mResultList->addRow(row); mResultList->addRow(row);
mGrid.resetCursor(); mGrid.resetCursor();
} }
}else{ }
else {
ComponentListRow row; ComponentListRow row;
for(size_t i = 0; i < results.size(); i++) for (size_t i = 0; i < results.size(); i++) {
{
row.elements.clear(); row.elements.clear();
row.addElement(std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(results.at(i).mdl.get("name")), font, color), true); row.addElement(std::make_shared<TextComponent>(mWindow,
Utils::String::toUpper(results.at(i).mdl.get("name")), font, color), true);
row.makeAcceptInputHandler([this, i] { returnResult(mScraperResults.at(i)); }); row.makeAcceptInputHandler([this, i] { returnResult(mScraperResults.at(i)); });
mResultList->addRow(row); mResultList->addRow(row);
} }
@ -263,14 +297,13 @@ void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>
mBlockAccept = false; mBlockAccept = false;
updateInfoPane(); updateInfoPane();
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) {
{ if (mScraperResults.size() == 0)
if(mScraperResults.size() == 0)
mSkipCallback(); mSkipCallback();
else else
returnResult(mScraperResults.front()); returnResult(mScraperResults.front());
}else if(mSearchType == ALWAYS_ACCEPT_MATCHING_CRC) }
{ else if (mSearchType == ALWAYS_ACCEPT_MATCHING_CRC) {
// TODO // TODO
} }
} }
@ -286,7 +319,7 @@ void ScraperSearchComponent::onSearchError(const std::string& error)
int ScraperSearchComponent::getSelectedIndex() int ScraperSearchComponent::getSelectedIndex()
{ {
if(!mScraperResults.size() || mGrid.getSelectedComponent() != mResultList) if (!mScraperResults.size() || mGrid.getSelectedComponent() != mResultList)
return -1; return -1;
return mResultList->getCursorId(); return mResultList->getCursorId();
@ -295,13 +328,10 @@ int ScraperSearchComponent::getSelectedIndex()
void ScraperSearchComponent::updateInfoPane() void ScraperSearchComponent::updateInfoPane()
{ {
int i = getSelectedIndex(); int i = getSelectedIndex();
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && mScraperResults.size()) if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && mScraperResults.size())
{
i = 0; i = 0;
}
if(i != -1 && (int)mScraperResults.size() > i) if (i != -1 && (int)mScraperResults.size() > i) {
{
ScraperSearchResult& res = mScraperResults.at(i); ScraperSearchResult& res = mScraperResults.at(i);
mResultName->setText(Utils::String::toUpper(res.mdl.get("name"))); mResultName->setText(Utils::String::toUpper(res.mdl.get("name")));
mResultDesc->setText(Utils::String::toUpper(res.mdl.get("desc"))); mResultDesc->setText(Utils::String::toUpper(res.mdl.get("desc")));
@ -309,14 +339,12 @@ void ScraperSearchComponent::updateInfoPane()
mResultThumbnail->setImage(""); mResultThumbnail->setImage("");
const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl; const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl;
if(!thumb.empty()) if (!thumb.empty())
{
mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb)); mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb));
}else{ else
mThumbnailReq.reset(); mThumbnailReq.reset();
}
// metadata // Metadata.
mMD_Rating->setValue(Utils::String::toUpper(res.mdl.get("rating"))); mMD_Rating->setValue(Utils::String::toUpper(res.mdl.get("rating")));
mMD_ReleaseDate->setValue(Utils::String::toUpper(res.mdl.get("releasedate"))); mMD_ReleaseDate->setValue(Utils::String::toUpper(res.mdl.get("releasedate")));
mMD_Developer->setText(Utils::String::toUpper(res.mdl.get("developer"))); mMD_Developer->setText(Utils::String::toUpper(res.mdl.get("developer")));
@ -324,12 +352,13 @@ void ScraperSearchComponent::updateInfoPane()
mMD_Genre->setText(Utils::String::toUpper(res.mdl.get("genre"))); mMD_Genre->setText(Utils::String::toUpper(res.mdl.get("genre")));
mMD_Players->setText(Utils::String::toUpper(res.mdl.get("players"))); mMD_Players->setText(Utils::String::toUpper(res.mdl.get("players")));
mGrid.onSizeChanged(); mGrid.onSizeChanged();
}else{ }
else {
mResultName->setText(""); mResultName->setText("");
mResultDesc->setText(""); mResultDesc->setText("");
mResultThumbnail->setImage(""); mResultThumbnail->setImage("");
// metadata // Metadata.
mMD_Rating->setValue(""); mMD_Rating->setValue("");
mMD_ReleaseDate->setValue(""); mMD_ReleaseDate->setValue("");
mMD_Developer->setText(""); mMD_Developer->setText("");
@ -341,9 +370,8 @@ void ScraperSearchComponent::updateInfoPane()
bool ScraperSearchComponent::input(InputConfig* config, Input input) bool ScraperSearchComponent::input(InputConfig* config, Input input)
{ {
if(config->isMappedTo("a", input) && input.value != 0) if (config->isMappedTo("a", input) && input.value != 0) {
{ if (mBlockAccept)
if(mBlockAccept)
return true; return true;
} }
@ -356,8 +384,7 @@ void ScraperSearchComponent::render(const Transform4x4f& parentTrans)
renderChildren(trans); renderChildren(trans);
if(mBlockAccept) if (mBlockAccept) {
{
Renderer::setMatrix(trans); Renderer::setMatrix(trans);
Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000011, 0x00000011); Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000011, 0x00000011);
@ -369,9 +396,8 @@ void ScraperSearchComponent::returnResult(ScraperSearchResult result)
{ {
mBlockAccept = true; mBlockAccept = true;
// resolve metadata image before returning // Resolve metadata image before returning.
if(!result.imageUrl.empty()) if (!result.imageUrl.empty()) {
{
mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch);
return; return;
} }
@ -383,46 +409,36 @@ void ScraperSearchComponent::update(int deltaTime)
{ {
GuiComponent::update(deltaTime); GuiComponent::update(deltaTime);
if(mBlockAccept) if (mBlockAccept)
{
mBusyAnim.update(deltaTime); mBusyAnim.update(deltaTime);
}
if(mThumbnailReq && mThumbnailReq->status() != HttpReq::REQ_IN_PROGRESS) if (mThumbnailReq && mThumbnailReq->status() != HttpReq::REQ_IN_PROGRESS)
{
updateThumbnail(); updateThumbnail();
}
if(mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) if (mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) {
{
auto status = mSearchHandle->status(); auto status = mSearchHandle->status();
auto results = mSearchHandle->getResults(); auto results = mSearchHandle->getResults();
auto statusString = mSearchHandle->getStatusString(); auto statusString = mSearchHandle->getStatusString();
// we reset here because onSearchDone in auto mode can call mSkipCallback() which can call // We reset here because onSearchDone in auto mode can call mSkipCallback() which
// another search() which will set our mSearchHandle to something important // can call another search() which will set our mSearchHandle to something important.
mSearchHandle.reset(); mSearchHandle.reset();
if(status == ASYNC_DONE) if (status == ASYNC_DONE)
{
onSearchDone(results); onSearchDone(results);
}else if(status == ASYNC_ERROR) else if (status == ASYNC_ERROR)
{
onSearchError(statusString); onSearchError(statusString);
}
} }
if(mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) {
{ if (mMDResolveHandle->status() == ASYNC_DONE) {
if(mMDResolveHandle->status() == ASYNC_DONE)
{
ScraperSearchResult result = mMDResolveHandle->getResult(); ScraperSearchResult result = mMDResolveHandle->getResult();
mMDResolveHandle.reset(); mMDResolveHandle.reset();
// This might end in us being deleted, depending on mAcceptCallback -
// this might end in us being deleted, depending on mAcceptCallback - so make sure this is the last thing we do in update() // so make sure this is the last thing we do in update().
returnResult(result); returnResult(result);
}else if(mMDResolveHandle->status() == ASYNC_ERROR) }
{ else if (mMDResolveHandle->status() == ASYNC_ERROR) {
onSearchError(mMDResolveHandle->getStatusString()); onSearchError(mMDResolveHandle->getStatusString());
mMDResolveHandle.reset(); mMDResolveHandle.reset();
} }
@ -431,12 +447,12 @@ void ScraperSearchComponent::update(int deltaTime)
void ScraperSearchComponent::updateThumbnail() void ScraperSearchComponent::updateThumbnail()
{ {
if(mThumbnailReq && mThumbnailReq->status() == HttpReq::REQ_SUCCESS) if (mThumbnailReq && mThumbnailReq->status() == HttpReq::REQ_SUCCESS) {
{
std::string content = mThumbnailReq->getContent(); std::string content = mThumbnailReq->getContent();
mResultThumbnail->setImage(content.data(), content.length()); mResultThumbnail->setImage(content.data(), content.length());
mGrid.onSizeChanged(); // a hack to fix the thumbnail position since its size changed mGrid.onSizeChanged(); // A hack to fix the thumbnail position since its size changed.
}else{ }
else {
LOG(LogWarning) << "thumbnail req failed: " << mThumbnailReq->getErrorMsg(); LOG(LogWarning) << "thumbnail req failed: " << mThumbnailReq->getErrorMsg();
mResultThumbnail->setImage(""); mResultThumbnail->setImage("");
} }
@ -446,15 +462,14 @@ void ScraperSearchComponent::updateThumbnail()
void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
{ {
auto searchForFunc = [&](const std::string& name) auto searchForFunc = [&](const std::string& name) {
{
params.nameOverride = name; params.nameOverride = name;
search(params); search(params);
}; };
stop(); stop();
mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR", mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
// initial value is last search if there was one, otherwise the clean path name // Initial value is last search if there was one, otherwise the clean path name.
params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride, params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride,
searchForFunc, false, "SEARCH")); searchForFunc, false, "SEARCH"));
} }
@ -462,7 +477,7 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts() std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts()
{ {
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts(); std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
if(getSelectedIndex() != -1) if (getSelectedIndex() != -1)
prompts.push_back(HelpPrompt("a", "accept result")); prompts.push_back(HelpPrompt("a", "accept result"));
return prompts; return prompts;

View file

@ -1,3 +1,15 @@
//
// ScraperSearchComponent.h
//
// User interface component for the scraper where the user is able to see an overview
// of the game being scraped and an option to override the game search string.
// Used by both single-game scraping from the GuiMetaDataEd menu as well as
// to resolve scraping conflicts when run from GuiScraperStart.
//
// This component is called from GuiGameScraper for single-game scraping and
// from GuiScraperMulti for multi-game scraping.
//
#pragma once #pragma once
#ifndef ES_APP_COMPONENTS_SCRAPER_SEARCH_COMPONENT_H #ifndef ES_APP_COMPONENTS_SCRAPER_SEARCH_COMPONENT_H
#define ES_APP_COMPONENTS_SCRAPER_SEARCH_COMPONENT_H #define ES_APP_COMPONENTS_SCRAPER_SEARCH_COMPONENT_H
@ -17,8 +29,7 @@ class TextComponent;
class ScraperSearchComponent : public GuiComponent class ScraperSearchComponent : public GuiComponent
{ {
public: public:
enum SearchType enum SearchType {
{
ALWAYS_ACCEPT_FIRST_RESULT, ALWAYS_ACCEPT_FIRST_RESULT,
ALWAYS_ACCEPT_MATCHING_CRC, ALWAYS_ACCEPT_MATCHING_CRC,
NEVER_AUTO_ACCEPT NEVER_AUTO_ACCEPT
@ -31,10 +42,14 @@ public:
void stop(); void stop();
inline SearchType getSearchType() const { return mSearchType; } inline SearchType getSearchType() const { return mSearchType; }
// Metadata assets will be resolved before calling the accept callback (e.g. result.mdl's "image" is automatically downloaded and properly set). // Metadata assets will be resolved before calling the accept callback
inline void setAcceptCallback(const std::function<void(const ScraperSearchResult&)>& acceptCallback) { mAcceptCallback = acceptCallback; } // (e.g. result.mdl's "image" is automatically downloaded and properly set).
inline void setSkipCallback(const std::function<void()>& skipCallback) { mSkipCallback = skipCallback; }; inline void setAcceptCallback(const std::function<void(const ScraperSearchResult&)>&
inline void setCancelCallback(const std::function<void()>& cancelCallback) { mCancelCallback = cancelCallback; } acceptCallback) { mAcceptCallback = acceptCallback; }
inline void setSkipCallback(const std::function<void()>&
skipCallback) { mSkipCallback = skipCallback; };
inline void setCancelCallback(const std::function<void()>&
cancelCallback) { mCancelCallback = cancelCallback; }
bool input(InputConfig* config, Input input) override; bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override; void update(int deltaTime) override;
@ -56,7 +71,7 @@ private:
int getSelectedIndex(); int getSelectedIndex();
// resolve any metadata assets that need to be downloaded and return // Resolve any metadata assets that need to be downloaded and return.
void returnResult(ScraperSearchResult result); void returnResult(ScraperSearchResult result);
ComponentGrid mGrid; ComponentGrid mGrid;
@ -75,14 +90,15 @@ private:
std::shared_ptr<TextComponent> mMD_Genre; std::shared_ptr<TextComponent> mMD_Genre;
std::shared_ptr<TextComponent> mMD_Players; std::shared_ptr<TextComponent> mMD_Players;
// label-component pair // Label-component pair.
struct MetaDataPair struct MetaDataPair {
{
std::shared_ptr<TextComponent> first; std::shared_ptr<TextComponent> first;
std::shared_ptr<GuiComponent> second; std::shared_ptr<GuiComponent> second;
bool resize; bool resize;
MetaDataPair(const std::shared_ptr<TextComponent>& f, const std::shared_ptr<GuiComponent>& s, bool r = true) : first(f), second(s), resize(r) {}; MetaDataPair(const std::shared_ptr<TextComponent>& f,
const std::shared_ptr<GuiComponent>& s, bool r = true)
: first(f), second(s), resize(r) {};
}; };
std::vector<MetaDataPair> mMD_Pairs; std::vector<MetaDataPair> mMD_Pairs;

View file

@ -1,3 +1,11 @@
//
// GuiGameScraper.cpp
//
// Single game scraping user interface.
// This interface is triggered from GuiMetaDataEd.
// ScraperSearchComponent is called from here.
//
#include "guis/GuiGameScraper.h" #include "guis/GuiGameScraper.h"
#include "components/ButtonComponent.h" #include "components/ButtonComponent.h"
@ -7,48 +15,57 @@
#include "PowerSaver.h" #include "PowerSaver.h"
#include "SystemData.h" #include "SystemData.h"
GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc) : GuiComponent(window), GuiGameScraper::GuiGameScraper(
mGrid(window, Vector2i(1, 7)), Window* window,
mBox(window, ":/frame.png"), ScraperSearchParams params,
mSearchParams(params), std::function<void(const ScraperSearchResult&)> doneFunc)
mClose(false) : GuiComponent(window),
mGrid(window, Vector2i(1, 7)),
mBox(window, ":/frame.png"),
mSearchParams(params),
mClose(false)
{ {
PowerSaver::pause(); PowerSaver::pause();
addChild(&mBox); addChild(&mBox);
addChild(&mGrid); addChild(&mGrid);
// row 0 is a spacer // Row 0 is a spacer.
mGameName = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchParams.game->getPath())), mGameName = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); Utils::FileSystem::getFileName(mSearchParams.game->getPath())),
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
mGrid.setEntry(mGameName, Vector2i(0, 1), false, true); mGrid.setEntry(mGameName, Vector2i(0, 1), false, true);
// row 2 is a spacer // Row 2 is a spacer.
mSystemName = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL), mSystemName = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(
0x888888FF, ALIGN_CENTER); mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL),
0x888888FF, ALIGN_CENTER);
mGrid.setEntry(mSystemName, Vector2i(0, 3), false, true); mGrid.setEntry(mSystemName, Vector2i(0, 3), false, true);
// row 4 is a spacer // Row 4 is a spacer.
// ScraperSearchComponent // ScraperSearchComponent.
mSearch = std::make_shared<ScraperSearchComponent>(window, ScraperSearchComponent::NEVER_AUTO_ACCEPT); mSearch = std::make_shared<ScraperSearchComponent>(window,
ScraperSearchComponent::NEVER_AUTO_ACCEPT);
mGrid.setEntry(mSearch, Vector2i(0, 5), true); mGrid.setEntry(mSearch, Vector2i(0, 5), true);
// buttons // Buttons
std::vector< std::shared_ptr<ButtonComponent> > buttons; std::vector< std::shared_ptr<ButtonComponent> > buttons;
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] { buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
mSearch->openInputScreen(mSearchParams); mSearch->openInputScreen(mSearchParams);
mGrid.resetCursor(); mGrid.resetCursor();
})); }));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; })); buttons.push_back(std::make_shared<ButtonComponent>(
mWindow, "CANCEL", "cancel", [&] { delete this; }));
mButtonGrid = makeButtonGrid(mWindow, buttons); mButtonGrid = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false);
// we call this->close() instead of just delete this; in the accept callback: // We call this->close() instead of just 'delete this' in the accept callback.
// this is because of how GuiComponent::update works. if it was just delete this, this would happen when the metadata resolver is done: // This is because of how GuiComponent::update works. If it was just 'delete this',
// the following would happen when the metadata resolver is done:
// GuiGameScraper::update() // GuiGameScraper::update()
// GuiComponent::update() // GuiComponent::update()
// it = mChildren.cbegin(); // it = mChildren.cbegin();
@ -56,9 +73,9 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std::
// it++; // it++;
// mSearchComponent::update() // mSearchComponent::update()
// acceptCallback -> delete this // acceptCallback -> delete this
// it++; // error, mChildren has been deleted because it was part of this // it++; // Error, mChildren has been deleted because it was part of 'this'.
// so instead we do this: // So instead we do this:
// GuiGameScraper::update() // GuiGameScraper::update()
// GuiComponent::update() // GuiComponent::update()
// it = mChildren.cbegin(); // it = mChildren.cbegin();
@ -66,17 +83,19 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std::
// it++; // it++;
// mSearchComponent::update() // mSearchComponent::update()
// acceptCallback -> close() -> mClose = true // acceptCallback -> close() -> mClose = true
// it++; // ok // it++; // OK.
// if(mClose) // if(mClose)
// delete this; // delete this;
mSearch->setAcceptCallback([this, doneFunc](const ScraperSearchResult& result) { doneFunc(result); close(); }); mSearch->setAcceptCallback([this, doneFunc](const ScraperSearchResult& result) {
doneFunc(result); close(); });
mSearch->setCancelCallback([&] { delete this; }); mSearch->setCancelCallback([&] { delete this; });
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.747f); setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.747f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() -
mSize.y()) / 2);
mGrid.resetCursor(); mGrid.resetCursor();
mSearch->search(params); // start the search mSearch->search(params); // Start the search.
} }
void GuiGameScraper::onSizeChanged() void GuiGameScraper::onSizeChanged()
@ -84,18 +103,19 @@ void GuiGameScraper::onSizeChanged()
mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32));
mGrid.setRowHeightPerc(0, 0.04f, false); mGrid.setRowHeightPerc(0, 0.04f, false);
mGrid.setRowHeightPerc(1, mGameName->getFont()->getLetterHeight() / mSize.y(), false); // game name mGrid.setRowHeightPerc(1, mGameName->getFont()->getLetterHeight() /
mSize.y(), false); // Game name.
mGrid.setRowHeightPerc(2, 0.04f, false); mGrid.setRowHeightPerc(2, 0.04f, false);
mGrid.setRowHeightPerc(3, mSystemName->getFont()->getLetterHeight() / mSize.y(), false); // system name mGrid.setRowHeightPerc(3, mSystemName->getFont()->getLetterHeight() /
mSize.y(), false); // System name.
mGrid.setRowHeightPerc(4, 0.04f, false); mGrid.setRowHeightPerc(4, 0.04f, false);
mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y() / mSize.y(), false); // buttons mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y() / mSize.y(), false); // Buttons.
mGrid.setSize(mSize); mGrid.setSize(mSize);
} }
bool GuiGameScraper::input(InputConfig* config, Input input) bool GuiGameScraper::input(InputConfig* config, Input input)
{ {
if(config->isMappedTo("b", input) && input.value) if(config->isMappedTo("b", input) && input.value) {
{
PowerSaver::resume(); PowerSaver::resume();
delete this; delete this;
return true; return true;

View file

@ -1,3 +1,11 @@
//
// GuiGameScraper.h
//
// Single game scraping user interface.
// This interface is triggered from GuiMetaDataEd.
// ScraperSearchComponent is called from here.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_GAME_SCRAPER_H #ifndef ES_APP_GUIS_GUI_GAME_SCRAPER_H
#define ES_APP_GUIS_GUI_GAME_SCRAPER_H #define ES_APP_GUIS_GUI_GAME_SCRAPER_H
@ -9,7 +17,10 @@
class GuiGameScraper : public GuiComponent class GuiGameScraper : public GuiComponent
{ {
public: public:
GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc); GuiGameScraper(
Window* window,
ScraperSearchParams params,
std::function<void(const ScraperSearchResult&)> doneFunc);
void onSizeChanged() override; void onSizeChanged() override;

View file

@ -1,10 +1,23 @@
//
// GuiGamelistFilter.cpp
//
// User interface for the gamelist filters.
// Triggered from the GuiGamelistOptions menu.
// Actual filter logic is covered by FileFilterIndex.
//
#include "guis/GuiGamelistFilter.h" #include "guis/GuiGamelistFilter.h"
#include "components/OptionListComponent.h" #include "components/OptionListComponent.h"
#include "views/UIModeController.h" #include "views/UIModeController.h"
#include "SystemData.h" #include "SystemData.h"
GuiGamelistFilter::GuiGamelistFilter(Window* window, SystemData* system) : GuiComponent(window), mMenu(window, "FILTER GAMELIST BY"), mSystem(system) GuiGamelistFilter::GuiGamelistFilter(
Window* window,
SystemData* system)
: GuiComponent(window),
mMenu(window, "FILTER GAMELIST BY"),
mSystem(system)
{ {
initializeMenu(); initializeMenu();
} }
@ -13,15 +26,15 @@ void GuiGamelistFilter::initializeMenu()
{ {
addChild(&mMenu); addChild(&mMenu);
// get filters from system // Get filters from system.
mFilterIndex = mSystem->getIndex(); mFilterIndex = mSystem->getIndex();
ComponentListRow row; ComponentListRow row;
// show filtered menu // Show filtered menu.
row.elements.clear(); row.elements.clear();
row.addElement(std::make_shared<TextComponent>(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(std::make_shared<TextComponent>(mWindow, "RESET ALL FILTERS",
Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this));
mMenu.addRow(row); mMenu.addRow(row);
row.elements.clear(); row.elements.clear();
@ -30,13 +43,15 @@ void GuiGamelistFilter::initializeMenu()
mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this));
mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2,
Renderer::getScreenHeight() * 0.15f);
} }
void GuiGamelistFilter::resetAllFilters() void GuiGamelistFilter::resetAllFilters()
{ {
mFilterIndex->resetFilters(); mFilterIndex->resetFilters();
for (std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string> >>::const_iterator it = mFilterOptions.cbegin(); it != mFilterOptions.cend(); ++it ) { for (std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string>
>>::const_iterator it = mFilterOptions.cbegin(); it != mFilterOptions.cend(); ++it ) {
std::shared_ptr< OptionListComponent<std::string> > optionList = it->second; std::shared_ptr< OptionListComponent<std::string> > optionList = it->second;
optionList->selectNone(); optionList->selectNone();
} }
@ -57,23 +72,22 @@ void GuiGamelistFilter::addFiltersToMenu()
if (UIModeController::getInstance()->isUIModeKid()) if (UIModeController::getInstance()->isUIModeKid())
skip = 2; skip = 2;
for (std::vector<FilterDataDecl>::const_iterator it = decls.cbegin(); it != decls.cend()-skip; ++it ) { for (std::vector<FilterDataDecl>::const_iterator it = decls.cbegin();
it != decls.cend()-skip; ++it ) {
FilterIndexType type = (*it).type; // type of filter FilterIndexType type = (*it).type; // Type of filter.
std::map<std::string, int>* allKeys = (*it).allIndexKeys; // all possible filters for this type // All possible filters for this type.
std::string menuLabel = (*it).menuLabel; // text to show in menu std::map<std::string, int>* allKeys = (*it).allIndexKeys;
std::string menuLabel = (*it).menuLabel; // Text to show in menu.
std::shared_ptr< OptionListComponent<std::string> > optionList; std::shared_ptr< OptionListComponent<std::string> > optionList;
// Add filters (with first one selected).
// add filters (with first one selected)
ComponentListRow row; ComponentListRow row;
// add genres // Add genres.
optionList = std::make_shared< OptionListComponent<std::string> >(mWindow, menuLabel, true); optionList = std::make_shared< OptionListComponent<std::string> >(mWindow, menuLabel, true);
for(auto it: *allKeys) for (auto it: *allKeys)
{
optionList->add(it.first, it.first, mFilterIndex->isKeyBeingFilteredBy(it.first, type)); optionList->add(it.first, it.first, mFilterIndex->isKeyBeingFilteredBy(it.first, type));
}
if (allKeys->size() > 0) if (allKeys->size() > 0)
mMenu.addWithLabel(menuLabel, optionList); mMenu.addWithLabel(menuLabel, optionList);
@ -84,27 +98,24 @@ void GuiGamelistFilter::addFiltersToMenu()
void GuiGamelistFilter::applyFilters() void GuiGamelistFilter::applyFilters()
{ {
std::vector<FilterDataDecl> decls = mFilterIndex->getFilterDataDecls(); std::vector<FilterDataDecl> decls = mFilterIndex->getFilterDataDecls();
for (std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string> >>::const_iterator it = mFilterOptions.cbegin(); it != mFilterOptions.cend(); ++it ) { for (std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string>
>>::const_iterator it = mFilterOptions.cbegin(); it != mFilterOptions.cend(); ++it ) {
std::shared_ptr< OptionListComponent<std::string> > optionList = it->second; std::shared_ptr< OptionListComponent<std::string> > optionList = it->second;
std::vector<std::string> filters = optionList->getSelectedObjects(); std::vector<std::string> filters = optionList->getSelectedObjects();
mFilterIndex->setFilter(it->first, &filters); mFilterIndex->setFilter(it->first, &filters);
} }
delete this; delete this;
} }
bool GuiGamelistFilter::input(InputConfig* config, Input input) bool GuiGamelistFilter::input(InputConfig* config, Input input)
{ {
bool consumed = GuiComponent::input(config, input); bool consumed = GuiComponent::input(config, input);
if(consumed) if (consumed)
return true; return true;
if(config->isMappedTo("b", input) && input.value != 0) if (config->isMappedTo("b", input) && input.value != 0)
{
applyFilters(); applyFilters();
}
return false; return false;
} }

View file

@ -1,3 +1,11 @@
//
// GuiGamelistFilter.h
//
// User interface for the gamelist filters.
// Triggered from the GuiGamelistOptions menu.
// Actual filter logic is covered by FileFilterIndex.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_GAME_LIST_FILTER_H #ifndef ES_APP_GUIS_GUI_GAME_LIST_FILTER_H
#define ES_APP_GUIS_GUI_GAME_LIST_FILTER_H #define ES_APP_GUIS_GUI_GAME_LIST_FILTER_H
@ -14,6 +22,7 @@ class GuiGamelistFilter : public GuiComponent
{ {
public: public:
GuiGamelistFilter(Window* window, SystemData* system); GuiGamelistFilter(Window* window, SystemData* system);
~GuiGamelistFilter(); ~GuiGamelistFilter();
bool input(InputConfig* config, Input input) override; bool input(InputConfig* config, Input input) override;

View file

@ -4,6 +4,9 @@
// Gamelist options menu for the 'Jump to...' quick selector, // Gamelist options menu for the 'Jump to...' quick selector,
// game sorting, game filters, and metadata edit. // game sorting, game filters, and metadata edit.
// //
// The filter interface is covered by GuiGamelistFilter and the
// metadata edit interface is covered by GuiMetaDataEd.
//
#include "GuiGamelistOptions.h" #include "GuiGamelistOptions.h"

View file

@ -4,6 +4,9 @@
// Gamelist options menu for the 'Jump to...' quick selector, // Gamelist options menu for the 'Jump to...' quick selector,
// game sorting, game filters, and metadata edit. // game sorting, game filters, and metadata edit.
// //
// The filter interface is covered by GuiGamelistFilter and the
// metadata edit interface is covered by GuiMetaDataEd.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_GAME_LIST_OPTIONS_H #ifndef ES_APP_GUIS_GUI_GAME_LIST_OPTIONS_H

View file

@ -1,3 +1,12 @@
//
// GuiMetaDataEd.cpp
//
// Game metadata edit user interface.
// This interface is triggered from the GuiGamelistOptions menu.
// The scraping interface is handled by GuiGameScraper which calls
// ScraperSearchComponent.
//
#include "guis/GuiMetaDataEd.h" #include "guis/GuiMetaDataEd.h"
#include "components/ButtonComponent.h" #include "components/ButtonComponent.h"
@ -20,25 +29,35 @@
#include "SystemData.h" #include "SystemData.h"
#include "Window.h" #include "Window.h"
GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams scraperParams, GuiMetaDataEd::GuiMetaDataEd(
const std::string& /*header*/, std::function<void()> saveCallback, std::function<void()> deleteFunc) : GuiComponent(window), Window* window,
mScraperParams(scraperParams), MetaDataList* md,
const std::vector<MetaDataDecl>& mdd,
ScraperSearchParams scraperParams,
const std::string& /*header*/,
std::function<void()> saveCallback,
std::function<void()> deleteFunc)
: GuiComponent(window),
mScraperParams(scraperParams),
mBackground(window, ":/frame.png"), mBackground(window, ":/frame.png"),
mGrid(window, Vector2i(1, 3)), mGrid(window, Vector2i(1, 3)),
mMetaDataDecl(mdd), mMetaDataDecl(mdd),
mMetaData(md), mMetaData(md),
mSavedCallback(saveCallback), mDeleteFunc(deleteFunc) mSavedCallback(saveCallback),
mDeleteFunc(deleteFunc)
{ {
addChild(&mBackground); addChild(&mBackground);
addChild(&mGrid); addChild(&mGrid);
mHeaderGrid = std::make_shared<ComponentGrid>(mWindow, Vector2i(1, 5)); mHeaderGrid = std::make_shared<ComponentGrid>(mWindow, Vector2i(1, 5));
mTitle = std::make_shared<TextComponent>(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mTitle = std::make_shared<TextComponent>(mWindow, "EDIT METADATA",
mSubtitle = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); mSubtitle = std::make_shared<TextComponent>(mWindow,
Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->
getPath())), Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER);
mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true);
mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true);
@ -47,19 +66,19 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
mList = std::make_shared<ComponentList>(mWindow); mList = std::make_shared<ComponentList>(mWindow);
mGrid.setEntry(mList, Vector2i(0, 1), true, true); mGrid.setEntry(mList, Vector2i(0, 1), true, true);
// populate list // Populate list.
for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) {
{
std::shared_ptr<GuiComponent> ed; std::shared_ptr<GuiComponent> ed;
// don't add statistics // Don't add statistics.
if(iter->isStatistic) if (iter->isStatistic)
continue; continue;
// Don't show the launch string override entry if this option has been disabled in the settings // Don't show the launch string override entry if this option has been disabled.
if(!Settings::getInstance()->getBool("LaunchstringOverride") && iter->type == MD_LAUNCHSTRING) if (!Settings::getInstance()->getBool("LaunchstringOverride") &&
{ iter->type == MD_LAUNCHSTRING) {
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL,
FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
assert(ed); assert(ed);
ed->setValue(mMetaData->get(iter->key)); ed->setValue(mMetaData->get(iter->key));
mEditors.push_back(ed); mEditors.push_back(ed);
@ -67,22 +86,20 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
continue; continue;
} }
// create ed and add it (and any related components) to mMenu // Create ed and add it (and any related components) to mMenu.
// ed's value will be set below // ed's value will be set below.
ComponentListRow row; ComponentListRow row;
auto lbl = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF); auto lbl = std::make_shared<TextComponent>(mWindow,
row.addElement(lbl, true); // label Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF);
row.addElement(lbl, true); // Label.
switch(iter->type) switch (iter->type) {
{ case MD_BOOL: {
case MD_BOOL:
{
ed = std::make_shared<SwitchComponent>(window); ed = std::make_shared<SwitchComponent>(window);
row.addElement(ed, false, true); row.addElement(ed, false, true);
break; break;
} }
case MD_RATING: case MD_RATING: {
{
ed = std::make_shared<RatingComponent>(window); ed = std::make_shared<RatingComponent>(window);
const float height = lbl->getSize().y() * 0.71f; const float height = lbl->getSize().y() * 0.71f;
ed->setSize(0, height); ed->setSize(0, height);
@ -92,13 +109,12 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
row.addElement(spacer, false); row.addElement(spacer, false);
// pass input to the actual RatingComponent instead of the spacer // Pass input to the actual RatingComponent instead of the spacer.
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2); row.input_handler = std::bind(&GuiComponent::input,
ed.get(), std::placeholders::_1, std::placeholders::_2);
break; break;
} }
case MD_DATE: case MD_DATE: {
{
ed = std::make_shared<DateTimeEditComponent>(window); ed = std::make_shared<DateTimeEditComponent>(window);
row.addElement(ed, false); row.addElement(ed, false);
@ -106,20 +122,20 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
row.addElement(spacer, false); row.addElement(spacer, false);
// pass input to the actual DateTimeEditComponent instead of the spacer // Pass input to the actual DateTimeEditComponent instead of the spacer.
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2); row.input_handler = std::bind(&GuiComponent::input, ed.get(),
std::placeholders::_1, std::placeholders::_2);
break; break;
} }
case MD_TIME: case MD_TIME: {
{ ed = std::make_shared<DateTimeEditComponent>(window,
ed = std::make_shared<DateTimeEditComponent>(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); DateTimeEditComponent::DISP_RELATIVE_TO_NOW);
row.addElement(ed, false); row.addElement(ed, false);
break; break;
} }
case MD_LAUNCHSTRING: case MD_LAUNCHSTRING: {
{ ed = std::make_shared<TextComponent>(window, "",
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
row.addElement(ed, true); row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>(mWindow); auto spacer = std::make_shared<GuiComponent>(mWindow);
@ -133,21 +149,26 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
bool multiLine = false; bool multiLine = false;
const std::string title = iter->displayPrompt; const std::string title = iter->displayPrompt;
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) auto updateVal = [ed](const std::string& newVal) {
ed->setValue(newVal); }; // OK callback (apply new value to ed).
std::string staticTextString = "Default value from es_systems.cfg:"; std::string staticTextString = "Default value from es_systems.cfg:";
std::string defaultLaunchString = scraperParams.system->getSystemEnvData()->mLaunchCommand; std::string defaultLaunchString = scraperParams.system->
getSystemEnvData()->mLaunchCommand;
row.makeAcceptInputHandler([this, title, staticTextString, defaultLaunchString, ed, updateVal, multiLine] { row.makeAcceptInputHandler([this, title, staticTextString,
mWindow->pushGui(new GuiComplexTextEditPopup(mWindow, title, staticTextString, defaultLaunchString, ed->getValue(), updateVal, multiLine)); defaultLaunchString, ed, updateVal, multiLine] {
mWindow->pushGui(new GuiComplexTextEditPopup(mWindow, title,
staticTextString, defaultLaunchString, ed->getValue(),
updateVal, multiLine));
}); });
break; break;
} }
case MD_MULTILINE_STRING: case MD_MULTILINE_STRING:
default: default: {
{ // MD_STRING.
// MD_STRING ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL,
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
row.addElement(ed, true); row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>(mWindow); auto spacer = std::make_shared<GuiComponent>(mWindow);
@ -161,9 +182,12 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
bool multiLine = iter->type == MD_MULTILINE_STRING; bool multiLine = iter->type == MD_MULTILINE_STRING;
const std::string title = iter->displayPrompt; const std::string title = iter->displayPrompt;
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed)
// OK callback (apply new value to ed).
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); };
row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] {
mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(),
updateVal, multiLine));
}); });
break; break;
} }
@ -177,26 +201,33 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
std::vector< std::shared_ptr<ButtonComponent> > buttons; std::vector< std::shared_ptr<ButtonComponent> > buttons;
if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this))); buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SCRAPE", "scrape",
std::bind(&GuiMetaDataEd::fetch, this)));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SAVE", "save", [&] { save(); delete this; })); buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SAVE", "save",
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; })); [&] { save(); delete this; }));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel",
[&] { delete this; }));
if(mDeleteFunc) if (mDeleteFunc) {
{
auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; };
auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "DELETE", "delete", deleteBtnFunc)); new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE "
"YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); };
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "DELETE",
"delete", deleteBtnFunc));
} }
mButtons = makeButtonGrid(mWindow, buttons); mButtons = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); mGrid.setEntry(mButtons, Vector2i(0, 2), true, false);
// resize + center // Resize + center.
float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); float width = (float)Math::min(Renderer::getScreenHeight(),
(int)(Renderer::getScreenWidth() * 0.90f));
setSize(width, Renderer::getScreenHeight() * 0.82f); setSize(width, Renderer::getScreenHeight() * 0.82f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2,
(Renderer::getScreenHeight() - mSize.y()) / 2);
} }
void GuiMetaDataEd::onSizeChanged() void GuiMetaDataEd::onSizeChanged()
@ -209,7 +240,8 @@ void GuiMetaDataEd::onSizeChanged()
const float subtitleHeight = mSubtitle->getFont()->getLetterHeight(); const float subtitleHeight = mSubtitle->getFont()->getLetterHeight();
const float titleSubtitleSpacing = mSize.y() * 0.03f; const float titleSubtitleSpacing = mSize.y() * 0.03f;
mGrid.setRowHeightPerc(0, (titleHeight + titleSubtitleSpacing + subtitleHeight + TITLE_VERT_PADDING) / mSize.y()); mGrid.setRowHeightPerc(0, (titleHeight + titleSubtitleSpacing + subtitleHeight +
TITLE_VERT_PADDING) / mSize.y());
mGrid.setRowHeightPerc(2, mButtons->getSize().y() / mSize.y()); mGrid.setRowHeightPerc(2, mButtons->getSize().y() / mSize.y());
mHeaderGrid->setRowHeightPerc(1, titleHeight / mHeaderGrid->getSize().y()); mHeaderGrid->setRowHeightPerc(1, titleHeight / mHeaderGrid->getSize().y());
@ -219,24 +251,23 @@ void GuiMetaDataEd::onSizeChanged()
void GuiMetaDataEd::save() void GuiMetaDataEd::save()
{ {
// remove game from index // Remove game from index.
mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game);
for(unsigned int i = 0; i < mEditors.size(); i++) for (unsigned int i = 0; i < mEditors.size(); i++) {
{ if (mMetaDataDecl.at(i).isStatistic)
if(mMetaDataDecl.at(i).isStatistic)
continue; continue;
mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue());
} }
// enter game in index // Enter game in index.
mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); mScraperParams.system->getIndex()->addToIndex(mScraperParams.game);
if(mSavedCallback) if (mSavedCallback)
mSavedCallback(); mSavedCallback();
// update respective Collection Entries // Update respective Collection Entries.
CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game);
mScraperParams.system->onMetaDataSavePoint(); mScraperParams.system->onMetaDataSavePoint();
@ -244,15 +275,15 @@ void GuiMetaDataEd::save()
void GuiMetaDataEd::fetch() void GuiMetaDataEd::fetch()
{ {
GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1)); GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams,
std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1));
mWindow->pushGui(scr); mWindow->pushGui(scr);
} }
void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
{ {
for(unsigned int i = 0; i < mEditors.size(); i++) for (unsigned int i = 0; i < mEditors.size(); i++) {
{ if (mMetaDataDecl.at(i).isStatistic)
if(mMetaDataDecl.at(i).isStatistic)
continue; continue;
const std::string& key = mMetaDataDecl.at(i).key; const std::string& key = mMetaDataDecl.at(i).key;
@ -262,52 +293,50 @@ void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
void GuiMetaDataEd::close(bool closeAllWindows) void GuiMetaDataEd::close(bool closeAllWindows)
{ {
// find out if the user made any changes // Find out if the user made any changes.
bool dirty = false; bool dirty = false;
for(unsigned int i = 0; i < mEditors.size(); i++) for (unsigned int i = 0; i < mEditors.size(); i++) {
{
const std::string& key = mMetaDataDecl.at(i).key; const std::string& key = mMetaDataDecl.at(i).key;
if(mMetaData->get(key) != mEditors.at(i)->getValue()) if (mMetaData->get(key) != mEditors.at(i)->getValue()) {
{
dirty = true; dirty = true;
break; break;
} }
} }
std::function<void()> closeFunc; std::function<void()> closeFunc;
if(!closeAllWindows) if (!closeAllWindows) {
{
closeFunc = [this] { delete this; }; closeFunc = [this] { delete this; };
}else{ }
else {
Window* window = mWindow; Window* window = mWindow;
closeFunc = [window, this] { closeFunc = [window, this] {
while(window->peekGui() != ViewController::get()) while (window->peekGui() != ViewController::get())
delete window->peekGui(); delete window->peekGui();
}; };
} }
if(dirty) if (dirty)
{ {
// changes were made, ask if the user wants to save them // Changes were made, ask if the user wants to save them.
mWindow->pushGui(new GuiMsgBox(mWindow, mWindow->pushGui(new GuiMsgBox(mWindow,
"SAVE CHANGES?", "SAVE CHANGES?",
"YES", [this, closeFunc] { save(); closeFunc(); }, "YES", [this, closeFunc] { save(); closeFunc(); },
"NO", closeFunc "NO", closeFunc
)); ));
}else{ }
else {
closeFunc(); closeFunc();
} }
} }
bool GuiMetaDataEd::input(InputConfig* config, Input input) bool GuiMetaDataEd::input(InputConfig* config, Input input)
{ {
if(GuiComponent::input(config, input)) if (GuiComponent::input(config, input))
return true; return true;
const bool isStart = config->isMappedTo("start", input); const bool isStart = config->isMappedTo("start", input);
if(input.value != 0 && (config->isMappedTo("b", input) || isStart)) if (input.value != 0 && (config->isMappedTo("b", input) || isStart)) {
{
close(isStart); close(isStart);
return true; return true;
} }

View file

@ -1,3 +1,12 @@
//
// GuiMetaDataEd.h
//
// Game metadata edit user interface.
// This interface is triggered from the GuiGamelistOptions menu.
// The scraping interface is handled by GuiGameScraper which calls
// ScraperSearchComponent.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_META_DATA_ED_H #ifndef ES_APP_GUIS_GUI_META_DATA_ED_H
#define ES_APP_GUIS_GUI_META_DATA_ED_H #define ES_APP_GUIS_GUI_META_DATA_ED_H
@ -14,8 +23,13 @@ class TextComponent;
class GuiMetaDataEd : public GuiComponent class GuiMetaDataEd : public GuiComponent
{ {
public: public:
GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams params, GuiMetaDataEd(
const std::string& header, std::function<void()> savedCallback, std::function<void()> deleteFunc); Window* window,
MetaDataList* md, const std::vector<MetaDataDecl>&mdd,
ScraperSearchParams params,
const std::string& header,
std::function<void()> savedCallback,
std::function<void()> deleteFunc);
bool input(InputConfig* config, Input input) override; bool input(InputConfig* config, Input input) override;
void onSizeChanged() override; void onSizeChanged() override;

View file

@ -1,3 +1,12 @@
//
// GuiScraperMulti.cpp
//
// Multiple game scraping user interface.
// Shows the progress for the scraping as it's running.
// This interface is triggered from the GuiScraperStart menu.
// ScraperSearchComponent is called from here.
//
#include "guis/GuiScraperMulti.h" #include "guis/GuiScraperMulti.h"
#include "components/ButtonComponent.h" #include "components/ButtonComponent.h"
@ -11,9 +20,14 @@
#include "SystemData.h" #include "SystemData.h"
#include "Window.h" #include "Window.h"
GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults) : GuiScraperMulti::GuiScraperMulti(
GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)), Window* window,
mSearchQueue(searches) const std::queue<ScraperSearchParams>& searches,
bool approveResults)
: GuiComponent(window),
mBackground(window, ":/frame.png"),
mGrid(window, Vector2i(1, 5)),
mSearchQueue(searches)
{ {
assert(mSearchQueue.size()); assert(mSearchQueue.size());
@ -28,27 +42,32 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue<ScraperSearchP
mTotalSuccessful = 0; mTotalSuccessful = 0;
mTotalSkipped = 0; mTotalSkipped = 0;
// set up grid // Set up grid.
mTitle = std::make_shared<TextComponent>(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mTitle = std::make_shared<TextComponent>(mWindow, "SCRAPING IN PROGRESS",
Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true);
mSystem = std::make_shared<TextComponent>(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); mSystem = std::make_shared<TextComponent>(mWindow, "SYSTEM",
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true);
mSubtitle = std::make_shared<TextComponent>(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); mSubtitle = std::make_shared<TextComponent>(mWindow, "subtitle text",
Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER);
mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true); mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true);
mSearchComp = std::make_shared<ScraperSearchComponent>(mWindow, mSearchComp = std::make_shared<ScraperSearchComponent>(mWindow,
approveResults ? ScraperSearchComponent::ALWAYS_ACCEPT_MATCHING_CRC : ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT); approveResults ? ScraperSearchComponent::ALWAYS_ACCEPT_MATCHING_CRC
mSearchComp->setAcceptCallback(std::bind(&GuiScraperMulti::acceptResult, this, std::placeholders::_1)); : ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT);
mSearchComp->setAcceptCallback(std::bind(&GuiScraperMulti::acceptResult,
this, std::placeholders::_1));
mSearchComp->setSkipCallback(std::bind(&GuiScraperMulti::skip, this)); mSearchComp->setSkipCallback(std::bind(&GuiScraperMulti::skip, this));
mSearchComp->setCancelCallback(std::bind(&GuiScraperMulti::finish, this)); mSearchComp->setCancelCallback(std::bind(&GuiScraperMulti::finish, this));
mGrid.setEntry(mSearchComp, Vector2i(0, 3), mSearchComp->getSearchType() != ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT, true); mGrid.setEntry(mSearchComp, Vector2i(0, 3), mSearchComp->getSearchType() !=
ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT, true);
std::vector< std::shared_ptr<ButtonComponent> > buttons; std::vector< std::shared_ptr<ButtonComponent> > buttons;
if(approveResults) if (approveResults) {
{
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] { buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
mSearchComp->openInputScreen(mSearchQueue.front()); mSearchComp->openInputScreen(mSearchQueue.front());
mGrid.resetCursor(); mGrid.resetCursor();
@ -60,21 +79,24 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue<ScraperSearchP
})); }));
} }
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "STOP", "stop (progress saved)", std::bind(&GuiScraperMulti::finish, this))); buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "STOP",
"stop (progress saved)", std::bind(&GuiScraperMulti::finish, this)));
mButtonGrid = makeButtonGrid(mWindow, buttons); mButtonGrid = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtonGrid, Vector2i(0, 4), true, false); mGrid.setEntry(mButtonGrid, Vector2i(0, 4), true, false);
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.849f); setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.849f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() -
mSize.y()) / 2);
doNextSearch(); doNextSearch();
} }
GuiScraperMulti::~GuiScraperMulti() GuiScraperMulti::~GuiScraperMulti()
{ {
// view type probably changed (basic -> detailed) // View type probably changed (basic -> detailed).
for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) for (auto it = SystemData::sSystemVector.cbegin();
it !=SystemData::sSystemVector.cend(); it++)
ViewController::get()->reloadGameListView(*it, false); ViewController::get()->reloadGameListView(*it, false);
} }
@ -91,19 +113,20 @@ void GuiScraperMulti::onSizeChanged()
void GuiScraperMulti::doNextSearch() void GuiScraperMulti::doNextSearch()
{ {
if(mSearchQueue.empty()) if (mSearchQueue.empty()) {
{
finish(); finish();
return; return;
} }
// update title // Update title.
std::stringstream ss; std::stringstream ss;
mSystem->setText(Utils::String::toUpper(mSearchQueue.front().system->getFullName())); mSystem->setText(Utils::String::toUpper(mSearchQueue.front().system->getFullName()));
// update subtitle // Update subtitle.
ss.str(""); // clear ss.str(""); // Clear.
ss << "GAME " << (mCurrentGame + 1) << " OF " << mTotalGames << " - " << Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchQueue.front().game->getPath())); ss << "GAME " << (mCurrentGame + 1) << " OF " << mTotalGames << " - " <<
Utils::String::toUpper(Utils::FileSystem::getFileName(
mSearchQueue.front().game->getPath()));
mSubtitle->setText(ss.str()); mSubtitle->setText(ss.str());
mSearchComp->search(mSearchQueue.front()); mSearchComp->search(mSearchQueue.front());
@ -133,18 +156,20 @@ void GuiScraperMulti::skip()
void GuiScraperMulti::finish() void GuiScraperMulti::finish()
{ {
std::stringstream ss; std::stringstream ss;
if(mTotalSuccessful == 0) if (mTotalSuccessful == 0) {
{
ss << "NO GAMES WERE SCRAPED."; ss << "NO GAMES WERE SCRAPED.";
}else{ }
ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") << " SUCCESSFULLY SCRAPED!"; else {
ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") <<
" SUCCESSFULLY SCRAPED!";
if(mTotalSkipped > 0) if (mTotalSkipped > 0)
ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") << " SKIPPED."; ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") <<
" SKIPPED.";
} }
mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), "OK", [&] {
"OK", [&] { delete this; })); delete this; }));
mIsProcessing = false; mIsProcessing = false;
PowerSaver::resume(); PowerSaver::resume();

View file

@ -1,3 +1,12 @@
//
// GuiScraperMulti.h
//
// Multiple game scraping user interface.
// Shows the progress for the scraping as it's running.
// This interface is triggered from the GuiScraperStart menu.
// ScraperSearchComponent is called from here.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_SCRAPER_MULTI_H #ifndef ES_APP_GUIS_GUI_SCRAPER_MULTI_H
#define ES_APP_GUIS_GUI_SCRAPER_MULTI_H #define ES_APP_GUIS_GUI_SCRAPER_MULTI_H
@ -13,7 +22,11 @@ class TextComponent;
class GuiScraperMulti : public GuiComponent class GuiScraperMulti : public GuiComponent
{ {
public: public:
GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults); GuiScraperMulti(
Window* window,
const std::queue<ScraperSearchParams>& searches,
bool approveResults);
virtual ~GuiScraperMulti(); virtual ~GuiScraperMulti();
void onSizeChanged() override; void onSizeChanged() override;

View file

@ -1,3 +1,10 @@
//
// GuiScraperStart.cpp
//
// Submenu to the GuiMenu main menu.
// Configuration options for the scraper and start button to intiate the scraping.
//
#include "guis/GuiScraperStart.h" #include "guis/GuiScraperStart.h"
#include "components/OptionListComponent.h" #include "components/OptionListComponent.h"
@ -13,19 +20,22 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window),
{ {
addChild(&mMenu); addChild(&mMenu);
// add filters (with first one selected) // Add filters (with first one selected).
mFilters = std::make_shared< OptionListComponent<GameFilterFunc> >(mWindow, "SCRAPE THESE GAMES", false); mFilters = std::make_shared< OptionListComponent<GameFilterFunc>
>(mWindow, "SCRAPE THESE GAMES", false);
mFilters->add("All Games", mFilters->add("All Games",
[](SystemData*, FileData*) -> bool { return true; }, false); [](SystemData*, FileData*) -> bool { return true; }, false);
mFilters->add("Only missing image", mFilters->add("Only missing image",
[](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); [](SystemData*, FileData* g) -> bool {
return g->metadata.get("image").empty(); }, true);
mMenu.addWithLabel("Filter", mFilters); mMenu.addWithLabel("Filter", mFilters);
//add systems (all with a platformid specified selected) // Add systems (all systems with an existing platform ID are listed).
mSystems = std::make_shared< OptionListComponent<SystemData*> >(mWindow, "SCRAPE THESE SYSTEMS", true); mSystems = std::make_shared< OptionListComponent<SystemData*>
for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) >(mWindow, "SCRAPE THESE SYSTEMS", true);
{ for (auto it = SystemData::sSystemVector.cbegin();
if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) it != SystemData::sSystemVector.cend(); it++) {
if (!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty());
} }
mMenu.addWithLabel("Systems", mSystems); mMenu.addWithLabel("Systems", mSystems);
@ -37,18 +47,19 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window),
mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this)); mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this));
mMenu.addButton("BACK", "back", [&] { delete this; }); mMenu.addButton("BACK", "back", [&] { delete this; });
mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2,
Renderer::getScreenHeight() * 0.15f);
} }
void GuiScraperStart::pressedStart() void GuiScraperStart::pressedStart()
{ {
std::vector<SystemData*> sys = mSystems->getSelectedObjects(); std::vector<SystemData*> sys = mSystems->getSelectedObjects();
for(auto it = sys.cbegin(); it != sys.cend(); it++) for (auto it = sys.cbegin(); it != sys.cend(); it++) {
{ if ((*it)->getPlatformIds().empty()) {
if((*it)->getPlatformIds().empty())
{
mWindow->pushGui(new GuiMsgBox(mWindow, mWindow->pushGui(new GuiMsgBox(mWindow,
Utils::String::toUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"), Utils::String::toUpper("Warning: some of your selected systems do not "
"have a platform set. Results may be even more inaccurate than "
"usual!\nContinue anyway?"),
"YES", std::bind(&GuiScraperStart::start, this), "YES", std::bind(&GuiScraperStart::start, this),
"NO", nullptr)); "NO", nullptr));
return; return;
@ -60,29 +71,28 @@ void GuiScraperStart::pressedStart()
void GuiScraperStart::start() void GuiScraperStart::start()
{ {
std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(), mFilters->getSelected()); std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(),
mFilters->getSelected());
if(searches.empty()) if (searches.empty()) {
{
mWindow->pushGui(new GuiMsgBox(mWindow, mWindow->pushGui(new GuiMsgBox(mWindow,
"NO GAMES FIT THAT CRITERIA.")); "NO GAMES FIT THAT CRITERIA."));
}else{ }
else {
GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState());
mWindow->pushGui(gsm); mWindow->pushGui(gsm);
delete this; delete this;
} }
} }
std::queue<ScraperSearchParams> GuiScraperStart::getSearches(std::vector<SystemData*> systems, GameFilterFunc selector) std::queue<ScraperSearchParams> GuiScraperStart::getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector)
{ {
std::queue<ScraperSearchParams> queue; std::queue<ScraperSearchParams> queue;
for(auto sys = systems.cbegin(); sys != systems.cend(); sys++) for (auto sys = systems.cbegin(); sys != systems.cend(); sys++) {
{
std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME); std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME);
for(auto game = games.cbegin(); game != games.cend(); game++) for (auto game = games.cbegin(); game != games.cend(); game++) {
{ if (selector((*sys), (*game))) {
if(selector((*sys), (*game)))
{
ScraperSearchParams search; ScraperSearchParams search;
search.game = *game; search.game = *game;
search.system = *sys; search.system = *sys;
@ -91,31 +101,27 @@ std::queue<ScraperSearchParams> GuiScraperStart::getSearches(std::vector<SystemD
} }
} }
} }
return queue; return queue;
} }
bool GuiScraperStart::input(InputConfig* config, Input input) bool GuiScraperStart::input(InputConfig* config, Input input)
{ {
bool consumed = GuiComponent::input(config, input); bool consumed = GuiComponent::input(config, input);
if(consumed) if (consumed)
return true; return true;
if(input.value != 0 && config->isMappedTo("b", input)) if (input.value != 0 && config->isMappedTo("b", input)) {
{
delete this; delete this;
return true; return true;
} }
if(config->isMappedTo("start", input) && input.value != 0) if (config->isMappedTo("start", input) && input.value != 0) {
{ // Close everything.
// close everything
Window* window = mWindow; Window* window = mWindow;
while(window->peekGui() && window->peekGui() != ViewController::get()) while (window->peekGui() && window->peekGui() != ViewController::get())
delete window->peekGui(); delete window->peekGui();
} }
return false; return false;
} }

View file

@ -1,3 +1,10 @@
//
// GuiScraperStart.h
//
// Submenu to the GuiMenu main menu.
// Configuration options for the scraper and start button to intiate the scraping.
//
#pragma once #pragma once
#ifndef ES_APP_GUIS_GUI_SCRAPER_START_H #ifndef ES_APP_GUIS_GUI_SCRAPER_START_H
#define ES_APP_GUIS_GUI_SCRAPER_START_H #define ES_APP_GUIS_GUI_SCRAPER_START_H
@ -13,9 +20,10 @@ class SystemData;
typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc; typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc;
//The starting point for a multi-game scrape. // The starting point for a multi-game scrape.
//Allows the user to set various parameters (to set filters, to set which systems to scrape, to enable manual mode). // Allows the user to set various parameters (filters, which systems to scrape and
//Generates a list of "searches" that will be carried out by GuiScraperLog. // whether to use manual mode). Generates a list of "searches" that will be carried
// out by GuiScraperLog.
class GuiScraperStart : public GuiComponent class GuiScraperStart : public GuiComponent
{ {
public: public:
@ -28,7 +36,8 @@ public:
private: private:
void pressedStart(); void pressedStart();
void start(); void start();
std::queue<ScraperSearchParams> getSearches(std::vector<SystemData*> systems, GameFilterFunc selector); std::queue<ScraperSearchParams> getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector);
std::shared_ptr< OptionListComponent<GameFilterFunc> > mFilters; std::shared_ptr< OptionListComponent<GameFilterFunc> > mFilters;
std::shared_ptr< OptionListComponent<SystemData*> > mSystems; std::shared_ptr< OptionListComponent<SystemData*> > mSystems;

View file

@ -7,6 +7,8 @@
// Improved and extended by the RetroPie community. // Improved and extended by the RetroPie community.
// Desktop Edition fork by Leon Styhre. // Desktop Edition fork by Leon Styhre.
// //
// The line length limit is 100 characters.
//
// main.cpp // main.cpp
// //
// Main program loop. Interprets command-line arguments, checks for the // Main program loop. Interprets command-line arguments, checks for the

View file

@ -1,3 +1,10 @@
//
// GamesDBJSONScraper.cpp
//
// Functions specifically for scraping from thegamesdb.net
// Called from Scraper.
//
#include <exception> #include <exception>
#include <map> #include <map>
@ -12,15 +19,14 @@
#include "utils/TimeUtil.h" #include "utils/TimeUtil.h"
#include <pugixml/src/pugixml.hpp> #include <pugixml/src/pugixml.hpp>
/* When raspbian will get an up to date version of rapidjson we'll be // When raspbian will get an up to date version of rapidjson we'll be
able to have it throw in case of error with the following: // able to have it throw in case of error with the following:
#ifndef RAPIDJSON_ASSERT //ifndef RAPIDJSON_ASSERT
#define RAPIDJSON_ASSERT(x) \ //#define RAPIDJSON_ASSERT(x) \
if (!(x)) { \ // if (!(x)) { \
throw std::runtime_error("rapidjson internal assertion failure: " #x); \ // throw std::runtime_error("rapidjson internal assertion failure: " #x); \
} // }
#endif // RAPIDJSON_ASSERT //#endif // RAPIDJSON_ASSERT
*/
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <rapidjson/error/en.h> #include <rapidjson/error/en.h>
@ -28,12 +34,11 @@
using namespace PlatformIds; using namespace PlatformIds;
using namespace rapidjson; using namespace rapidjson;
namespace namespace {
{
TheGamesDBJSONRequestResources resources; TheGamesDBJSONRequestResources resources;
} }
const std::map<PlatformId, std::string> gamesdb_new_platformid_map{ const std::map<PlatformId, std::string> gamesdb_new_platformid_map {
{ THREEDO, "25" }, { THREEDO, "25" },
{ AMIGA, "4911" }, { AMIGA, "4911" },
{ AMSTRAD_CPC, "4914" }, { AMSTRAD_CPC, "4914" },
@ -88,8 +93,8 @@ const std::map<PlatformId, std::string> gamesdb_new_platformid_map{
{ PLAYSTATION_VITA, "39" }, { PLAYSTATION_VITA, "39" },
{ PLAYSTATION_PORTABLE, "13" }, { PLAYSTATION_PORTABLE, "13" },
{ SUPER_NINTENDO, "6" }, { SUPER_NINTENDO, "6" },
{ TURBOGRAFX_16, "34" }, // HuCards only { TURBOGRAFX_16, "34" }, // HuCards only.
{ TURBOGRAFX_CD, "4955" }, // CD-ROMs only { TURBOGRAFX_CD, "4955" }, // CD-ROMs only.
{ WONDERSWAN, "4925" }, { WONDERSWAN, "4925" },
{ WONDERSWAN_COLOR, "4926" }, { WONDERSWAN_COLOR, "4926" },
{ ZX_SPECTRUM, "4913" }, { ZX_SPECTRUM, "4913" },
@ -99,16 +104,17 @@ const std::map<PlatformId, std::string> gamesdb_new_platformid_map{
{ TANDY, "4941" }, { TANDY, "4941" },
}; };
void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, void thegamesdb_generate_json_scraper_requests(
std::queue<std::unique_ptr<ScraperRequest>>& requests, std::vector<ScraperSearchResult>& results) const ScraperSearchParams& params,
std::queue<std::unique_ptr<ScraperRequest>>& requests,
std::vector<ScraperSearchResult>& results)
{ {
resources.prepare(); resources.prepare();
std::string path = "https://api.thegamesdb.net/v1"; std::string path = "https://api.thegamesdb.net/v1";
bool usingGameID = false; bool usingGameID = false;
const std::string apiKey = std::string("apikey=") + resources.getApiKey(); const std::string apiKey = std::string("apikey=") + resources.getApiKey();
std::string cleanName = params.nameOverride; std::string cleanName = params.nameOverride;
if (!cleanName.empty() && cleanName.substr(0, 3) == "id:") if (!cleanName.empty() && cleanName.substr(0, 3) == "id:") {
{
std::string gameID = cleanName.substr(3); std::string gameID = cleanName.substr(3);
path += "/Games/ByGameID?" + apiKey + path += "/Games/ByGameID?" + apiKey +
"&fields=players,publishers,genres,overview,last_updated,rating," "&fields=players,publishers,genres,overview,last_updated,rating,"
@ -116,42 +122,37 @@ void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params
"include=boxart&id=" + "include=boxart&id=" +
HttpReq::urlEncode(gameID); HttpReq::urlEncode(gameID);
usingGameID = true; usingGameID = true;
} else }
{ else {
if (cleanName.empty()) if (cleanName.empty())
cleanName = params.game->getCleanName(); cleanName = params.game->getCleanName();
path += "/Games/ByGameName?" + apiKey + path += "/Games/ByGameName?" + apiKey +
"&fields=players,publishers,genres,overview,last_updated,rating," "&fields=players,publishers,genres,overview,last_updated,rating,"
"platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&" "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&"
"include=boxart&name=" + "include=boxart&name=" +
HttpReq::urlEncode(cleanName); HttpReq::urlEncode(cleanName);
} }
if (usingGameID) if (usingGameID) {
{ // Ff we have the ID already, we don't need the GetGameList request.
// if we have the ID already, we don't need the GetGameList request
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBJSONRequest(results, path))); requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBJSONRequest(results, path)));
} else }
{ else {
std::string platformQueryParam; std::string platformQueryParam;
auto& platforms = params.system->getPlatformIds(); auto& platforms = params.system->getPlatformIds();
if (!platforms.empty()) if (!platforms.empty()) {
{
bool first = true; bool first = true;
platformQueryParam += "&filter%5Bplatform%5D="; platformQueryParam += "&filter%5Bplatform%5D=";
for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) for (auto platformIt = platforms.cbegin();
{ platformIt != platforms.cend(); platformIt++) {
auto mapIt = gamesdb_new_platformid_map.find(*platformIt); auto mapIt = gamesdb_new_platformid_map.find(*platformIt);
if (mapIt != gamesdb_new_platformid_map.cend()) if (mapIt != gamesdb_new_platformid_map.cend()) {
{
if (!first) if (!first)
{
platformQueryParam += ","; platformQueryParam += ",";
}
platformQueryParam += HttpReq::urlEncode(mapIt->second); platformQueryParam += HttpReq::urlEncode(mapIt->second);
first = false; first = false;
} else }
{ else {
LOG(LogWarning) << "TheGamesDB scraper warning - no support for platform " LOG(LogWarning) << "TheGamesDB scraper warning - no support for platform "
<< getPlatformName(*platformIt); << getPlatformName(*platformIt);
} }
@ -159,7 +160,8 @@ void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params
path += platformQueryParam; path += platformQueryParam;
} }
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBJSONRequest(requests, results, path))); requests.push(std::unique_ptr<ScraperRequest>
(new TheGamesDBJSONRequest(requests, results, path)));
} }
} }
@ -168,26 +170,25 @@ namespace
std::string getStringOrThrow(const Value& v, const std::string& key) std::string getStringOrThrow(const Value& v, const std::string& key)
{ {
if (!v.HasMember(key.c_str()) || !v[key.c_str()].IsString()) if (!v.HasMember(key.c_str()) || !v[key.c_str()].IsString()) {
{ throw std::runtime_error(
throw std::runtime_error("rapidjson internal assertion failure: missing or non string key:" + key); "rapidjson internal assertion failure: missing or non string key:" + key);
} }
return v[key.c_str()].GetString(); return v[key.c_str()].GetString();
} }
int getIntOrThrow(const Value& v, const std::string& key) int getIntOrThrow(const Value& v, const std::string& key)
{ {
if (!v.HasMember(key.c_str()) || !v[key.c_str()].IsInt()) if (!v.HasMember(key.c_str()) || !v[key.c_str()].IsInt()) {
{ throw std::runtime_error(
throw std::runtime_error("rapidjson internal assertion failure: missing or non int key:" + key); "rapidjson internal assertion failure: missing or non int key:" + key);
} }
return v[key.c_str()].GetInt(); return v[key.c_str()].GetInt();
} }
int getIntOrThrow(const Value& v) int getIntOrThrow(const Value& v)
{ {
if (!v.IsInt()) if (!v.IsInt()) {
{
throw std::runtime_error("rapidjson internal assertion failure: not an int"); throw std::runtime_error("rapidjson internal assertion failure: not an int");
} }
return v.GetInt(); return v.GetInt();
@ -196,18 +197,14 @@ int getIntOrThrow(const Value& v)
std::string getBoxartImage(const Value& v) std::string getBoxartImage(const Value& v)
{ {
if (!v.IsArray() || v.Size() == 0) if (!v.IsArray() || v.Size() == 0)
{
return ""; return "";
}
for (int i = 0; i < (int)v.Size(); ++i) for (int i = 0; i < (int)v.Size(); ++i) {
{
auto& im = v[i]; auto& im = v[i];
std::string type = getStringOrThrow(im, "type"); std::string type = getStringOrThrow(im, "type");
std::string side = getStringOrThrow(im, "side"); std::string side = getStringOrThrow(im, "side");
if (type == "boxart" && side == "front") if (type == "boxart" && side == "front")
{
return getStringOrThrow(im, "filename"); return getStringOrThrow(im, "filename");
}
} }
return getStringOrThrow(v[0], "filename"); return getStringOrThrow(v[0], "filename");
} }
@ -215,22 +212,19 @@ std::string getBoxartImage(const Value& v)
std::string getDeveloperString(const Value& v) std::string getDeveloperString(const Value& v)
{ {
if (!v.IsArray()) if (!v.IsArray())
{
return ""; return "";
}
std::string out = ""; std::string out = "";
bool first = true; bool first = true;
for (int i = 0; i < (int)v.Size(); ++i) for (int i = 0; i < (int)v.Size(); ++i) {
{
auto mapIt = resources.gamesdb_new_developers_map.find(getIntOrThrow(v[i])); auto mapIt = resources.gamesdb_new_developers_map.find(getIntOrThrow(v[i]));
if (mapIt == resources.gamesdb_new_developers_map.cend()) if (mapIt == resources.gamesdb_new_developers_map.cend())
{
continue; continue;
}
if (!first) if (!first)
{
out += ", "; out += ", ";
}
out += mapIt->second; out += mapIt->second;
first = false; first = false;
} }
@ -240,22 +234,19 @@ std::string getDeveloperString(const Value& v)
std::string getPublisherString(const Value& v) std::string getPublisherString(const Value& v)
{ {
if (!v.IsArray()) if (!v.IsArray())
{
return ""; return "";
}
std::string out = ""; std::string out = "";
bool first = true; bool first = true;
for (int i = 0; i < (int)v.Size(); ++i) for (int i = 0; i < (int)v.Size(); ++i) {
{
auto mapIt = resources.gamesdb_new_publishers_map.find(getIntOrThrow(v[i])); auto mapIt = resources.gamesdb_new_publishers_map.find(getIntOrThrow(v[i]));
if (mapIt == resources.gamesdb_new_publishers_map.cend()) if (mapIt == resources.gamesdb_new_publishers_map.cend())
{
continue; continue;
}
if (!first) if (!first)
{
out += ", "; out += ", ";
}
out += mapIt->second; out += mapIt->second;
first = false; first = false;
} }
@ -265,22 +256,18 @@ std::string getPublisherString(const Value& v)
std::string getGenreString(const Value& v) std::string getGenreString(const Value& v)
{ {
if (!v.IsArray()) if (!v.IsArray())
{
return ""; return "";
}
std::string out = ""; std::string out = "";
bool first = true; bool first = true;
for (int i = 0; i < (int)v.Size(); ++i) for (int i = 0; i < (int)v.Size(); ++i) {
{
auto mapIt = resources.gamesdb_new_genres_map.find(getIntOrThrow(v[i])); auto mapIt = resources.gamesdb_new_genres_map.find(getIntOrThrow(v[i]));
if (mapIt == resources.gamesdb_new_genres_map.cend()) if (mapIt == resources.gamesdb_new_genres_map.cend())
{
continue; continue;
}
if (!first) if (!first)
{
out += ", "; out += ", ";
}
out += mapIt->second; out += mapIt->second;
first = false; first = false;
} }
@ -296,38 +283,27 @@ void processGame(const Value& game, const Value& boxart, std::vector<ScraperSear
result.mdl.set("name", getStringOrThrow(game, "game_title")); result.mdl.set("name", getStringOrThrow(game, "game_title"));
if (game.HasMember("overview") && game["overview"].IsString()) if (game.HasMember("overview") && game["overview"].IsString())
{
result.mdl.set("desc", game["overview"].GetString()); result.mdl.set("desc", game["overview"].GetString());
}
if (game.HasMember("release_date") && game["release_date"].IsString()) if (game.HasMember("release_date") && game["release_date"].IsString())
{ result.mdl.set("releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(
result.mdl.set( game["release_date"].GetString(), "%Y-%m-%d")));
"releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(game["release_date"].GetString(), "%Y-%m-%d")));
}
if (game.HasMember("developers") && game["developers"].IsArray()) if (game.HasMember("developers") && game["developers"].IsArray())
{
result.mdl.set("developer", getDeveloperString(game["developers"])); result.mdl.set("developer", getDeveloperString(game["developers"]));
}
if (game.HasMember("publishers") && game["publishers"].IsArray()) if (game.HasMember("publishers") && game["publishers"].IsArray())
{
result.mdl.set("publisher", getPublisherString(game["publishers"])); result.mdl.set("publisher", getPublisherString(game["publishers"]));
}
if (game.HasMember("genres") && game["genres"].IsArray()) if (game.HasMember("genres") && game["genres"].IsArray())
{
result.mdl.set("genre", getGenreString(game["genres"])); result.mdl.set("genre", getGenreString(game["genres"]));
}
if (game.HasMember("players") && game["players"].IsInt()) if (game.HasMember("players") && game["players"].IsInt())
{
result.mdl.set("players", std::to_string(game["players"].GetInt())); result.mdl.set("players", std::to_string(game["players"].GetInt()));
}
if (boxart.HasMember("data") && boxart["data"].IsObject()) {
if (boxart.HasMember("data") && boxart["data"].IsObject())
{
std::string id = std::to_string(getIntOrThrow(game, "id")); std::string id = std::to_string(getIntOrThrow(game, "id"));
if (boxart["data"].HasMember(id.c_str())) if (boxart["data"].HasMember(id.c_str())) {
{
std::string image = getBoxartImage(boxart["data"][id.c_str()]); std::string image = getBoxartImage(boxart["data"][id.c_str()]);
result.thumbnailUrl = baseImageUrlThumb + "/" + image; result.thumbnailUrl = baseImageUrlThumb + "/" + image;
result.imageUrl = baseImageUrlLarge + "/" + image; result.imageUrl = baseImageUrlLarge + "/" + image;
@ -338,32 +314,32 @@ void processGame(const Value& game, const Value& boxart, std::vector<ScraperSear
} }
} // namespace } // namespace
void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results)
{ {
assert(req->status() == HttpReq::REQ_SUCCESS); assert(req->status() == HttpReq::REQ_SUCCESS);
Document doc; Document doc;
doc.Parse(req->getContent().c_str()); doc.Parse(req->getContent().c_str());
if (doc.HasParseError()) if (doc.HasParseError()) {
{
std::string err = std::string err =
std::string("TheGamesDBJSONRequest - Error parsing JSON. \n\t") + GetParseError_En(doc.GetParseError()); std::string("TheGamesDBJSONRequest - Error parsing JSON. \n\t") +
GetParseError_En(doc.GetParseError());
setError(err); setError(err);
LOG(LogError) << err; LOG(LogError) << err;
return; return;
} }
if (!doc.HasMember("data") || !doc["data"].HasMember("games") || !doc["data"]["games"].IsArray()) if (!doc.HasMember("data") || !doc["data"].HasMember("games") ||
{ !doc["data"]["games"].IsArray()) {
std::string warn = "TheGamesDBJSONRequest - Response had no game data.\n"; std::string warn = "TheGamesDBJSONRequest - Response had no game data.\n";
LOG(LogWarning) << warn; LOG(LogWarning) << warn;
return; return;
} }
const Value& games = doc["data"]["games"]; const Value& games = doc["data"]["games"];
if (!doc.HasMember("include") || !doc["include"].HasMember("boxart")) if (!doc.HasMember("include") || !doc["include"].HasMember("boxart")) {
{
std::string warn = "TheGamesDBJSONRequest - Response had no include boxart data.\n"; std::string warn = "TheGamesDBJSONRequest - Response had no include boxart data.\n";
LOG(LogWarning) << warn; LOG(LogWarning) << warn;
return; return;
@ -371,8 +347,7 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req, std::ve
const Value& boxart = doc["include"]["boxart"]; const Value& boxart = doc["include"]["boxart"];
if (!boxart.HasMember("base_url") || !boxart.HasMember("data") || !boxart.IsObject()) if (!boxart.HasMember("base_url") || !boxart.HasMember("data") || !boxart.IsObject()) {
{
std::string warn = "TheGamesDBJSONRequest - Response include had no usable boxart data.\n"; std::string warn = "TheGamesDBJSONRequest - Response include had no usable boxart data.\n";
LOG(LogWarning) << warn; LOG(LogWarning) << warn;
return; return;
@ -380,16 +355,12 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req, std::ve
resources.ensureResources(); resources.ensureResources();
for (int i = 0; i < (int)games.Size(); ++i) {
for (int i = 0; i < (int)games.Size(); ++i)
{
auto& v = games[i]; auto& v = games[i];
try try {
{
processGame(v, boxart, results); processGame(v, boxart, results);
} }
catch (std::runtime_error& e) catch (std::runtime_error& e) {
{
LOG(LogError) << "Error while processing game: " << e.what(); LOG(LogError) << "Error while processing game: " << e.what();
} }
} }

View file

@ -1,34 +1,49 @@
//
// GamesDBJSONScraper.h
//
// Functions specifically for scraping from thegamesdb.net
// Called from Scraper.
//
#pragma once #pragma once
#ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_H #ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_H
#define ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_H #define ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_H
#include "scrapers/Scraper.h" #include "scrapers/Scraper.h"
namespace pugi namespace pugi {
{
class xml_document; class xml_document;
} }
void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, void thegamesdb_generate_json_scraper_requests(
std::queue<std::unique_ptr<ScraperRequest>>& requests, std::vector<ScraperSearchResult>& results); const ScraperSearchParams& params,
std::queue<std::unique_ptr<ScraperRequest>>& requests,
std::vector<ScraperSearchResult>& results);
class TheGamesDBJSONRequest : public ScraperHttpRequest class TheGamesDBJSONRequest : public ScraperHttpRequest
{ {
public: public:
// ctor for a GetGameList request // ctor for a GetGameList request.
TheGamesDBJSONRequest(std::queue<std::unique_ptr<ScraperRequest>>& requestsWrite, TheGamesDBJSONRequest(
std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) std::queue<std::unique_ptr<ScraperRequest>>& requestsWrite,
: ScraperHttpRequest(resultsWrite, url), mRequestQueue(&requestsWrite) std::vector<ScraperSearchResult>& resultsWrite,
const std::string& url)
: ScraperHttpRequest(resultsWrite, url),
mRequestQueue(&requestsWrite)
{ {
} }
// ctor for a GetGame request // ctor for a GetGame request
TheGamesDBJSONRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) TheGamesDBJSONRequest(
: ScraperHttpRequest(resultsWrite, url), mRequestQueue(nullptr) std::vector<ScraperSearchResult>& resultsWrite,
const std::string& url)
: ScraperHttpRequest(resultsWrite, url),
mRequestQueue(nullptr)
{ {
} }
protected: protected:
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override; void process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results) override;
bool isGameRequest() { return !mRequestQueue; } bool isGameRequest() { return !mRequestQueue; }
std::queue<std::unique_ptr<ScraperRequest>>* mRequestQueue; std::queue<std::unique_ptr<ScraperRequest>>* mRequestQueue;

View file

@ -1,3 +1,10 @@
//
// GamesDBJSONScraperResources.cpp
//
// Functions specifically for scraping from thegamesdb.net
// Called from GamesDBJSONScraper.
//
#include <chrono> #include <chrono>
#include <fstream> #include <fstream>
#include <memory> #include <memory>
@ -8,17 +15,14 @@
#include "scrapers/GamesDBJSONScraperResources.h" #include "scrapers/GamesDBJSONScraperResources.h"
#include "utils/FileSystemUtil.h" #include "utils/FileSystemUtil.h"
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <rapidjson/error/en.h> #include <rapidjson/error/en.h>
using namespace rapidjson; using namespace rapidjson;
namespace {
namespace constexpr char GamesDBAPIKey[] =
{ "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600";
constexpr char GamesDBAPIKey[] = "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600";
constexpr int MAX_WAIT_MS = 90000; constexpr int MAX_WAIT_MS = 90000;
constexpr int POLL_TIME_MS = 500; constexpr int POLL_TIME_MS = 500;
@ -46,73 +50,57 @@ void ensureScrapersResourcesDir()
} // namespace } // namespace
std::string getScrapersResouceDir() std::string getScrapersResouceDir()
{ {
return Utils::FileSystem::getGenericPath( return Utils::FileSystem::getGenericPath(
Utils::FileSystem::getHomePath() + "/.emulationstation/" + SCRAPER_RESOURCES_DIR); Utils::FileSystem::getHomePath() + "/.emulationstation/" + SCRAPER_RESOURCES_DIR);
} }
std::string TheGamesDBJSONRequestResources::getApiKey() const { return GamesDBAPIKey; } std::string TheGamesDBJSONRequestResources::getApiKey() const { return GamesDBAPIKey; }
void TheGamesDBJSONRequestResources::prepare() void TheGamesDBJSONRequestResources::prepare()
{ {
if (checkLoaded()) if (checkLoaded())
{
return; return;
}
if (loadResource(gamesdb_new_developers_map, "developers", genFilePath(DEVELOPERS_JSON_FILE)) && if (loadResource(gamesdb_new_developers_map, "developers",
!gamesdb_developers_resource_request) genFilePath(DEVELOPERS_JSON_FILE)) && !gamesdb_developers_resource_request)
{
gamesdb_developers_resource_request = fetchResource(DEVELOPERS_ENDPOINT); gamesdb_developers_resource_request = fetchResource(DEVELOPERS_ENDPOINT);
}
if (loadResource(gamesdb_new_publishers_map, "publishers", genFilePath(PUBLISHERS_JSON_FILE)) && if (loadResource(gamesdb_new_publishers_map, "publishers",
!gamesdb_publishers_resource_request) genFilePath(PUBLISHERS_JSON_FILE)) && !gamesdb_publishers_resource_request)
{
gamesdb_publishers_resource_request = fetchResource(PUBLISHERS_ENDPOINT); gamesdb_publishers_resource_request = fetchResource(PUBLISHERS_ENDPOINT);
}
if (loadResource(gamesdb_new_genres_map, "genres", genFilePath(GENRES_JSON_FILE)) && !gamesdb_genres_resource_request) if (loadResource(gamesdb_new_genres_map, "genres",
{ genFilePath(GENRES_JSON_FILE)) && !gamesdb_genres_resource_request)
gamesdb_genres_resource_request = fetchResource(GENRES_ENDPOINT); gamesdb_genres_resource_request = fetchResource(GENRES_ENDPOINT);
}
} }
void TheGamesDBJSONRequestResources::ensureResources() void TheGamesDBJSONRequestResources::ensureResources()
{ {
if (checkLoaded()) if (checkLoaded())
{
return; return;
}
for (int i = 0; i < MAX_WAIT_ITER; ++i) {
for (int i = 0; i < MAX_WAIT_ITER; ++i)
{
if (gamesdb_developers_resource_request && if (gamesdb_developers_resource_request &&
saveResource(gamesdb_developers_resource_request.get(), gamesdb_new_developers_map, "developers", saveResource(gamesdb_developers_resource_request.get(),
genFilePath(DEVELOPERS_JSON_FILE))) gamesdb_new_developers_map, "developers", genFilePath(DEVELOPERS_JSON_FILE)))
{
gamesdb_developers_resource_request.reset(nullptr); gamesdb_developers_resource_request.reset(nullptr);
}
if (gamesdb_publishers_resource_request &&
saveResource(gamesdb_publishers_resource_request.get(), gamesdb_new_publishers_map, "publishers",
genFilePath(PUBLISHERS_JSON_FILE)))
{
gamesdb_publishers_resource_request.reset(nullptr);
}
if (gamesdb_genres_resource_request && saveResource(gamesdb_genres_resource_request.get(), gamesdb_new_genres_map,
"genres", genFilePath(GENRES_JSON_FILE)))
{
gamesdb_genres_resource_request.reset(nullptr);
}
if (!gamesdb_developers_resource_request && !gamesdb_publishers_resource_request && !gamesdb_genres_resource_request) if (gamesdb_publishers_resource_request &&
{ saveResource(gamesdb_publishers_resource_request.get(),
gamesdb_new_publishers_map, "publishers", genFilePath(PUBLISHERS_JSON_FILE)))
gamesdb_publishers_resource_request.reset(nullptr);
if (gamesdb_genres_resource_request && saveResource(gamesdb_genres_resource_request.get(),
gamesdb_new_genres_map, "genres", genFilePath(GENRES_JSON_FILE)))
gamesdb_genres_resource_request.reset(nullptr);
if (!gamesdb_developers_resource_request && !gamesdb_publishers_resource_request &&
!gamesdb_genres_resource_request)
return; return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(POLL_TIME_MS)); std::this_thread::sleep_for(std::chrono::milliseconds(POLL_TIME_MS));
} }
LOG(LogError) << "Timed out while waiting for resources\n"; LOG(LogError) << "Timed out while waiting for resources\n";
@ -120,26 +108,28 @@ void TheGamesDBJSONRequestResources::ensureResources()
bool TheGamesDBJSONRequestResources::checkLoaded() bool TheGamesDBJSONRequestResources::checkLoaded()
{ {
return !gamesdb_new_genres_map.empty() && !gamesdb_new_developers_map.empty() && !gamesdb_new_publishers_map.empty(); return !gamesdb_new_genres_map.empty() && !gamesdb_new_developers_map.empty() &&
!gamesdb_new_publishers_map.empty();
} }
bool TheGamesDBJSONRequestResources::saveResource(HttpReq* req, std::unordered_map<int, std::string>& resource, bool TheGamesDBJSONRequestResources::saveResource(
const std::string& resource_name, const std::string& file_name) HttpReq* req,
std::unordered_map<int, std::string>& resource,
const std::string& resource_name,
const std::string& file_name)
{ {
if (req == nullptr) if (req == nullptr) {
{
LOG(LogError) << "Http request pointer was null\n"; LOG(LogError) << "Http request pointer was null\n";
return true; return true;
} }
if (req->status() == HttpReq::REQ_IN_PROGRESS) if (req->status() == HttpReq::REQ_IN_PROGRESS) {
{ return false; // Not ready: wait some more.
return false; // Not ready: wait some more
} }
if (req->status() != HttpReq::REQ_SUCCESS) if (req->status() != HttpReq::REQ_SUCCESS) {
{ LOG(LogError) << "Resource request for " << file_name <<
LOG(LogError) << "Resource request for " << file_name << " failed:\n\t" << req->getErrorMsg(); " failed:\n\t" << req->getErrorMsg();
return true; // Request failed, resetting request. return true; // Request failed, resetting request..
} }
ensureScrapersResourcesDir(); ensureScrapersResourcesDir();
@ -160,47 +150,42 @@ std::unique_ptr<HttpReq> TheGamesDBJSONRequestResources::fetchResource(const std
return std::unique_ptr<HttpReq>(new HttpReq(path)); return std::unique_ptr<HttpReq>(new HttpReq(path));
} }
int TheGamesDBJSONRequestResources::loadResource( int TheGamesDBJSONRequestResources::loadResource(
std::unordered_map<int, std::string>& resource, const std::string& resource_name, const std::string& file_name) std::unordered_map<int, std::string>& resource,
const std::string& resource_name,
const std::string& file_name)
{ {
std::ifstream fin(file_name); std::ifstream fin(file_name);
if (!fin.good()) if (!fin.good())
{
return 1; return 1;
}
std::stringstream buffer; std::stringstream buffer;
buffer << fin.rdbuf(); buffer << fin.rdbuf();
Document doc; Document doc;
doc.Parse(buffer.str().c_str()); doc.Parse(buffer.str().c_str());
if (doc.HasParseError()) if (doc.HasParseError()) {
{ std::string err = std::string("TheGamesDBJSONRequest - "
std::string err = std::string("TheGamesDBJSONRequest - Error parsing JSON for resource file ") + file_name + "Error parsing JSON for resource file ") + file_name +
":\n\t" + GetParseError_En(doc.GetParseError()); ":\n\t" + GetParseError_En(doc.GetParseError());
LOG(LogError) << err; LOG(LogError) << err;
return 1; return 1;
} }
if (!doc.HasMember("data") || !doc["data"].HasMember(resource_name.c_str()) || if (!doc.HasMember("data") || !doc["data"].HasMember(resource_name.c_str()) ||
!doc["data"][resource_name.c_str()].IsObject()) !doc["data"][resource_name.c_str()].IsObject()) {
{
std::string err = "TheGamesDBJSONRequest - Response had no resource data.\n"; std::string err = "TheGamesDBJSONRequest - Response had no resource data.\n";
LOG(LogError) << err; LOG(LogError) << err;
return 1; return 1;
} }
auto& data = doc["data"][resource_name.c_str()]; auto& data = doc["data"][resource_name.c_str()];
for (Value::ConstMemberIterator itr = data.MemberBegin(); itr != data.MemberEnd(); ++itr) for (Value::ConstMemberIterator itr = data.MemberBegin(); itr != data.MemberEnd(); ++itr) {
{
auto& entry = itr->value; auto& entry = itr->value;
if (!entry.IsObject() || !entry.HasMember("id") || !entry["id"].IsInt() || !entry.HasMember("name") || if (!entry.IsObject() || !entry.HasMember("id") || !entry["id"].IsInt() ||
!entry["name"].IsString()) !entry.HasMember("name") || !entry["name"].IsString())
{
continue; continue;
}
resource[entry["id"].GetInt()] = entry["name"].GetString(); resource[entry["id"].GetInt()] = entry["name"].GetString();
} }
return resource.empty(); return resource.empty();

View file

@ -1,3 +1,10 @@
//
// GamesDBJSONScraperResources.h
//
// Functions specifically for scraping from thegamesdb.net
// Called from GamesDBJSONScraper.
//
#pragma once #pragma once
#ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H #ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H
#define ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H #define ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H
@ -9,9 +16,7 @@
#include "HttpReq.h" #include "HttpReq.h"
struct TheGamesDBJSONRequestResources {
struct TheGamesDBJSONRequestResources
{
TheGamesDBJSONRequestResources() = default; TheGamesDBJSONRequestResources() = default;
void prepare(); void prepare();
@ -25,12 +30,17 @@ struct TheGamesDBJSONRequestResources
private: private:
bool checkLoaded(); bool checkLoaded();
bool saveResource(HttpReq* req, std::unordered_map<int, std::string>& resource, const std::string& resource_name, bool saveResource(
const std::string& file_name); HttpReq* req,
std::unique_ptr<HttpReq> fetchResource(const std::string& endpoint); std::unordered_map<int, std::string>& resource,
const std::string& resource_name,
const std::string& file_name);
std::unique_ptr<HttpReq> fetchResource(const std::string& endpoint);
int loadResource( int loadResource(
std::unordered_map<int, std::string>& resource, const std::string& resource_name, const std::string& file_name); std::unordered_map<int, std::string>& resource,
const std::string& resource_name,
const std::string& file_name);
std::unique_ptr<HttpReq> gamesdb_developers_resource_request; std::unique_ptr<HttpReq> gamesdb_developers_resource_request;
std::unique_ptr<HttpReq> gamesdb_publishers_resource_request; std::unique_ptr<HttpReq> gamesdb_publishers_resource_request;
@ -39,4 +49,4 @@ struct TheGamesDBJSONRequestResources
std::string getScrapersResouceDir(); std::string getScrapersResouceDir();
#endif // ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_H #endif // ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H

View file

@ -1,3 +1,11 @@
//
// Scraper.cpp
//
// Main scraper logic.
// Called from ScraperSearchComponent.
// Calls either GamesDBJSONScraper or ScreenScraper.
//
#include "scrapers/Scraper.h" #include "scrapers/Scraper.h"
#include "FileData.h" #include "FileData.h"
@ -19,15 +27,11 @@ std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParam
const std::string& name = Settings::getInstance()->getString("Scraper"); const std::string& name = Settings::getInstance()->getString("Scraper");
std::unique_ptr<ScraperSearchHandle> handle(new ScraperSearchHandle()); std::unique_ptr<ScraperSearchHandle> handle(new ScraperSearchHandle());
// Check if the Scraper in the settings still exists as a registered scraping source. // Check if the scraper in the settings still exists as a registered scraping source.
if (scraper_request_funcs.find(name) == scraper_request_funcs.end()) if (scraper_request_funcs.find(name) == scraper_request_funcs.end())
{
LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted."; LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted.";
}
else else
{
scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults); scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults);
}
return handle; return handle;
} }
@ -36,9 +40,7 @@ std::vector<std::string> getScraperList()
{ {
std::vector<std::string> list; std::vector<std::string> list;
for(auto it = scraper_request_funcs.cbegin(); it != scraper_request_funcs.cend(); it++) for(auto it = scraper_request_funcs.cbegin(); it != scraper_request_funcs.cend(); it++)
{
list.push_back(it->first); list.push_back(it->first);
}
return list; return list;
} }
@ -49,7 +51,7 @@ bool isValidConfiguredScraper()
return scraper_request_funcs.find(name) != scraper_request_funcs.end(); return scraper_request_funcs.find(name) != scraper_request_funcs.end();
} }
// ScraperSearchHandle // ScraperSearchHandle.
ScraperSearchHandle::ScraperSearchHandle() ScraperSearchHandle::ScraperSearchHandle()
{ {
setStatus(ASYNC_IN_PROGRESS); setStatus(ASYNC_IN_PROGRESS);
@ -62,51 +64,45 @@ void ScraperSearchHandle::update()
if(!mRequestQueue.empty()) if(!mRequestQueue.empty())
{ {
// a request can add more requests to the queue while running, // A request can add more requests to the queue while running,
// so be careful with references into the queue // so be careful with references into the queue.
auto& req = *(mRequestQueue.front()); auto& req = *(mRequestQueue.front());
AsyncHandleStatus status = req.status(); AsyncHandleStatus status = req.status();
if(status == ASYNC_ERROR) if(status == ASYNC_ERROR) {
{ // Propagate error.
// propegate error
setError(req.getStatusString()); setError(req.getStatusString());
// empty our queue // Empty our queue.
while(!mRequestQueue.empty()) while(!mRequestQueue.empty())
mRequestQueue.pop(); mRequestQueue.pop();
return; return;
} }
// finished this one, see if we have any more // Finished this one, see if we have any more.
if(status == ASYNC_DONE) if(status == ASYNC_DONE)
{
mRequestQueue.pop(); mRequestQueue.pop();
}
// status == ASYNC_IN_PROGRESS // Status == ASYNC_IN_PROGRESS.
} }
// we finished without any errors! // We finished without any errors!
if(mRequestQueue.empty()) if(mRequestQueue.empty()) {
{
setStatus(ASYNC_DONE); setStatus(ASYNC_DONE);
return; return;
} }
} }
// ScraperRequest.
ScraperRequest::ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite)
// ScraperRequest : mResults(resultsWrite)
ScraperRequest::ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite) : mResults(resultsWrite)
{ {
} }
// ScraperHttpRequest.
// ScraperHttpRequest ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>&
ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) resultsWrite, const std::string& url) : ScraperRequest(resultsWrite)
: ScraperRequest(resultsWrite)
{ {
setStatus(ASYNC_IN_PROGRESS); setStatus(ASYNC_IN_PROGRESS);
mReq = std::unique_ptr<HttpReq>(new HttpReq(url)); mReq = std::unique_ptr<HttpReq>(new HttpReq(url));
@ -117,41 +113,43 @@ void ScraperHttpRequest::update()
HttpReq::Status status = mReq->status(); HttpReq::Status status = mReq->status();
if(status == HttpReq::REQ_SUCCESS) if(status == HttpReq::REQ_SUCCESS)
{ {
setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR // If process() has an error, status will be changed to ASYNC_ERROR.
setStatus(ASYNC_DONE);
process(mReq, mResults); process(mReq, mResults);
return; return;
} }
// not ready yet // Not ready yet.
if(status == HttpReq::REQ_IN_PROGRESS) if(status == HttpReq::REQ_IN_PROGRESS)
return; return;
// everything else is some sort of error // Everything else is some sort of error.
LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg(); LOG(LogError) << "ScraperHttpRequest network error (status: " << status<< ") - "
<< mReq->getErrorMsg();
setError(mReq->getErrorMsg()); setError(mReq->getErrorMsg());
} }
// Metadata resolving stuff.
// metadata resolving stuff std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result,
const ScraperSearchParams& search)
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search)
{ {
return std::unique_ptr<MDResolveHandle>(new MDResolveHandle(result, search)); return std::unique_ptr<MDResolveHandle>(new MDResolveHandle(result, search));
} }
MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result) MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result,
const ScraperSearchParams& search) : mResult(result)
{ {
if(!result.imageUrl.empty()) if(!result.imageUrl.empty()) {
{
std::string ext; std::string ext;
// If we have a file extension returned by the scraper, then use it. // If we have a file extension returned by the scraper, then use it.
// Otherwise, try to guess it by the name of the URL, which point to an image. // Otherwise, try to guess it by the name of the URL, which point to an image.
if (!result.imageType.empty()) if (!result.imageType.empty()) {
{
ext = result.imageType; ext = result.imageType;
}else{ }
else {
size_t dot = result.imageUrl.find_last_of('.'); size_t dot = result.imageUrl.find_last_of('.');
if (dot != std::string::npos) if (dot != std::string::npos)
@ -160,8 +158,8 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape
std::string imgPath = getSaveAsPath(search, "image", ext); std::string imgPath = getSaveAsPath(search, "image", ext);
mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath),
{ [this, imgPath] {
mResult.mdl.set("image", imgPath); mResult.mdl.set("image", imgPath);
mResult.imageUrl = ""; mResult.imageUrl = "";
})); }));
@ -174,14 +172,12 @@ void MDResolveHandle::update()
return; return;
auto it = mFuncs.cbegin(); auto it = mFuncs.cbegin();
while(it != mFuncs.cend()) while(it != mFuncs.cend()) {
{ if(it->first->status() == ASYNC_ERROR) {
if(it->first->status() == ASYNC_ERROR)
{
setError(it->first->getStatusString()); setError(it->first->getStatusString());
return; return;
}else if(it->first->status() == ASYNC_DONE) }
{ else if(it->first->status() == ASYNC_DONE) {
it->second(); it->second();
it = mFuncs.erase(it); it = mFuncs.erase(it);
continue; continue;
@ -193,14 +189,17 @@ void MDResolveHandle::update()
setStatus(ASYNC_DONE); setStatus(ASYNC_DONE);
} }
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs) std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url,
const std::string& saveAs)
{ {
return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs, return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs,
Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"))); Settings::getInstance()->getInt("ScraperResizeWidth"),
Settings::getInstance()->getInt("ScraperResizeHeight")));
} }
ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : ImageDownloadHandle::ImageDownloadHandle(const std::string& url,
mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) const std::string& path, int maxWidth, int maxHeight) : mSavePath(path),
mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url))
{ {
} }
@ -209,18 +208,16 @@ void ImageDownloadHandle::update()
if(mReq->status() == HttpReq::REQ_IN_PROGRESS) if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
return; return;
if(mReq->status() != HttpReq::REQ_SUCCESS) if(mReq->status() != HttpReq::REQ_SUCCESS) {
{
std::stringstream ss; std::stringstream ss;
ss << "Network error: " << mReq->getErrorMsg(); ss << "Network error: " << mReq->getErrorMsg();
setError(ss.str()); setError(ss.str());
return; return;
} }
// download is done, save it to disk // Download is done, save it to disk.
std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary); std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary);
if(stream.bad()) if(stream.bad()) {
{
setError("Failed to open image path to write. Permission error? Disk full?"); setError("Failed to open image path to write. Permission error? Disk full?");
return; return;
} }
@ -228,15 +225,13 @@ void ImageDownloadHandle::update()
const std::string& content = mReq->getContent(); const std::string& content = mReq->getContent();
stream.write(content.data(), content.length()); stream.write(content.data(), content.length());
stream.close(); stream.close();
if(stream.bad()) if(stream.bad()) {
{
setError("Failed to save image. Disk full?"); setError("Failed to save image. Disk full?");
return; return;
} }
// resize it // Resize it.
if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) {
{
setError("Error saving resized image. Out of memory? Disk full?"); setError("Error saving resized image. Out of memory? Disk full?");
return; return;
} }
@ -244,31 +239,30 @@ void ImageDownloadHandle::update()
setStatus(ASYNC_DONE); setStatus(ASYNC_DONE);
} }
//you can pass 0 for width or height to keep aspect ratio // You can pass 0 for width or height to keep aspect ratio.
bool resizeImage(const std::string& path, int maxWidth, int maxHeight) bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
{ {
// nothing to do // Nothing to do.
if(maxWidth == 0 && maxHeight == 0) if(maxWidth == 0 && maxHeight == 0)
return true; return true;
FREE_IMAGE_FORMAT format = FIF_UNKNOWN; FREE_IMAGE_FORMAT format = FIF_UNKNOWN;
FIBITMAP* image = NULL; FIBITMAP* image = NULL;
//detect the filetype // Detect the filetype.
format = FreeImage_GetFileType(path.c_str(), 0); format = FreeImage_GetFileType(path.c_str(), 0);
if(format == FIF_UNKNOWN) if(format == FIF_UNKNOWN)
format = FreeImage_GetFIFFromFilename(path.c_str()); format = FreeImage_GetFIFFromFilename(path.c_str());
if(format == FIF_UNKNOWN) if(format == FIF_UNKNOWN) {
{
LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!"; LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!";
return false; return false;
} }
//make sure we can read this filetype first, then load it // Make sure we can read this filetype first, then load it.
if(FreeImage_FIFSupportsReading(format)) if(FreeImage_FIFSupportsReading(format)) {
{
image = FreeImage_Load(format, path.c_str()); image = FreeImage_Load(format, path.c_str());
}else{ }
else {
LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!"; LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!";
return false; return false;
} }
@ -277,18 +271,14 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
float height = (float)FreeImage_GetHeight(image); float height = (float)FreeImage_GetHeight(image);
if(maxWidth == 0) if(maxWidth == 0)
{
maxWidth = (int)((maxHeight / height) * width); maxWidth = (int)((maxHeight / height) * width);
}else if(maxHeight == 0) else if(maxHeight == 0)
{
maxHeight = (int)((maxWidth / width) * height); maxHeight = (int)((maxWidth / width) * height);
}
FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR); FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR);
FreeImage_Unload(image); FreeImage_Unload(image);
if(imageRescaled == NULL) if(imageRescaled == NULL) {
{
LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)"; LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)";
return false; return false;
} }
@ -302,7 +292,8 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
return saved; return saved;
} }
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& extension) std::string getSaveAsPath(const ScraperSearchParams& params,
const std::string& suffix, const std::string& extension)
{ {
const std::string subdirectory = params.system->getName(); const std::string subdirectory = params.system->getName();
const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix;
@ -317,7 +308,6 @@ std::string getSaveAsPath(const ScraperSearchParams& params, const std::string&
if(!Utils::FileSystem::exists(path)) if(!Utils::FileSystem::exists(path))
Utils::FileSystem::createDirectory(path); Utils::FileSystem::createDirectory(path);
path += name + extension; path += name + extension;
return path; return path;
} }

View file

@ -1,3 +1,11 @@
//
// Scraper.h
//
// Main scraper logic.
// Called from ScraperSearchComponent.
// Calls either GamesDBJSONScraper or ScreenScraper.
//
#pragma once #pragma once
#ifndef ES_APP_SCRAPERS_SCRAPER_H #ifndef ES_APP_SCRAPERS_SCRAPER_H
#define ES_APP_SCRAPERS_SCRAPER_H #define ES_APP_SCRAPERS_SCRAPER_H
@ -16,31 +24,29 @@
class FileData; class FileData;
class SystemData; class SystemData;
struct ScraperSearchParams struct ScraperSearchParams {
{
SystemData* system; SystemData* system;
FileData* game; FileData* game;
std::string nameOverride; std::string nameOverride;
}; };
struct ScraperSearchResult struct ScraperSearchResult {
{
ScraperSearchResult() : mdl(GAME_METADATA) {}; ScraperSearchResult() : mdl(GAME_METADATA) {};
MetaDataList mdl; MetaDataList mdl;
std::string imageUrl; std::string imageUrl;
std::string thumbnailUrl; std::string thumbnailUrl;
// Needed to pre-set the image type // Needed to pre-set the image type.
std::string imageType; std::string imageType;
}; };
// So let me explain why I've abstracted this so heavily. // So let me explain why I've abstracted this so heavily.
// There are two ways I can think of that you'd want to write a scraper. // There are two ways I can think of that you'd want to write a scraper.
// 1. Do some HTTP request(s) -> process it -> return the results // 1. Do some HTTP request(s) -> process it -> return the results.
// 2. Do some local filesystem queries (an offline scraper) -> return the results // 2. Do some local filesystem queries (an offline scraper) -> return the results.
// The first way needs to be asynchronous while it's waiting for the HTTP request to return. // The first way needs to be asynchronous while it's waiting for the HTTP request to return.
// The second doesn't. // The second doesn't.
@ -51,25 +57,26 @@ struct ScraperSearchResult
// ... process search ... // ... process search ...
// return results; // return results;
// We could do this if we used threads. Right now ES doesn't because I'm pretty sure I'll fuck it up, // We could do this if we used threads. Right now ES doesn't because I'm pretty sure I'll
// and I'm not sure of the performance of threads on the Pi (single-core ARM). // fuck it up, and I'm not sure of the performance of threads on the Pi (single-core ARM).
// We could also do this if we used coroutines. // We could also do this if we used coroutines.
// I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows), // I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows),
// and I don't want to spend more time chasing libraries than just writing it the long way once. // and I don't want to spend more time chasing libraries than just writing it the long way once.
// So, I did it the "long" way. // So, I did it the "long" way.
// ScraperSearchHandle - one logical search, e.g. "search for mario" // ScraperSearchHandle - one logical search, e.g. "search for mario".
// ScraperRequest - encapsulates some sort of asynchronous request that will ultimately return some results // ScraperRequest - encapsulates some sort of asynchronous request that will ultimately
// ScraperHttpRequest - implementation of ScraperRequest that waits on an HttpReq, then processes it with some processing function. // return some results.
// ScraperHttpRequest - implementation of ScraperRequest that waits on an HttpReq, then
// processes it with some processing function.
// A scraper search gathers results from (potentially multiple) ScraperRequests.
// a scraper search gathers results from (potentially multiple) ScraperRequests
class ScraperRequest : public AsyncHandle class ScraperRequest : public AsyncHandle
{ {
public: public:
ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite); ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite);
// returns "true" once we're done // Returns "true" once we're done.
virtual void update() = 0; virtual void update() = 0;
protected: protected:
@ -77,7 +84,7 @@ protected:
}; };
// a single HTTP request that needs to be processed to get the results // A single HTTP request that needs to be processed to get the results.
class ScraperHttpRequest : public ScraperRequest class ScraperHttpRequest : public ScraperRequest
{ {
public: public:
@ -85,43 +92,46 @@ public:
virtual void update() override; virtual void update() override;
protected: protected:
virtual void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) = 0; virtual void process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results) = 0;
private: private:
std::unique_ptr<HttpReq> mReq; std::unique_ptr<HttpReq> mReq;
}; };
// a request to get a list of results // A request to get a list of results.
class ScraperSearchHandle : public AsyncHandle class ScraperSearchHandle : public AsyncHandle
{ {
public: public:
ScraperSearchHandle(); ScraperSearchHandle();
void update(); void update();
inline const std::vector<ScraperSearchResult>& getResults() const { assert(mStatus != ASYNC_IN_PROGRESS); return mResults; } inline const std::vector<ScraperSearchResult>& getResults() const {
assert(mStatus != ASYNC_IN_PROGRESS); return mResults; }
protected: protected:
friend std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params); friend std::unique_ptr<ScraperSearchHandle>
startScraperSearch(const ScraperSearchParams& params);
std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue; std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue;
std::vector<ScraperSearchResult> mResults; std::vector<ScraperSearchResult> mResults;
}; };
// will use the current scraper settings to pick the result source // Will use the current scraper settings to pick the result source.
std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params); std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params);
// returns a list of valid scraper names // Returns a list of valid scraper names.
std::vector<std::string> getScraperList(); std::vector<std::string> getScraperList();
// returns true if the scraper configured in the settings is still valid // Returns true if the scraper configured in the settings is still valid.
bool isValidConfiguredScraper(); bool isValidConfiguredScraper();
typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests, std::vector<ScraperSearchResult>& results); typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params,
std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Meta data asset downloading stuff. // Meta data asset downloading stuff.
class MDResolveHandle : public AsyncHandle class MDResolveHandle : public AsyncHandle
{ {
@ -129,7 +139,8 @@ public:
MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search); MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search);
void update() override; void update() override;
inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; } inline const ScraperSearchResult& getResult() const
{ assert(mStatus == ASYNC_DONE); return mResult; }
private: private:
ScraperSearchResult mResult; ScraperSearchResult mResult;
@ -141,7 +152,8 @@ private:
class ImageDownloadHandle : public AsyncHandle class ImageDownloadHandle : public AsyncHandle
{ {
public: public:
ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight); ImageDownloadHandle(const std::string& url, const std::string& path,
int maxWidth, int maxHeight);
void update() override; void update() override;
@ -152,19 +164,24 @@ private:
int mMaxHeight; int mMaxHeight;
}; };
//About the same as "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]". // About the same as:
//Will create the "downloaded_images" and "subdirectory" directories if they do not exist. // "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]".
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url); // Will create the "downloaded_images" and "subdirectory" directories if they do not exist.
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix,
const std::string& url);
//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight"). // Will resize according to Settings::getInt("ScraperResizeWidth") and
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs); // Settings::getInt("ScraperResizeHeight").
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url,
const std::string& saveAs);
// Resolves all metadata assets that need to be downloaded. // Resolves all metadata assets that need to be downloaded.
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search); std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result,
const ScraperSearchParams& search);
//You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio. // You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio.
//Will overwrite the image at [path] with the new resized one. // It will overwrite the image at [path] with the new resized one.
//Returns true if successful, false otherwise. // Returns true if successful, false otherwise.
bool resizeImage(const std::string& path, int maxWidth, int maxHeight); bool resizeImage(const std::string& path, int maxWidth, int maxHeight);
#endif // ES_APP_SCRAPERS_SCRAPER_H #endif // ES_APP_SCRAPERS_SCRAPER_H

View file

@ -1,3 +1,10 @@
//
// ScreenScraper.cpp
//
// Functions specifically for scraping from screenscraper.fr
// Called from Scraper.
//
#include "scrapers/ScreenScraper.h" #include "scrapers/ScreenScraper.h"
#include "utils/TimeUtil.h" #include "utils/TimeUtil.h"
@ -12,17 +19,15 @@
using namespace PlatformIds; using namespace PlatformIds;
/** // List of systems and their IDs from:
List of systems and their IDs from // https://www.screenscraper.fr/api/systemesListe.php?devid=xxx&devpassword=yyy&softname=zzz&output=XML
https://www.screenscraper.fr/api/systemesListe.php?devid=xxx&devpassword=yyy&softname=zzz&output=XML const std::map<PlatformId, unsigned short> screenscraper_platformid_map {
**/
const std::map<PlatformId, unsigned short> screenscraper_platformid_map{
{ THREEDO, 29 }, { THREEDO, 29 },
{ AMIGA, 64 }, { AMIGA, 64 },
{ AMSTRAD_CPC, 65 }, { AMSTRAD_CPC, 65 },
{ APPLE_II, 86 }, { APPLE_II, 86 },
{ ARCADE, 75 }, { ARCADE, 75 },
{ ATARI_800, 26 }, // Use ATARI_2600 as an alias for atari 800 { ATARI_800, 26 }, // Use ATARI_2600 as an alias for atari 800.
{ ATARI_2600, 26 }, { ATARI_2600, 26 },
{ ATARI_5200, 40 }, { ATARI_5200, 40 },
{ ATARI_7800, 41 }, { ATARI_7800, 41 },
@ -30,7 +35,7 @@ const std::map<PlatformId, unsigned short> screenscraper_platformid_map{
{ ATARI_JAGUAR_CD, 171 }, { ATARI_JAGUAR_CD, 171 },
{ ATARI_LYNX, 28 }, { ATARI_LYNX, 28 },
{ ATARI_ST, 42}, { ATARI_ST, 42},
// missing Atari XE ? // Missing Atari XE ?
{ COLECOVISION, 48 }, { COLECOVISION, 48 },
{ COMMODORE_64, 66 }, { COMMODORE_64, 66 },
{ INTELLIVISION, 115 }, { INTELLIVISION, 115 },
@ -72,7 +77,7 @@ const std::map<PlatformId, unsigned short> screenscraper_platformid_map{
{ PLAYSTATION, 57 }, { PLAYSTATION, 57 },
{ PLAYSTATION_2, 58 }, { PLAYSTATION_2, 58 },
{ PLAYSTATION_3, 59 }, { PLAYSTATION_3, 59 },
// missing Sony Playstation 4 ? // Missing Sony Playstation 4 ?
{ PLAYSTATION_VITA, 62 }, { PLAYSTATION_VITA, 62 },
{ PLAYSTATION_PORTABLE, 61 }, { PLAYSTATION_PORTABLE, 61 },
{ SUPER_NINTENDO, 4 }, { SUPER_NINTENDO, 4 },
@ -88,12 +93,11 @@ const std::map<PlatformId, unsigned short> screenscraper_platformid_map{
{ TANDY, 144 } { TANDY, 144 }
}; };
// Helper XML parsing method, finding a node-by-name recursively. // Helper XML parsing method, finding a node-by-name recursively.
pugi::xml_node find_node_by_name_re(const pugi::xml_node& node, const std::vector<std::string> node_names) { pugi::xml_node find_node_by_name_re(const pugi::xml_node& node,
const std::vector<std::string> node_names) {
for (const std::string& _val : node_names) for (const std::string& _val : node_names) {
{
pugi::xpath_query query_node_name((static_cast<std::string>("//") + _val).c_str()); pugi::xpath_query query_node_name((static_cast<std::string>("//") + _val).c_str());
pugi::xpath_node_set results = node.select_nodes(query_node_name); pugi::xpath_node_set results = node.select_nodes(query_node_name);
@ -104,26 +108,25 @@ pugi::xml_node find_node_by_name_re(const pugi::xml_node& node, const std::vecto
return pugi::xml_node(); return pugi::xml_node();
} }
// Help XML parsing method, finding an direct child XML node starting from the parent and filtering by an attribute value list. // Help XML parsing method, finding an direct child XML node starting from the parent and
pugi::xml_node find_child_by_attribute_list(const pugi::xml_node& node_parent, const std::string& node_name, const std::string& attribute_name, const std::vector<std::string> attribute_values) // filtering by an attribute value list.
pugi::xml_node find_child_by_attribute_list(const pugi::xml_node& node_parent,
const std::string& node_name, const std::string& attribute_name,
const std::vector<std::string> attribute_values)
{ {
for (auto _val : attribute_values) for (auto _val : attribute_values) {
{ for (pugi::xml_node node : node_parent.children(node_name.c_str())) {
for (pugi::xml_node node : node_parent.children(node_name.c_str()))
{
if (strcmp(node.attribute(attribute_name.c_str()).value(), _val.c_str()) == 0) if (strcmp(node.attribute(attribute_name.c_str()).value(), _val.c_str()) == 0)
return node; return node;
} }
} }
return pugi::xml_node(NULL); return pugi::xml_node(NULL);
} }
void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, void screenscraper_generate_scraper_requests(const ScraperSearchParams& params,
std::queue< std::unique_ptr<ScraperRequest> >& requests, std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results) std::vector<ScraperSearchResult>& results)
{ {
std::string path; std::string path;
@ -133,22 +136,23 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params,
auto& platforms = params.system->getPlatformIds(); auto& platforms = params.system->getPlatformIds();
std::vector<unsigned short> p_ids; std::vector<unsigned short> p_ids;
// Get the IDs of each platform from the ScreenScraper list // Get the IDs of each platform from the ScreenScraper list.
for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) {
{
auto mapIt = screenscraper_platformid_map.find(*platformIt); auto mapIt = screenscraper_platformid_map.find(*platformIt);
if (mapIt != screenscraper_platformid_map.cend()) if (mapIt != screenscraper_platformid_map.cend()) {
{
p_ids.push_back(mapIt->second); p_ids.push_back(mapIt->second);
}else{ }
LOG(LogWarning) << "ScreenScraper: no support for platform " << getPlatformName(*platformIt); else {
// Add the scrape request without a platform/system ID LOG(LogWarning) << "ScreenScraper: no support for platform " <<
requests.push(std::unique_ptr<ScraperRequest>(new ScreenScraperRequest(requests, results, path))); getPlatformName(*platformIt);
// Add the scrape request without a platform/system ID.
requests.push(std::unique_ptr<ScraperRequest>
(new ScreenScraperRequest(requests, results, path)));
} }
} }
// Sort the platform IDs and remove duplicates // Sort the platform IDs and remove duplicates.
std::sort(p_ids.begin(), p_ids.end()); std::sort(p_ids.begin(), p_ids.end());
auto last = std::unique(p_ids.begin(), p_ids.end()); auto last = std::unique(p_ids.begin(), p_ids.end());
p_ids.erase(last, p_ids.end()); p_ids.erase(last, p_ids.end());
@ -157,22 +161,24 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params,
{ {
path += "&systemeid="; path += "&systemeid=";
path += HttpReq::urlEncode(std::to_string(*platform)); path += HttpReq::urlEncode(std::to_string(*platform));
requests.push(std::unique_ptr<ScraperRequest>(new ScreenScraperRequest(requests, results, path))); requests.push(std::unique_ptr<ScraperRequest>
(new ScreenScraperRequest(requests, results, path)));
} }
} }
void ScreenScraperRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) void ScreenScraperRequest::process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results)
{ {
assert(req->status() == HttpReq::REQ_SUCCESS); assert(req->status() == HttpReq::REQ_SUCCESS);
pugi::xml_document doc; pugi::xml_document doc;
pugi::xml_parse_result parseResult = doc.load_string(req->getContent().c_str()); pugi::xml_parse_result parseResult = doc.load_string(req->getContent().c_str());
if (!parseResult) if (!parseResult) {
{
std::stringstream ss; std::stringstream ss;
ss << "ScreenScraperRequest - Error parsing XML." << std::endl << parseResult.description() << ""; ss << "ScreenScraperRequest - Error parsing XML." << std::endl <<
parseResult.description() << "";
std::string err = ss.str(); std::string err = ss.str();
setError(err); setError(err);
@ -182,17 +188,15 @@ void ScreenScraperRequest::process(const std::unique_ptr<HttpReq>& req, std::vec
} }
processGame(doc, results); processGame(doc, results);
} }
void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc,
void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& out_results) std::vector<ScraperSearchResult>& out_results)
{ {
pugi::xml_node data = xmldoc.child("Data"); pugi::xml_node data = xmldoc.child("Data");
pugi::xml_node game = data.child("jeu"); pugi::xml_node game = data.child("jeu");
if (game) if (game) {
{
ScraperSearchResult result; ScraperSearchResult result;
ScreenScraperRequest::ScreenScraperConfig ssConfig; ScreenScraperRequest::ScreenScraperConfig ssConfig;
@ -200,114 +204,116 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve
std::string language = Utils::String::toLower(ssConfig.language).c_str(); std::string language = Utils::String::toLower(ssConfig.language).c_str();
// Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ).
result.mdl.set("name", find_child_by_attribute_list(game.child("noms"), "nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get()); result.mdl.set("name", find_child_by_attribute_list(game.child("noms"),
"nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get());
// Description fallback language: EN, WOR(LD) // Description fallback language: EN, WOR(LD).
std::string description = find_child_by_attribute_list(game.child("synopsis"), "synopsis", "langue", { language, "en", "wor" }).text().get(); std::string description = find_child_by_attribute_list(game.child("synopsis"),
"synopsis", "langue", { language, "en", "wor" }).text().get();
if (!description.empty()) { if (!description.empty())
result.mdl.set("desc", Utils::String::replace(description, "&nbsp;", " ")); result.mdl.set("desc", Utils::String::replace(description, "&nbsp;", " "));
}
// Genre fallback language: EN. ( Xpath: Data/jeu[0]/genres/genre[*] ) // Genre fallback language: EN. ( Xpath: Data/jeu[0]/genres/genre[*] ).
result.mdl.set("genre", find_child_by_attribute_list(game.child("genres"), "genre", "langue", { language, "en" }).text().get()); result.mdl.set("genre", find_child_by_attribute_list(game.child("genres"),
"genre", "langue", { language, "en" }).text().get());
LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); LOG(LogDebug) << "Genre: " << result.mdl.get("genre");
// Get the date proper. The API returns multiple 'date' children nodes to the 'dates' main child of 'jeu'. // Get the date proper. The API returns multiple 'date' children nodes to the 'dates'
// Date fallback: WOR(LD), US, SS, JP, EU // main child of 'jeu'.
std::string _date = find_child_by_attribute_list(game.child("dates"), "date", "region", { region, "wor", "us", "ss", "jp", "eu" }).text().get(); // Date fallback: WOR(LD), US, SS, JP, EU.
std::string _date = find_child_by_attribute_list(game.child("dates"), "date", "region",
{ region, "wor", "us", "ss", "jp", "eu" }).text().get();
LOG(LogDebug) << "Release Date (unparsed): " << _date; LOG(LogDebug) << "Release Date (unparsed): " << _date;
// Date can be YYYY-MM-DD or just YYYY. // Date can be YYYY-MM-DD or just YYYY.
if (_date.length() > 4) if (_date.length() > 4) {
{ result.mdl.set("releasedate", Utils::Time::DateTime(
result.mdl.set("releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(_date, "%Y-%m-%d"))); Utils::Time::stringToTime(_date, "%Y-%m-%d")));
} else if (_date.length() > 0) }
{ else if (_date.length() > 0) {
result.mdl.set("releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(_date, "%Y"))); result.mdl.set("releasedate", Utils::Time::DateTime(
Utils::Time::stringToTime(_date, "%Y")));
} }
LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate"); LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate");
/// Developer for the game( Xpath: Data/jeu[0]/developpeur ) /// Developer for the game( Xpath: Data/jeu[0]/developpeur ).
std::string developer = game.child("developpeur").text().get(); std::string developer = game.child("developpeur").text().get();
if (!developer.empty()) if (!developer.empty())
result.mdl.set("developer", Utils::String::replace(developer, "&nbsp;", " ")); result.mdl.set("developer", Utils::String::replace(developer, "&nbsp;", " "));
// Publisher for the game ( Xpath: Data/jeu[0]/editeur ) // Publisher for the game ( Xpath: Data/jeu[0]/editeur ).
std::string publisher = game.child("editeur").text().get(); std::string publisher = game.child("editeur").text().get();
if (!publisher.empty()) if (!publisher.empty())
result.mdl.set("publisher", Utils::String::replace(publisher, "&nbsp;", " ")); result.mdl.set("publisher", Utils::String::replace(publisher, "&nbsp;", " "));
// Players // Players.
result.mdl.set("players", game.child("joueurs").text().get()); result.mdl.set("players", game.child("joueurs").text().get());
// TODO: Validate rating // TODO: Validate rating.
if (Settings::getInstance()->getBool("ScrapeRatings") && game.child("note")) if (Settings::getInstance()->getBool("ScrapeRatings") && game.child("note")) {
{
float ratingVal = (game.child("note").text().as_int() / 20.0f); float ratingVal = (game.child("note").text().as_int() / 20.0f);
std::stringstream ss; std::stringstream ss;
ss << ratingVal; ss << ratingVal;
result.mdl.set("rating", ss.str()); result.mdl.set("rating", ss.str());
} }
// Media super-node // Media super-node.
pugi::xml_node media_list = game.child("medias"); pugi::xml_node media_list = game.child("medias");
if (media_list) if (media_list) {
{
pugi::xml_node art = pugi::xml_node(NULL); pugi::xml_node art = pugi::xml_node(NULL);
// Do an XPath query for media[type='$media_type'], then filter by region // Do an XPath query for media[type='$media_type'], then filter by region.
// We need to do this because any child of 'medias' has the form // We need to do this because any child of 'medias' has the form
// <media type="..." region="..." format="..."> // <media type="..." region="..." format="...">
// and we need to find the right media for the region. // and we need to find the right media for the region.
pugi::xpath_node_set results = media_list.select_nodes((static_cast<std::string>("media[@type='") + ssConfig.media_name + "']").c_str()); pugi::xpath_node_set results = media_list.select_nodes((static_cast<std::string>
("media[@type='") + ssConfig.media_name + "']").c_str());
if (results.size()) if (results.size()) {
{ // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU.
// Region fallback: WOR(LD), US, CUS(TOM?), JP, EU for (auto _region : std::vector<std::string>{ region,
for (auto _region : std::vector<std::string>{ region, "wor", "us", "cus", "jp", "eu" }) "wor", "us", "cus", "jp", "eu" }) {
{
if (art) if (art)
break; break;
for (auto node : results) for (auto node : results) {
{ if (node.node().attribute("region").value() == _region) {
if (node.node().attribute("region").value() == _region)
{
art = node.node(); art = node.node();
break; break;
} }
} }
} }
} // results } // Results.
if (art) if (art) {
{ // Sending a 'softname' containing space will make the image URLs returned
// Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. // by the API also contain the space. Escape any spaces in the URL here
// Escape any spaces in the URL here
result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20");
// Get the media type returned by ScreenScraper // Get the media type returned by ScreenScraper.
std::string media_type = art.attribute("format").value(); std::string media_type = art.attribute("format").value();
if (!media_type.empty()) if (!media_type.empty())
result.imageType = "." + media_type; result.imageType = "." + media_type;
// Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping // Ask for the same image, but with a smaller size, for the thumbnail
// displayed during scraping.
result.thumbnailUrl = result.imageUrl + "&maxheight=250"; result.thumbnailUrl = result.imageUrl + "&maxheight=250";
}else{ }
else {
LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name; LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name;
} }
} }
out_results.push_back(result); out_results.push_back(result);
} // game } // Game.
} }
// Currently not used in this module // Currently not used in this module.
void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& results) void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc,
std::vector<ScraperSearchResult>& results)
{ {
assert(mRequestQueue != nullptr); assert(mRequestQueue != nullptr);
@ -321,26 +327,26 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve
ScreenScraperRequest::ScreenScraperConfig ssConfig; ScreenScraperRequest::ScreenScraperConfig ssConfig;
// limit the number of results per platform, not in total. // Limit the number of results per platform, not in total.
// otherwise if the first platform returns >= 7 games // Otherwise if the first platform returns >= 7 games
// but the second platform contains the relevant game, // but the second platform contains the relevant game,
// the relevant result would not be shown. // the relevant result would not be shown.
for (int i = 0; game && i < MAX_SCRAPER_RESULTS; i++) for (int i = 0; game && i < MAX_SCRAPER_RESULTS; i++) {
{
std::string id = game.child("id").text().get(); std::string id = game.child("id").text().get();
std::string name = game.child("nom").text().get(); std::string name = game.child("nom").text().get();
std::string platformId = game.child("systemeid").text().get(); std::string platformId = game.child("systemeid").text().get();
std::string path = ssConfig.getGameSearchUrl(name) + "&systemeid=" + platformId + "&gameid=" + id; std::string path = ssConfig.getGameSearchUrl(name) + "&systemeid=" +
platformId + "&gameid=" + id;
mRequestQueue->push(std::unique_ptr<ScraperRequest>(new ScreenScraperRequest(results, path))); mRequestQueue->push(std::unique_ptr<ScraperRequest>
(new ScreenScraperRequest(results, path)));
game = game.next_sibling("jeu"); game = game.next_sibling("jeu");
} }
} }
std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName) const std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(
const std::string gameName) const
{ {
return API_URL_BASE return API_URL_BASE
+ "/jeuInfos.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + "/jeuInfos.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY)
@ -348,5 +354,4 @@ std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const st
+ "&softname=" + HttpReq::urlEncode(API_SOFT_NAME) + "&softname=" + HttpReq::urlEncode(API_SOFT_NAME)
+ "&output=xml" + "&output=xml"
+ "&romnom=" + HttpReq::urlEncode(gameName); + "&romnom=" + HttpReq::urlEncode(gameName);
} }

View file

@ -1,3 +1,10 @@
//
// ScreenScraper.h
//
// Functions specifically for scraping from screenscraper.fr
// Called from Scraper.
//
#pragma once #pragma once
#ifndef ES_APP_SCRAPERS_SCREEN_SCRAPER_H #ifndef ES_APP_SCRAPERS_SCREEN_SCRAPER_H
#define ES_APP_SCRAPERS_SCREEN_SCRAPER_H #define ES_APP_SCRAPERS_SCREEN_SCRAPER_H
@ -7,67 +14,76 @@
namespace pugi { class xml_document; } namespace pugi { class xml_document; }
void screenscraper_generate_scraper_requests(
void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests, const ScraperSearchParams& params,
std::vector<ScraperSearchResult>& results); std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results);
class ScreenScraperRequest : public ScraperHttpRequest class ScreenScraperRequest : public ScraperHttpRequest
{ {
public: public:
// ctor for a GetGameList request // ctor for a GetGameList request.
ScreenScraperRequest(std::queue< std::unique_ptr<ScraperRequest> >& requestsWrite, std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url), mRequestQueue(&requestsWrite) {} ScreenScraperRequest(std::queue< std::unique_ptr<ScraperRequest> >& requestsWrite,
// ctor for a GetGame request std::vector<ScraperSearchResult>& resultsWrite,
ScreenScraperRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url), mRequestQueue(nullptr) {} const std::string& url) : ScraperHttpRequest(resultsWrite, url),
mRequestQueue(&requestsWrite) {}
// Settings for the scraper // ctor for a GetGame request.
ScreenScraperRequest(std::vector<ScraperSearchResult>& resultsWrite,
const std::string& url) : ScraperHttpRequest(resultsWrite, url),
mRequestQueue(nullptr) {}
// Settings for the scraper.
static const struct ScreenScraperConfig { static const struct ScreenScraperConfig {
std::string getGameSearchUrl(const std::string gameName) const; std::string getGameSearchUrl(const std::string gameName) const;
// Access to the API // Access to the API.
const std::string API_DEV_U = { 91, 32, 7, 17 }; const std::string API_DEV_U =
const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; { 91, 32, 7, 17 };
const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_DEV_P =
{ 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 };
const std::string API_DEV_KEY =
{ 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 };
const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2";
const std::string API_SOFT_NAME = "Emulationstation-DE " + static_cast<std::string>(PROGRAM_VERSION_STRING); const std::string API_SOFT_NAME = "Emulationstation-DE " +
static_cast<std::string>(PROGRAM_VERSION_STRING);
/** Which type of image artwork we need. Possible values (not a comprehensive list): // Which type of image artwork we need. Possible values (not a comprehensive list):
- ss: in-game screenshot // - ss: in-game screenshot
- box-3D: 3D boxart // - box-3D: 3D boxart
- box-2D: 2D boxart (default) // - box-2D: 2D boxart (default)
- screenmarque : marquee // - screenmarque : marquee
- sstitle: in-game start screenshot // - sstitle: in-game start screenshot
- steamgrid: Steam artwork // - steamgrid: Steam artwork
- wheel: spine // - wheel: spine
- support-2D: media showing the 2d boxart on the cart // - support-2D: media showing the 2d boxart on the cart
- support-3D: media showing the 3d boxart on the cart // - support-3D: media showing the 3d boxart on the cart
//
Note that no all games contain values for these, so we default to "box-2D" since it's the most common. // Note that not all games contain values for all these, so we default to "box-2D"
**/ // since it's the most common.
//
std::string media_name = "box-2D"; std::string media_name = "box-2D";
// Which Region to use when selecting the artwork // Which Region to use when selecting the artwork.
// Applies to: artwork, name of the game, date of release // Applies to: artwork, name of the game, date of release.
std::string region = "US"; std::string region = "US";
// Which Language to use when selecting the textual information // Which Language to use when selecting the textual information.
// Applies to: description, genre // Applies to: description, genre.
std::string language = "EN"; std::string language = "EN";
ScreenScraperConfig() {}; ScreenScraperConfig() {};
} configuration; } configuration;
protected: protected:
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override; void process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results) override;
void processList(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& results); void processList(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& results);
void processGame(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& results); void processGame(const pugi::xml_document& xmldoc, std::vector<ScraperSearchResult>& results);
bool isGameRequest() { return !mRequestQueue; } bool isGameRequest() { return !mRequestQueue; }
std::queue< std::unique_ptr<ScraperRequest> >* mRequestQueue; std::queue< std::unique_ptr<ScraperRequest> >* mRequestQueue;
}; };
#endif // ES_APP_SCRAPERS_SCREEN_SCRAPER_H #endif // ES_APP_SCRAPERS_SCREEN_SCRAPER_H

View file

@ -1,17 +1,22 @@
//
// AsyncHandle.h
//
// Asynchronous operations used by ScraperSearchComponent and Scraper.
//
#pragma once #pragma once
#ifndef ES_CORE_ASYNC_HANDLE_H #ifndef ES_CORE_ASYNC_HANDLE_H
#define ES_CORE_ASYNC_HANDLE_H #define ES_CORE_ASYNC_HANDLE_H
#include <string> #include <string>
enum AsyncHandleStatus enum AsyncHandleStatus {
{
ASYNC_IN_PROGRESS, ASYNC_IN_PROGRESS,
ASYNC_ERROR, ASYNC_ERROR,
ASYNC_DONE ASYNC_DONE
}; };
// Handle for some asynchronous operation. // Handle for some asynchronous operations.
class AsyncHandle class AsyncHandle
{ {
public: public:
@ -23,11 +28,11 @@ public:
// Update and return the latest status. // Update and return the latest status.
inline AsyncHandleStatus status() { update(); return mStatus; } inline AsyncHandleStatus status() { update(); return mStatus; }
// User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. // User-friendly string of our current status.
// Will return error message if status() == SEARCH_ERROR.
inline std::string getStatusString() inline std::string getStatusString()
{ {
switch(mStatus) switch (mStatus) {
{
case ASYNC_IN_PROGRESS: case ASYNC_IN_PROGRESS:
return "in progress"; return "in progress";
case ASYNC_ERROR: case ASYNC_ERROR:

View file

@ -1,3 +1,11 @@
//
// HttpReq.cpp
//
// HTTP request functions.
// Used by Scraper, GamesDBJSONScraper, GamesDBJSONScraperResources and
// ScreenScraper to download game information and media files.
//
#include "HttpReq.h" #include "HttpReq.h"
#include "utils/FileSystemUtil.h" #include "utils/FileSystemUtil.h"
@ -10,17 +18,15 @@ std::map<CURL*, HttpReq*> HttpReq::s_requests;
std::string HttpReq::urlEncode(const std::string &s) std::string HttpReq::urlEncode(const std::string &s)
{ {
const std::string unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; const std::string unreserved =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~";
std::string escaped=""; std::string escaped="";
for(size_t i=0; i<s.length(); i++) for (size_t i=0; i<s.length(); i++) {
{ if (unreserved.find_first_of(s[i]) != std::string::npos) {
if (unreserved.find_first_of(s[i]) != std::string::npos)
{
escaped.push_back(s[i]); escaped.push_back(s[i]);
} }
else else {
{
escaped.append("%"); escaped.append("%");
char buf[3]; char buf[3];
sprintf(buf, "%.2X", (unsigned char)s[i]); sprintf(buf, "%.2X", (unsigned char)s[i]);
@ -32,81 +38,75 @@ std::string HttpReq::urlEncode(const std::string &s)
bool HttpReq::isUrl(const std::string& str) bool HttpReq::isUrl(const std::string& str)
{ {
//the worst guess // The worst guess.
return (!str.empty() && !Utils::FileSystem::exists(str) && return (!str.empty() && !Utils::FileSystem::exists(str) &&
(str.find("http://") != std::string::npos || str.find("https://") != std::string::npos || str.find("www.") != std::string::npos)); (str.find("http://") != std::string::npos || str.find("https://") !=
std::string::npos || str.find("www.") != std::string::npos));
} }
HttpReq::HttpReq(const std::string& url) HttpReq::HttpReq(const std::string& url)
: mStatus(REQ_IN_PROGRESS), mHandle(NULL) : mStatus(REQ_IN_PROGRESS), mHandle(NULL)
{ {
mHandle = curl_easy_init(); mHandle = curl_easy_init();
if(mHandle == NULL) if (mHandle == NULL) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError("curl_easy_init failed"); onError("curl_easy_init failed");
return; return;
} }
//set the url // Set the url.
CURLcode err = curl_easy_setopt(mHandle, CURLOPT_URL, url.c_str()); CURLcode err = curl_easy_setopt(mHandle, CURLOPT_URL, url.c_str());
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//set curl to handle redirects // Set curl to handle redirects.
err = curl_easy_setopt(mHandle, CURLOPT_FOLLOWLOCATION, 1L); err = curl_easy_setopt(mHandle, CURLOPT_FOLLOWLOCATION, 1L);
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//set curl max redirects // Set curl max redirects.
err = curl_easy_setopt(mHandle, CURLOPT_MAXREDIRS, 2L); err = curl_easy_setopt(mHandle, CURLOPT_MAXREDIRS, 2L);
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//set curl restrict redirect protocols // Set curl restrict redirect protocols.
err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//tell curl how to write the data // Tell curl how to write the data.
err = curl_easy_setopt(mHandle, CURLOPT_WRITEFUNCTION, &HttpReq::write_content); err = curl_easy_setopt(mHandle, CURLOPT_WRITEFUNCTION, &HttpReq::write_content);
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//give curl a pointer to this HttpReq so we know where to write the data *to* in our write function // Give curl a pointer to this HttpReq so we know where to write the
// data *to* in our write function.
err = curl_easy_setopt(mHandle, CURLOPT_WRITEDATA, this); err = curl_easy_setopt(mHandle, CURLOPT_WRITEDATA, this);
if(err != CURLE_OK) if (err != CURLE_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err)); onError(curl_easy_strerror(err));
return; return;
} }
//add the handle to our multi // Add the handle to our multi.
CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle);
if(merr != CURLM_OK) if (merr != CURLM_OK) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_multi_strerror(merr)); onError(curl_multi_strerror(merr));
return; return;
@ -117,14 +117,14 @@ HttpReq::HttpReq(const std::string& url)
HttpReq::~HttpReq() HttpReq::~HttpReq()
{ {
if(mHandle) if (mHandle) {
{
s_requests.erase(mHandle); s_requests.erase(mHandle);
CURLMcode merr = curl_multi_remove_handle(s_multi_handle, mHandle); CURLMcode merr = curl_multi_remove_handle(s_multi_handle, mHandle);
if(merr != CURLM_OK) if (merr != CURLM_OK)
LOG(LogError) << "Error removing curl_easy handle from curl_multi: " << curl_multi_strerror(merr); LOG(LogError) << "Error removing curl_easy handle from curl_multi: " <<
curl_multi_strerror(merr);
curl_easy_cleanup(mHandle); curl_easy_cleanup(mHandle);
} }
@ -132,12 +132,10 @@ HttpReq::~HttpReq()
HttpReq::Status HttpReq::status() HttpReq::Status HttpReq::status()
{ {
if(mStatus == REQ_IN_PROGRESS) if (mStatus == REQ_IN_PROGRESS) {
{
int handle_count; int handle_count;
CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count); CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count);
if(merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) if (merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) {
{
mStatus = REQ_IO_ERROR; mStatus = REQ_IO_ERROR;
onError(curl_multi_strerror(merr)); onError(curl_multi_strerror(merr));
return mStatus; return mStatus;
@ -145,22 +143,19 @@ HttpReq::Status HttpReq::status()
int msgs_left; int msgs_left;
CURLMsg* msg; CURLMsg* msg;
while((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) while ((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) {
{ if (msg->msg == CURLMSG_DONE) {
if(msg->msg == CURLMSG_DONE)
{
HttpReq* req = s_requests[msg->easy_handle]; HttpReq* req = s_requests[msg->easy_handle];
if(req == NULL) if (req == NULL) {
{
LOG(LogError) << "Cannot find easy handle!"; LOG(LogError) << "Cannot find easy handle!";
continue; continue;
} }
if(msg->data.result == CURLE_OK) if (msg->data.result == CURLE_OK) {
{
req->mStatus = REQ_SUCCESS; req->mStatus = REQ_SUCCESS;
}else{ }
else {
req->mStatus = REQ_IO_ERROR; req->mStatus = REQ_IO_ERROR;
req->onError(curl_easy_strerror(msg->data.result)); req->onError(curl_easy_strerror(msg->data.result));
} }
@ -187,9 +182,9 @@ std::string HttpReq::getErrorMsg()
return mErrorMsg; return mErrorMsg;
} }
//used as a curl callback // Used as a curl callback.
//size = size of an element, nmemb = number of elements // size = size of an element, nmemb = number of elements.
//return value is number of elements successfully read // Return value is number of elements successfully read.
size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_ptr) size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_ptr)
{ {
std::stringstream& ss = ((HttpReq*)req_ptr)->mContent; std::stringstream& ss = ((HttpReq*)req_ptr)->mContent;
@ -198,8 +193,8 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p
return nmemb; return nmemb;
} }
//used as a curl callback // Used as a curl callback.
/*int HttpReq::update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow) //int HttpReq::update_progress(void* req_ptr, double dlTotal,
{ // double dlNow, double ulTotal, double ulNow)
//{
}*/ //}

View file

@ -1,3 +1,11 @@
//
// HttpReq.h
//
// HTTP request functions.
// Used by Scraper, GamesDBJSONScraper, GamesDBJSONScraperResources and
// ScreenScraper to download game information and media files.
//
#pragma once #pragma once
#ifndef ES_CORE_HTTP_REQ_H #ifndef ES_CORE_HTTP_REQ_H
#define ES_CORE_HTTP_REQ_H #define ES_CORE_HTTP_REQ_H
@ -6,22 +14,27 @@
#include <map> #include <map>
#include <sstream> #include <sstream>
/* Usage: // Usage:
* HttpReq myRequest("www.google.com", "/index.html"); // HttpReq myRequest("www.duckduckgo.com", "/index.html");
* //for blocking behavior: while(myRequest.status() == HttpReq::REQ_IN_PROGRESS); //
* //for non-blocking behavior: check if(myRequest.status() != HttpReq::REQ_IN_PROGRESS) in some sort of update method // For blocking behavior:
* // while (myRequest.status() == HttpReq::REQ_IN_PROGRESS);
* //once one of those completes, the request is ready //
* if(myRequest.status() != REQ_SUCCESS) // For non-blocking behavior:
* { // Check 'if(myRequest.status() != HttpReq::REQ_IN_PROGRESS)' in some sort of update method.
* //an error occured //
* LOG(LogError) << "HTTP request error - " << myRequest.getErrorMessage(); // Once one of those calls complete, the request is ready.
* return; //
* } // Do something like this to capture errors:
* // if(myRequest.status() != REQ_SUCCESS)
* std::string content = myRequest.getContent(); // {
* //process contents... // // An error occured.
*/ // LOG(LogError) << "HTTP request error - " << myRequest.getErrorMessage();
// return;
// }
//
// This is how to read the returned content:
// std::string content = myRequest.getContent();
class HttpReq class HttpReq
{ {
@ -30,31 +43,31 @@ public:
~HttpReq(); ~HttpReq();
enum Status enum Status {
{ REQ_IN_PROGRESS, // Request is in progress.
REQ_IN_PROGRESS, //request is in progress REQ_SUCCESS, // Request completed successfully, get it with getContent().
REQ_SUCCESS, //request completed successfully, get it with getContent()
REQ_IO_ERROR, //some error happened, get it with getErrorMsg() REQ_IO_ERROR, // Some error happened, get it with getErrorMsg().
REQ_BAD_STATUS_CODE, //some invalid HTTP response status code happened (non-200) REQ_BAD_STATUS_CODE, // Some invalid HTTP response status code happened (non-200).
REQ_INVALID_RESPONSE //the HTTP response was invalid REQ_INVALID_RESPONSE // The HTTP response was invalid.
}; };
Status status(); //process any received data and return the status afterwards Status status(); // Process any received data and return the status afterwards.
std::string getErrorMsg(); std::string getErrorMsg();
std::string getContent() const; // mStatus must be REQ_SUCCESS std::string getContent() const; // mStatus must be REQ_SUCCESS.
static std::string urlEncode(const std::string &s); static std::string urlEncode(const std::string &s);
static bool isUrl(const std::string& s); static bool isUrl(const std::string& s);
private: private:
static size_t write_content(void* buff, size_t size, size_t nmemb, void* req_ptr); static size_t write_content(void* buff, size_t size, size_t nmemb, void* req_ptr);
//static int update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow); //static int update_progress(void* req_ptr, double dlTotal, double dlNow,
// double ulTotal, double ulNow);
//god dammit libcurl why can't you have some way to check the status of an individual handle // God dammit libcurl why can't you have some way to check the status of an
//why do I have to handle ALL messages at once // individual handle why do I have to handle ALL messages at once.
static std::map<CURL*, HttpReq*> s_requests; static std::map<CURL*, HttpReq*> s_requests;
static CURLM* s_multi_handle; static CURLM* s_multi_handle;

View file

@ -1,9 +1,8 @@
// //
// Settings.cpp // Settings.cpp
// //
// Functions to read from and write to the configuration file es_settings.cfg // Functions to read from and write to the configuration file es_settings.cfg.
// are included here. The default values for the application settings are // The default values for the application settings are defined here as well.
// defined here as well.
// //
#include "Settings.h" #include "Settings.h"

View file

@ -1,9 +1,8 @@
// //
// Settings.h // Settings.h
// //
// Functions to read from and write to the configuration file es_settings.cfg // Functions to read from and write to the configuration file es_settings.cfg.
// are included here. The default values for the application settings are // The default values for the application settings are defined here as well.
// defined here as well.
// //
#pragma once #pragma once