From 90735d44e3b2e55eeb153314701578dd26411cc5 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 6 Jun 2020 13:10:33 +0200 Subject: [PATCH] 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. --- es-app/CMakeLists.txt | 4 +- es-app/src/CollectionSystemManager.cpp | 1 - es-app/src/FileData.cpp | 139 ++++---- es-app/src/FileData.h | 12 +- es-app/src/MetaData.cpp | 56 +-- es-app/src/MetaData.h | 8 +- es-app/src/SystemData.cpp | 3 +- es-app/src/SystemData.h | 3 + .../src/components/ScraperSearchComponent.cpp | 237 +++++++++++-- .../src/components/ScraperSearchComponent.h | 9 +- es-app/src/components/TextListComponent.h | 5 +- es-app/src/guis/GuiGamelistOptions.cpp | 4 +- es-app/src/guis/GuiMenu.cpp | 141 +++----- es-app/src/guis/GuiMetaDataEd.cpp | 36 +- es-app/src/guis/GuiMetaDataEd.h | 2 + es-app/src/guis/GuiScraperMenu.cpp | 319 ++++++++++++++++++ es-app/src/guis/GuiScraperMenu.h | 53 +++ es-app/src/guis/GuiScraperMulti.cpp | 11 +- es-app/src/guis/GuiScraperMulti.h | 4 +- es-app/src/guis/GuiScraperStart.cpp | 134 -------- es-app/src/guis/GuiScraperStart.h | 49 --- es-app/src/scrapers/GamesDBJSONScraper.cpp | 165 ++++++--- es-app/src/scrapers/GamesDBJSONScraper.h | 7 + .../scrapers/GamesDBJSONScraperResources.cpp | 5 + .../scrapers/GamesDBJSONScraperResources.h | 5 + es-app/src/scrapers/Scraper.cpp | 231 ++++++++++--- es-app/src/scrapers/Scraper.h | 55 ++- es-app/src/scrapers/ScreenScraper.cpp | 122 ++++--- es-app/src/scrapers/ScreenScraper.h | 16 +- es-app/src/views/SystemView.cpp | 12 +- es-app/src/views/ViewController.cpp | 15 +- .../src/views/gamelist/GridGameListView.cpp | 2 +- .../views/gamelist/ISimpleGameListView.cpp | 10 +- .../src/views/gamelist/VideoGameListView.cpp | 1 - es-core/src/HttpReq.cpp | 3 +- es-core/src/HttpReq.h | 3 +- es-core/src/MameNames.cpp | 126 ++++--- es-core/src/MameNames.h | 14 +- es-core/src/Settings.cpp | 26 +- es-core/src/Sound.cpp | 82 ++--- es-core/src/Sound.h | 12 +- es-core/src/components/MenuComponent.cpp | 17 + es-core/src/components/MenuComponent.h | 5 + es-core/src/components/OptionListComponent.h | 24 ++ 44 files changed, 1439 insertions(+), 749 deletions(-) create mode 100644 es-app/src/guis/GuiScraperMenu.cpp create mode 100644 es-app/src/guis/GuiScraperMenu.h delete mode 100644 es-app/src/guis/GuiScraperStart.cpp delete mode 100644 es-app/src/guis/GuiScraperStart.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 4ff4fdcce..962abd29b 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -31,8 +31,8 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSlideshowScreensaverOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.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/GuiScraperStart.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.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/GuiMenu.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/GuiScraperStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 6c2a7149b..7e3fec132 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -354,7 +354,6 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS } else { ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); - std::string teststring1 = rootFolder->getPath(); // If it's a custom collection and the collections // are grouped, update the parent instead. if (sysData.decl.isCustom && diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index d16666ab9..afefa0e77 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -39,8 +39,18 @@ FileData::FileData( metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) { // Metadata needs at least a name field (since that's what getName() will return). - if (metadata.get("name").empty()) - metadata.set("name", getDisplayName()); + if (metadata.get("name").empty()) { + 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(); metadata.resetChangedFlag(); } @@ -88,7 +98,7 @@ const bool FileData::getFavorite() return false; } -const std::string FileData::getMediaDirectory() const +const std::string FileData::getMediaDirectory() { std::string mediaDirSetting = Settings::getInstance()->getString("MediaDirectory"); std::string mediaDirPath = ""; @@ -111,12 +121,13 @@ const std::string FileData::getMediaDirectory() const 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" }; - 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++) { std::string mediaPath = tempPath + extList[i]; if (Utils::FileSystem::exists(mediaPath)) @@ -125,11 +136,10 @@ const std::string FileData::getThumbnailPath() const // No media found in the media directory, so look // 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++) { std::string localMediaPath = mEnvData->mStartPath + "/images/" + - getDisplayName() + "-thumbnail" + extList[i]; + getDisplayName() + "-" + mediatype + extList[i]; if (Utils::FileSystem::exists(localMediaPath)) return localMediaPath; } @@ -138,6 +148,52 @@ const std::string FileData::getThumbnailPath() const 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 char* extList[5] = { ".avi", ".mkv", ".mov", ".mp4", ".wmv" }; @@ -155,7 +211,7 @@ const std::string FileData::getVideoPath() const if (Settings::getInstance()->getBool("LocalArt")) { for (int i = 0; i < 5; i++) { - std::string localMediaPath = mEnvData->mStartPath + "/images/" + getDisplayName() + + std::string localMediaPath = mEnvData->mStartPath + "/videos/" + getDisplayName() + "-video" + extList[i]; if (Utils::FileSystem::exists(localMediaPath)) return localMediaPath; @@ -165,68 +221,6 @@ const std::string FileData::getVideoPath() const 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::getChildrenListToDisplay() { @@ -312,7 +306,6 @@ void FileData::removeChild(FileData* file) // File somehow wasn't in our children. assert(false); - } void FileData::sort(ComparisonFunction& comparator, bool ascending) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index eb5321311..a2244977c 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -57,11 +57,17 @@ public: inline const std::vector& getChildren() const { return mChildren; } inline SystemData* getSystem() const { return mSystem; } 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 getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; const std::vector& getChildrenListToDisplay(); std::vector getFilesRecursive(unsigned int typeMask, diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 3e7ec983e..130ce2344 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -12,38 +12,40 @@ #include MetaDataDecl gameDecls[] = { - // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, - {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, - {"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, - {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, - {"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, - {"players", MD_INT, "1", false, "players", "enter number of players"}, - {"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, - {"completed", MD_BOOL, "false", false, "completed", "enter completed off/on"}, - {"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on"}, - {"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on"}, - {"launchstring", MD_LAUNCHSTRING, "", false, "launch string", "enter game launch string (emulator override)"}, - {"playcount", MD_INT, "0", false, "play count", "enter number of times played"}, - {"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} +// key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd, shouldScrape +{"name", MD_STRING, "", false, "name", "enter game name", true}, +{"sortname", MD_STRING, "", false, "sortname", "enter game sort name", false}, +{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description", true}, +{"rating", MD_RATING, "0", false, "rating", "enter rating", true}, +{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date", true}, +{"developer", MD_STRING, "unknown", false, "developer", "enter game developer", true}, +{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher", true}, +{"genre", MD_STRING, "unknown", false, "genre", "enter game genre", true}, +{"players", MD_INT, "unknown", false, "players", "enter number of players", true}, +{"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on", false}, +{"completed", MD_BOOL, "false", false, "completed", "enter completed off/on", false}, +{"broken", MD_BOOL, "false", false, "broken/not working", "enter broken off/on", false}, +{"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on", false}, +{"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on", false}, +{"launchstring", MD_LAUNCHSTRING, "", false, "launch string", "enter game launch string " + "(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 gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0])); MetaDataDecl folderDecls[] = { - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, - {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, - {"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, - {"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, - {"genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, - {"players", MD_INT, "1", false, "players", "enter number of players"} +{"name", MD_STRING, "", false, "name", "enter game name", true}, +{"sortname", MD_STRING, "", false, "sortname", "enter game sort name", false}, +{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description", true}, +{"rating", MD_RATING, "0", false, "rating", "enter rating", true}, +{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date", true}, +{"developer", MD_STRING, "unknown", false, "developer", "enter game developer", true}, +{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher", true}, +{"genre", MD_STRING, "unknown", false, "genre", "enter game genre", true}, +{"players", MD_INT, "unknown", false, "players", "enter number of players", true} }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); @@ -78,7 +80,7 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { 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. std::string value = md.text().get(); if (iter->type == MD_PATH) diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 19bf97cf9..d5db59a42 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -15,19 +15,19 @@ namespace pugi { class xml_node; } enum MetaDataType { - // Generic types + // Generic types. MD_STRING, MD_INT, MD_FLOAT, MD_BOOL, - // Specialized types + // Specialized types. MD_MULTILINE_STRING, MD_LAUNCHSTRING, MD_PATH, MD_RATING, MD_DATE, - MD_TIME // Used for lastplayed + MD_TIME // Used for lastplayed. }; struct MetaDataDecl { @@ -40,6 +40,8 @@ struct MetaDataDecl { std::string displayName; // Phrase displayed in editors when prompted to enter value (currently only for strings). std::string displayPrompt; + // If set to false, the scraper will not overwrite this metadata. + bool shouldScrape; }; enum MetaDataListType { diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 417d541bd..c96b233c9 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -38,7 +38,8 @@ SystemData::SystemData( mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), - mIsGameSystem(true) + mIsGameSystem(true), + mScrapeFlag(false) { mFilterIndex = new FileFilterIndex(); diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index e98485c55..21ef066d1 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -62,6 +62,8 @@ public: unsigned int getGameCount() const; unsigned int getDisplayedGameCount() const; + bool getScrapeFlag() { return mScrapeFlag; }; + void setScrapeFlag(bool scrapeflag) { mScrapeFlag = scrapeflag; } static void deleteSystems(); // Load the system config file at getConfigPath(). @@ -100,6 +102,7 @@ public: private: bool mIsCollectionSystem; bool mIsGameSystem; + bool mScrapeFlag; // Only used by scraper GUI to remember which systems to scrape. std::string mName; std::string mFullName; SystemEnvironmentData* mEnvData; diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index ffd638641..1c57320d8 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -4,7 +4,8 @@ // User interface component for the scraper where the user is able to see an overview // of the game being scraped and an option to override the game search string. // Used by both single-game scraping from the GuiMetaDataEd menu as well as -// to resolve scraping conflicts when run from GuiScraperStart. +// 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 // from GuiScraperMulti for multi-game scraping. @@ -22,6 +23,9 @@ #include "guis/GuiTextEditPopup.h" #include "resources/Font.h" #include "utils/StringUtil.h" +#include "PlatformId.h" +#include "MameNames.h" +#include "SystemData.h" #include "FileData.h" #include "Log.h" #include "Window.h" @@ -69,8 +73,10 @@ ScraperSearchComponent::ScraperSearchComponent( mMD_Genre = std::make_shared(mWindow, "", font, mdColor); mMD_Players = std::make_shared(mWindow, "", font, mdColor); - mMD_Pairs.push_back(MetaDataPair(std::make_shared - (mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); + if (Settings::getInstance()->getBool("ScrapeRatings") && + Settings::getInstance()->getString("Scraper") != "TheGamesDB") + mMD_Pairs.push_back(MetaDataPair(std::make_shared + (mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); mMD_Pairs.push_back(MetaDataPair(std::make_shared (mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate)); mMD_Pairs.push_back(MetaDataPair(std::make_shared @@ -144,7 +150,7 @@ void ScraperSearchComponent::onSizeChanged() mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3); else mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, - mResultDesc->getFont()->getHeight() * 8); + mResultDesc->getFont()->getHeight() * 7); // Make description text wrap at edge of container. mResultDesc->setSize(mDescContainer->getSize().x(), 0); @@ -184,9 +190,12 @@ void ScraperSearchComponent::resizeMetadata() mMD_Grid->setColWidthPerc(0, maxLblWidth / mMD_Grid->getSize().x()); - // Rating is manually sized. - mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f); - mMD_Grid->onSizeChanged(); + if (Settings::getInstance()->getBool("ScrapeRatings") && + Settings::getInstance()->getString("Scraper") != "TheGamesDB") { + // Rating is manually sized. + mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f); + mMD_Grid->onSizeChanged(); + } // Make result font follow label font. mResultDesc->setFont(Font::get(fontHeight, FONT_PATH_REGULAR)); @@ -239,6 +248,7 @@ void ScraperSearchComponent::search(const ScraperSearchParams& params) mResultList->clear(); mScraperResults.clear(); + mMDRetrieveURLsHandle.reset(); mThumbnailReq.reset(); mMDResolveHandle.reset(); updateInfoPane(); @@ -252,6 +262,7 @@ void ScraperSearchComponent::stop() mThumbnailReq.reset(); mSearchHandle.reset(); mMDResolveHandle.reset(); + mMDRetrieveURLsHandle.reset(); mBlockAccept = false; } @@ -297,15 +308,18 @@ void ScraperSearchComponent::onSearchDone(const std::vector mBlockAccept = false; 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 (mScraperResults.size() == 0) - mSkipCallback(); - else - returnResult(mScraperResults.front()); - } - else if (mSearchType == ALWAYS_ACCEPT_MATCHING_CRC) { - // TODO + if (mScraperResults.size() == 0 || (mScraperResults.size() > 0 && + mScraperResults.front().ThumbnailImageUrl == "")) { + if (mScraperResults.size() == 0) + mSkipCallback(); + else + returnResult(mScraperResults.front()); + } } + } void ScraperSearchComponent::onSearchError(const std::string& error) @@ -338,14 +352,35 @@ void ScraperSearchComponent::updateInfoPane() mDescContainer->reset(); mResultThumbnail->setImage(""); - const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl; - if (!thumb.empty()) - mThumbnailReq = std::unique_ptr(new HttpReq(thumb)); - else - mThumbnailReq.reset(); + const std::string& thumb = res.screenshotUrl.empty() ? res.coverUrl : res.screenshotUrl; + mScraperResults[i].ThumbnailImageUrl = thumb; + + // Cache the thumbnail image in mScraperResults so that we don't need to download + // 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(new HttpReq(thumb)); + } + } + else { + mThumbnailReq.reset(); + } + } // 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_Developer->setText(Utils::String::toUpper(res.mdl.get("developer"))); mMD_Publisher->setText(Utils::String::toUpper(res.mdl.get("publisher"))); @@ -359,7 +394,9 @@ void ScraperSearchComponent::updateInfoPane() mResultThumbnail->setImage(""); // Metadata. - mMD_Rating->setValue(""); + if (Settings::getInstance()->getBool("ScrapeRatings") && + Settings::getInstance()->getString("Scraper") != "TheGamesDB") + mMD_Rating->setValue(""); mMD_ReleaseDate->setValue(""); mMD_Developer->setText(""); mMD_Publisher->setText(""); @@ -397,7 +434,8 @@ void ScraperSearchComponent::returnResult(ScraperSearchResult result) mBlockAccept = true; // Resolve metadata image before returning. - if (!result.imageUrl.empty()) { + if (result.mediaFilesDownloadStatus != COMPLETED) { + result.mediaFilesDownloadStatus = IN_PROGRESS; mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); return; } @@ -417,22 +455,69 @@ void ScraperSearchComponent::update(int deltaTime) if (mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) { auto status = mSearchHandle->status(); - auto results = mSearchHandle->getResults(); + mScraperResults = mSearchHandle->getResults(); auto statusString = mSearchHandle->getStatusString(); // We reset here because onSearchDone in auto mode can call mSkipCallback() which // can call another search() which will set our mSearchHandle to something important. mSearchHandle.reset(); - if (status == ASYNC_DONE) - onSearchDone(results); - else if (status == ASYNC_ERROR) + if (status == ASYNC_DONE && mScraperResults.size() == 0) + onSearchDone(mScraperResults); + + 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); + } + } + + 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->status() == ASYNC_DONE) { ScraperSearchResult result = mMDResolveHandle->getResult(); + result.mediaFilesDownloadStatus = COMPLETED; mMDResolveHandle.reset(); // This might end in us being deleted, depending on mAcceptCallback - // so make sure this is the last thing we do in update(). @@ -448,6 +533,15 @@ void ScraperSearchComponent::update(int deltaTime) void ScraperSearchComponent::updateThumbnail() { 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(); mResultThumbnail->setImage(content.data(), content.length()); mGrid.onSizeChanged(); // A hack to fix the thumbnail position since its size changed. @@ -458,6 +552,21 @@ void ScraperSearchComponent::updateThumbnail() } 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) @@ -468,10 +577,78 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) }; stop(); - 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")); + + if (params.system->hasPlatformId(PlatformIds::ARCADE) || + params.system->hasPlatformId(PlatformIds::NEOGEO)) { + 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 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 ScraperSearchComponent::getHelpPrompts() diff --git a/es-app/src/components/ScraperSearchComponent.h b/es-app/src/components/ScraperSearchComponent.h index af0776454..914e57220 100644 --- a/es-app/src/components/ScraperSearchComponent.h +++ b/es-app/src/components/ScraperSearchComponent.h @@ -4,7 +4,8 @@ // User interface component for the scraper where the user is able to see an overview // of the game being scraped and an option to override the game search string. // Used by both single-game scraping from the GuiMetaDataEd menu as well as -// to resolve scraping conflicts when run from GuiScraperStart. +// 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 // from GuiScraperMulti for multi-game scraping. @@ -41,6 +42,7 @@ public: void openInputScreen(ScraperSearchParams& from); void stop(); inline SearchType getSearchType() const { return mSearchType; } + static bool saveMetadata(const ScraperSearchResult& result, MetaDataList& metadata); // Metadata assets will be resolved before calling the accept callback // (e.g. result.mdl's "image" is automatically downloaded and properly set). @@ -71,6 +73,10 @@ private: 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. void returnResult(ScraperSearchResult result); @@ -111,6 +117,7 @@ private: bool mBlockAccept; std::unique_ptr mSearchHandle; + std::unique_ptr mMDRetrieveURLsHandle; std::unique_ptr mMDResolveHandle; std::vector mScraperResults; std::unique_ptr mThumbnailReq; diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 272b80ff0..558f3548f 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -80,7 +80,8 @@ public: inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; } protected: - virtual void onScroll() { navigationsounds.playThemeNavigationSound(SCROLLSOUND); } + virtual void onScroll() { + NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); } virtual void onCursorChanged(const CursorState& state); private: @@ -389,7 +390,7 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c setFont(Font::getFromTheme(elem, properties, mFont)); const float selectorHeight = Math::max(mFont->getHeight(1.0), (float)mFont->getSize()) * mLineSpacing; setSelectorHeight(selectorHeight); - + if(properties & ALIGNMENT) { if(elem->has("alignment")) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 4813ef22b..947a8ca04 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -103,7 +103,7 @@ GuiGamelistOptions::GuiGamelistOptions( row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { if (config->isMappedTo("a", input) && input.value) { - navigationsounds.playThemeNavigationSound(SCROLLSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); if (mJumpToLetterList->getSelected() == FAVORITE_CHAR) jumpToFirstRow(); else @@ -196,7 +196,7 @@ GuiGamelistOptions::~GuiGamelistOptions() // If a new sorting type was selected, then sort and update mSortTypeString for the system. if ((*mListSort->getSelected()).description != root->getSortTypeString()) { - navigationsounds.playThemeNavigationSound(SCROLLSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); // This will also recursively sort children. root->sort(*mListSort->getSelected(), mFavoritesSorting); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 3af2da11b..a87602ab2 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -14,7 +14,7 @@ #include "guis/GuiDetectDevice.h" #include "guis/GuiGeneralScreensaverOptions.h" #include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" +#include "guis/GuiScraperMenu.h" #include "guis/GuiSettings.h" #include "views/UIModeController.h" #include "views/ViewController.h" @@ -36,6 +36,9 @@ GuiMenu::GuiMenu( { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + if (isFullUI) + addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); + if (isFullUI) addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); @@ -45,9 +48,6 @@ GuiMenu::GuiMenu( addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - if (isFullUI) - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - if (isFullUI) addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); @@ -65,44 +65,8 @@ GuiMenu::GuiMenu( void GuiMenu::openScraperSettings() { - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // Scrape from. - auto scraper_list = std::make_shared< OptionListComponent< std::string > - >(mWindow, "SCRAPE FROM", false); - std::vector 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(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 openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared - (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); + // Open the scrape menu. + mWindow->pushGui(new GuiScraperMenu(mWindow)); } void GuiMenu::openSoundSettings() @@ -174,28 +138,6 @@ void GuiMenu::openSoundSettings() }); #endif - // Video audio. - auto video_audio = std::make_shared(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(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_ // OMX player Audio Device auto omx_audio_dev = std::make_shared< OptionListComponent @@ -221,6 +163,27 @@ void GuiMenu::openSoundSettings() Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); }); #endif + + // Video audio. + auto video_audio = std::make_shared(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(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); @@ -281,9 +244,9 @@ void GuiMenu::openUISettings() getString("TransitionStyle") == *it); s->addWithLabel("TRANSITION STYLE", transition_style); s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" && + transition_style->getSelected() != "instant" && + PowerSaver::getMode() == PowerSaver::INSTANT) { Settings::getInstance()->setString("PowerSaverMode", "default"); PowerSaver::init(); } @@ -315,8 +278,7 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - if (needReload) - { + if (needReload) { Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); @@ -404,9 +366,9 @@ void GuiMenu::openUISettings() move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) { + if (move_carousel->getState() && + !Settings::getInstance()->getBool("MoveCarousel") && + PowerSaver::getMode() == PowerSaver::INSTANT) { Settings::getInstance()->setString("PowerSaverMode", "default"); PowerSaver::init(); } @@ -460,8 +422,8 @@ void GuiMenu::openOtherSettings() fullscreen_mode->add(*it, *it, Settings::getInstance()->getString("FullscreenMode") == *it); s->addWithLabel("FULLSCREEN MODE (REQUIRES RESTART)", fullscreen_mode); s->addSaveFunc([fullscreen_mode] { - if (Settings::getInstance()->getString("FullscreenMode") == "normal" - && fullscreen_mode->getSelected() != "normal") { + if (Settings::getInstance()->getString("FullscreenMode") == "normal" && + fullscreen_mode->getSelected() != "normal") { Settings::getInstance()->setString("PowerSaverMode", "default"); PowerSaver::init(); } @@ -542,7 +504,7 @@ void GuiMenu::openOtherSettings() auto local_art = std::make_shared(mWindow); 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()-> setBool("LocalArt", local_art->getState()); }); @@ -583,7 +545,7 @@ void GuiMenu::openConfigInput() Window* window = mWindow; window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); + window->pushGui(new GuiDetectDevice(window, false, nullptr)); }, "NO", nullptr) ); } @@ -600,8 +562,8 @@ void GuiMenu::openQuitMenu() row.makeAcceptInputHandler([window] { window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES", [] { - Scripting::fireEvent("quit"); - quitES(); + Scripting::fireEvent("quit"); + quitES(); }, "NO", nullptr)); }); row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", @@ -615,10 +577,10 @@ void GuiMenu::openQuitMenu() row.makeAcceptInputHandler([window] { window->pushGui(new GuiMsgBox(window, "REALLY REBOOT?", "YES", [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES(QuitMode::REBOOT) != 0) - LOG(LogWarning) << "Reboot terminated with non-zero result!"; + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES(QuitMode::REBOOT) != 0) + LOG(LogWarning) << "Reboot terminated with non-zero result!"; }, "NO", nullptr)); }); row.addElement(std::make_shared(window, "REBOOT SYSTEM", @@ -631,10 +593,10 @@ void GuiMenu::openQuitMenu() row.makeAcceptInputHandler([window] { window->pushGui(new GuiMsgBox(window, "REALLY POWER OFF?", "YES", [] { - Scripting::fireEvent("quit", "poweroff"); - Scripting::fireEvent("poweroff"); - if (quitES(QuitMode::POWEROFF) != 0) - LOG(LogWarning) << "Power off terminated with non-zero result!"; + Scripting::fireEvent("quit", "poweroff"); + Scripting::fireEvent("poweroff"); + if (quitES(QuitMode::POWEROFF) != 0) + LOG(LogWarning) << "Power off terminated with non-zero result!"; }, "NO", nullptr)); }); row.addElement(std::make_shared(window, "POWER OFF SYSTEM", @@ -668,11 +630,8 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry( - const char* name, - unsigned int color, - bool add_arrow, - const std::function& func) +void GuiMenu::addEntry(const char* name, unsigned int color, + bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index f3701b63d..aaed45431 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -46,7 +46,8 @@ GuiMetaDataEd::GuiMetaDataEd( mMetaDataDecl(mdd), mMetaData(md), mSavedCallback(saveCallback), - mDeleteFunc(deleteFunc) + mDeleteFunc(deleteFunc), + mMetadataUpdated(false) { addChild(&mBackground); addChild(&mGrid); @@ -82,7 +83,6 @@ GuiMetaDataEd::GuiMetaDataEd( assert(ed); ed->setValue(mMetaData->get(iter->key)); mEditors.push_back(ed); - continue; } @@ -282,22 +282,41 @@ void GuiMetaDataEd::fetch() void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) { - for (unsigned int i = 0; i < mEditors.size(); i++) { - if (mMetaDataDecl.at(i).isStatistic) - continue; + // Clone the mMetaData object. + MetaDataList* metadata = nullptr; + 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; - 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) { // Find out if the user made any changes. - bool dirty = false; + bool dirty = mMetadataUpdated; for (unsigned int i = 0; i < mEditors.size(); i++) { 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; break; } @@ -315,7 +334,6 @@ void GuiMetaDataEd::close(bool closeAllWindows) }; } - if (dirty) { // Changes were made, ask if the user wants to save them. diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index dcc520a38..a494e7b1c 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -58,6 +58,8 @@ private: MetaDataList* mMetaData; std::function mSavedCallback; std::function mDeleteFunc; + + bool mMetadataUpdated; }; #endif // ES_APP_GUIS_GUI_META_DATA_ED_H diff --git a/es-app/src/guis/GuiScraperMenu.cpp b/es-app/src/guis/GuiScraperMenu.cpp new file mode 100644 index 000000000..637827252 --- /dev/null +++ b/es-app/src/guis/GuiScraperMenu.cpp @@ -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> + (mWindow, "SCRAPE FROM", false); + std::vector 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> + (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> + (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 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(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(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(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(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(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(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(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> + (mWindow, "REGION", false); + std::vector 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> + (mWindow, "LANGUAGE", false); + std::vector 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(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(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 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 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 GuiScraperMenu::getSearches( + std::vector systems, GameFilterFunc selector) +{ + std::queue queue; + for (auto sys = systems.cbegin(); sys != systems.cend(); sys++) { + std::vector 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& func) +{ + std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + + // Populate the list. + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if (add_arrow) { + std::shared_ptr 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 GuiScraperMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("a", "select")); + prompts.push_back(HelpPrompt("start", "close")); + return prompts; +} diff --git a/es-app/src/guis/GuiScraperMenu.h b/es-app/src/guis/GuiScraperMenu.h new file mode 100644 index 000000000..cc15fa077 --- /dev/null +++ b/es-app/src/guis/GuiScraperMenu.h @@ -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 +class OptionListComponent; +class SwitchComponent; +class SystemData; + +typedef std::function GameFilterFunc; + +class GuiScraperMenu : public GuiComponent +{ +public: + GuiScraperMenu(Window* window); + ~GuiScraperMenu(); + + bool input(InputConfig* config, Input input) override; + + std::vector getHelpPrompts() override; + HelpStyle getHelpStyle() override; + +private: + void pressedStart(); + void start(); + + void addEntry(const char* name, unsigned int color, + bool add_arrow, const std::function& func); + void openContentSettings(); + void openOtherSettings(); + + std::queue getSearches( + std::vector systems, GameFilterFunc selector); + + std::shared_ptr> mFilters; + std::shared_ptr> mSystems; + + MenuComponent mMenu; +}; + +#endif // ES_APP_GUIS_GUI_SCRAPER_MENU_H diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 8cfc0383b..d9b84b1bc 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -3,7 +3,7 @@ // // Multiple game scraping user interface. // 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. // @@ -136,7 +136,8 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); - search.game->metadata = result.mdl; + ScraperSearchComponent::saveMetadata(result, search.game->metadata); + updateGamelist(search.system); mSearchQueue.pop(); @@ -157,15 +158,15 @@ void GuiScraperMulti::finish() { std::stringstream ss; if (mTotalSuccessful == 0) { - ss << "NO GAMES WERE SCRAPED."; + ss << "NO GAMES WERE SCRAPED"; } else { ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") << - " SUCCESSFULLY SCRAPED!"; + " SUCCESSFULLY SCRAPED"; if (mTotalSkipped > 0) ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") << - " SKIPPED."; + " SKIPPED"; } mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), "OK", [&] { diff --git a/es-app/src/guis/GuiScraperMulti.h b/es-app/src/guis/GuiScraperMulti.h index e10d1217f..3ce33ef55 100644 --- a/es-app/src/guis/GuiScraperMulti.h +++ b/es-app/src/guis/GuiScraperMulti.h @@ -3,7 +3,7 @@ // // Multiple game scraping user interface. // 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. // @@ -15,6 +15,7 @@ #include "components/NinePatchComponent.h" #include "scrapers/Scraper.h" #include "GuiComponent.h" +#include "MetaData.h" class ScraperSearchComponent; class TextComponent; @@ -44,6 +45,7 @@ private: unsigned int mTotalSuccessful; unsigned int mTotalSkipped; std::queue mSearchQueue; + std::vector mMetaDataDecl; NinePatchComponent mBackground; ComponentGrid mGrid; diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp deleted file mode 100644 index e6f63ce39..000000000 --- a/es-app/src/guis/GuiScraperStart.cpp +++ /dev/null @@ -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 - >(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 - >(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(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 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 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 GuiScraperStart::getSearches( - std::vector systems, GameFilterFunc selector) -{ - std::queue queue; - for (auto sys = systems.cbegin(); sys != systems.cend(); sys++) { - std::vector 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 GuiScraperStart::getHelpPrompts() -{ - std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); - return prompts; -} diff --git a/es-app/src/guis/GuiScraperStart.h b/es-app/src/guis/GuiScraperStart.h deleted file mode 100644 index 66cb4a0d3..000000000 --- a/es-app/src/guis/GuiScraperStart.h +++ /dev/null @@ -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 -class OptionListComponent; -class SwitchComponent; -class SystemData; - -typedef std::function 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 getHelpPrompts() override; - -private: - void pressedStart(); - void start(); - std::queue getSearches( - std::vector systems, GameFilterFunc selector); - - std::shared_ptr< OptionListComponent > mFilters; - std::shared_ptr< OptionListComponent > mSystems; - std::shared_ptr mApproveResults; - - MenuComponent mMenu; -}; - -#endif // ES_APP_GUIS_GUI_SCRAPER_START_H diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 2d8293ed7..e8f2401d8 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -16,6 +16,7 @@ #include "PlatformId.h" #include "Settings.h" #include "SystemData.h" +#include "MameNames.h" #include "utils/TimeUtil.h" #include @@ -118,23 +119,30 @@ void thegamesdb_generate_json_scraper_requests( std::string gameID = cleanName.substr(3); path += "/Games/ByGameID?" + apiKey + "&fields=players,publishers,genres,overview,last_updated,rating," - "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&" - "include=boxart&id=" + + "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&id=" + HttpReq::urlEncode(gameID); usingGameID = true; } else { 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 + "&fields=players,publishers,genres,overview,last_updated,rating," - "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&" - "include=boxart&name=" + + "platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates&name=" + HttpReq::urlEncode(cleanName); } 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(new TheGamesDBJSONRequest(results, path))); } else { @@ -165,6 +173,21 @@ void thegamesdb_generate_json_scraper_requests( } } +void thegamesdb_generate_json_scraper_requests( + const std::string& gameIDs, + std::queue>& requests, + std::vector& 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 + (new TheGamesDBJSONRequest(requests, results, path))); +} + namespace { @@ -194,21 +217,6 @@ int getIntOrThrow(const Value& v) 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) { if (!v.IsArray()) @@ -274,13 +282,13 @@ std::string getGenreString(const Value& v) return out; } -void processGame(const Value& game, const Value& boxart, std::vector& results) +void processGame(const Value& game, std::vector& results) { - std::string baseImageUrlThumb = getStringOrThrow(boxart["base_url"], "thumb"); - std::string baseImageUrlLarge = getStringOrThrow(boxart["base_url"], "large"); - 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")); if (game.HasMember("overview") && game["overview"].IsString()) result.mdl.set("desc", game["overview"].GetString()); @@ -301,19 +309,52 @@ void processGame(const Value& game, const Value& boxart, std::vector& 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& req, std::vector& results) { @@ -331,34 +372,58 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, 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") || !doc["data"]["games"].IsArray()) { std::string warn = "TheGamesDBJSONRequest - Response had no game data.\n"; LOG(LogWarning) << warn; return; } + 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(); for (int i = 0; i < (int)games.Size(); ++i) { auto& v = games[i]; try { - processGame(v, boxart, results); + processGame(v, results); } catch (std::runtime_error& e) { LOG(LogError) << "Error while processing game: " << e.what(); diff --git a/es-app/src/scrapers/GamesDBJSONScraper.h b/es-app/src/scrapers/GamesDBJSONScraper.h index b6bd449bb..3c7b1783d 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.h +++ b/es-app/src/scrapers/GamesDBJSONScraper.h @@ -20,6 +20,11 @@ void thegamesdb_generate_json_scraper_requests( std::queue>& requests, std::vector& results); +void thegamesdb_generate_json_scraper_requests( + const std::string& gameIDs, + std::queue>& requests, + std::vector& results); + class TheGamesDBJSONRequest : public ScraperHttpRequest { public: @@ -42,6 +47,8 @@ class TheGamesDBJSONRequest : public ScraperHttpRequest } protected: + //void retrieveMediaURLs() + void process(const std::unique_ptr& req, std::vector& results) override; bool isGameRequest() { return !mRequestQueue; } diff --git a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp index ae3606ec1..b66b68c62 100644 --- a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp @@ -4,6 +4,11 @@ // Functions specifically for scraping from thegamesdb.net // Called from GamesDBJSONScraper. // +// Downloads these resource files to ~/.emulationstation/scrapers: +// gamesdb_developers.json +// gamesdb_genres.json +// gamesdb_publishers.json +// #include #include diff --git a/es-app/src/scrapers/GamesDBJSONScraperResources.h b/es-app/src/scrapers/GamesDBJSONScraperResources.h index da45eb3ee..0966169d4 100644 --- a/es-app/src/scrapers/GamesDBJSONScraperResources.h +++ b/es-app/src/scrapers/GamesDBJSONScraperResources.h @@ -4,6 +4,11 @@ // Functions specifically for scraping from thegamesdb.net // Called from GamesDBJSONScraper. // +// Downloads these resource files to ~/.emulationstation/scrapers: +// gamesdb_developers.json +// gamesdb_genres.json +// gamesdb_publishers.json +// #pragma once #ifndef ES_APP_SCRAPERS_GAMES_DB_JSON_SCRAPER_RESOURCES_H diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 852cb1073..7e88a22f9 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -36,10 +36,28 @@ std::unique_ptr startScraperSearch(const ScraperSearchParam return handle; } +std::unique_ptr startMediaURLsFetch(const std::string& gameIDs) +{ + const std::string& name = Settings::getInstance()->getString("Scraper"); + std::unique_ptr 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 getScraperList() { std::vector 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); return list; @@ -59,36 +77,35 @@ ScraperSearchHandle::ScraperSearchHandle() void ScraperSearchHandle::update() { - if(mStatus == ASYNC_DONE) + if (mStatus == ASYNC_DONE) return; - if(!mRequestQueue.empty()) - { + if (!mRequestQueue.empty()) { // A request can add more requests to the queue while running, // so be careful with references into the queue. auto& req = *(mRequestQueue.front()); AsyncHandleStatus status = req.status(); - if(status == ASYNC_ERROR) { + if (status == ASYNC_ERROR) { // Propagate error. setError(req.getStatusString()); // Empty our queue. - while(!mRequestQueue.empty()) + while (!mRequestQueue.empty()) mRequestQueue.pop(); return; } // Finished this one, see if we have any more. - if(status == ASYNC_DONE) + if (status == ASYNC_DONE) mRequestQueue.pop(); // Status == ASYNC_IN_PROGRESS. } // We finished without any errors! - if(mRequestQueue.empty()) { + if (mRequestQueue.empty()) { setStatus(ASYNC_DONE); return; } @@ -101,8 +118,8 @@ ScraperRequest::ScraperRequest(std::vector& resultsWrite) } // ScraperHttpRequest. -ScraperHttpRequest::ScraperHttpRequest(std::vector& - resultsWrite, const std::string& url) : ScraperRequest(resultsWrite) +ScraperHttpRequest::ScraperHttpRequest(std::vector& resultsWrite, + const std::string& url) : ScraperRequest(resultsWrite) { setStatus(ASYNC_IN_PROGRESS); mReq = std::unique_ptr(new HttpReq(url)); @@ -111,8 +128,7 @@ ScraperHttpRequest::ScraperHttpRequest(std::vector& void ScraperHttpRequest::update() { 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. setStatus(ASYNC_DONE); process(mReq, mResults); @@ -120,7 +136,7 @@ void ScraperHttpRequest::update() } // Not ready yet. - if(status == HttpReq::REQ_IN_PROGRESS) + if (status == HttpReq::REQ_IN_PROGRESS) return; // Everything else is some sort of error. @@ -129,8 +145,7 @@ void ScraperHttpRequest::update() setError(mReq->getErrorMsg()); } -// Metadata resolving stuff. - +// Download and write the media files to disk. std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search) { @@ -140,44 +155,124 @@ std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult MDResolveHandle::MDResolveHandle(const ScraperSearchResult& 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 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; // 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. - if (!result.imageType.empty()) { - ext = result.imageType; + if (!it->fileFormat.empty()) { + ext = it->fileFormat; } else { - size_t dot = result.imageUrl.find_last_of('.'); + size_t dot = it->fileURL.find_last_of('.'); 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), - [this, imgPath] { - mResult.mdl.set("image", imgPath); - mResult.imageUrl = ""; - })); + // If there is an existing media file on disk and the setting to overwrite data + // has been set to no, then don't proceed with downloading or saving a new file. + if (it->existingMediaFile != "" && + !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() { - if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR) + if (mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR) return; auto it = mFuncs.cbegin(); - while(it != mFuncs.cend()) { - if(it->first->status() == ASYNC_ERROR) { + while (it != mFuncs.cend()) { + + if (it->first->status() == ASYNC_ERROR) { setError(it->first->getStatusString()); return; } - else if(it->first->status() == ASYNC_DONE) { + else if (it->first->status() == ASYNC_DONE) { it->second(); it = mFuncs.erase(it); continue; @@ -185,30 +280,41 @@ void MDResolveHandle::update() it++; } - if(mFuncs.empty()) + if (mFuncs.empty()) setStatus(ASYNC_DONE); } std::unique_ptr downloadImageAsync(const std::string& url, - const std::string& saveAs) + const std::string& saveAs, const std::string& existingMediaFile) { - return std::unique_ptr(new ImageDownloadHandle(url, saveAs, + return std::unique_ptr(new ImageDownloadHandle( + url, + saveAs, + existingMediaFile, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"))); } -ImageDownloadHandle::ImageDownloadHandle(const std::string& url, - const std::string& path, int maxWidth, int maxHeight) : mSavePath(path), - mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) +ImageDownloadHandle::ImageDownloadHandle( + const std::string& 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() { - if(mReq->status() == HttpReq::REQ_IN_PROGRESS) + if (mReq->status() == HttpReq::REQ_IN_PROGRESS) return; - if(mReq->status() != HttpReq::REQ_SUCCESS) { + if (mReq->status() != HttpReq::REQ_SUCCESS) { std::stringstream ss; ss << "Network error: " << mReq->getErrorMsg(); setError(ss.str()); @@ -216,8 +322,16 @@ void ImageDownloadHandle::update() } // 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); - if(stream.bad()) { + if (stream.bad()) { setError("Failed to open image path to write. Permission error? Disk full?"); return; } @@ -225,13 +339,13 @@ void ImageDownloadHandle::update() const std::string& content = mReq->getContent(); stream.write(content.data(), content.length()); stream.close(); - if(stream.bad()) { + if (stream.bad()) { setError("Failed to save image. Disk full?"); return; } // Resize it. - if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) { + if (!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) { setError("Error saving resized image. Out of memory? Disk full?"); return; } @@ -243,7 +357,7 @@ void ImageDownloadHandle::update() bool resizeImage(const std::string& path, int maxWidth, int maxHeight) { // Nothing to do. - if(maxWidth == 0 && maxHeight == 0) + if (maxWidth == 0 && maxHeight == 0) return true; FREE_IMAGE_FORMAT format = FIF_UNKNOWN; @@ -251,15 +365,15 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) // Detect the filetype. format = FreeImage_GetFileType(path.c_str(), 0); - if(format == FIF_UNKNOWN) + if (format == FIF_UNKNOWN) format = FreeImage_GetFIFFromFilename(path.c_str()); - if(format == FIF_UNKNOWN) { + if (format == FIF_UNKNOWN) { LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!"; return false; } // 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()); } else { @@ -270,15 +384,20 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) float width = (float)FreeImage_GetWidth(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); - else if(maxHeight == 0) + else if (maxHeight == 0) maxHeight = (int)((maxWidth / width) * height); FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR); FreeImage_Unload(image); - if(imageRescaled == NULL) { + if (imageRescaled == NULL) { LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)"; 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); FreeImage_Unload(imageRescaled); - if(!saved) + if (!saved) LOG(LogError) << "Failed to save resized image!"; return saved; } 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 name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; + const std::string systemsubdirectory = params.system->getName(); + 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); - path += subdirectory + "/"; + path += systemsubdirectory + "/" + filetypeSubdirectory + "/"; - if(!Utils::FileSystem::exists(path)) + if (!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); path += name + extension; diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index eea7eee05..afdff86b4 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -24,6 +24,12 @@ class FileData; class SystemData; +enum eDownloadStatus { + NOT_STARTED, + IN_PROGRESS, + COMPLETED +}; + struct ScraperSearchParams { SystemData* system; FileData* game; @@ -35,11 +41,29 @@ struct ScraperSearchResult { ScraperSearchResult() : mdl(GAME_METADATA) {}; MetaDataList mdl; - std::string imageUrl; - std::string thumbnailUrl; + std::string gameID; + + // 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. - 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. @@ -83,7 +107,6 @@ protected: std::vector& mResults; }; - // A single HTTP request that needs to be processed to get the results. class ScraperHttpRequest : public ScraperRequest { @@ -113,6 +136,9 @@ protected: friend std::unique_ptr startScraperSearch(const ScraperSearchParams& params); + friend std::unique_ptr + startMediaURLsFetch(const std::string& gameIDs); + std::queue< std::unique_ptr > mRequestQueue; std::vector mResults; }; @@ -120,6 +146,8 @@ protected: // Will use the current scraper settings to pick the result source. std::unique_ptr startScraperSearch(const ScraperSearchParams& params); +std::unique_ptr startMediaURLsFetch(const std::string& gameIDs); + // Returns a list of valid scraper names. std::vector getScraperList(); @@ -127,7 +155,7 @@ std::vector getScraperList(); bool isValidConfiguredScraper(); typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params, - std::queue< std::unique_ptr >& requests, + std::queue>& requests, std::vector& results); // ------------------------------------------------------------------------- @@ -145,21 +173,26 @@ public: private: ScraperSearchResult mResult; - typedef std::pair< std::unique_ptr, std::function > ResolvePair; + typedef std::pair, std::function> ResolvePair; std::vector mFuncs; }; class ImageDownloadHandle : public AsyncHandle { public: - ImageDownloadHandle(const std::string& url, const std::string& path, - int maxWidth, int maxHeight); + ImageDownloadHandle( + const std::string& url, + const std::string& path, + const std::string& existingMediaPath, + int maxWidth, + int maxHeight); void update() override; private: std::unique_ptr mReq; std::string mSavePath; + std::string mExistingMediaFile; int mMaxWidth; int mMaxHeight; }; @@ -167,13 +200,13 @@ private: // About the same as: // "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]". // Will create the "downloaded_images" and "subdirectory" directories if they do not exist. -std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, - const std::string& url); +std::string getSaveAsPath(const ScraperSearchParams& params, + const std::string& filetypeSubdirectory, const std::string& url); // Will resize according to Settings::getInt("ScraperResizeWidth") and // Settings::getInt("ScraperResizeHeight"). std::unique_ptr 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. std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult& result, diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index fda737dee..152794aa9 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -14,6 +14,7 @@ #include "PlatformId.h" #include "Settings.h" #include "SystemData.h" +#include "math/Misc.h" #include #include @@ -132,7 +133,11 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, 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(); std::vector p_ids; @@ -164,7 +169,6 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, requests.push(std::unique_ptr (new ScreenScraperRequest(requests, results, path))); } - } void ScreenScraperRequest::process(const std::unique_ptr& req, @@ -200,8 +204,20 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; - std::string region = Utils::String::toLower(ssConfig.region).c_str(); - std::string language = Utils::String::toLower(ssConfig.language).c_str(); + result.gameID = game.attribute("id").as_string(); + + // 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[*] ). 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. result.mdl.set("players", game.child("joueurs").text().get()); - // TODO: Validate rating. + // Validate rating. if (Settings::getInstance()->getBool("ScrapeRatings") && game.child("note")) { 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; ss << ratingVal; 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"); 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. - // We need to do this because any child of 'medias' has the form - // - // and we need to find the right media for the region. - pugi::xpath_node_set results = media_list.select_nodes((static_cast - ("media[@type='") + ssConfig.media_name + "']").c_str()); +void ScreenScraperRequest::processMedia( + ScraperSearchResult& result, + const pugi::xml_node& media_list, + std::string mediaType, + std::string& fileURL, + std::string& fileFormat, + std::string region) +{ + pugi::xml_node art = pugi::xml_node(NULL); - if (results.size()) { - // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU. - for (auto _region : std::vector{ region, - "wor", "us", "cus", "jp", "eu" }) { - if (art) + // Do an XPath query for media[type='$media_type'], then filter by region. + // We need to do this because any child of 'medias' has the form + // + // and we need to find the right media for the region. + pugi::xpath_node_set results = media_list.select_nodes((static_cast + ("media[@type='") + mediaType + "']").c_str()); + + if (results.size()) { + // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU. + for (auto _region : std::vector{ + region, "wor", "us", "cus", "jp", "eu" }) { + if (art) + break; + + for (auto node : results) { + if (node.node().attribute("region").value() == _region) { + art = node.node(); 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); - } // Game. + 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. + 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. diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index a168b4434..ed66fdf41 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -62,15 +62,19 @@ public: // Note that not all games contain values for all these, so we default to "box-2D" // since it's the most common. // - std::string media_name = "box-2D"; + + std::string media_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. // 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. // Applies to: description, genre. - std::string language = "EN"; + // This is read from es_settings.cfg, setting 'ScraperLanguage'. ScreenScraperConfig() {}; } configuration; @@ -81,6 +85,12 @@ protected: void processList(const pugi::xml_document& xmldoc, std::vector& results); void processGame(const pugi::xml_document& xmldoc, std::vector& 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; } std::queue< std::unique_ptr >* mRequestQueue; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index d00d22b19..afd07954e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -155,13 +155,13 @@ bool SystemView::input(InputConfig* config, Input input) case VERTICAL_WHEEL: if (config->isMappedLike("up", input)) { - navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND); listInput(-1); return true; } if (config->isMappedLike("down", input)) { - navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND); listInput(1); return true; } @@ -171,13 +171,13 @@ bool SystemView::input(InputConfig* config, Input input) default: if (config->isMappedLike("left", input)) { - navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND); listInput(-1); return true; } if (config->isMappedLike("right", input)) { - navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND); listInput(1); return true; } @@ -188,14 +188,14 @@ bool SystemView::input(InputConfig* config, Input input) { stopScrolling(); ViewController::get()->goToGameList(getSelected()); - navigationsounds.playThemeNavigationSound(SELECTSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SELECTSOUND); return true; } if (config->isMappedTo("x", input)) { // get random system // go to system - navigationsounds.playThemeNavigationSound(SYSTEMBROWSESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SYSTEMBROWSESOUND); setCursor(SystemData::getRandomSystem()); return true; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 17b77abe7..fc6ecf563 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -28,7 +28,6 @@ #include "Sound.h" ViewController* ViewController::sInstance = nullptr; -NavigationSounds navigationsounds; ViewController* ViewController::get() { @@ -117,7 +116,7 @@ void ViewController::goToNextGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - navigationsounds.playThemeNavigationSound(QUICKSYSSELECTSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(QUICKSYSSELECTSOUND); goToGameList(system->getNext()); } @@ -126,7 +125,7 @@ void ViewController::goToPrevGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - navigationsounds.playThemeNavigationSound(QUICKSYSSELECTSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(QUICKSYSSELECTSOUND); goToGameList(system->getPrev()); } @@ -239,9 +238,9 @@ void ViewController::launch(FileData* game, Vector3f center) 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. - while(navigationsounds.isPlayingThemeNavigationSound(LAUNCHSOUND)); + while (NavigationSounds::getInstance()->isPlayingThemeNavigationSound(LAUNCHSOUND)); if (transition_style == "fade") { // Fade out, launch game, fade back in. @@ -472,7 +471,8 @@ void ViewController::preload() getGameListView(*it); } // Load navigation sounds. - navigationsounds.loadThemeNavigationSounds(SystemData::sSystemVector[0]->getTheme()); + NavigationSounds::getInstance()->loadThemeNavigationSounds( + SystemData::sSystemVector.front()->getTheme()); } void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) @@ -539,7 +539,8 @@ void ViewController::reloadAll() } // Load navigation sounds. - navigationsounds.loadThemeNavigationSounds(SystemData::sSystemVector[0]->getTheme()); + NavigationSounds::getInstance()->loadThemeNavigationSounds( + SystemData::sSystemVector.front()->getTheme()); updateHelpPrompts(); } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 85aa7424f..e457e5c81 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -176,7 +176,7 @@ bool GridGameListView::input(InputConfig* config, Input input) config->isMappedLike("right", input) || (config->isMappedLike("up", input)) || (config->isMappedLike("down", input)) )) - navigationsounds.playThemeNavigationSound(SCROLLSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); if (config->isMappedLike("left", input) || config->isMappedLike("right", input)) return GuiComponent::input(config, input); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 7cf25ba66..be90c9740 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -98,7 +98,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) else { // It's a folder. if (cursor->getChildren().size() > 0) { - navigationsounds.playThemeNavigationSound(SELECTSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SELECTSOUND); mCursorStack.push(cursor); populateList(cursor->getChildrenListToDisplay()); FileData* cursor = getCursor(); @@ -110,13 +110,13 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } else if (config->isMappedTo("b", input)) { if (mCursorStack.size()) { - navigationsounds.playThemeNavigationSound(BACKSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(BACKSOUND); populateList(mCursorStack.top()->getParent()->getChildren()); setCursor(mCursorStack.top()); mCursorStack.pop(); } else { - navigationsounds.playThemeNavigationSound(BACKSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(BACKSOUND); onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); if (systemToView->isCollection()) @@ -145,7 +145,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { // Go to random system game. - navigationsounds.playThemeNavigationSound(SCROLLSOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); FileData* randomGame = getCursor()->getSystem()->getRandomGame(); if (randomGame) setCursor(randomGame); @@ -155,7 +155,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) { if (mRoot->getSystem()->isGameSystem()) { - navigationsounds.playThemeNavigationSound(FAVORITESOUND); + NavigationSounds::getInstance()->playThemeNavigationSound(FAVORITESOUND); if (CollectionSystemManager::get()->toggleGameInCollection(getCursor())) return true; } diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index e6734234b..46a6cd4b8 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -287,7 +287,6 @@ void VideoGameListView::updateInfoPanel() mVideo->setImage(file->getImagePath()); mThumbnail.setImage(file->getThumbnailPath()); mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 48d50d908..8eaf39d4b 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -44,8 +44,7 @@ bool HttpReq::isUrl(const std::string& str) std::string::npos || str.find("www.") != std::string::npos)); } -HttpReq::HttpReq(const std::string& url) - : mStatus(REQ_IN_PROGRESS), mHandle(NULL) +HttpReq::HttpReq(const std::string& url) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { mHandle = curl_easy_init(); diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index dde0bac6b..edbf6acb5 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -26,8 +26,7 @@ // Once one of those calls complete, the request is ready. // // Do something like this to capture errors: -// if(myRequest.status() != REQ_SUCCESS) -// { +// if(myRequest.status() != REQ_SUCCESS) { // // An error occured. // LOG(LogError) << "HTTP request error - " << myRequest.getErrorMessage(); // return; diff --git a/es-core/src/MameNames.cpp b/es-core/src/MameNames.cpp index e0dee5c4b..c222021f7 100644 --- a/es-core/src/MameNames.cpp +++ b/es-core/src/MameNames.cpp @@ -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 "resources/ResourceManager.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include #include @@ -10,35 +20,31 @@ MameNames* MameNames::sInstance = nullptr; void MameNames::init() { - if(!sInstance) + if (!sInstance) sInstance = new MameNames(); - -} // init +} void MameNames::deinit() { - if(sInstance) - { + if (sInstance) { delete sInstance; sInstance = nullptr; } - -} // deinit +} MameNames* MameNames::getInstance() { - if(!sInstance) + if (!sInstance) sInstance = new MameNames(); return sInstance; - -} // getInstance +} MameNames::MameNames() { std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamenames.xml"); - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; @@ -46,115 +52,123 @@ MameNames::MameNames() pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); - if(!result) - { - LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); + if (!result) { + LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " + << result.description(); return; } - 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() }; + 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() + }; mNamePairs.push_back(namePair); } - // Read bios + // Read BIOS file. xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamebioses.xml"); - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; result = doc.load_file(xmlpath.c_str()); - if(!result) - { - LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); + if (!result) { + LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " + << result.description(); 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(); mMameBioses.push_back(bios); } - // Read devices + // Read devices file. xmlpath = ResourceManager::getInstance()->getResourcePath(":/mamedevices.xml"); - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; result = doc.load_file(xmlpath.c_str()); - if(!result) - { - LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); + if (!result) { + LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " + << result.description(); 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(); mMameDevices.push_back(device); } -} // MameNames +} // MameNames. MameNames::~MameNames() { - -} // ~MameNames +} std::string MameNames::getRealName(const std::string& _mameName) { size_t start = 0; - size_t end = mNamePairs.size(); + size_t end = mNamePairs.size(); - while(start < end) - { - const size_t index = (start + end) / 2; - const int compare = strcmp(mNamePairs[index].mameName.c_str(), _mameName.c_str()); + while (start < end) { + const size_t index = (start + end) / 2; + const int compare = strcmp(mNamePairs[index].mameName.c_str(), _mameName.c_str()); - if(compare < 0) start = index + 1; - else if( compare > 0) end = index; - else return mNamePairs[index].realName; + if (compare < 0) + start = index + 1; + else if (compare > 0) + end = index; + else + return mNamePairs[index].realName; } 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) { return MameNames::find(mMameBioses, _biosName); - -} // isBios +} const bool MameNames::isDevice(const std::string& _deviceName) { return MameNames::find(mMameDevices, _deviceName); -} // isDevice +} const bool MameNames::find(std::vector devices, const std::string& name) { size_t start = 0; - size_t end = devices.size(); + size_t end = devices.size(); - while(start < end) - { - const size_t index = (start + end) / 2; - const int compare = strcmp(devices[index].c_str(), name.c_str()); + while (start < end) { + const size_t index = (start + end) / 2; + const int compare = strcmp(devices[index].c_str(), name.c_str()); - if(compare < 0) start = index + 1; - else if( compare > 0) end = index; - else return true; + if (compare < 0) + start = index + 1; + else if (compare > 0) + end = index; + else + return true; } - return false; - } diff --git a/es-core/src/MameNames.h b/es-core/src/MameNames.h index 02036d65d..9260dacfa 100644 --- a/es-core/src/MameNames.h +++ b/es-core/src/MameNames.h @@ -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 #ifndef ES_CORE_MAMENAMES_H #define ES_CORE_MAMENAMES_H @@ -5,6 +14,7 @@ #include #include +// Expand MAME names to full game names. class MameNames { public: @@ -13,13 +23,13 @@ public: static void deinit (); static MameNames* getInstance(); std::string getRealName(const std::string& _mameName); + std::string getCleanName(const std::string& _mameName); const bool isBios(const std::string& _biosName); const bool isDevice(const std::string& _deviceName); private: - struct NamePair - { + struct NamePair { std::string mameName; std::string realName; }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index b0812cedf..c37409483 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -132,7 +132,19 @@ void Settings::setDefaults() // Scraper. 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["Scrape3DBoxes"] = true; + mBoolMap["ScrapeCovers"] = true; + mBoolMap["ScrapeMarquees"] = true; + mBoolMap["ScrapeScreenshots"] = true; // Other settings. #ifdef _RPI_ @@ -189,6 +201,16 @@ void Settings::setDefaults() mIntMap["ScreenOffsetY"] = 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. // @@ -197,10 +219,6 @@ void Settings::setDefaults() mBoolMap["DebugGrid"] = false; mBoolMap["DebugText"] = false; mBoolMap["DebugImage"] = false; - mStringMap["DefaultSortOrder"] = "filename, ascending"; - mStringMap["MediaDirectory"] = ""; - mIntMap["ScraperResizeWidth"] = 400; - mIntMap["ScraperResizeHeight"] = 0; mBoolMap["SplashScreenProgress"] = true; mStringMap["UIMode_passkey"] = "uuddlrlrba"; } diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index 2103a60f8..988d554b7 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -5,6 +5,8 @@ #include "Settings.h" #include "ThemeData.h" +NavigationSounds* NavigationSounds::sInstance = nullptr; + std::map< std::string, std::shared_ptr > Sound::sMap; std::shared_ptr Sound::get(const std::string& path) @@ -26,79 +28,41 @@ std::shared_ptr Sound::getFromTheme(const std::shared_ptr& the const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound"); 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(""); } - - LOG(LogInfo) << "[" << element << "] found, ready to play sound file"; + + LOG(LogInfo) << "[" << element << "] found, ready to load sound file"; return get(elem->get("path")); } +NavigationSounds* NavigationSounds::getInstance() +{ + if (sInstance == NULL) + sInstance = new NavigationSounds(); + + return sInstance; +} + void NavigationSounds::loadThemeNavigationSounds(const std::shared_ptr& theme) { - systembrowseSound = Sound::getFromTheme(theme, "all", "systembrowseSound"); - quicksysselectSound = Sound::getFromTheme(theme, "all", "quicksysselectSound"); - selectSound = Sound::getFromTheme(theme, "all", "selectSound"); - backSound = Sound::getFromTheme(theme, "all", "backSound"); - scrollSound = Sound::getFromTheme(theme, "all", "scrollSound"); - favoriteSound = Sound::getFromTheme(theme, "all", "favoriteSound"); - launchSound = Sound::getFromTheme(theme, "all", "launchSound"); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "systembrowseSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "quicksysselectSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "selectSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "backSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "scrollSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "favoriteSound")); + navigationSounds.push_back(Sound::getFromTheme(theme, "all", "launchSound")); } void NavigationSounds::playThemeNavigationSound(NavigationSoundsID soundID) { - - 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(); - } + NavigationSounds::getInstance()->navigationSounds[soundID]->play(); } bool NavigationSounds::isPlayingThemeNavigationSound(NavigationSoundsID soundID) { - switch(soundID) - { - 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; + return NavigationSounds::getInstance()->navigationSounds[soundID]->isPlaying(); } Sound::Sound(const std::string & path) : mSampleData(NULL), mSamplePos(0), mSampleLength(0), playing(false) @@ -130,7 +94,7 @@ void Sound::init() Uint8 * data = NULL; Uint32 dlen = 0; 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; } //build conversion buffer diff --git a/es-core/src/Sound.h b/es-core/src/Sound.h index 1b84e9987..034dc6c26 100644 --- a/es-core/src/Sound.h +++ b/es-core/src/Sound.h @@ -5,6 +5,7 @@ #include "SDL_audio.h" #include #include +#include class ThemeData; @@ -57,18 +58,15 @@ enum NavigationSoundsID class NavigationSounds { public: + static NavigationSounds* getInstance(); + void loadThemeNavigationSounds(const std::shared_ptr& theme); void playThemeNavigationSound(NavigationSoundsID soundID); bool isPlayingThemeNavigationSound(NavigationSoundsID soundID); private: - std::shared_ptr systembrowseSound; - std::shared_ptr quicksysselectSound; - std::shared_ptr selectSound; - std::shared_ptr backSound; - std::shared_ptr scrollSound; - std::shared_ptr favoriteSound; - std::shared_ptr launchSound; + static NavigationSounds* sInstance; + std::vector> navigationSounds; }; extern NavigationSounds navigationsounds; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b5945..319c6cb27 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -1,6 +1,7 @@ #include "components/MenuComponent.h" #include "components/ButtonComponent.h" +#include "Settings.h" #define BUTTON_GRID_VERT_PADDING 32 #define BUTTON_GRID_HORIZ_PADDING 10 @@ -32,6 +33,22 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share 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) { mTitle->setText(Utils::String::toUpper(title)); diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index ad2f69db1..1ec0359db 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -20,7 +20,9 @@ class MenuComponent : public GuiComponent { public: MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + virtual ~MenuComponent(); // just calls save(); + void save(); void onSizeChanged() override; inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } @@ -33,6 +35,8 @@ public: addRow(row, setCursorHere); } + inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + void addButton(const std::string& label, const std::string& helpText, const std::function& callback); void setTitle(const char* title, const std::shared_ptr& font); @@ -54,6 +58,7 @@ private: std::shared_ptr mList; std::shared_ptr mButtonGrid; std::vector< std::shared_ptr > mButtons; + std::vector< std::function > mSaveFuncs; }; #endif // ES_CORE_COMPONENTS_MENU_COMPONENT_H diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index bf922acdc..cfbf98568 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -250,6 +250,30 @@ public: 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() { for(unsigned int i = 0; i < mEntries.size(); i++)