Major update to scraper including support for new media handling logic, ability to download more media file types (screenshot, cover, marquee, 3D box) and an improved scraper GUI. As well a rewrite of the navigation sound code.

This commit is contained in:
Leon Styhre 2020-06-06 13:10:33 +02:00
parent d85ad49523
commit 90735d44e3
44 changed files with 1439 additions and 749 deletions

View file

@ -31,8 +31,8 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSlideshowScreensaverOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSlideshowScreensaverOptions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h
@ -89,8 +89,8 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSlideshowScreensaverOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSlideshowScreensaverOptions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp

View file

@ -354,7 +354,6 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS
} }
else { else {
ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); ViewController::get()->onFileChanged(rootFolder, FILE_SORTED);
std::string teststring1 = rootFolder->getPath();
// If it's a custom collection and the collections // If it's a custom collection and the collections
// are grouped, update the parent instead. // are grouped, update the parent instead.
if (sysData.decl.isCustom && if (sysData.decl.isCustom &&

View file

@ -39,8 +39,18 @@ FileData::FileData(
metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA)
{ {
// Metadata needs at least a name field (since that's what getName() will return). // Metadata needs at least a name field (since that's what getName() will return).
if (metadata.get("name").empty()) if (metadata.get("name").empty()) {
metadata.set("name", getDisplayName()); if ((system->hasPlatformId(PlatformIds::ARCADE) ||
system->hasPlatformId(PlatformIds::NEOGEO)) &&
metadata.getType() != FOLDER_METADATA) {
// If it's a MAME or Neo Geo game, expand the game name accordingly.
metadata.set("name",
MameNames::getInstance()->getCleanName(getCleanName()));
}
else {
metadata.set("name", getDisplayName());
}
}
mSystemName = system->getName(); mSystemName = system->getName();
metadata.resetChangedFlag(); metadata.resetChangedFlag();
} }
@ -88,7 +98,7 @@ const bool FileData::getFavorite()
return false; return false;
} }
const std::string FileData::getMediaDirectory() const const std::string FileData::getMediaDirectory()
{ {
std::string mediaDirSetting = Settings::getInstance()->getString("MediaDirectory"); std::string mediaDirSetting = Settings::getInstance()->getString("MediaDirectory");
std::string mediaDirPath = ""; std::string mediaDirPath = "";
@ -111,12 +121,13 @@ const std::string FileData::getMediaDirectory() const
return mediaDirPath; return mediaDirPath;
} }
const std::string FileData::getThumbnailPath() const const std::string FileData::getMediafilePath(std::string subdirectory, std::string mediatype) const
{ {
const char* extList[2] = { ".png", ".jpg" }; const char* extList[2] = { ".png", ".jpg" };
std::string tempPath = getMediaDirectory() + mSystemName + "/thumbnails/" + getDisplayName();
// Look for media in the media directory. // Look for an image file in the media directory.
std::string tempPath = getMediaDirectory() + mSystemName + "/" +
subdirectory + "/" + getDisplayName();
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
std::string mediaPath = tempPath + extList[i]; std::string mediaPath = tempPath + extList[i];
if (Utils::FileSystem::exists(mediaPath)) if (Utils::FileSystem::exists(mediaPath))
@ -125,11 +136,10 @@ const std::string FileData::getThumbnailPath() const
// No media found in the media directory, so look // No media found in the media directory, so look
// for local art as well (if configured to do so). // for local art as well (if configured to do so).
if (Settings::getInstance()->getBool("LocalArt")) if (Settings::getInstance()->getBool("LocalArt")) {
{
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
std::string localMediaPath = mEnvData->mStartPath + "/images/" + std::string localMediaPath = mEnvData->mStartPath + "/images/" +
getDisplayName() + "-thumbnail" + extList[i]; getDisplayName() + "-" + mediatype + extList[i];
if (Utils::FileSystem::exists(localMediaPath)) if (Utils::FileSystem::exists(localMediaPath))
return localMediaPath; return localMediaPath;
} }
@ -138,6 +148,52 @@ const std::string FileData::getThumbnailPath() const
return ""; return "";
} }
const std::string FileData::getImagePath() const
{
// Look for a mix image (a combination of screenshot, 2D/3D box and marquee).
std::string image = getMediafilePath("miximages", "miximage");
if (image != "")
return image;
// If no mix image was found, try screenshot instead.
image = getMediafilePath("screenshots", "screenshot");
if (image != "")
return image;
// If no screenshot was found either, try cover.
return getMediafilePath("covers", "cover");
}
const std::string FileData::get3DBoxPath() const
{
return getMediafilePath("3dboxes", "3dbox");
}
const std::string FileData::getCoverPath() const
{
return getMediafilePath("covers", "cover");
}
const std::string FileData::getMarqueePath() const
{
return getMediafilePath("marquees", "marquee");
}
const std::string FileData::getMiximagePath() const
{
return getMediafilePath("miximages", "miximage");
}
const std::string FileData::getScreenshotPath() const
{
return getMediafilePath("screenshots", "screenshot");
}
const std::string FileData::getThumbnailPath() const
{
return getMediafilePath("thumbnails", "thumbnail");
}
const std::string FileData::getVideoPath() const const std::string FileData::getVideoPath() const
{ {
const char* extList[5] = { ".avi", ".mkv", ".mov", ".mp4", ".wmv" }; const char* extList[5] = { ".avi", ".mkv", ".mov", ".mp4", ".wmv" };
@ -155,7 +211,7 @@ const std::string FileData::getVideoPath() const
if (Settings::getInstance()->getBool("LocalArt")) if (Settings::getInstance()->getBool("LocalArt"))
{ {
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
std::string localMediaPath = mEnvData->mStartPath + "/images/" + getDisplayName() + std::string localMediaPath = mEnvData->mStartPath + "/videos/" + getDisplayName() +
"-video" + extList[i]; "-video" + extList[i];
if (Utils::FileSystem::exists(localMediaPath)) if (Utils::FileSystem::exists(localMediaPath))
return localMediaPath; return localMediaPath;
@ -165,68 +221,6 @@ const std::string FileData::getVideoPath() const
return ""; return "";
} }
const std::string FileData::getMarqueePath() const
{
const char* extList[2] = { ".png", ".jpg" };
std::string tempPath = getMediaDirectory() + mSystemName + "/marquees/" + getDisplayName();
// Look for media in the media directory.
for (int i = 0; i < 2; i++) {
std::string mediaPath = tempPath + extList[i];
if (Utils::FileSystem::exists(mediaPath))
return mediaPath;
}
// No media found in the media directory, so look
// for local art as well (if configured to do so).
if (Settings::getInstance()->getBool("LocalArt"))
{
for (int i = 0; i < 2; i++) {
std::string localMediaPath = mEnvData->mStartPath + "/images/" + getDisplayName() +
"-marquee" + extList[i];
if (Utils::FileSystem::exists(localMediaPath))
return localMediaPath;
}
}
return "";
}
const std::string FileData::getImagePath() const
{
const char* extList[2] = { ".png", ".jpg" };
// Look for mix image (a combination of screenshot, 3D box and marquee) in the media directory.
std::string tempPath = getMediaDirectory() + mSystemName + "/miximages/" + getDisplayName();
for (int i = 0; i < 2; i++) {
std::string mediaPath = tempPath + extList[i];
if (Utils::FileSystem::exists(mediaPath))
return mediaPath;
}
// If no mix image exists, try normal screenshot.
tempPath = getMediaDirectory() + mSystemName + "/screenshots/" + getDisplayName();
for (int i = 0; i < 2; i++) {
std::string mediaPath = tempPath + extList[i];
if (Utils::FileSystem::exists(mediaPath))
return mediaPath;
}
// No media found in the media directory, so look
// for local art as well (if configured to do so).
if (Settings::getInstance()->getBool("LocalArt")) {
for (int i = 0; i < 2; i++) {
std::string localMediaPath = mEnvData->mStartPath + "/images/" +
getDisplayName() + "-image" + extList[i];
if (Utils::FileSystem::exists(localMediaPath))
return localMediaPath;
}
}
return "";
}
const std::vector<FileData*>& FileData::getChildrenListToDisplay() const std::vector<FileData*>& FileData::getChildrenListToDisplay()
{ {
@ -312,7 +306,6 @@ void FileData::removeChild(FileData* file)
// File somehow wasn't in our children. // File somehow wasn't in our children.
assert(false); assert(false);
} }
void FileData::sort(ComparisonFunction& comparator, bool ascending) void FileData::sort(ComparisonFunction& comparator, bool ascending)

View file

@ -57,11 +57,17 @@ public:
inline const std::vector<FileData*>& getChildren() const { return mChildren; } inline const std::vector<FileData*>& getChildren() const { return mChildren; }
inline SystemData* getSystem() const { return mSystem; } inline SystemData* getSystem() const { return mSystem; }
inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; }
virtual const std::string getMediaDirectory() const; static const std::string getMediaDirectory();
virtual const std::string getMediafilePath(
std::string subdirectory, std::string mediatype) const;
virtual const std::string getImagePath() const;
virtual const std::string get3DBoxPath() const;
virtual const std::string getCoverPath() const;
virtual const std::string getMarqueePath() const;
virtual const std::string getMiximagePath() const;
virtual const std::string getScreenshotPath() const;
virtual const std::string getThumbnailPath() const; virtual const std::string getThumbnailPath() const;
virtual const std::string getVideoPath() const; virtual const std::string getVideoPath() const;
virtual const std::string getMarqueePath() const;
virtual const std::string getImagePath() const;
const std::vector<FileData*>& getChildrenListToDisplay(); const std::vector<FileData*>& getChildrenListToDisplay();
std::vector<FileData*> getFilesRecursive(unsigned int typeMask, std::vector<FileData*> getFilesRecursive(unsigned int typeMask,

View file

@ -12,38 +12,40 @@
#include <pugixml/src/pugixml.hpp> #include <pugixml/src/pugixml.hpp>
MetaDataDecl gameDecls[] = { MetaDataDecl gameDecls[] = {
// key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd, shouldScrape
{"name", MD_STRING, "", false, "name", "enter game name"}, {"name", MD_STRING, "", false, "name", "enter game name", true},
{"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"sortname", MD_STRING, "", false, "sortname", "enter game sort name", false},
{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description", true},
{"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, {"rating", MD_RATING, "0", false, "rating", "enter rating", true},
{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date", true},
{"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, {"developer", MD_STRING, "unknown", false, "developer", "enter game developer", true},
{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher", true},
{"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, {"genre", MD_STRING, "unknown", false, "genre", "enter game genre", true},
{"players", MD_INT, "1", false, "players", "enter number of players"}, {"players", MD_INT, "unknown", false, "players", "enter number of players", true},
{"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, {"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on", false},
{"completed", MD_BOOL, "false", false, "completed", "enter completed off/on"}, {"completed", MD_BOOL, "false", false, "completed", "enter completed off/on", false},
{"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on"}, {"broken", MD_BOOL, "false", false, "broken/not working", "enter broken off/on", false},
{"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on"}, {"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on", false},
{"launchstring", MD_LAUNCHSTRING, "", false, "launch string", "enter game launch string (emulator override)"}, {"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on", false},
{"playcount", MD_INT, "0", false, "play count", "enter number of times played"}, {"launchstring", MD_LAUNCHSTRING, "", false, "launch string", "enter game launch string "
{"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} "(emulator override)", false},
{"playcount", MD_INT, "0", false, "play count", "enter number of times played", false},
{"lastplayed", MD_TIME, "0", true, "last played", "enter last played date", false}
}; };
const std::vector<MetaDataDecl> gameMDD(gameDecls, gameDecls + const std::vector<MetaDataDecl> gameMDD(gameDecls, gameDecls +
sizeof(gameDecls) / sizeof(gameDecls[0])); sizeof(gameDecls) / sizeof(gameDecls[0]));
MetaDataDecl folderDecls[] = { MetaDataDecl folderDecls[] = {
{"name", MD_STRING, "", false, "name", "enter game name"}, {"name", MD_STRING, "", false, "name", "enter game name", true},
{"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"sortname", MD_STRING, "", false, "sortname", "enter game sort name", false},
{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description", true},
{"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, {"rating", MD_RATING, "0", false, "rating", "enter rating", true},
{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date", true},
{"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, {"developer", MD_STRING, "unknown", false, "developer", "enter game developer", true},
{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher", true},
{"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, {"genre", MD_STRING, "unknown", false, "genre", "enter game genre", true},
{"players", MD_INT, "1", false, "players", "enter number of players"} {"players", MD_INT, "unknown", false, "players", "enter number of players", true}
}; };
const std::vector<MetaDataDecl> folderMDD(folderDecls, folderDecls + const std::vector<MetaDataDecl> folderMDD(folderDecls, folderDecls +
sizeof(folderDecls) / sizeof(folderDecls[0])); sizeof(folderDecls) / sizeof(folderDecls[0]));
@ -78,7 +80,7 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type,
for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) {
pugi::xml_node md = node.child(iter->key.c_str()); pugi::xml_node md = node.child(iter->key.c_str());
if (md) { if (md && !md.text().empty()) {
// If it's a path, resolve relative paths. // If it's a path, resolve relative paths.
std::string value = md.text().get(); std::string value = md.text().get();
if (iter->type == MD_PATH) if (iter->type == MD_PATH)

View file

@ -15,19 +15,19 @@
namespace pugi { class xml_node; } namespace pugi { class xml_node; }
enum MetaDataType { enum MetaDataType {
// Generic types // Generic types.
MD_STRING, MD_STRING,
MD_INT, MD_INT,
MD_FLOAT, MD_FLOAT,
MD_BOOL, MD_BOOL,
// Specialized types // Specialized types.
MD_MULTILINE_STRING, MD_MULTILINE_STRING,
MD_LAUNCHSTRING, MD_LAUNCHSTRING,
MD_PATH, MD_PATH,
MD_RATING, MD_RATING,
MD_DATE, MD_DATE,
MD_TIME // Used for lastplayed MD_TIME // Used for lastplayed.
}; };
struct MetaDataDecl { struct MetaDataDecl {
@ -40,6 +40,8 @@ struct MetaDataDecl {
std::string displayName; std::string displayName;
// Phrase displayed in editors when prompted to enter value (currently only for strings). // Phrase displayed in editors when prompted to enter value (currently only for strings).
std::string displayPrompt; std::string displayPrompt;
// If set to false, the scraper will not overwrite this metadata.
bool shouldScrape;
}; };
enum MetaDataListType { enum MetaDataListType {

View file

@ -38,7 +38,8 @@ SystemData::SystemData(
mEnvData(envData), mEnvData(envData),
mThemeFolder(themeFolder), mThemeFolder(themeFolder),
mIsCollectionSystem(CollectionSystem), mIsCollectionSystem(CollectionSystem),
mIsGameSystem(true) mIsGameSystem(true),
mScrapeFlag(false)
{ {
mFilterIndex = new FileFilterIndex(); mFilterIndex = new FileFilterIndex();

View file

@ -62,6 +62,8 @@ public:
unsigned int getGameCount() const; unsigned int getGameCount() const;
unsigned int getDisplayedGameCount() const; unsigned int getDisplayedGameCount() const;
bool getScrapeFlag() { return mScrapeFlag; };
void setScrapeFlag(bool scrapeflag) { mScrapeFlag = scrapeflag; }
static void deleteSystems(); static void deleteSystems();
// Load the system config file at getConfigPath(). // Load the system config file at getConfigPath().
@ -100,6 +102,7 @@ public:
private: private:
bool mIsCollectionSystem; bool mIsCollectionSystem;
bool mIsGameSystem; bool mIsGameSystem;
bool mScrapeFlag; // Only used by scraper GUI to remember which systems to scrape.
std::string mName; std::string mName;
std::string mFullName; std::string mFullName;
SystemEnvironmentData* mEnvData; SystemEnvironmentData* mEnvData;

View file

@ -4,7 +4,8 @@
// User interface component for the scraper where the user is able to see an overview // 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. // 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 // Used by both single-game scraping from the GuiMetaDataEd menu as well as
// to resolve scraping conflicts when run from GuiScraperStart. // to resolve scraping conflicts when run from GuiScraperMenu.
// The function to properly save scraped metadata is located here too.
// //
// This component is called from GuiGameScraper for single-game scraping and // This component is called from GuiGameScraper for single-game scraping and
// from GuiScraperMulti for multi-game scraping. // from GuiScraperMulti for multi-game scraping.
@ -22,6 +23,9 @@
#include "guis/GuiTextEditPopup.h" #include "guis/GuiTextEditPopup.h"
#include "resources/Font.h" #include "resources/Font.h"
#include "utils/StringUtil.h" #include "utils/StringUtil.h"
#include "PlatformId.h"
#include "MameNames.h"
#include "SystemData.h"
#include "FileData.h" #include "FileData.h"
#include "Log.h" #include "Log.h"
#include "Window.h" #include "Window.h"
@ -69,8 +73,10 @@ ScraperSearchComponent::ScraperSearchComponent(
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> if (Settings::getInstance()->getBool("ScrapeRatings") &&
(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); Settings::getInstance()->getString("Scraper") != "TheGamesDB")
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, "RELEASED:", font, mdLblColor), mMD_ReleaseDate));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent> mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>
@ -144,7 +150,7 @@ void ScraperSearchComponent::onSizeChanged()
mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3); mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3);
else else
mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale,
mResultDesc->getFont()->getHeight() * 8); mResultDesc->getFont()->getHeight() * 7);
// Make description text wrap at edge of container. // Make description text wrap at edge of container.
mResultDesc->setSize(mDescContainer->getSize().x(), 0); mResultDesc->setSize(mDescContainer->getSize().x(), 0);
@ -184,9 +190,12 @@ 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. if (Settings::getInstance()->getBool("ScrapeRatings") &&
mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f); Settings::getInstance()->getString("Scraper") != "TheGamesDB") {
mMD_Grid->onSizeChanged(); // Rating is manually sized.
mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f);
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));
@ -239,6 +248,7 @@ void ScraperSearchComponent::search(const ScraperSearchParams& params)
mResultList->clear(); mResultList->clear();
mScraperResults.clear(); mScraperResults.clear();
mMDRetrieveURLsHandle.reset();
mThumbnailReq.reset(); mThumbnailReq.reset();
mMDResolveHandle.reset(); mMDResolveHandle.reset();
updateInfoPane(); updateInfoPane();
@ -252,6 +262,7 @@ void ScraperSearchComponent::stop()
mThumbnailReq.reset(); mThumbnailReq.reset();
mSearchHandle.reset(); mSearchHandle.reset();
mMDResolveHandle.reset(); mMDResolveHandle.reset();
mMDRetrieveURLsHandle.reset();
mBlockAccept = false; mBlockAccept = false;
} }
@ -297,15 +308,18 @@ void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>
mBlockAccept = false; mBlockAccept = false;
updateInfoPane(); updateInfoPane();
// If there is no scraping result or if there is no game media to download
// as a thumbnail, then proceed directly.
if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) { if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) {
if (mScraperResults.size() == 0) if (mScraperResults.size() == 0 || (mScraperResults.size() > 0 &&
mSkipCallback(); mScraperResults.front().ThumbnailImageUrl == "")) {
else if (mScraperResults.size() == 0)
returnResult(mScraperResults.front()); mSkipCallback();
} else
else if (mSearchType == ALWAYS_ACCEPT_MATCHING_CRC) { returnResult(mScraperResults.front());
// TODO }
} }
} }
void ScraperSearchComponent::onSearchError(const std::string& error) void ScraperSearchComponent::onSearchError(const std::string& error)
@ -338,14 +352,35 @@ void ScraperSearchComponent::updateInfoPane()
mDescContainer->reset(); mDescContainer->reset();
mResultThumbnail->setImage(""); mResultThumbnail->setImage("");
const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl; const std::string& thumb = res.screenshotUrl.empty() ? res.coverUrl : res.screenshotUrl;
if (!thumb.empty()) mScraperResults[i].ThumbnailImageUrl = thumb;
mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb));
else // Cache the thumbnail image in mScraperResults so that we don't need to download
mThumbnailReq.reset(); // it every time the list is scrolled back and forth.
if (mScraperResults[i].ThumbnailImageData.size() > 0) {
std::string content = mScraperResults[i].ThumbnailImageData;
mResultThumbnail->setImage(content.data(), content.length());
mGrid.onSizeChanged(); // A hack to fix the thumbnail position since its size changed.
}
// If it's not cached in mScraperResults it should mean that it's the first time
// we access the entry, and therefore we need to download the image.
else {
if (!thumb.empty()) {
// Make sure we don't attempt to download the same thumbnail twice.
if (!mThumbnailReq && mScraperResults[i].thumbnailDownloadStatus != IN_PROGRESS) {
mScraperResults[i].thumbnailDownloadStatus = IN_PROGRESS;
mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb));
}
}
else {
mThumbnailReq.reset();
}
}
// Metadata. // Metadata.
mMD_Rating->setValue(Utils::String::toUpper(res.mdl.get("rating"))); if (Settings::getInstance()->getBool("ScrapeRatings") &&
Settings::getInstance()->getString("Scraper") != "TheGamesDB")
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")));
mMD_Publisher->setText(Utils::String::toUpper(res.mdl.get("publisher"))); mMD_Publisher->setText(Utils::String::toUpper(res.mdl.get("publisher")));
@ -359,7 +394,9 @@ void ScraperSearchComponent::updateInfoPane()
mResultThumbnail->setImage(""); mResultThumbnail->setImage("");
// Metadata. // Metadata.
mMD_Rating->setValue(""); if (Settings::getInstance()->getBool("ScrapeRatings") &&
Settings::getInstance()->getString("Scraper") != "TheGamesDB")
mMD_Rating->setValue("");
mMD_ReleaseDate->setValue(""); mMD_ReleaseDate->setValue("");
mMD_Developer->setText(""); mMD_Developer->setText("");
mMD_Publisher->setText(""); mMD_Publisher->setText("");
@ -397,7 +434,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.mediaFilesDownloadStatus != COMPLETED) {
result.mediaFilesDownloadStatus = IN_PROGRESS;
mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch);
return; return;
} }
@ -417,22 +455,69 @@ void ScraperSearchComponent::update(int deltaTime)
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(); mScraperResults = mSearchHandle->getResults();
auto statusString = mSearchHandle->getStatusString(); auto statusString = mSearchHandle->getStatusString();
// We reset here because onSearchDone in auto mode can call mSkipCallback() which // We reset here because onSearchDone in auto mode can call mSkipCallback() which
// can call 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 && mScraperResults.size() == 0)
onSearchDone(results); onSearchDone(mScraperResults);
else if (status == ASYNC_ERROR)
if (status == ASYNC_DONE && mScraperResults.size() > 0) {
if (mScraperResults.front().mediaURLFetch == COMPLETED) {
onSearchDone(mScraperResults);
}
else {
std::string gameIDs;
for (auto it = mScraperResults.cbegin(); it != mScraperResults.cend(); it++)
gameIDs += it->gameID + ',';
// Remove the last comma
gameIDs.pop_back();
mMDRetrieveURLsHandle = startMediaURLsFetch(gameIDs);
}
}
else if (status == ASYNC_ERROR) {
onSearchError(statusString); onSearchError(statusString);
}
}
if (mMDRetrieveURLsHandle && mMDRetrieveURLsHandle->status() != ASYNC_IN_PROGRESS) {
if (mMDRetrieveURLsHandle->status() == ASYNC_DONE) {
auto status_media = mMDRetrieveURLsHandle->status();
auto results_media = mMDRetrieveURLsHandle->getResults();
auto statusString_media = mMDRetrieveURLsHandle->getStatusString();
auto results_scrape = mScraperResults;
mMDRetrieveURLsHandle.reset();
mScraperResults.clear();
// Combine the intial scrape results with the media URL results.
for (auto it = results_media.cbegin(); it != results_media.cend(); it++) {
for (unsigned int i = 0; i < results_scrape.size(); i++) {
if (results_scrape[i].gameID == it->gameID) {
results_scrape[i].box3dUrl = it->box3dUrl;
results_scrape[i].coverUrl = it->coverUrl;
results_scrape[i].marqueeUrl = it->marqueeUrl;
results_scrape[i].screenshotUrl = it->screenshotUrl;
results_scrape[i].scraperRequestAllowance = it->scraperRequestAllowance;
results_scrape[i].mediaURLFetch = COMPLETED;
}
}
}
onSearchDone(results_scrape);
}
else if (mMDRetrieveURLsHandle->status() == ASYNC_ERROR) {
onSearchError(mMDRetrieveURLsHandle->getStatusString());
mMDRetrieveURLsHandle.reset();
}
} }
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();
result.mediaFilesDownloadStatus = COMPLETED;
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().
@ -448,6 +533,15 @@ 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) {
// Save thumbnail to mScraperResults cache and set the flag that the
// thumbnail download has been completed for this game.
for (auto i = 0; i < mScraperResults.size(); i++) {
if (mScraperResults[i].thumbnailDownloadStatus == IN_PROGRESS) {
mScraperResults[i].ThumbnailImageData = mThumbnailReq->getContent();
mScraperResults[i].thumbnailDownloadStatus = COMPLETED;
}
}
// Activate the thumbnail in the GUI.
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.
@ -458,6 +552,21 @@ void ScraperSearchComponent::updateThumbnail()
} }
mThumbnailReq.reset(); mThumbnailReq.reset();
// When the thumbnail has been downloaded and we are in non-interactive
// mode, we proceed to automatically download the rest of the media files.
// The reason to always complete the thumbnail download first is that it looks
// a lot more consistent in the GUI. And since the thumbnail is being cached
// anyway, this hardly takes any more time. Maybe rather the opposite as the
// image used for the thumbnail (cover or screenshot) would have had to be
// requested from the server again.
if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT &&
mScraperResults.front().thumbnailDownloadStatus == COMPLETED) {
if (mScraperResults.size() == 0)
mSkipCallback();
else
returnResult(mScraperResults.front());
}
} }
void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
@ -468,10 +577,78 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
}; };
stop(); stop();
mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
// Initial value is last search if there was one, otherwise the clean path name. if (params.system->hasPlatformId(PlatformIds::ARCADE) ||
params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride, params.system->hasPlatformId(PlatformIds::NEOGEO)) {
searchForFunc, false, "SEARCH")); mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
// Initial value is last search if there was one, otherwise the clean path name.
// If it's a MAME or Neo Geo game, expand the game name accordingly.
params.nameOverride.empty() ?
MameNames::getInstance()->getCleanName(params.game->getCleanName()) :
params.nameOverride,
searchForFunc, false, "SEARCH"));
}
else {
mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
// Initial value is last search if there was one, otherwise the clean path name.
params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride,
searchForFunc, false, "SEARCH"));
}
}
bool ScraperSearchComponent::saveMetadata(
const ScraperSearchResult& result, MetaDataList& metadata)
{
bool mMetadataUpdated = false;
std::vector<MetaDataDecl> mMetaDataDecl = metadata.getMDD();
for (unsigned int i = 0; i < mMetaDataDecl.size(); i++) {
// Skip elements that are tagged not to be scraped.
if (!mMetaDataDecl.at(i).shouldScrape)
continue;
const std::string& key = mMetaDataDecl.at(i).key;
// Skip element if the setting to not scrape metadata has been set,
// unless its type is rating or name.
if (!Settings::getInstance()->getBool("ScrapeMetadata") &&
(key != "rating" && key != "name"))
continue;
// Skip saving of rating if the corresponding option has been set to false.
if (key == "rating" && !Settings::getInstance()->getBool("ScrapeRatings"))
continue;
// Skip saving of game name if the corresponding option has been set to false.
if (key == "name" && !Settings::getInstance()->getBool("ScrapeGameNames"))
continue;
// Skip elements that are empty.
if (result.mdl.get(key) == "")
continue;
// Skip elements that are the same as the default metadata value.
if (result.mdl.get(key) == mMetaDataDecl.at(i).defaultValue)
continue;
// Skip elements that are identical to the existing value.
if (result.mdl.get(key) == metadata.get(key))
continue;
// Overwrite all the other values if the flag to overwrite data has been set.
if (Settings::getInstance()->getBool("ScraperOverwriteData")) {
metadata.set(key, result.mdl.get(key));
mMetadataUpdated = true;
}
// Else only update the value if it is set to the default metadata value.
else if (metadata.get(key) == mMetaDataDecl.at(i).defaultValue) {
metadata.set(key, result.mdl.get(key));
mMetadataUpdated = true;
}
}
return mMetadataUpdated;
} }
std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts() std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts()

View file

@ -4,7 +4,8 @@
// User interface component for the scraper where the user is able to see an overview // 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. // 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 // Used by both single-game scraping from the GuiMetaDataEd menu as well as
// to resolve scraping conflicts when run from GuiScraperStart. // to resolve scraping conflicts when run from GuiScraperMenu.
// The function to properly save scraped metadata is located here too.
// //
// This component is called from GuiGameScraper for single-game scraping and // This component is called from GuiGameScraper for single-game scraping and
// from GuiScraperMulti for multi-game scraping. // from GuiScraperMulti for multi-game scraping.
@ -41,6 +42,7 @@ public:
void openInputScreen(ScraperSearchParams& from); void openInputScreen(ScraperSearchParams& from);
void stop(); void stop();
inline SearchType getSearchType() const { return mSearchType; } inline SearchType getSearchType() const { return mSearchType; }
static bool saveMetadata(const ScraperSearchResult& result, MetaDataList& metadata);
// Metadata assets will be resolved before calling the accept callback // Metadata assets will be resolved before calling the accept callback
// (e.g. result.mdl's "image" is automatically downloaded and properly set). // (e.g. result.mdl's "image" is automatically downloaded and properly set).
@ -71,6 +73,10 @@ private:
int getSelectedIndex(); int getSelectedIndex();
// For TheGamesDB, retrieve URLs for the additional metadata assets
// that need to be downloaded.
void retrieveMediaURLs(ScraperSearchResult result);
// 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);
@ -111,6 +117,7 @@ private:
bool mBlockAccept; bool mBlockAccept;
std::unique_ptr<ScraperSearchHandle> mSearchHandle; std::unique_ptr<ScraperSearchHandle> mSearchHandle;
std::unique_ptr<ScraperSearchHandle> mMDRetrieveURLsHandle;
std::unique_ptr<MDResolveHandle> mMDResolveHandle; std::unique_ptr<MDResolveHandle> mMDResolveHandle;
std::vector<ScraperSearchResult> mScraperResults; std::vector<ScraperSearchResult> mScraperResults;
std::unique_ptr<HttpReq> mThumbnailReq; std::unique_ptr<HttpReq> mThumbnailReq;

View file

@ -80,7 +80,8 @@ public:
inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; } inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; }
protected: protected:
virtual void onScroll() { navigationsounds.playThemeNavigationSound(SCROLLSOUND); } virtual void onScroll() {
NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); }
virtual void onCursorChanged(const CursorState& state); virtual void onCursorChanged(const CursorState& state);
private: private:
@ -389,7 +390,7 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, c
setFont(Font::getFromTheme(elem, properties, mFont)); setFont(Font::getFromTheme(elem, properties, mFont));
const float selectorHeight = Math::max(mFont->getHeight(1.0), (float)mFont->getSize()) * mLineSpacing; const float selectorHeight = Math::max(mFont->getHeight(1.0), (float)mFont->getSize()) * mLineSpacing;
setSelectorHeight(selectorHeight); setSelectorHeight(selectorHeight);
if(properties & ALIGNMENT) if(properties & ALIGNMENT)
{ {
if(elem->has("alignment")) if(elem->has("alignment"))

View file

@ -103,7 +103,7 @@ GuiGamelistOptions::GuiGamelistOptions(
row.addElement(mJumpToLetterList, false); row.addElement(mJumpToLetterList, false);
row.input_handler = [&](InputConfig* config, Input input) { row.input_handler = [&](InputConfig* config, Input input) {
if (config->isMappedTo("a", input) && input.value) { if (config->isMappedTo("a", input) && input.value) {
navigationsounds.playThemeNavigationSound(SCROLLSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND);
if (mJumpToLetterList->getSelected() == FAVORITE_CHAR) if (mJumpToLetterList->getSelected() == FAVORITE_CHAR)
jumpToFirstRow(); jumpToFirstRow();
else else
@ -196,7 +196,7 @@ GuiGamelistOptions::~GuiGamelistOptions()
// If a new sorting type was selected, then sort and update mSortTypeString for the system. // If a new sorting type was selected, then sort and update mSortTypeString for the system.
if ((*mListSort->getSelected()).description != root->getSortTypeString()) { if ((*mListSort->getSelected()).description != root->getSortTypeString()) {
navigationsounds.playThemeNavigationSound(SCROLLSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND);
// This will also recursively sort children. // This will also recursively sort children.
root->sort(*mListSort->getSelected(), mFavoritesSorting); root->sort(*mListSort->getSelected(), mFavoritesSorting);

View file

@ -14,7 +14,7 @@
#include "guis/GuiDetectDevice.h" #include "guis/GuiDetectDevice.h"
#include "guis/GuiGeneralScreensaverOptions.h" #include "guis/GuiGeneralScreensaverOptions.h"
#include "guis/GuiMsgBox.h" #include "guis/GuiMsgBox.h"
#include "guis/GuiScraperStart.h" #include "guis/GuiScraperMenu.h"
#include "guis/GuiSettings.h" #include "guis/GuiSettings.h"
#include "views/UIModeController.h" #include "views/UIModeController.h"
#include "views/ViewController.h" #include "views/ViewController.h"
@ -36,6 +36,9 @@ GuiMenu::GuiMenu(
{ {
bool isFullUI = UIModeController::getInstance()->isUIModeFull(); bool isFullUI = UIModeController::getInstance()->isUIModeFull();
if (isFullUI)
addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); });
if (isFullUI) if (isFullUI)
addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); });
@ -45,9 +48,6 @@ GuiMenu::GuiMenu(
addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] {
openCollectionSystemSettings(); }); openCollectionSystemSettings(); });
if (isFullUI)
addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); });
if (isFullUI) if (isFullUI)
addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); });
@ -65,44 +65,8 @@ GuiMenu::GuiMenu(
void GuiMenu::openScraperSettings() void GuiMenu::openScraperSettings()
{ {
auto s = new GuiSettings(mWindow, "SCRAPER"); // Open the scrape menu.
mWindow->pushGui(new GuiScraperMenu(mWindow));
// Scrape from.
auto scraper_list = std::make_shared< OptionListComponent< std::string >
>(mWindow, "SCRAPE FROM", false);
std::vector<std::string> scrapers = getScraperList();
// Select either the first entry or the one read from the settings,
// just in case the scraper from settings has vanished.
for (auto it = scrapers.cbegin(); it != scrapers.cend(); it++)
scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper"));
s->addWithLabel("SCRAPE FROM", scraper_list);
s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper",
scraper_list->getSelected()); });
// Scrape ratings.
auto scrape_ratings = std::make_shared<SwitchComponent>(mWindow);
scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings"));
s->addWithLabel("SCRAPE RATINGS", scrape_ratings);
s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings",
scrape_ratings->getState()); });
// Scrape now.
ComponentListRow row;
auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); };
std::function<void()> openAndSave = openScrapeNow;
openAndSave = [s, openAndSave] { s->save(); openAndSave(); };
row.makeAcceptInputHandler(openAndSave);
auto scrape_now = std::make_shared<TextComponent>
(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
auto bracket = makeArrow(mWindow);
row.addElement(scrape_now, true);
row.addElement(bracket, false);
s->addRow(row);
mWindow->pushGui(s);
} }
void GuiMenu::openSoundSettings() void GuiMenu::openSoundSettings()
@ -174,28 +138,6 @@ void GuiMenu::openSoundSettings()
}); });
#endif #endif
// Video audio.
auto video_audio = std::make_shared<SwitchComponent>(mWindow);
video_audio->setState(Settings::getInstance()->getBool("VideoAudio"));
s->addWithLabel("ENABLE AUDIO FOR VIDEO FILES", video_audio);
s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio",
video_audio->getState()); });
// Navigation sounds.
auto sounds_enabled = std::make_shared<SwitchComponent>(mWindow);
sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds"));
s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled);
s->addSaveFunc([sounds_enabled] {
if (sounds_enabled->getState()
&& !Settings::getInstance()->getBool("EnableSounds")
&& PowerSaver::getMode() == PowerSaver::INSTANT)
{
Settings::getInstance()->setString("PowerSaverMode", "default");
PowerSaver::init();
}
Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState());
});
#ifdef _RPI_ #ifdef _RPI_
// OMX player Audio Device // OMX player Audio Device
auto omx_audio_dev = std::make_shared< OptionListComponent<std::string> auto omx_audio_dev = std::make_shared< OptionListComponent<std::string>
@ -221,6 +163,27 @@ void GuiMenu::openSoundSettings()
Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected());
}); });
#endif #endif
// Video audio.
auto video_audio = std::make_shared<SwitchComponent>(mWindow);
video_audio->setState(Settings::getInstance()->getBool("VideoAudio"));
s->addWithLabel("ENABLE AUDIO FOR VIDEO FILES", video_audio);
s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio",
video_audio->getState()); });
// Navigation sounds.
auto sounds_enabled = std::make_shared<SwitchComponent>(mWindow);
sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds"));
s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled);
s->addSaveFunc([sounds_enabled] {
if (sounds_enabled->getState() &&
!Settings::getInstance()->getBool("EnableSounds") &&
PowerSaver::getMode() == PowerSaver::INSTANT) {
Settings::getInstance()->setString("PowerSaverMode", "default");
PowerSaver::init();
}
Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState());
});
} }
mWindow->pushGui(s); mWindow->pushGui(s);
@ -281,9 +244,9 @@ void GuiMenu::openUISettings()
getString("TransitionStyle") == *it); getString("TransitionStyle") == *it);
s->addWithLabel("TRANSITION STYLE", transition_style); s->addWithLabel("TRANSITION STYLE", transition_style);
s->addSaveFunc([transition_style] { s->addSaveFunc([transition_style] {
if (Settings::getInstance()->getString("TransitionStyle") == "instant" if (Settings::getInstance()->getString("TransitionStyle") == "instant" &&
&& transition_style->getSelected() != "instant" transition_style->getSelected() != "instant" &&
&& PowerSaver::getMode() == PowerSaver::INSTANT) { PowerSaver::getMode() == PowerSaver::INSTANT) {
Settings::getInstance()->setString("PowerSaverMode", "default"); Settings::getInstance()->setString("PowerSaverMode", "default");
PowerSaver::init(); PowerSaver::init();
} }
@ -315,8 +278,7 @@ void GuiMenu::openUISettings()
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
if (needReload) if (needReload) {
{
Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme);
CollectionSystemManager::get()->updateSystemsList(); CollectionSystemManager::get()->updateSystemsList();
ViewController::get()->goToStart(); ViewController::get()->goToStart();
@ -404,9 +366,9 @@ void GuiMenu::openUISettings()
move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel"));
s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel);
s->addSaveFunc([move_carousel] { s->addSaveFunc([move_carousel] {
if (move_carousel->getState() if (move_carousel->getState() &&
&& !Settings::getInstance()->getBool("MoveCarousel") !Settings::getInstance()->getBool("MoveCarousel") &&
&& PowerSaver::getMode() == PowerSaver::INSTANT) { PowerSaver::getMode() == PowerSaver::INSTANT) {
Settings::getInstance()->setString("PowerSaverMode", "default"); Settings::getInstance()->setString("PowerSaverMode", "default");
PowerSaver::init(); PowerSaver::init();
} }
@ -460,8 +422,8 @@ void GuiMenu::openOtherSettings()
fullscreen_mode->add(*it, *it, Settings::getInstance()->getString("FullscreenMode") == *it); fullscreen_mode->add(*it, *it, Settings::getInstance()->getString("FullscreenMode") == *it);
s->addWithLabel("FULLSCREEN MODE (REQUIRES RESTART)", fullscreen_mode); s->addWithLabel("FULLSCREEN MODE (REQUIRES RESTART)", fullscreen_mode);
s->addSaveFunc([fullscreen_mode] { s->addSaveFunc([fullscreen_mode] {
if (Settings::getInstance()->getString("FullscreenMode") == "normal" if (Settings::getInstance()->getString("FullscreenMode") == "normal" &&
&& fullscreen_mode->getSelected() != "normal") { fullscreen_mode->getSelected() != "normal") {
Settings::getInstance()->setString("PowerSaverMode", "default"); Settings::getInstance()->setString("PowerSaverMode", "default");
PowerSaver::init(); PowerSaver::init();
} }
@ -542,7 +504,7 @@ void GuiMenu::openOtherSettings()
auto local_art = std::make_shared<SwitchComponent>(mWindow); auto local_art = std::make_shared<SwitchComponent>(mWindow);
local_art->setState(Settings::getInstance()->getBool("LocalArt")); local_art->setState(Settings::getInstance()->getBool("LocalArt"));
s->addWithLabel("SEARCH FOR GAME ART IN ROM DIRECTORIES", local_art); s->addWithLabel("DISPLAY GAME ART FROM ROM DIRECTORIES", local_art);
s->addSaveFunc([local_art] { Settings::getInstance()-> s->addSaveFunc([local_art] { Settings::getInstance()->
setBool("LocalArt", local_art->getState()); }); setBool("LocalArt", local_art->getState()); });
@ -583,7 +545,7 @@ void GuiMenu::openConfigInput()
Window* window = mWindow; Window* window = mWindow;
window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES",
[window] { [window] {
window->pushGui(new GuiDetectDevice(window, false, nullptr)); window->pushGui(new GuiDetectDevice(window, false, nullptr));
}, "NO", nullptr) }, "NO", nullptr)
); );
} }
@ -600,8 +562,8 @@ void GuiMenu::openQuitMenu()
row.makeAcceptInputHandler([window] { row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES", window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES",
[] { [] {
Scripting::fireEvent("quit"); Scripting::fireEvent("quit");
quitES(); quitES();
}, "NO", nullptr)); }, "NO", nullptr));
}); });
row.addElement(std::make_shared<TextComponent>(window, "QUIT EMULATIONSTATION", row.addElement(std::make_shared<TextComponent>(window, "QUIT EMULATIONSTATION",
@ -615,10 +577,10 @@ void GuiMenu::openQuitMenu()
row.makeAcceptInputHandler([window] { row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY REBOOT?", "YES", window->pushGui(new GuiMsgBox(window, "REALLY REBOOT?", "YES",
[] { [] {
Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("quit", "reboot");
Scripting::fireEvent("reboot"); Scripting::fireEvent("reboot");
if (quitES(QuitMode::REBOOT) != 0) if (quitES(QuitMode::REBOOT) != 0)
LOG(LogWarning) << "Reboot terminated with non-zero result!"; LOG(LogWarning) << "Reboot terminated with non-zero result!";
}, "NO", nullptr)); }, "NO", nullptr));
}); });
row.addElement(std::make_shared<TextComponent>(window, "REBOOT SYSTEM", row.addElement(std::make_shared<TextComponent>(window, "REBOOT SYSTEM",
@ -631,10 +593,10 @@ void GuiMenu::openQuitMenu()
row.makeAcceptInputHandler([window] { row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY POWER OFF?", "YES", window->pushGui(new GuiMsgBox(window, "REALLY POWER OFF?", "YES",
[] { [] {
Scripting::fireEvent("quit", "poweroff"); Scripting::fireEvent("quit", "poweroff");
Scripting::fireEvent("poweroff"); Scripting::fireEvent("poweroff");
if (quitES(QuitMode::POWEROFF) != 0) if (quitES(QuitMode::POWEROFF) != 0)
LOG(LogWarning) << "Power off terminated with non-zero result!"; LOG(LogWarning) << "Power off terminated with non-zero result!";
}, "NO", nullptr)); }, "NO", nullptr));
}); });
row.addElement(std::make_shared<TextComponent>(window, "POWER OFF SYSTEM", row.addElement(std::make_shared<TextComponent>(window, "POWER OFF SYSTEM",
@ -668,11 +630,8 @@ void GuiMenu::onSizeChanged()
mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); mVersion.setPosition(0, mSize.y() - mVersion.getSize().y());
} }
void GuiMenu::addEntry( void GuiMenu::addEntry(const char* name, unsigned int color,
const char* name, bool add_arrow, const std::function<void()>& func)
unsigned int color,
bool add_arrow,
const std::function<void()>& func)
{ {
std::shared_ptr<Font> font = Font::get(FONT_SIZE_MEDIUM); std::shared_ptr<Font> font = Font::get(FONT_SIZE_MEDIUM);

View file

@ -46,7 +46,8 @@ GuiMetaDataEd::GuiMetaDataEd(
mMetaDataDecl(mdd), mMetaDataDecl(mdd),
mMetaData(md), mMetaData(md),
mSavedCallback(saveCallback), mSavedCallback(saveCallback),
mDeleteFunc(deleteFunc) mDeleteFunc(deleteFunc),
mMetadataUpdated(false)
{ {
addChild(&mBackground); addChild(&mBackground);
addChild(&mGrid); addChild(&mGrid);
@ -82,7 +83,6 @@ GuiMetaDataEd::GuiMetaDataEd(
assert(ed); assert(ed);
ed->setValue(mMetaData->get(iter->key)); ed->setValue(mMetaData->get(iter->key));
mEditors.push_back(ed); mEditors.push_back(ed);
continue; continue;
} }
@ -282,22 +282,41 @@ void GuiMetaDataEd::fetch()
void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
{ {
for (unsigned int i = 0; i < mEditors.size(); i++) { // Clone the mMetaData object.
if (mMetaDataDecl.at(i).isStatistic) MetaDataList* metadata = nullptr;
continue; metadata = new MetaDataList(*mMetaData);
mMetadataUpdated = ScraperSearchComponent::saveMetadata(result, *metadata);
// Update the list with the scraped metadata values.
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;
mEditors.at(i)->setValue(result.mdl.get(key)); // if (mEditors.at(i)->getValue() != metadata->get(key)) {
// mEditors.at(i)->setOpacity(150);
// }
mEditors.at(i)->setValue(metadata->get(key));
} }
delete metadata;
} }
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 = mMetadataUpdated;
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()) { std::string mMetaDataValue = mMetaData->get(key);
std::string mEditorsValue = mEditors.at(i)->getValue();
// Incredibly ugly workaround to avoid the "SAVE CHANGES?" window for games
// with mising metadata for rating and release date.
if (key == "rating" && (mMetaDataValue == "" || mMetaDataValue == "0.000000"))
mMetaDataValue = "0";
if (key == "releasedate" && (mMetaDataValue == "" || mMetaDataValue == "not-a-date-time"))
mMetaDataValue = "19700101T010000";
if (mMetaDataValue != mEditorsValue) {
dirty = true; dirty = true;
break; break;
} }
@ -315,7 +334,6 @@ void GuiMetaDataEd::close(bool closeAllWindows)
}; };
} }
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.

View file

@ -58,6 +58,8 @@ private:
MetaDataList* mMetaData; MetaDataList* mMetaData;
std::function<void()> mSavedCallback; std::function<void()> mSavedCallback;
std::function<void()> mDeleteFunc; std::function<void()> mDeleteFunc;
bool mMetadataUpdated;
}; };
#endif // ES_APP_GUIS_GUI_META_DATA_ED_H #endif // ES_APP_GUIS_GUI_META_DATA_ED_H

View file

@ -0,0 +1,319 @@
//
// GuiScraperMenu.cpp
//
// Game media scraper, including settings as well as the scraping start button.
// Submenu to the GuiMenu main menu.
// Will call GuiScraperMulti to perform the actual scraping.
//
#include "guis/GuiScraperMenu.h"
#include "components/OptionListComponent.h"
#include "components/SwitchComponent.h"
#include "guis/GuiMsgBox.h"
#include "guis/GuiScraperMulti.h"
#include "views/ViewController.h"
#include "FileData.h"
#include "SystemData.h"
#include "guis/GuiSettings.h"
GuiScraperMenu::GuiScraperMenu(Window* window) : GuiComponent(window),
mMenu(window, "SCRAPER")
{
// Scrape from.
auto scraper_list = std::make_shared< OptionListComponent<std::string>>
(mWindow, "SCRAPE FROM", false);
std::vector<std::string> scrapers = getScraperList();
// Select either the first entry or the one read from the settings,
// just in case the scraper from settings has vanished.
for (auto it = scrapers.cbegin(); it != scrapers.cend(); it++)
scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper"));
mMenu.addWithLabel("SCRAPE FROM", scraper_list);
mMenu.addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper",
scraper_list->getSelected()); });
// Search filters, getSearches() will generate a queue of games to scrape
// based on the outcome of the checks below.
mFilters = std::make_shared< OptionListComponent<GameFilterFunc>>
(mWindow, "SCRAPE THESE GAMES", false);
mFilters->add("ALL GAMES",
[](SystemData*, FileData*) -> bool { return true; }, true);
mFilters->add("NO METADATA",
[](SystemData*, FileData* g) -> bool {
return g->metadata.get("desc").empty(); }, false);
mFilters->add("NO GAME IMAGE",
[](SystemData*, FileData* g) -> bool {
return g->getImagePath().empty(); }, false);
mMenu.addWithLabel("Filter", mFilters);
// Add systems (all systems with an existing platform ID are listed).
mSystems = std::make_shared< OptionListComponent<SystemData*>>
(mWindow, "SCRAPE THESE SYSTEMS", true);
for (unsigned int i = 0; i < SystemData::sSystemVector.size(); i++) {
if (!SystemData::sSystemVector[i]->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) {
mSystems->add(SystemData::sSystemVector[i]->getFullName(),
SystemData::sSystemVector[i],
!SystemData::sSystemVector[i]->getPlatformIds().empty());
SystemData::sSystemVector[i]->getScrapeFlag() ?
mSystems->selectEntry(i) : mSystems->unselectEntry(i);
}
}
mMenu.addWithLabel("Systems", mSystems);
addEntry("CONTENT SETTINGS", 0x777777FF, true, [this] { openContentSettings(); });
addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); });
addChild(&mMenu);
mMenu.addButton("START", "start", std::bind(&GuiScraperMenu::pressedStart, this));
mMenu.addButton("BACK", "back", [&] { delete this; });
setSize(mMenu.getSize());
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2,
Renderer::getScreenHeight() * 0.15f);
}
GuiScraperMenu::~GuiScraperMenu()
{
// Save the scrape flags to the system settings so that they are
// remembered throughout the program session.
std::vector<SystemData*> sys = mSystems->getSelectedObjects();
for (auto it = SystemData::sSystemVector.cbegin();
it != SystemData::sSystemVector.cend(); it++) {
(*it)->setScrapeFlag(false);
for (auto it_sys = sys.cbegin(); it_sys != sys.cend(); it_sys++) {
if ((*it)->getFullName() == (*it_sys)->getFullName())
(*it)->setScrapeFlag(true);
}
}
}
void GuiScraperMenu::openContentSettings()
{
auto s = new GuiSettings(mWindow, "SCRAPER CONTENT SETTINGS");
// Scrape metadata.
auto scrape_metadata = std::make_shared<SwitchComponent>(mWindow);
scrape_metadata->setState(Settings::getInstance()->getBool("ScrapeMetadata"));
s->addWithLabel("SCRAPE METADATA", scrape_metadata);
s->addSaveFunc([scrape_metadata] { Settings::getInstance()->setBool("ScrapeMetadata",
scrape_metadata->getState()); });
// Scrape game names.
auto scrape_gamename = std::make_shared<SwitchComponent>(mWindow);
scrape_gamename->setState(Settings::getInstance()->getBool("ScrapeGameNames"));
s->addWithLabel("SCRAPE GAME NAMES", scrape_gamename);
s->addSaveFunc([scrape_gamename] { Settings::getInstance()->setBool("ScrapeGameNames",
scrape_gamename->getState()); });
// Scrape ratings.
auto scrape_ratings = std::make_shared<SwitchComponent>(mWindow);
scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings"));
s->addWithLabel("SCRAPE RATINGS", scrape_ratings);
s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings",
scrape_ratings->getState()); });
// Scrape screenshots images.
auto scrape_screenshots = std::make_shared<SwitchComponent>(mWindow);
scrape_screenshots->setState(Settings::getInstance()->getBool("ScrapeScreenshots"));
s->addWithLabel("SCRAPE SCREENSHOT IMAGES", scrape_screenshots);
s->addSaveFunc([scrape_screenshots] { Settings::getInstance()->setBool("ScrapeScreenshots",
scrape_screenshots->getState()); });
// Scrape cover images.
auto scrape_covers = std::make_shared<SwitchComponent>(mWindow);
scrape_covers->setState(Settings::getInstance()->getBool("ScrapeCovers"));
s->addWithLabel("SCRAPE BOX COVER IMAGES", scrape_covers);
s->addSaveFunc([scrape_covers] { Settings::getInstance()->setBool("ScrapeCovers",
scrape_covers->getState()); });
// Scrape marquee images.
auto scrape_marquees = std::make_shared<SwitchComponent>(mWindow);
scrape_marquees->setState(Settings::getInstance()->getBool("ScrapeMarquees"));
s->addWithLabel("SCRAPE MARQUEE (WHEEL) IMAGES", scrape_marquees);
s->addSaveFunc([scrape_marquees] { Settings::getInstance()->setBool("ScrapeMarquees",
scrape_marquees->getState()); });
// Scrape 3D box images.
auto scrape_3dboxes = std::make_shared<SwitchComponent>(mWindow);
scrape_3dboxes->setState(Settings::getInstance()->getBool("Scrape3DBoxes"));
s->addWithLabel("SCRAPE 3D BOX IMAGES", scrape_3dboxes);
s->addSaveFunc([scrape_3dboxes] { Settings::getInstance()->setBool("Scrape3DBoxes",
scrape_3dboxes->getState()); });
mWindow->pushGui(s);
}
void GuiScraperMenu::openOtherSettings()
{
auto s = new GuiSettings(mWindow, "OTHER SCRAPER SETTINGS");
// Scraper region.
auto scraper_region = std::make_shared<OptionListComponent<std::string>>
(mWindow, "REGION", false);
std::vector<std::string> transitions_rg;
transitions_rg.push_back("eu");
transitions_rg.push_back("jp");
transitions_rg.push_back("us");
transitions_rg.push_back("ss");
transitions_rg.push_back("wor");
if (Settings::getInstance()->getString("ScraperRegion") != "") {
if (std::find(transitions_rg.begin(), transitions_rg.end(),
Settings::getInstance()->getString("ScraperRegion")) == transitions_rg.end()) {
transitions_rg.push_back(Settings::getInstance()->getString("ScraperRegion"));
}
}
for (auto it = transitions_rg.cbegin(); it != transitions_rg.cend(); it++)
scraper_region->add(*it, *it, Settings::getInstance()->getString("ScraperRegion") == *it);
s->addWithLabel("REGION", scraper_region);
s->addSaveFunc([scraper_region] {
Settings::getInstance()->setString("ScraperRegion", scraper_region->getSelected());
});
// Scraper language.
auto scraper_language = std::make_shared<OptionListComponent<std::string>>
(mWindow, "LANGUAGE", false);
std::vector<std::string> transitions_lg;
transitions_lg.push_back("en");
transitions_lg.push_back("wor");
if (Settings::getInstance()->getString("ScraperLanguage") != "") {
if (std::find(transitions_lg.begin(), transitions_lg.end(),
Settings::getInstance()->getString("ScraperLanguage")) == transitions_lg.end()) {
transitions_lg.push_back(Settings::getInstance()->getString("ScraperLanguage"));
}
}
for (auto it = transitions_lg.cbegin(); it != transitions_lg.cend(); it++)
scraper_language->add(*it, *it,
Settings::getInstance()->getString("ScraperLanguage") == *it);
s->addWithLabel("LANGUAGE", scraper_language);
s->addSaveFunc([scraper_language] {
Settings::getInstance()->setString("ScraperLanguage", scraper_language->getSelected());
});
// Overwrite files and data.
auto scrape_overwrite = std::make_shared<SwitchComponent>(mWindow);
scrape_overwrite->setState(Settings::getInstance()->getBool("ScraperOverwriteData"));
s->addWithLabel("OVERWRITE FILES AND DATA", scrape_overwrite);
s->addSaveFunc([scrape_overwrite] { Settings::getInstance()->setBool("ScraperOverwriteData",
scrape_overwrite->getState()); });
// Automatic scraping.
auto scraper_interactive = std::make_shared<SwitchComponent>(mWindow);
scraper_interactive->setState(Settings::getInstance()->getBool("ScraperInteractive"));
s->addWithLabel("INTERACTIVE MODE", scraper_interactive);
s->addSaveFunc([scraper_interactive] { Settings::getInstance()->setBool("ScraperInteractive",
scraper_interactive->getState()); });
mWindow->pushGui(s);
}
void GuiScraperMenu::pressedStart()
{
// Save any GUI settings that may have been modified.
mMenu.save();
std::vector<SystemData*> sys = mSystems->getSelectedObjects();
for (auto it = sys.cbegin(); it != sys.cend(); it++) {
if ((*it)->getPlatformIds().empty()) {
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?"),
"YES", std::bind(&GuiScraperMenu::start, this),
"NO", nullptr));
return;
}
}
start();
}
void GuiScraperMenu::start()
{
std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(),
mFilters->getSelected());
if (searches.empty()) {
mWindow->pushGui(new GuiMsgBox(mWindow,
"NO GAMES TO SCRAPE"));
}
else {
bool testbool = Settings::getInstance()->getBool("ScraperInteractive");
GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches,
Settings::getInstance()->getBool("ScraperInteractive"));
mWindow->pushGui(gsm);
delete this;
}
}
std::queue<ScraperSearchParams> GuiScraperMenu::getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector)
{
std::queue<ScraperSearchParams> queue;
for (auto sys = systems.cbegin(); sys != systems.cend(); sys++) {
std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME);
for (auto game = games.cbegin(); game != games.cend(); game++) {
if (selector((*sys), (*game))) {
ScraperSearchParams search;
search.game = *game;
search.system = *sys;
queue.push(search);
}
}
}
return queue;
}
void GuiScraperMenu::addEntry(const char* name, unsigned int color,
bool add_arrow, const std::function<void()>& func)
{
std::shared_ptr<Font> font = Font::get(FONT_SIZE_MEDIUM);
// Populate the list.
ComponentListRow row;
row.addElement(std::make_shared<TextComponent>(mWindow, name, font, color), true);
if (add_arrow) {
std::shared_ptr<ImageComponent> bracket = makeArrow(mWindow);
row.addElement(bracket, false);
}
row.makeAcceptInputHandler(func);
mMenu.addRow(row);
}
bool GuiScraperMenu::input(InputConfig* config, Input input)
{
if (GuiComponent::input(config, input))
return true;
if ((config->isMappedTo("b", input) || config->isMappedTo("start", input)) &&
input.value != 0) {
delete this;
return true;
}
return false;
}
HelpStyle GuiScraperMenu::getHelpStyle()
{
HelpStyle style = HelpStyle();
style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system");
return style;
}
std::vector<HelpPrompt> GuiScraperMenu::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("up/down", "choose"));
prompts.push_back(HelpPrompt("a", "select"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

View file

@ -0,0 +1,53 @@
//
// GuiScraperMenu.h
//
// Game media scraper, including settings as well as the scraping start button.
// Submenu to the GuiMenu main menu.
// Will call GuiScraperMulti to perform the actual scraping.
//
#pragma once
#ifndef ES_APP_GUIS_GUI_SCRAPER_MENU_H
#define ES_APP_GUIS_GUI_SCRAPER_MENU_H
#include "components/MenuComponent.h"
#include "scrapers/Scraper.h"
class FileData;
template<typename T>
class OptionListComponent;
class SwitchComponent;
class SystemData;
typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc;
class GuiScraperMenu : public GuiComponent
{
public:
GuiScraperMenu(Window* window);
~GuiScraperMenu();
bool input(InputConfig* config, Input input) override;
std::vector<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override;
private:
void pressedStart();
void start();
void addEntry(const char* name, unsigned int color,
bool add_arrow, const std::function<void()>& func);
void openContentSettings();
void openOtherSettings();
std::queue<ScraperSearchParams> getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector);
std::shared_ptr<OptionListComponent<GameFilterFunc>> mFilters;
std::shared_ptr<OptionListComponent<SystemData*>> mSystems;
MenuComponent mMenu;
};
#endif // ES_APP_GUIS_GUI_SCRAPER_MENU_H

View file

@ -3,7 +3,7 @@
// //
// Multiple game scraping user interface. // Multiple game scraping user interface.
// Shows the progress for the scraping as it's running. // Shows the progress for the scraping as it's running.
// This interface is triggered from the GuiScraperStart menu. // This interface is triggered from GuiScraperMenu.
// ScraperSearchComponent is called from here. // ScraperSearchComponent is called from here.
// //
@ -136,7 +136,8 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result)
{ {
ScraperSearchParams& search = mSearchQueue.front(); ScraperSearchParams& search = mSearchQueue.front();
search.game->metadata = result.mdl; ScraperSearchComponent::saveMetadata(result, search.game->metadata);
updateGamelist(search.system); updateGamelist(search.system);
mSearchQueue.pop(); mSearchQueue.pop();
@ -157,15 +158,15 @@ 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 { else {
ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") << ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") <<
" SUCCESSFULLY SCRAPED!"; " SUCCESSFULLY SCRAPED";
if (mTotalSkipped > 0) if (mTotalSkipped > 0)
ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") << ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") <<
" SKIPPED."; " SKIPPED";
} }
mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), "OK", [&] { mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), "OK", [&] {

View file

@ -3,7 +3,7 @@
// //
// Multiple game scraping user interface. // Multiple game scraping user interface.
// Shows the progress for the scraping as it's running. // Shows the progress for the scraping as it's running.
// This interface is triggered from the GuiScraperStart menu. // This interface is triggered from GuiScraperMenu.
// ScraperSearchComponent is called from here. // ScraperSearchComponent is called from here.
// //
@ -15,6 +15,7 @@
#include "components/NinePatchComponent.h" #include "components/NinePatchComponent.h"
#include "scrapers/Scraper.h" #include "scrapers/Scraper.h"
#include "GuiComponent.h" #include "GuiComponent.h"
#include "MetaData.h"
class ScraperSearchComponent; class ScraperSearchComponent;
class TextComponent; class TextComponent;
@ -44,6 +45,7 @@ private:
unsigned int mTotalSuccessful; unsigned int mTotalSuccessful;
unsigned int mTotalSkipped; unsigned int mTotalSkipped;
std::queue<ScraperSearchParams> mSearchQueue; std::queue<ScraperSearchParams> mSearchQueue;
std::vector<MetaDataDecl> mMetaDataDecl;
NinePatchComponent mBackground; NinePatchComponent mBackground;
ComponentGrid mGrid; ComponentGrid mGrid;

View file

@ -1,134 +0,0 @@
//
// 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 "components/OptionListComponent.h"
#include "components/SwitchComponent.h"
#include "guis/GuiMsgBox.h"
#include "guis/GuiScraperMulti.h"
#include "views/ViewController.h"
#include "FileData.h"
#include "SystemData.h"
GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window),
mMenu(window, "SCRAPE NOW")
{
addChild(&mMenu);
// Add filters (with first one selected).
mFilters = std::make_shared< OptionListComponent<GameFilterFunc>
>(mWindow, "SCRAPE THESE GAMES", false);
mFilters->add("All Games",
[](SystemData*, FileData*) -> bool { return true; }, false);
mFilters->add("Only missing image",
[](SystemData*, FileData* g) -> bool {
return g->metadata.get("image").empty(); }, true);
mMenu.addWithLabel("Filter", mFilters);
// Add systems (all systems with an existing platform ID are listed).
mSystems = std::make_shared< OptionListComponent<SystemData*>
>(mWindow, "SCRAPE THESE SYSTEMS", true);
for (auto it = SystemData::sSystemVector.cbegin();
it != SystemData::sSystemVector.cend(); it++) {
if (!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty());
}
mMenu.addWithLabel("Systems", mSystems);
mApproveResults = std::make_shared<SwitchComponent>(mWindow);
mApproveResults->setState(true);
mMenu.addWithLabel("User decides on conflicts", mApproveResults);
mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this));
mMenu.addButton("BACK", "back", [&] { delete this; });
mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2,
Renderer::getScreenHeight() * 0.15f);
}
void GuiScraperStart::pressedStart()
{
std::vector<SystemData*> sys = mSystems->getSelectedObjects();
for (auto it = sys.cbegin(); it != sys.cend(); it++) {
if ((*it)->getPlatformIds().empty()) {
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?"),
"YES", std::bind(&GuiScraperStart::start, this),
"NO", nullptr));
return;
}
}
start();
}
void GuiScraperStart::start()
{
std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(),
mFilters->getSelected());
if (searches.empty()) {
mWindow->pushGui(new GuiMsgBox(mWindow,
"NO GAMES FIT THAT CRITERIA."));
}
else {
GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState());
mWindow->pushGui(gsm);
delete this;
}
}
std::queue<ScraperSearchParams> GuiScraperStart::getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector)
{
std::queue<ScraperSearchParams> queue;
for (auto sys = systems.cbegin(); sys != systems.cend(); sys++) {
std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME);
for (auto game = games.cbegin(); game != games.cend(); game++) {
if (selector((*sys), (*game))) {
ScraperSearchParams search;
search.game = *game;
search.system = *sys;
queue.push(search);
}
}
}
return queue;
}
bool GuiScraperStart::input(InputConfig* config, Input input)
{
bool consumed = GuiComponent::input(config, input);
if (consumed)
return true;
if (input.value != 0 && config->isMappedTo("b", input)) {
delete this;
return true;
}
if (config->isMappedTo("start", input) && input.value != 0) {
// Close everything.
Window* window = mWindow;
while (window->peekGui() && window->peekGui() != ViewController::get())
delete window->peekGui();
}
return false;
}
std::vector<HelpPrompt> GuiScraperStart::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

View file

@ -1,49 +0,0 @@
//
// GuiScraperStart.h
//
// Submenu to the GuiMenu main menu.
// Configuration options for the scraper and start button to intiate the scraping.
//
#pragma once
#ifndef ES_APP_GUIS_GUI_SCRAPER_START_H
#define ES_APP_GUIS_GUI_SCRAPER_START_H
#include "components/MenuComponent.h"
#include "scrapers/Scraper.h"
class FileData;
template<typename T>
class OptionListComponent;
class SwitchComponent;
class SystemData;
typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc;
// The starting point for a multi-game scrape.
// Allows the user to set various parameters (filters, which systems to scrape and
// whether to use manual mode). Generates a list of "searches" that will be carried
// out by GuiScraperLog.
class GuiScraperStart : public GuiComponent
{
public:
GuiScraperStart(Window* window);
bool input(InputConfig* config, Input input) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void pressedStart();
void start();
std::queue<ScraperSearchParams> getSearches(
std::vector<SystemData*> systems, GameFilterFunc selector);
std::shared_ptr< OptionListComponent<GameFilterFunc> > mFilters;
std::shared_ptr< OptionListComponent<SystemData*> > mSystems;
std::shared_ptr<SwitchComponent> mApproveResults;
MenuComponent mMenu;
};
#endif // ES_APP_GUIS_GUI_SCRAPER_START_H

View file

@ -16,6 +16,7 @@
#include "PlatformId.h" #include "PlatformId.h"
#include "Settings.h" #include "Settings.h"
#include "SystemData.h" #include "SystemData.h"
#include "MameNames.h"
#include "utils/TimeUtil.h" #include "utils/TimeUtil.h"
#include <pugixml/src/pugixml.hpp> #include <pugixml/src/pugixml.hpp>
@ -118,23 +119,30 @@ void thegamesdb_generate_json_scraper_requests(
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,"
"platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&" "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&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(); // If it's an arcade game (MAME or Neo Geo) then use the regular name.
if (params.system->hasPlatformId(PlatformIds::ARCADE) ||
params.system->hasPlatformId(PlatformIds::NEOGEO)) {
cleanName = params.game->getName();
cleanName = MameNames::getInstance()->getCleanName(params.game->getCleanName());
}
else
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&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 {
@ -165,6 +173,21 @@ void thegamesdb_generate_json_scraper_requests(
} }
} }
void thegamesdb_generate_json_scraper_requests(
const std::string& gameIDs,
std::queue<std::unique_ptr<ScraperRequest>>& requests,
std::vector<ScraperSearchResult>& results)
{
resources.prepare();
std::string path = "https://api.thegamesdb.net/v1";
const std::string apiKey = std::string("apikey=") + resources.getApiKey();
path += "/Games/Images/GamesImages?" + apiKey + "&games_id=" + gameIDs;
requests.push(std::unique_ptr<ScraperRequest>
(new TheGamesDBJSONRequest(requests, results, path)));
}
namespace namespace
{ {
@ -194,21 +217,6 @@ int getIntOrThrow(const Value& v)
return v.GetInt(); return v.GetInt();
} }
std::string getBoxartImage(const Value& v)
{
if (!v.IsArray() || v.Size() == 0)
return "";
for (int i = 0; i < (int)v.Size(); ++i) {
auto& im = v[i];
std::string type = getStringOrThrow(im, "type");
std::string side = getStringOrThrow(im, "side");
if (type == "boxart" && side == "front")
return getStringOrThrow(im, "filename");
}
return getStringOrThrow(v[0], "filename");
}
std::string getDeveloperString(const Value& v) std::string getDeveloperString(const Value& v)
{ {
if (!v.IsArray()) if (!v.IsArray())
@ -274,13 +282,13 @@ std::string getGenreString(const Value& v)
return out; return out;
} }
void processGame(const Value& game, const Value& boxart, std::vector<ScraperSearchResult>& results) void processGame(const Value& game, std::vector<ScraperSearchResult>& results)
{ {
std::string baseImageUrlThumb = getStringOrThrow(boxart["base_url"], "thumb");
std::string baseImageUrlLarge = getStringOrThrow(boxart["base_url"], "large");
ScraperSearchResult result; ScraperSearchResult result;
if (game.HasMember("id") && game["id"].IsInt())
result.gameID = std::to_string(getIntOrThrow(game, "id"));
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());
@ -301,19 +309,52 @@ void processGame(const Value& game, const Value& boxart, std::vector<ScraperSear
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()) { result.mediaURLFetch = NOT_STARTED;
std::string id = std::to_string(getIntOrThrow(game, "id"));
if (boxart["data"].HasMember(id.c_str())) {
std::string image = getBoxartImage(boxart["data"][id.c_str()]);
result.thumbnailUrl = baseImageUrlThumb + "/" + image;
result.imageUrl = baseImageUrlLarge + "/" + image;
}
}
results.push_back(result); results.push_back(result);
} }
} // namespace } // namespace
void processMediaURLs(const Value& images, const std::string& base_url,
std::vector<ScraperSearchResult>& results)
{
ScraperSearchResult result;
// Step through each game ID in the JSON server response.
for (auto it = images.MemberBegin(); it != images.MemberEnd(); it++) {
result.gameID = it->name.GetString();
const Value& gameMedia = images[it->name];
result.coverUrl = "";
result.marqueeUrl = "";
result.screenshotUrl = "";
// Quite excessive testing for valid values, but you never know
// what the server has returned and we don't want to crash the
// program due to malformed data.
if (gameMedia.IsArray()) {
for (SizeType i = 0; i < gameMedia.Size(); i++) {
std::string mediatype;
std::string mediaside;
if (gameMedia[i]["type"].IsString())
mediatype = gameMedia[i]["type"].GetString();
if (gameMedia[i]["side"].IsString())
mediaside = gameMedia[i]["side"].GetString();
if (mediatype == "boxart" && mediaside == "front")
if (gameMedia[i]["filename"].IsString())
result.coverUrl = base_url + gameMedia[i]["filename"].GetString();
if (mediatype == "clearlogo")
if (gameMedia[i]["filename"].IsString())
result.marqueeUrl = base_url + gameMedia[i]["filename"].GetString();
if (mediatype == "screenshot")
if (gameMedia[i]["filename"].IsString())
result.screenshotUrl = base_url + gameMedia[i]["filename"].GetString();
}
}
result.mediaURLFetch = COMPLETED;
results.push_back(result);
}
}
void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req, void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results) std::vector<ScraperSearchResult>& results)
{ {
@ -331,34 +372,58 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr<HttpReq>& req,
return; return;
} }
// If the response contains the 'images' object, then it's a game media URL request.
if (doc.HasMember("data") && doc["data"].HasMember("images") &&
doc["data"]["images"].IsObject()) {
const Value& images = doc["data"]["images"];
const Value& base_url = doc["data"]["base_url"];
std::string baseImageUrlLarge;
if (base_url.HasMember("large") && base_url["large"].IsString()) {
baseImageUrlLarge = base_url["large"].GetString();
}
else {
std::string warn = "TheGamesDBJSONRequest - No URL path for large images.\n";
LOG(LogWarning) << warn;
return;
}
try {
processMediaURLs(images, baseImageUrlLarge, results);
}
catch (std::runtime_error& e) {
LOG(LogError) << "Error while processing media URLs: " << e.what();
}
// Find how many more requests we can make before the scraper
// request allowance counter is reset.
if (doc.HasMember("remaining_monthly_allowance") && doc.HasMember("extra_allowance")) {
for (auto i = 0; i < results.size(); i++) {
results[i].scraperRequestAllowance =
doc["remaining_monthly_allowance"].GetInt() +
doc["extra_allowance"].GetInt();
}
}
return;
}
// These process steps are for the initial scraping response.
if (!doc.HasMember("data") || !doc["data"].HasMember("games") || if (!doc.HasMember("data") || !doc["data"].HasMember("games") ||
!doc["data"]["games"].IsArray()) { !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")) {
std::string warn = "TheGamesDBJSONRequest - Response had no include boxart data.\n";
LOG(LogWarning) << warn;
return;
}
const Value& boxart = doc["include"]["boxart"];
if (!boxart.HasMember("base_url") || !boxart.HasMember("data") || !boxart.IsObject()) {
std::string warn = "TheGamesDBJSONRequest - Response include had no usable boxart data.\n";
LOG(LogWarning) << warn;
return;
}
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, 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

@ -20,6 +20,11 @@ void thegamesdb_generate_json_scraper_requests(
std::queue<std::unique_ptr<ScraperRequest>>& requests, std::queue<std::unique_ptr<ScraperRequest>>& requests,
std::vector<ScraperSearchResult>& results); std::vector<ScraperSearchResult>& results);
void thegamesdb_generate_json_scraper_requests(
const std::string& gameIDs,
std::queue<std::unique_ptr<ScraperRequest>>& requests,
std::vector<ScraperSearchResult>& results);
class TheGamesDBJSONRequest : public ScraperHttpRequest class TheGamesDBJSONRequest : public ScraperHttpRequest
{ {
public: public:
@ -42,6 +47,8 @@ class TheGamesDBJSONRequest : public ScraperHttpRequest
} }
protected: protected:
//void retrieveMediaURLs()
void process(const std::unique_ptr<HttpReq>& req, void process(const std::unique_ptr<HttpReq>& req,
std::vector<ScraperSearchResult>& results) override; std::vector<ScraperSearchResult>& results) override;
bool isGameRequest() { return !mRequestQueue; } bool isGameRequest() { return !mRequestQueue; }

View file

@ -4,6 +4,11 @@
// Functions specifically for scraping from thegamesdb.net // Functions specifically for scraping from thegamesdb.net
// Called from GamesDBJSONScraper. // Called from GamesDBJSONScraper.
// //
// Downloads these resource files to ~/.emulationstation/scrapers:
// gamesdb_developers.json
// gamesdb_genres.json
// gamesdb_publishers.json
//
#include <chrono> #include <chrono>
#include <fstream> #include <fstream>

View file

@ -4,6 +4,11 @@
// Functions specifically for scraping from thegamesdb.net // Functions specifically for scraping from thegamesdb.net
// Called from GamesDBJSONScraper. // Called from GamesDBJSONScraper.
// //
// Downloads these resource files to ~/.emulationstation/scrapers:
// gamesdb_developers.json
// gamesdb_genres.json
// gamesdb_publishers.json
//
#pragma once #pragma once
#ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H #ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H

View file

@ -36,10 +36,28 @@ std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParam
return handle; return handle;
} }
std::unique_ptr<ScraperSearchHandle> startMediaURLsFetch(const std::string& gameIDs)
{
const std::string& name = Settings::getInstance()->getString("Scraper");
std::unique_ptr<ScraperSearchHandle> handle(new ScraperSearchHandle());
ScraperSearchParams params;
// Check if the scraper in the settings still exists as a registered scraping source.
if (scraper_request_funcs.find(name) == scraper_request_funcs.end())
LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted.";
else
// Specifically use the TheGamesDB function as this type of request
// will never occur for ScreenScraper.
thegamesdb_generate_json_scraper_requests(gameIDs, handle->mRequestQueue,
handle->mResults);
return handle;
}
std::vector<std::string> getScraperList() 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;
@ -59,36 +77,35 @@ ScraperSearchHandle::ScraperSearchHandle()
void ScraperSearchHandle::update() void ScraperSearchHandle::update()
{ {
if(mStatus == ASYNC_DONE) if (mStatus == ASYNC_DONE)
return; return;
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. // Propagate 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;
} }
@ -101,8 +118,8 @@ ScraperRequest::ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite)
} }
// ScraperHttpRequest. // ScraperHttpRequest.
ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>& ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite,
resultsWrite, const std::string& url) : ScraperRequest(resultsWrite) const std::string& url) : 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));
@ -111,8 +128,7 @@ ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>&
void ScraperHttpRequest::update() void ScraperHttpRequest::update()
{ {
HttpReq::Status status = mReq->status(); HttpReq::Status status = mReq->status();
if(status == HttpReq::REQ_SUCCESS) if (status == HttpReq::REQ_SUCCESS) {
{
// 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); setStatus(ASYNC_DONE);
process(mReq, mResults); process(mReq, mResults);
@ -120,7 +136,7 @@ void ScraperHttpRequest::update()
} }
// 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.
@ -129,8 +145,7 @@ void ScraperHttpRequest::update()
setError(mReq->getErrorMsg()); setError(mReq->getErrorMsg());
} }
// Metadata resolving stuff. // Download and write the media files to disk.
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result,
const ScraperSearchParams& search) const ScraperSearchParams& search)
{ {
@ -140,44 +155,124 @@ std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult
MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result,
const ScraperSearchParams& search) : mResult(result) const ScraperSearchParams& search) : mResult(result)
{ {
if(!result.imageUrl.empty()) { struct mediaFileInfoStruct {
std::string fileURL;
std::string fileFormat;
std::string subDirectory;
std::string existingMediaFile;
} mediaFileInfo;
std::vector<struct mediaFileInfoStruct> scrapeFiles;
if (Settings::getInstance()->getBool("Scrape3DBoxes") && result.box3dUrl != "") {
mediaFileInfo.fileURL = result.box3dUrl;
mediaFileInfo.fileFormat = result.box3dFormat;
mediaFileInfo.subDirectory = "3dboxes";
mediaFileInfo.existingMediaFile = search.game->get3DBoxPath();
scrapeFiles.push_back(mediaFileInfo);
}
if (Settings::getInstance()->getBool("ScrapeCovers") && result.coverUrl != "") {
mediaFileInfo.fileURL = result.coverUrl;
mediaFileInfo.fileFormat = result.coverFormat;
mediaFileInfo.subDirectory = "covers";
mediaFileInfo.existingMediaFile = search.game->getCoverPath();
scrapeFiles.push_back(mediaFileInfo);
}
if (Settings::getInstance()->getBool("ScrapeMarquees") && result.marqueeUrl != "") {
mediaFileInfo.fileURL = result.marqueeUrl;
mediaFileInfo.fileFormat = result.marqueeFormat;
mediaFileInfo.subDirectory = "marquees";
mediaFileInfo.existingMediaFile = search.game->getMarqueePath();
scrapeFiles.push_back(mediaFileInfo);
}
if (Settings::getInstance()->getBool("ScrapeScreenshots") && result.screenshotUrl != "") {
mediaFileInfo.fileURL = result.screenshotUrl;
mediaFileInfo.fileFormat = result.screenshotFormat;
mediaFileInfo.subDirectory = "screenshots";
mediaFileInfo.existingMediaFile = search.game->getScreenshotPath();
scrapeFiles.push_back(mediaFileInfo);
}
for (auto it = scrapeFiles.cbegin(); it != scrapeFiles.cend(); it++) {
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 (!it->fileFormat.empty()) {
ext = result.imageType; ext = it->fileFormat;
} }
else { else {
size_t dot = result.imageUrl.find_last_of('.'); size_t dot = it->fileURL.find_last_of('.');
if (dot != std::string::npos) if (dot != std::string::npos)
ext = result.imageUrl.substr(dot, std::string::npos); ext = it->fileURL.substr(dot, std::string::npos);
} }
std::string imgPath = getSaveAsPath(search, "image", ext); std::string filePath = getSaveAsPath(search, it->subDirectory, ext);
mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), // If there is an existing media file on disk and the setting to overwrite data
[this, imgPath] { // has been set to no, then don't proceed with downloading or saving a new file.
mResult.mdl.set("image", imgPath); if (it->existingMediaFile != "" &&
mResult.imageUrl = ""; !Settings::getInstance()->getBool("ScraperOverwriteData"))
})); continue;
// If the image is cached already as the thumbnail, then we don't need
// to download it again, in this case just save it to disk and resize it.
if (mResult.ThumbnailImageUrl == it->fileURL &&
mResult.ThumbnailImageData.size() > 0) {
// Remove any existing media file before attempting to write a new one.
// This avoids the problem where there's already a file for this media type
// with a different format/extension (e.g. game.jpg and we're going to write
// game.png) which would lead to two media files for this game.
if(it->existingMediaFile != "")
Utils::FileSystem::removeFile(it->existingMediaFile);
std::ofstream stream(filePath, std::ios_base::out | std::ios_base::binary);
if (stream.bad()) {
setError("Failed to open image path to write. Permission error? Disk full?");
return;
}
const std::string& content = mResult.ThumbnailImageData;
stream.write(content.data(), content.length());
stream.close();
if (stream.bad()) {
setError("Failed to save image. Disk full?");
return;
}
// Resize it.
if (!resizeImage(filePath, Settings::getInstance()->getInt("ScraperResizeWidth"),
Settings::getInstance()->getInt("ScraperResizeHeight"))) {
setError("Error saving resized image. Out of memory? Disk full?");
return;
}
}
// If it's not cached, then initiate the download.
else {
mFuncs.push_back(ResolvePair(downloadImageAsync(it->fileURL, filePath,
it->existingMediaFile), [this, filePath] {
// mResult.mdl.set("image", filePath);
}));
}
} }
} }
void MDResolveHandle::update() void MDResolveHandle::update()
{ {
if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR) if (mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR)
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;
@ -185,30 +280,41 @@ void MDResolveHandle::update()
it++; it++;
} }
if(mFuncs.empty()) if (mFuncs.empty())
setStatus(ASYNC_DONE); setStatus(ASYNC_DONE);
} }
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url,
const std::string& saveAs) const std::string& saveAs, const std::string& existingMediaFile)
{ {
return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs, return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(
url,
saveAs,
existingMediaFile,
Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeWidth"),
Settings::getInstance()->getInt("ScraperResizeHeight"))); Settings::getInstance()->getInt("ScraperResizeHeight")));
} }
ImageDownloadHandle::ImageDownloadHandle(const std::string& url, ImageDownloadHandle::ImageDownloadHandle(
const std::string& path, int maxWidth, int maxHeight) : mSavePath(path), const std::string& url,
mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) const std::string& path,
const std::string& existingMediaPath,
int maxWidth,
int maxHeight)
: mSavePath(path),
mExistingMediaFile(existingMediaPath),
mMaxWidth(maxWidth),
mMaxHeight(maxHeight),
mReq(new HttpReq(url))
{ {
} }
void ImageDownloadHandle::update() 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());
@ -216,8 +322,16 @@ void ImageDownloadHandle::update()
} }
// Download is done, save it to disk. // Download is done, save it to disk.
// Remove any existing media file before attempting to write a new one.
// This avoids the problem where there's already a file for this media type
// with a different format/extension (e.g. game.jpg and we're going to write
// game.png) which would lead to two media files for this game.
if(mExistingMediaFile != "")
Utils::FileSystem::removeFile(mExistingMediaFile);
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;
} }
@ -225,13 +339,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;
} }
@ -243,7 +357,7 @@ void ImageDownloadHandle::update()
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;
@ -251,15 +365,15 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
// 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 {
@ -270,15 +384,20 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
float width = (float)FreeImage_GetWidth(image); float width = (float)FreeImage_GetWidth(image);
float height = (float)FreeImage_GetHeight(image); float height = (float)FreeImage_GetHeight(image);
if(maxWidth == 0) // If the image is smaller than maxWidth or maxHeight, then don't do any
// scaling. It doesn't make sense to upscale the image and waste disk space.
if (maxWidth > width || maxHeight > height)
return true;
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;
} }
@ -286,26 +405,26 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
bool saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0); bool saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0);
FreeImage_Unload(imageRescaled); FreeImage_Unload(imageRescaled);
if(!saved) if (!saved)
LOG(LogError) << "Failed to save resized image!"; LOG(LogError) << "Failed to save resized image!";
return saved; return saved;
} }
std::string getSaveAsPath(const ScraperSearchParams& params, std::string getSaveAsPath(const ScraperSearchParams& params,
const std::string& suffix, const std::string& extension) const std::string& filetypeSubdirectory, const std::string& extension)
{ {
const std::string subdirectory = params.system->getName(); const std::string systemsubdirectory = params.system->getName();
const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; const std::string name = Utils::FileSystem::getStem(params.game->getPath());
std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/downloaded_images/"; std::string path = FileData::getMediaDirectory();
if(!Utils::FileSystem::exists(path)) if (!Utils::FileSystem::exists(path))
Utils::FileSystem::createDirectory(path); Utils::FileSystem::createDirectory(path);
path += subdirectory + "/"; path += systemsubdirectory + "/" + filetypeSubdirectory + "/";
if(!Utils::FileSystem::exists(path)) if (!Utils::FileSystem::exists(path))
Utils::FileSystem::createDirectory(path); Utils::FileSystem::createDirectory(path);
path += name + extension; path += name + extension;

View file

@ -24,6 +24,12 @@
class FileData; class FileData;
class SystemData; class SystemData;
enum eDownloadStatus {
NOT_STARTED,
IN_PROGRESS,
COMPLETED
};
struct ScraperSearchParams { struct ScraperSearchParams {
SystemData* system; SystemData* system;
FileData* game; FileData* game;
@ -35,11 +41,29 @@ struct ScraperSearchResult {
ScraperSearchResult() : mdl(GAME_METADATA) {}; ScraperSearchResult() : mdl(GAME_METADATA) {};
MetaDataList mdl; MetaDataList mdl;
std::string imageUrl; std::string gameID;
std::string thumbnailUrl;
// How many more objects the scraper service allows to be downloaded
// within a given time period.
unsigned int scraperRequestAllowance;
enum eDownloadStatus mediaURLFetch = NOT_STARTED;
enum eDownloadStatus thumbnailDownloadStatus = NOT_STARTED;
enum eDownloadStatus mediaFilesDownloadStatus = NOT_STARTED;
std::string ThumbnailImageData; // Thumbnail cache, will containe entire image.
std::string ThumbnailImageUrl;
std::string box3dUrl;
std::string coverUrl;
std::string marqueeUrl;
std::string screenshotUrl;
// Needed to pre-set the image type. // Needed to pre-set the image type.
std::string imageType; std::string box3dFormat;
std::string coverFormat;
std::string marqueeFormat;
std::string screenshotFormat;
}; };
// So let me explain why I've abstracted this so heavily. // So let me explain why I've abstracted this so heavily.
@ -83,7 +107,6 @@ protected:
std::vector<ScraperSearchResult>& mResults; std::vector<ScraperSearchResult>& mResults;
}; };
// 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
{ {
@ -113,6 +136,9 @@ protected:
friend std::unique_ptr<ScraperSearchHandle> friend std::unique_ptr<ScraperSearchHandle>
startScraperSearch(const ScraperSearchParams& params); startScraperSearch(const ScraperSearchParams& params);
friend std::unique_ptr<ScraperSearchHandle>
startMediaURLsFetch(const std::string& gameIDs);
std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue; std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue;
std::vector<ScraperSearchResult> mResults; std::vector<ScraperSearchResult> mResults;
}; };
@ -120,6 +146,8 @@ protected:
// 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);
std::unique_ptr<ScraperSearchHandle> startMediaURLsFetch(const std::string& gameIDs);
// Returns a list of valid scraper names. // Returns a list of valid scraper names.
std::vector<std::string> getScraperList(); std::vector<std::string> getScraperList();
@ -127,7 +155,7 @@ std::vector<std::string> getScraperList();
bool isValidConfiguredScraper(); bool isValidConfiguredScraper();
typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params, typedef void (*generate_scraper_requests_func)(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);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -145,21 +173,26 @@ public:
private: private:
ScraperSearchResult mResult; ScraperSearchResult mResult;
typedef std::pair< std::unique_ptr<AsyncHandle>, std::function<void()> > ResolvePair; typedef std::pair<std::unique_ptr<AsyncHandle>, std::function<void()>> ResolvePair;
std::vector<ResolvePair> mFuncs; std::vector<ResolvePair> mFuncs;
}; };
class ImageDownloadHandle : public AsyncHandle class ImageDownloadHandle : public AsyncHandle
{ {
public: public:
ImageDownloadHandle(const std::string& url, const std::string& path, ImageDownloadHandle(
int maxWidth, int maxHeight); const std::string& url,
const std::string& path,
const std::string& existingMediaPath,
int maxWidth,
int maxHeight);
void update() override; void update() override;
private: private:
std::unique_ptr<HttpReq> mReq; std::unique_ptr<HttpReq> mReq;
std::string mSavePath; std::string mSavePath;
std::string mExistingMediaFile;
int mMaxWidth; int mMaxWidth;
int mMaxHeight; int mMaxHeight;
}; };
@ -167,13 +200,13 @@ private:
// About the same as: // About the same as:
// "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]". // "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]".
// Will create the "downloaded_images" and "subdirectory" directories if they do not exist. // Will create the "downloaded_images" and "subdirectory" directories if they do not exist.
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, std::string getSaveAsPath(const ScraperSearchParams& params,
const std::string& url); const std::string& filetypeSubdirectory, const std::string& url);
// Will resize according to Settings::getInt("ScraperResizeWidth") and // Will resize according to Settings::getInt("ScraperResizeWidth") and
// Settings::getInt("ScraperResizeHeight"). // Settings::getInt("ScraperResizeHeight").
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url,
const std::string& saveAs); const std::string& saveAs, const std::string& existingMediaPath);
// 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, std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result,

View file

@ -14,6 +14,7 @@
#include "PlatformId.h" #include "PlatformId.h"
#include "Settings.h" #include "Settings.h"
#include "SystemData.h" #include "SystemData.h"
#include "math/Misc.h"
#include <pugixml/src/pugixml.hpp> #include <pugixml/src/pugixml.hpp>
#include <cstring> #include <cstring>
@ -132,7 +133,11 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params,
ScreenScraperRequest::ScreenScraperConfig ssConfig; ScreenScraperRequest::ScreenScraperConfig ssConfig;
path = ssConfig.getGameSearchUrl(params.game->getFileName()); if (params.nameOverride == "")
path = ssConfig.getGameSearchUrl(params.game->getCleanName());
else
path = ssConfig.getGameSearchUrl(params.nameOverride);
auto& platforms = params.system->getPlatformIds(); auto& platforms = params.system->getPlatformIds();
std::vector<unsigned short> p_ids; std::vector<unsigned short> p_ids;
@ -164,7 +169,6 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params,
requests.push(std::unique_ptr<ScraperRequest> requests.push(std::unique_ptr<ScraperRequest>
(new ScreenScraperRequest(requests, results, path))); (new ScreenScraperRequest(requests, results, path)));
} }
} }
void ScreenScraperRequest::process(const std::unique_ptr<HttpReq>& req, void ScreenScraperRequest::process(const std::unique_ptr<HttpReq>& req,
@ -200,8 +204,20 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc,
ScraperSearchResult result; ScraperSearchResult result;
ScreenScraperRequest::ScreenScraperConfig ssConfig; ScreenScraperRequest::ScreenScraperConfig ssConfig;
std::string region = Utils::String::toLower(ssConfig.region).c_str(); result.gameID = game.attribute("id").as_string();
std::string language = Utils::String::toLower(ssConfig.language).c_str();
// Find how many more requests we can make before the scraper request
// allowance counter is reset.
unsigned requestsToday =
data.child("ssuser").child("requeststoday").text().as_uint();
unsigned maxRequestsPerDay =
data.child("ssuser").child("maxrequestsperday").text().as_uint();
result.scraperRequestAllowance = maxRequestsPerDay - requestsToday;
std::string region =
Utils::String::toLower(Settings::getInstance()->getString("ScraperRegion"));
std::string language =
Utils::String::toLower(Settings::getInstance()->getString("ScraperLanguage"));
// 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"), result.mdl.set("name", find_child_by_attribute_list(game.child("noms"),
@ -251,9 +267,11 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc,
// Players. // Players.
result.mdl.set("players", game.child("joueurs").text().get()); result.mdl.set("players", game.child("joueurs").text().get());
// TODO: Validate rating. // 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);
// Round up to the closest .1 value, i.e. to the closest half-star.
ratingVal = Math::ceilf(ratingVal / 0.1) / 10;
std::stringstream ss; std::stringstream ss;
ss << ratingVal; ss << ratingVal;
result.mdl.set("rating", ss.str()); result.mdl.set("rating", ss.str());
@ -263,52 +281,70 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc,
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); // 3D box
processMedia(result, media_list, ssConfig.media_3dbox,
result.box3dUrl, result.box3dFormat, region);
// Cover
processMedia(result, media_list, ssConfig.media_cover,
result.coverUrl, result.coverFormat, region);
// Marquee (wheel)
processMedia(result, media_list, ssConfig.media_marquee,
result.marqueeUrl, result.marqueeFormat, region);
// Screenshot
processMedia(result, media_list, ssConfig.media_screenshot,
result.screenshotUrl, result.screenshotFormat, region);
}
result.mediaURLFetch = COMPLETED;
out_results.push_back(result);
} // Game.
}
// Do an XPath query for media[type='$media_type'], then filter by region. void ScreenScraperRequest::processMedia(
// We need to do this because any child of 'medias' has the form ScraperSearchResult& result,
// <media type="..." region="..." format="..."> const pugi::xml_node& media_list,
// and we need to find the right media for the region. std::string mediaType,
pugi::xpath_node_set results = media_list.select_nodes((static_cast<std::string> std::string& fileURL,
("media[@type='") + ssConfig.media_name + "']").c_str()); std::string& fileFormat,
std::string region)
{
pugi::xml_node art = pugi::xml_node(NULL);
if (results.size()) { // Do an XPath query for media[type='$media_type'], then filter by region.
// Region fallback: WOR(LD), US, CUS(TOM?), JP, EU. // We need to do this because any child of 'medias' has the form
for (auto _region : std::vector<std::string>{ region, // <media type="..." region="..." format="...">
"wor", "us", "cus", "jp", "eu" }) { // and we need to find the right media for the region.
if (art) pugi::xpath_node_set results = media_list.select_nodes((static_cast<std::string>
("media[@type='") + mediaType + "']").c_str());
if (results.size()) {
// Region fallback: WOR(LD), US, CUS(TOM?), JP, EU.
for (auto _region : std::vector<std::string>{
region, "wor", "us", "cus", "jp", "eu" }) {
if (art)
break;
for (auto node : results) {
if (node.node().attribute("region").value() == _region) {
art = node.node();
break; break;
for (auto node : results) {
if (node.node().attribute("region").value() == _region) {
art = node.node();
break;
}
} }
} }
} // Results.
if (art) {
// Sending a 'softname' containing space will make the image URLs returned
// by the API also contain the space. Escape any spaces in the URL here
result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20");
// Get the media type returned by ScreenScraper.
std::string media_type = art.attribute("format").value();
if (!media_type.empty())
result.imageType = "." + media_type;
// Ask for the same image, but with a smaller size, for the thumbnail
// displayed during scraping.
result.thumbnailUrl = result.imageUrl + "&maxheight=250";
}
else {
LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name;
} }
} }
out_results.push_back(result); if (art) {
} // Game. // Sending a 'softname' containing space will make the image URLs returned
// by the API also contain the space. Escape any spaces in the URL here.
fileURL = Utils::String::replace(art.text().get(), " ", "%20");
// Get the media type returned by ScreenScraper.
std::string media_type = art.attribute("format").value();
if (!media_type.empty())
fileFormat = "." + media_type;
}
else {
LOG(LogDebug) << "Failed to find media XML node with name=" << mediaType;
}
} }
// Currently not used in this module. // Currently not used in this module.

View file

@ -62,15 +62,19 @@ public:
// Note that not all games contain values for all these, so we default to "box-2D" // Note that not all games contain values for all these, so we default to "box-2D"
// since it's the most common. // since it's the most common.
// //
std::string media_name = "box-2D";
std::string media_3dbox = "box-3D";
std::string media_cover = "box-2D";
std::string media_marquee = "wheel";
std::string media_screenshot = "ss";
// 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"; // This is read from es_settings.cfg, setting 'ScraperRegion'.
// 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"; // This is read from es_settings.cfg, setting 'ScraperLanguage'.
ScreenScraperConfig() {}; ScreenScraperConfig() {};
} configuration; } configuration;
@ -81,6 +85,12 @@ protected:
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);
void processMedia(ScraperSearchResult& result,
const pugi::xml_node& media_list,
std::string mediaType,
std::string& fileURL,
std::string& fileFormat,
std::string region);
bool isGameRequest() { return !mRequestQueue; } bool isGameRequest() { return !mRequestQueue; }
std::queue< std::unique_ptr<ScraperRequest> >* mRequestQueue; std::queue< std::unique_ptr<ScraperRequest> >* mRequestQueue;

View file

@ -155,13 +155,13 @@ bool SystemView::input(InputConfig* config, Input input)
case VERTICAL_WHEEL: case VERTICAL_WHEEL:
if (config->isMappedLike("up", input)) if (config->isMappedLike("up", input))
{ {
navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND);
listInput(-1); listInput(-1);
return true; return true;
} }
if (config->isMappedLike("down", input)) if (config->isMappedLike("down", input))
{ {
navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND);
listInput(1); listInput(1);
return true; return true;
} }
@ -171,13 +171,13 @@ bool SystemView::input(InputConfig* config, Input input)
default: default:
if (config->isMappedLike("left", input)) if (config->isMappedLike("left", input))
{ {
navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND);
listInput(-1); listInput(-1);
return true; return true;
} }
if (config->isMappedLike("right", input)) if (config->isMappedLike("right", input))
{ {
navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND);
listInput(1); listInput(1);
return true; return true;
} }
@ -188,14 +188,14 @@ bool SystemView::input(InputConfig* config, Input input)
{ {
stopScrolling(); stopScrolling();
ViewController::get()->goToGameList(getSelected()); ViewController::get()->goToGameList(getSelected());
navigationsounds.playThemeNavigationSound(SELECTSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SELECTSOUND);
return true; return true;
} }
if (config->isMappedTo("x", input)) if (config->isMappedTo("x", input))
{ {
// get random system // get random system
// go to system // go to system
navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND);
setCursor(SystemData::getRandomSystem()); setCursor(SystemData::getRandomSystem());
return true; return true;
} }

View file

@ -28,7 +28,6 @@
#include "Sound.h" #include "Sound.h"
ViewController* ViewController::sInstance = nullptr; ViewController* ViewController::sInstance = nullptr;
NavigationSounds navigationsounds;
ViewController* ViewController::get() ViewController* ViewController::get()
{ {
@ -117,7 +116,7 @@ void ViewController::goToNextGameList()
assert(mState.viewing == GAME_LIST); assert(mState.viewing == GAME_LIST);
SystemData* system = getState().getSystem(); SystemData* system = getState().getSystem();
assert(system); assert(system);
navigationsounds.playThemeNavigationSound(QUICKSYSSELECTSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(QUICKSYSSELECTSOUND);
goToGameList(system->getNext()); goToGameList(system->getNext());
} }
@ -126,7 +125,7 @@ void ViewController::goToPrevGameList()
assert(mState.viewing == GAME_LIST); assert(mState.viewing == GAME_LIST);
SystemData* system = getState().getSystem(); SystemData* system = getState().getSystem();
assert(system); assert(system);
navigationsounds.playThemeNavigationSound(QUICKSYSSELECTSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(QUICKSYSSELECTSOUND);
goToGameList(system->getPrev()); goToGameList(system->getPrev());
} }
@ -239,9 +238,9 @@ void ViewController::launch(FileData* game, Vector3f center)
std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); std::string transition_style = Settings::getInstance()->getString("TransitionStyle");
navigationsounds.playThemeNavigationSound(LAUNCHSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(LAUNCHSOUND);
// Let launch sound play to the end before launching game. // Let launch sound play to the end before launching game.
while(navigationsounds.isPlayingThemeNavigationSound(LAUNCHSOUND)); while (NavigationSounds::getInstance()->isPlayingThemeNavigationSound(LAUNCHSOUND));
if (transition_style == "fade") { if (transition_style == "fade") {
// Fade out, launch game, fade back in. // Fade out, launch game, fade back in.
@ -472,7 +471,8 @@ void ViewController::preload()
getGameListView(*it); getGameListView(*it);
} }
// Load navigation sounds. // Load navigation sounds.
navigationsounds.loadThemeNavigationSounds(SystemData::sSystemVector[0]->getTheme()); NavigationSounds::getInstance()->loadThemeNavigationSounds(
SystemData::sSystemVector.front()->getTheme());
} }
void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme)
@ -539,7 +539,8 @@ void ViewController::reloadAll()
} }
// Load navigation sounds. // Load navigation sounds.
navigationsounds.loadThemeNavigationSounds(SystemData::sSystemVector[0]->getTheme()); NavigationSounds::getInstance()->loadThemeNavigationSounds(
SystemData::sSystemVector.front()->getTheme());
updateHelpPrompts(); updateHelpPrompts();
} }

View file

@ -176,7 +176,7 @@ bool GridGameListView::input(InputConfig* config, Input input)
config->isMappedLike("right", input) || config->isMappedLike("right", input) ||
(config->isMappedLike("up", input)) || (config->isMappedLike("up", input)) ||
(config->isMappedLike("down", input)) )) (config->isMappedLike("down", input)) ))
navigationsounds.playThemeNavigationSound(SCROLLSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND);
if (config->isMappedLike("left", input) || config->isMappedLike("right", input)) if (config->isMappedLike("left", input) || config->isMappedLike("right", input))
return GuiComponent::input(config, input); return GuiComponent::input(config, input);

View file

@ -98,7 +98,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
else { else {
// It's a folder. // It's a folder.
if (cursor->getChildren().size() > 0) { if (cursor->getChildren().size() > 0) {
navigationsounds.playThemeNavigationSound(SELECTSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SELECTSOUND);
mCursorStack.push(cursor); mCursorStack.push(cursor);
populateList(cursor->getChildrenListToDisplay()); populateList(cursor->getChildrenListToDisplay());
FileData* cursor = getCursor(); FileData* cursor = getCursor();
@ -110,13 +110,13 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
} }
else if (config->isMappedTo("b", input)) { else if (config->isMappedTo("b", input)) {
if (mCursorStack.size()) { if (mCursorStack.size()) {
navigationsounds.playThemeNavigationSound(BACKSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(BACKSOUND);
populateList(mCursorStack.top()->getParent()->getChildren()); populateList(mCursorStack.top()->getParent()->getChildren());
setCursor(mCursorStack.top()); setCursor(mCursorStack.top());
mCursorStack.pop(); mCursorStack.pop();
} }
else { else {
navigationsounds.playThemeNavigationSound(BACKSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(BACKSOUND);
onFocusLost(); onFocusLost();
SystemData* systemToView = getCursor()->getSystem(); SystemData* systemToView = getCursor()->getSystem();
if (systemToView->isCollection()) if (systemToView->isCollection())
@ -145,7 +145,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
else if (config->isMappedTo("x", input)) { else if (config->isMappedTo("x", input)) {
if (mRoot->getSystem()->isGameSystem()) { if (mRoot->getSystem()->isGameSystem()) {
// Go to random system game. // Go to random system game.
navigationsounds.playThemeNavigationSound(SCROLLSOUND); NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND);
FileData* randomGame = getCursor()->getSystem()->getRandomGame(); FileData* randomGame = getCursor()->getSystem()->getRandomGame();
if (randomGame) if (randomGame)
setCursor(randomGame); setCursor(randomGame);
@ -155,7 +155,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
else if (config->isMappedTo("y", input) && else if (config->isMappedTo("y", input) &&
!UIModeController::getInstance()->isUIModeKid()) { !UIModeController::getInstance()->isUIModeKid()) {
if (mRoot->getSystem()->isGameSystem()) { if (mRoot->getSystem()->isGameSystem()) {
navigationsounds.playThemeNavigationSound(FAVORITESOUND); NavigationSounds::getInstance()->playThemeNavigationSound(FAVORITESOUND);
if (CollectionSystemManager::get()->toggleGameInCollection(getCursor())) if (CollectionSystemManager::get()->toggleGameInCollection(getCursor()))
return true; return true;
} }

View file

@ -287,7 +287,6 @@ void VideoGameListView::updateInfoPanel()
mVideo->setImage(file->getImagePath()); mVideo->setImage(file->getImagePath());
mThumbnail.setImage(file->getThumbnailPath()); mThumbnail.setImage(file->getThumbnailPath());
mMarquee.setImage(file->getMarqueePath()); mMarquee.setImage(file->getMarqueePath());
mImage.setImage(file->getImagePath());
mDescription.setText(file->metadata.get("desc")); mDescription.setText(file->metadata.get("desc"));
mDescContainer.reset(); mDescContainer.reset();

View file

@ -44,8 +44,7 @@ bool HttpReq::isUrl(const std::string& str)
std::string::npos || str.find("www.") != std::string::npos)); 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();

View file

@ -26,8 +26,7 @@
// Once one of those calls complete, the request is ready. // Once one of those calls complete, the request is ready.
// //
// Do something like this to capture errors: // Do something like this to capture errors:
// if(myRequest.status() != REQ_SUCCESS) // if(myRequest.status() != REQ_SUCCESS) {
// {
// // An error occured. // // An error occured.
// LOG(LogError) << "HTTP request error - " << myRequest.getErrorMessage(); // LOG(LogError) << "HTTP request error - " << myRequest.getErrorMessage();
// return; // return;

View file

@ -1,7 +1,17 @@
//
// MameNames.cpp
//
// Provides expanded game names based on short MAME name arguments. Also contains
// functions to check whether a passed argument is a MAME BIOS or a MAME device.
// The data sources are stored in the .emulationstation/resources directory
// as the files mamebioses.xml, mamedevices.xml and mamenames.xml.
//
#include "MameNames.h" #include "MameNames.h"
#include "resources/ResourceManager.h" #include "resources/ResourceManager.h"
#include "utils/FileSystemUtil.h" #include "utils/FileSystemUtil.h"
#include "utils/StringUtil.h"
#include "Log.h" #include "Log.h"
#include <pugixml/src/pugixml.hpp> #include <pugixml/src/pugixml.hpp>
#include <string.h> #include <string.h>
@ -10,35 +20,31 @@ MameNames* MameNames::sInstance = nullptr;
void MameNames::init() void MameNames::init()
{ {
if(!sInstance) if (!sInstance)
sInstance = new MameNames(); sInstance = new MameNames();
}
} // init
void MameNames::deinit() void MameNames::deinit()
{ {
if(sInstance) if (sInstance) {
{
delete sInstance; delete sInstance;
sInstance = nullptr; sInstance = nullptr;
} }
}
} // deinit
MameNames* MameNames::getInstance() MameNames* MameNames::getInstance()
{ {
if(!sInstance) if (!sInstance)
sInstance = new MameNames(); sInstance = new MameNames();
return sInstance; return sInstance;
}
} // getInstance
MameNames::MameNames() MameNames::MameNames()
{ {
std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamenames.xml"); std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamenames.xml");
if(!Utils::FileSystem::exists(xmlpath)) if (!Utils::FileSystem::exists(xmlpath))
return; return;
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"...";
@ -46,115 +52,123 @@ MameNames::MameNames()
pugi::xml_document doc; pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); pugi::xml_parse_result result = doc.load_file(xmlpath.c_str());
if(!result) if (!result) {
{ LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n "
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); << result.description();
return; return;
} }
for(pugi::xml_node gameNode = doc.child("game"); gameNode; gameNode = gameNode.next_sibling("game")) for (pugi::xml_node gameNode = doc.child("game");
{ gameNode; gameNode = gameNode.next_sibling("game")) {
NamePair namePair = { gameNode.child("mamename").text().get(), gameNode.child("realname").text().get() }; NamePair namePair = {
gameNode.child("mamename").text().get(),
gameNode.child("realname").text().get()
};
mNamePairs.push_back(namePair); mNamePairs.push_back(namePair);
} }
// Read bios // Read BIOS file.
xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamebioses.xml"); xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamebioses.xml");
if(!Utils::FileSystem::exists(xmlpath)) if (!Utils::FileSystem::exists(xmlpath))
return; return;
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"...";
result = doc.load_file(xmlpath.c_str()); result = doc.load_file(xmlpath.c_str());
if(!result) if (!result) {
{ LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n "
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); << result.description();
return; return;
} }
for(pugi::xml_node biosNode = doc.child("bios"); biosNode; biosNode = biosNode.next_sibling("bios")) for (pugi::xml_node biosNode = doc.child("bios");
{ biosNode; biosNode = biosNode.next_sibling("bios")) {
std::string bios = biosNode.text().get(); std::string bios = biosNode.text().get();
mMameBioses.push_back(bios); mMameBioses.push_back(bios);
} }
// Read devices // Read devices file.
xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamedevices.xml"); xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamedevices.xml");
if(!Utils::FileSystem::exists(xmlpath)) if (!Utils::FileSystem::exists(xmlpath))
return; return;
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"...";
result = doc.load_file(xmlpath.c_str()); result = doc.load_file(xmlpath.c_str());
if(!result) if (!result) {
{ LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n "
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); << result.description();
return; return;
} }
for(pugi::xml_node deviceNode = doc.child("device"); deviceNode; deviceNode = deviceNode.next_sibling("device")) for (pugi::xml_node deviceNode = doc.child("device");
{ deviceNode; deviceNode = deviceNode.next_sibling("device")) {
std::string device = deviceNode.text().get(); std::string device = deviceNode.text().get();
mMameDevices.push_back(device); mMameDevices.push_back(device);
} }
} // MameNames } // MameNames.
MameNames::~MameNames() MameNames::~MameNames()
{ {
}
} // ~MameNames
std::string MameNames::getRealName(const std::string& _mameName) std::string MameNames::getRealName(const std::string& _mameName)
{ {
size_t start = 0; size_t start = 0;
size_t end = mNamePairs.size(); size_t end = mNamePairs.size();
while(start < end) while (start < end) {
{ const size_t index = (start + end) / 2;
const size_t index = (start + end) / 2; const int compare = strcmp(mNamePairs[index].mameName.c_str(), _mameName.c_str());
const int compare = strcmp(mNamePairs[index].mameName.c_str(), _mameName.c_str());
if(compare < 0) start = index + 1; if (compare < 0)
else if( compare > 0) end = index; start = index + 1;
else return mNamePairs[index].realName; else if (compare > 0)
end = index;
else
return mNamePairs[index].realName;
} }
return _mameName; return _mameName;
}
} // getRealName std::string MameNames::getCleanName(const std::string& _mameName)
{
std::string cleanName = Utils::String::removeParenthesis(getRealName(_mameName));
return cleanName;
}
const bool MameNames::isBios(const std::string& _biosName) const bool MameNames::isBios(const std::string& _biosName)
{ {
return MameNames::find(mMameBioses, _biosName); return MameNames::find(mMameBioses, _biosName);
}
} // isBios
const bool MameNames::isDevice(const std::string& _deviceName) const bool MameNames::isDevice(const std::string& _deviceName)
{ {
return MameNames::find(mMameDevices, _deviceName); return MameNames::find(mMameDevices, _deviceName);
} // isDevice }
const bool MameNames::find(std::vector<std::string> devices, const std::string& name) const bool MameNames::find(std::vector<std::string> devices, const std::string& name)
{ {
size_t start = 0; size_t start = 0;
size_t end = devices.size(); size_t end = devices.size();
while(start < end) while (start < end) {
{ const size_t index = (start + end) / 2;
const size_t index = (start + end) / 2; const int compare = strcmp(devices[index].c_str(), name.c_str());
const int compare = strcmp(devices[index].c_str(), name.c_str());
if(compare < 0) start = index + 1; if (compare < 0)
else if( compare > 0) end = index; start = index + 1;
else return true; else if (compare > 0)
end = index;
else
return true;
} }
return false; return false;
} }

View file

@ -1,3 +1,12 @@
//
// MameNames.h
//
// Provides expanded game names based on short MAME name arguments. Also contains
// functions to check whether a passed argument is a MAME BIOS or a MAME device.
// The data sources are stored in the .emulationstation/resources directory
// as the files mamebioses.xml, mamedevices.xml and mamenames.xml.
//
#pragma once #pragma once
#ifndef ES_CORE_MAMENAMES_H #ifndef ES_CORE_MAMENAMES_H
#define ES_CORE_MAMENAMES_H #define ES_CORE_MAMENAMES_H
@ -5,6 +14,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
// Expand MAME names to full game names.
class MameNames class MameNames
{ {
public: public:
@ -13,13 +23,13 @@ public:
static void deinit (); static void deinit ();
static MameNames* getInstance(); static MameNames* getInstance();
std::string getRealName(const std::string& _mameName); std::string getRealName(const std::string& _mameName);
std::string getCleanName(const std::string& _mameName);
const bool isBios(const std::string& _biosName); const bool isBios(const std::string& _biosName);
const bool isDevice(const std::string& _deviceName); const bool isDevice(const std::string& _deviceName);
private: private:
struct NamePair struct NamePair {
{
std::string mameName; std::string mameName;
std::string realName; std::string realName;
}; };

View file

@ -132,7 +132,19 @@ void Settings::setDefaults()
// Scraper. // Scraper.
mStringMap["Scraper"] = "ScreenScraper"; mStringMap["Scraper"] = "ScreenScraper";
mStringMap["ScraperRegion"] = "eu";
mStringMap["ScraperLanguage"] = "en";
// mBoolMap["ScraperGenerateMiximages"] = false;
// mBoolMap["ScraperGenerateThumbnails"] = false;
mBoolMap["ScraperInteractive"] = true;
mBoolMap["ScraperOverwriteData"] = false;
mBoolMap["ScrapeMetadata"] = true;
mBoolMap["ScrapeGameNames"] = true;
mBoolMap["ScrapeRatings"] = true; mBoolMap["ScrapeRatings"] = true;
mBoolMap["Scrape3DBoxes"] = true;
mBoolMap["ScrapeCovers"] = true;
mBoolMap["ScrapeMarquees"] = true;
mBoolMap["ScrapeScreenshots"] = true;
// Other settings. // Other settings.
#ifdef _RPI_ #ifdef _RPI_
@ -189,6 +201,16 @@ void Settings::setDefaults()
mIntMap["ScreenOffsetY"] = 0; mIntMap["ScreenOffsetY"] = 0;
mIntMap["ScreenRotate"] = 0; mIntMap["ScreenRotate"] = 0;
//
// Settings that can be changed in es_settings.cfg
// but that are not configurable via the GUI (yet).
//
mStringMap["DefaultSortOrder"] = "filename, ascending";
mStringMap["MediaDirectory"] = "";
mIntMap["ScraperResizeWidth"] = 600;
mIntMap["ScraperResizeHeight"] = 0;
// //
// Hardcoded or program-internal settings. // Hardcoded or program-internal settings.
// //
@ -197,10 +219,6 @@ void Settings::setDefaults()
mBoolMap["DebugGrid"] = false; mBoolMap["DebugGrid"] = false;
mBoolMap["DebugText"] = false; mBoolMap["DebugText"] = false;
mBoolMap["DebugImage"] = false; mBoolMap["DebugImage"] = false;
mStringMap["DefaultSortOrder"] = "filename, ascending";
mStringMap["MediaDirectory"] = "";
mIntMap["ScraperResizeWidth"] = 400;
mIntMap["ScraperResizeHeight"] = 0;
mBoolMap["SplashScreenProgress"] = true; mBoolMap["SplashScreenProgress"] = true;
mStringMap["UIMode_passkey"] = "uuddlrlrba"; mStringMap["UIMode_passkey"] = "uuddlrlrba";
} }

View file

@ -5,6 +5,8 @@
#include "Settings.h" #include "Settings.h"
#include "ThemeData.h" #include "ThemeData.h"
NavigationSounds* NavigationSounds::sInstance = nullptr;
std::map< std::string, std::shared_ptr<Sound> > Sound::sMap; std::map< std::string, std::shared_ptr<Sound> > Sound::sMap;
std::shared_ptr<Sound> Sound::get(const std::string& path) std::shared_ptr<Sound> Sound::get(const std::string& path)
@ -26,79 +28,41 @@ std::shared_ptr<Sound> Sound::getFromTheme(const std::shared_ptr<ThemeData>& the
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound"); const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound");
if(!elem || !elem->has("path")) if(!elem || !elem->has("path"))
{ {
LOG(LogInfo) << "[" << element << "] not found, can't play sound file"; LOG(LogInfo) << "[" << element << "] not found, won't load any sound file";
return get(""); return get("");
} }
LOG(LogInfo) << "[" << element << "] found, ready to play sound file"; LOG(LogInfo) << "[" << element << "] found, ready to load sound file";
return get(elem->get<std::string>("path")); return get(elem->get<std::string>("path"));
} }
NavigationSounds* NavigationSounds::getInstance()
{
if (sInstance == NULL)
sInstance = new NavigationSounds();
return sInstance;
}
void NavigationSounds::loadThemeNavigationSounds(const std::shared_ptr<ThemeData>& theme) void NavigationSounds::loadThemeNavigationSounds(const std::shared_ptr<ThemeData>& theme)
{ {
systembrowseSound = Sound::getFromTheme(theme, "all", "systembrowseSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "systembrowseSound"));
quicksysselectSound = Sound::getFromTheme(theme, "all", "quicksysselectSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "quicksysselectSound"));
selectSound = Sound::getFromTheme(theme, "all", "selectSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "selectSound"));
backSound = Sound::getFromTheme(theme, "all", "backSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "backSound"));
scrollSound = Sound::getFromTheme(theme, "all", "scrollSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "scrollSound"));
favoriteSound = Sound::getFromTheme(theme, "all", "favoriteSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "favoriteSound"));
launchSound = Sound::getFromTheme(theme, "all", "launchSound"); navigationSounds.push_back(Sound::getFromTheme(theme, "all", "launchSound"));
} }
void NavigationSounds::playThemeNavigationSound(NavigationSoundsID soundID) void NavigationSounds::playThemeNavigationSound(NavigationSoundsID soundID)
{ {
NavigationSounds::getInstance()->navigationSounds[soundID]->play();
switch(soundID)
{
case SYSTEMBROWSESOUND:
navigationsounds.systembrowseSound->play();
break;
case QUICKSYSSELECTSOUND:
navigationsounds.quicksysselectSound->play();
break;
case SELECTSOUND:
navigationsounds.selectSound->play();
break;
case BACKSOUND:
navigationsounds.backSound->play();
break;
case SCROLLSOUND:
navigationsounds.scrollSound->play();
break;
case FAVORITESOUND:
navigationsounds.favoriteSound->play();
break;
case LAUNCHSOUND:
navigationsounds.launchSound->play();
}
} }
bool NavigationSounds::isPlayingThemeNavigationSound(NavigationSoundsID soundID) bool NavigationSounds::isPlayingThemeNavigationSound(NavigationSoundsID soundID)
{ {
switch(soundID) return NavigationSounds::getInstance()->navigationSounds[soundID]->isPlaying();
{
case SYSTEMBROWSESOUND:
return navigationsounds.systembrowseSound->isPlaying();
break;
case QUICKSYSSELECTSOUND:
return navigationsounds.quicksysselectSound->isPlaying();
break;
case SELECTSOUND:
return navigationsounds.selectSound->isPlaying();
break;
case BACKSOUND:
return navigationsounds.backSound->isPlaying();
break;
case SCROLLSOUND:
return navigationsounds.scrollSound->isPlaying();
break;
case FAVORITESOUND:
return navigationsounds.favoriteSound->isPlaying();
break;
case LAUNCHSOUND:
return navigationsounds.launchSound->isPlaying();
}
return false;
} }
Sound::Sound(const std::string & path) : mSampleData(NULL), mSamplePos(0), mSampleLength(0), playing(false) Sound::Sound(const std::string & path) : mSampleData(NULL), mSamplePos(0), mSampleLength(0), playing(false)
@ -130,7 +94,7 @@ void Sound::init()
Uint8 * data = NULL; Uint8 * data = NULL;
Uint32 dlen = 0; Uint32 dlen = 0;
if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == NULL) { if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == NULL) {
LOG(LogError) << "Error loading sound \"" << mPath << "\"!\n" << " " << SDL_GetError(); LOG(LogError) << "Error loading sound file \"" << mPath << "\"!\n" << " " << SDL_GetError();
return; return;
} }
//build conversion buffer //build conversion buffer

View file

@ -5,6 +5,7 @@
#include "SDL_audio.h" #include "SDL_audio.h"
#include <map> #include <map>
#include <memory> #include <memory>
#include <vector>
class ThemeData; class ThemeData;
@ -57,18 +58,15 @@ enum NavigationSoundsID
class NavigationSounds class NavigationSounds
{ {
public: public:
static NavigationSounds* getInstance();
void loadThemeNavigationSounds(const std::shared_ptr<ThemeData>& theme); void loadThemeNavigationSounds(const std::shared_ptr<ThemeData>& theme);
void playThemeNavigationSound(NavigationSoundsID soundID); void playThemeNavigationSound(NavigationSoundsID soundID);
bool isPlayingThemeNavigationSound(NavigationSoundsID soundID); bool isPlayingThemeNavigationSound(NavigationSoundsID soundID);
private: private:
std::shared_ptr<Sound> systembrowseSound; static NavigationSounds* sInstance;
std::shared_ptr<Sound> quicksysselectSound; std::vector<std::shared_ptr<Sound>> navigationSounds;
std::shared_ptr<Sound> selectSound;
std::shared_ptr<Sound> backSound;
std::shared_ptr<Sound> scrollSound;
std::shared_ptr<Sound> favoriteSound;
std::shared_ptr<Sound> launchSound;
}; };
extern NavigationSounds navigationsounds; extern NavigationSounds navigationsounds;

View file

@ -1,6 +1,7 @@
#include "components/MenuComponent.h" #include "components/MenuComponent.h"
#include "components/ButtonComponent.h" #include "components/ButtonComponent.h"
#include "Settings.h"
#define BUTTON_GRID_VERT_PADDING 32 #define BUTTON_GRID_VERT_PADDING 32
#define BUTTON_GRID_HORIZ_PADDING 10 #define BUTTON_GRID_HORIZ_PADDING 10
@ -32,6 +33,22 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share
mGrid.resetCursor(); mGrid.resetCursor();
} }
MenuComponent::~MenuComponent()
{
save();
}
void MenuComponent::save()
{
if(!mSaveFuncs.size())
return;
for(auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++)
(*it)();
Settings::getInstance()->saveFile();
}
void MenuComponent::setTitle(const char* title, const std::shared_ptr<Font>& font) void MenuComponent::setTitle(const char* title, const std::shared_ptr<Font>& font)
{ {
mTitle->setText(Utils::String::toUpper(title)); mTitle->setText(Utils::String::toUpper(title));

View file

@ -20,7 +20,9 @@ class MenuComponent : public GuiComponent
{ {
public: public:
MenuComponent(Window* window, const char* title, const std::shared_ptr<Font>& titleFont = Font::get(FONT_SIZE_LARGE)); MenuComponent(Window* window, const char* title, const std::shared_ptr<Font>& titleFont = Font::get(FONT_SIZE_LARGE));
virtual ~MenuComponent(); // just calls save();
void save();
void onSizeChanged() override; void onSizeChanged() override;
inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); }
@ -33,6 +35,8 @@ public:
addRow(row, setCursorHere); addRow(row, setCursorHere);
} }
inline void addSaveFunc(const std::function<void()>& func) { mSaveFuncs.push_back(func); };
void addButton(const std::string& label, const std::string& helpText, const std::function<void()>& callback); void addButton(const std::string& label, const std::string& helpText, const std::function<void()>& callback);
void setTitle(const char* title, const std::shared_ptr<Font>& font); void setTitle(const char* title, const std::shared_ptr<Font>& font);
@ -54,6 +58,7 @@ private:
std::shared_ptr<ComponentList> mList; std::shared_ptr<ComponentList> mList;
std::shared_ptr<ComponentGrid> mButtonGrid; std::shared_ptr<ComponentGrid> mButtonGrid;
std::vector< std::shared_ptr<ButtonComponent> > mButtons; std::vector< std::shared_ptr<ButtonComponent> > mButtons;
std::vector< std::function<void()> > mSaveFuncs;
}; };
#endif // ES_CORE_COMPONENTS_MENU_COMPONENT_H #endif // ES_CORE_COMPONENTS_MENU_COMPONENT_H

View file

@ -250,6 +250,30 @@ public:
onSelectedChanged(); onSelectedChanged();
} }
bool selectEntry(unsigned int entry)
{
if (entry > mEntries.size()) {
return false;
}
else {
mEntries.at(entry).selected = true;
onSelectedChanged();
return true;
}
}
bool unselectEntry(unsigned int entry)
{
if (entry > mEntries.size()) {
return false;
}
else {
mEntries.at(entry).selected = false;
onSelectedChanged();
return true;
}
}
void selectAll() void selectAll()
{ {
for(unsigned int i = 0; i < mEntries.size(); i++) for(unsigned int i = 0; i < mEntries.size(); i++)