Skip to content

Commit b2d8ad6

Browse files
committed
boxart: use flycast-content github repo for arcade games screenshots
1 parent f9b47b6 commit b2d8ad6

File tree

10 files changed

+133
-35
lines changed

10 files changed

+133
-35
lines changed

core/ui/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ if(NOT LIBRETRO)
2222
settings_general.cpp
2323
settings_network.cpp
2424
settings_video.cpp
25+
boxart/arcade_scraper.h
2526
boxart/boxart.cpp
2627
boxart/boxart.h
2728
boxart/gamesdb.cpp
2829
boxart/gamesdb.h
30+
boxart/pvrparser.h
2931
boxart/scraper.cpp
3032
boxart/scraper.h)
3133

core/ui/boxart/arcade_scraper.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2025 flyinghead
3+
4+
This file is part of Flycast.
5+
6+
Flycast is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 2 of the License, or
9+
(at your option) any later version.
10+
11+
Flycast is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
#pragma once
20+
#include "scraper.h"
21+
#include "oslib/http_client.h"
22+
23+
class ArcadeScraper : public Scraper
24+
{
25+
public:
26+
bool initialize(const std::string& saveDirectory) override;
27+
void scrape(GameBoxart& item) override;
28+
};
29+
30+
bool ArcadeScraper::initialize(const std::string& saveDirectory)
31+
{
32+
if (!Scraper::initialize(saveDirectory))
33+
return false;
34+
35+
http::init();
36+
37+
return true;
38+
}
39+
40+
void ArcadeScraper::scrape(GameBoxart& item)
41+
{
42+
if (item.fileName.empty() || !item.arcade)
43+
// invalid rom or not an arcade game
44+
return;
45+
std::string filename = makeUniqueFilename("dummy.jpg");
46+
std::string extension = get_file_extension(item.fileName);
47+
if (extension == "zip" || extension == "7z")
48+
{
49+
std::string url = "https://flyinghead.github.io/flycast-content/arcade/jpg/" + get_file_basename(item.fileName) + ".jpg";
50+
if (downloadImage(url, filename, true))
51+
{
52+
item.setBoxartPath(filename);
53+
item.boxartUrl = url;
54+
item.scraped = true;
55+
return;
56+
}
57+
}
58+
if (!item.uniqueId.empty())
59+
{
60+
bool valid = true;
61+
for (char c : item.uniqueId)
62+
if (c < ' ' || c > '~') {
63+
valid = false;
64+
break;
65+
}
66+
if (valid)
67+
{
68+
std::string url = "https://flyinghead.github.io/flycast-content/arcade/jpg/" + http::urlEncode(item.uniqueId) + ".jpg";
69+
if (downloadImage(url, filename, true))
70+
{
71+
item.setBoxartPath(filename);
72+
item.boxartUrl = url;
73+
item.scraped = true;
74+
}
75+
}
76+
}
77+
}

core/ui/boxart/boxart.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "../game_scanner.h"
2222
#include "oslib/oslib.h"
2323
#include "cfg/option.h"
24+
#include "arcade_scraper.h"
2425
#include <chrono>
2526

2627
GameBoxart Boxart::getBoxart(const GameMedia& media)
@@ -50,6 +51,7 @@ GameBoxart Boxart::getBoxartAndLoad(const GameMedia& media)
5051
{
5152
boxart.busy = it->second.busy = true;
5253
boxart.gamePath = media.path;
54+
boxart.arcade = media.arcade;
5355
toFetch.push_back(boxart);
5456
}
5557
}
@@ -60,6 +62,7 @@ GameBoxart Boxart::getBoxartAndLoad(const GameMedia& media)
6062
boxart.name = media.name;
6163
boxart.searchName = media.gameName; // for arcade games
6264
boxart.busy = true;
65+
boxart.arcade = media.arcade;
6366
games[boxart.fileName] = boxart;
6467
toFetch.push_back(boxart);
6568
}
@@ -83,14 +86,21 @@ void Boxart::fetchBoxart()
8386
offlineScraper = std::unique_ptr<Scraper>(new OfflineScraper());
8487
offlineScraper->initialize(getSaveDirectory());
8588
}
86-
if (config::FetchBoxart && scraper == nullptr)
89+
if (config::FetchBoxart)
8790
{
88-
scraper = std::unique_ptr<Scraper>(new TheGamesDb());
89-
if (!scraper->initialize(getSaveDirectory()))
91+
if (scraper == nullptr)
9092
{
91-
ERROR_LOG(COMMON, "thegamesdb scraper initialization failed");
92-
scraper.reset();
93-
return;
93+
scraper = std::unique_ptr<Scraper>(new TheGamesDb());
94+
if (!scraper->initialize(getSaveDirectory()))
95+
{
96+
ERROR_LOG(COMMON, "thegamesdb scraper initialization failed");
97+
scraper.reset();
98+
return;
99+
}
100+
}
101+
if (arcadeScraper == nullptr) {
102+
arcadeScraper = std::make_unique<ArcadeScraper>();
103+
arcadeScraper->initialize(getSaveDirectory());
94104
}
95105
}
96106
std::vector<GameBoxart> boxart;
@@ -116,6 +126,7 @@ void Boxart::fetchBoxart()
116126
if (config::FetchBoxart)
117127
{
118128
try {
129+
arcadeScraper->scrape(boxart);
119130
scraper->scrape(boxart);
120131
{
121132
std::lock_guard<std::mutex> guard(mutex);

core/ui/boxart/boxart.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Boxart
4949
std::mutex mutex;
5050
std::unique_ptr<Scraper> scraper;
5151
std::unique_ptr<Scraper> offlineScraper;
52+
std::unique_ptr<Scraper> arcadeScraper;
5253
bool databaseLoaded = false;
5354
bool databaseDirty = false;
5455

core/ui/boxart/gamesdb.cpp

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ bool TheGamesDb::initialize(const std::string& saveDirectory)
3939
return true;
4040
}
4141

42-
TheGamesDb::~TheGamesDb()
43-
{
44-
http::term();
45-
}
46-
4742
void TheGamesDb::copyFile(const std::string& from, const std::string& to)
4843
{
4944
FILE *ffrom = nowide::fopen(from.c_str(), "rb");
@@ -317,8 +312,8 @@ bool TheGamesDb::fetchGameInfo(GameBoxart& item, const std::string& url, const s
317312

318313
void TheGamesDb::scrape(GameBoxart& item)
319314
{
320-
if (item.searchName.empty())
321-
// invalid rom or disk
315+
if (item.searchName.empty() || item.arcade)
316+
// invalid rom or disk, or arcade game
322317
return;
323318
fetchPlatforms();
324319

@@ -395,8 +390,8 @@ void TheGamesDb::scrape(std::vector<GameBoxart>& items)
395390
fetchByName(item);
396391
else if (item.gamePath.empty())
397392
{
398-
std::string localPath = makeUniqueFilename("dreamcast_logo_grey.png");
399-
std::string biosArtUrl{ "https://flyinghead.github.io/flycast-builds/dreamcast_logo_grey.png" };
393+
std::string localPath = makeUniqueFilename("dreamcast_logo_grey.jpg");
394+
std::string biosArtUrl{ "https://flyinghead.github.io/flycast-content/console/jpg/dreamcast_logo_grey.jpg" };
400395
if (downloadImage(biosArtUrl, localPath)) {
401396
item.setBoxartPath(localPath);
402397
item.boxartUrl = biosArtUrl;

core/ui/boxart/gamesdb.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class TheGamesDb : public Scraper
3232
bool initialize(const std::string& saveDirectory) override;
3333
void scrape(GameBoxart& item) override;
3434
void scrape(std::vector<GameBoxart>& items) override;
35-
~TheGamesDb() override;
3635

3736
private:
3837
void fetchPlatforms();

core/ui/boxart/scraper.cpp

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
#include "scraper.h"
2020
#include "oslib/http_client.h"
21+
#include "oslib/storage.h"
2122
#include "stdclass.h"
2223
#include "emulator.h"
2324
#include "imgread/common.h"
@@ -47,6 +48,7 @@ json GameBoxart::to_json(const std::string& baseArtPath) const
4748
{ "boxart_url", boxartUrl },
4849
{ "parsed", parsed },
4950
{ "scraped", scraped },
51+
{ "arcade", arcade },
5052
};
5153
return j;
5254
}
@@ -64,28 +66,32 @@ GameBoxart::GameBoxart(const json& j, const std::string& baseArtPath)
6466
loadProperty(boxartUrl, j, "boxart_url");
6567
loadProperty(parsed, j, "parsed");
6668
loadProperty(scraped, j, "scraped");
69+
loadProperty(arcade, j, "arcade");
6770
if (!boxartPath.empty() && !isAbsolutePath(boxartPath))
6871
boxartPath = baseArtPath + boxartPath;
6972
}
7073

71-
bool Scraper::downloadImage(const std::string& url, const std::string& localName)
74+
bool Scraper::downloadImage(const std::string& url, const std::string& localName, bool mute)
7275
{
7376
DEBUG_LOG(COMMON, "downloading %s", url.c_str());
7477
std::vector<u8> content;
7578
std::string contentType;
7679
if (!http::success(http::get(url, content, contentType)))
7780
{
78-
WARN_LOG(COMMON, "downloadImage http error: %s", url.c_str());
81+
if (!mute)
82+
WARN_LOG(COMMON, "downloadImage http error: %s", url.c_str());
7983
return false;
8084
}
8185
if (contentType.substr(0, 6) != "image/")
8286
{
83-
WARN_LOG(COMMON, "downloadImage bad content type %s", contentType.c_str());
87+
if (!mute)
88+
WARN_LOG(COMMON, "downloadImage bad content type %s", contentType.c_str());
8489
return false;
8590
}
8691
if (content.empty())
8792
{
88-
WARN_LOG(COMMON, "downloadImage: empty content");
93+
if (!mute)
94+
WARN_LOG(COMMON, "downloadImage: empty content");
8995
return false;
9096
}
9197
FILE *f = nowide::fopen(localName.c_str(), "wb");
@@ -251,5 +257,18 @@ void OfflineScraper::scrape(GameBoxart& item)
251257
break;
252258
item.searchName = trim_trailing_ws(item.searchName.substr(0, pos));
253259
}
260+
std::string extension = get_file_extension(item.fileName);
261+
if (extension != "zip" && extension != "7z" && extension != "lst")
262+
{
263+
FILE *file = hostfs::storage().openFile(item.gamePath, "rb");
264+
if (file != nullptr)
265+
{
266+
fseek(file, 0x30, SEEK_SET);
267+
u8 buf[0x20];
268+
if (fread(buf, 1, sizeof(buf), file) == sizeof(buf))
269+
item.uniqueId = trim_trailing_ws(std::string(&buf[0], &buf[0x20]));
270+
fclose(file);
271+
}
272+
}
254273
}
255274
}

core/ui/boxart/scraper.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ struct GameBoxart
4040
std::string boxartPath;
4141
std::string boxartUrl;
4242

43+
bool arcade = false;
4344
bool parsed = false;
4445
bool scraped = false;
4546
bool busy = false;
@@ -87,7 +88,7 @@ class Scraper
8788
virtual ~Scraper() = default;
8889

8990
protected:
90-
bool downloadImage(const std::string& url, const std::string& localName);
91+
bool downloadImage(const std::string& url, const std::string& localName, bool mute = false);
9192
std::string makeUniqueFilename(const std::string& url);
9293

9394
private:

core/ui/game_scanner.cpp

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424

2525
static bool operator<(const GameMedia &left, const GameMedia &right)
2626
{
27-
return left.name < right.name;
27+
return (int)left.arcade < (int)right.arcade
28+
|| (left.arcade == right.arcade && left.name < right.name);
2829
}
2930

3031
void GameScanner::insert_game(const GameMedia& game)
@@ -33,11 +34,6 @@ void GameScanner::insert_game(const GameMedia& game)
3334
game_list.insert(std::upper_bound(game_list.begin(), game_list.end(), game), game);
3435
}
3536

36-
void GameScanner::insert_arcade_game(const GameMedia& game)
37-
{
38-
arcade_game_list.insert(std::upper_bound(arcade_game_list.begin(), arcade_game_list.end(), game), game);
39-
}
40-
4137
void GameScanner::add_game_directory(const std::string& path)
4238
{
4339
hostfs::DirectoryTree tree(path);
@@ -83,13 +79,13 @@ void GameScanner::add_game_directory(const std::string& path)
8379
continue;
8480
gameName = it->second->description;
8581
fileName = fileName + " (" + gameName + ")";
86-
insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName });
82+
insert_game(GameMedia{ fileName, item.path, item.name, gameName, true });
8783
continue;
8884
}
8985
else if (extension == "bin" || extension == "lst" || extension == "dat")
9086
{
9187
if (!config::HideLegacyNaomiRoms)
92-
insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName });
88+
insert_game(GameMedia{ fileName, item.path, item.name, gameName, true });
9389
continue;
9490
}
9591
else if (extension == "chd" || extension == "gdi")
@@ -139,7 +135,6 @@ void GameScanner::fetch_game_list()
139135
LockGuard _(mutex);
140136
game_list.clear();
141137
}
142-
arcade_game_list.clear();
143138
for (const auto& path : config::ContentPath.get())
144139
{
145140
try {
@@ -162,13 +157,11 @@ void GameScanner::fetch_game_list()
162157
name = it->substr(4);
163158
else
164159
name = *it;
165-
game_list.insert(game_list.begin(), { name, *it, name, "", true });
160+
game_list.insert(game_list.begin(), { name, *it, name, "", false, true });
166161
}
167162
// Dreamcast BIOS
168163
if (!dcbios.empty())
169164
game_list.insert(game_list.begin(), { "Dreamcast BIOS" });
170-
// Arcade games
171-
game_list.insert(game_list.end(), arcade_game_list.begin(), arcade_game_list.end());
172165
}
173166
if (running)
174167
scan_done = true;

core/ui/game_scanner.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ struct GameMedia
3232
std::string path; // Full path to rom. May be an encoded uri
3333
std::string fileName; // Last component of the path, decoded
3434
std::string gameName; // for arcade games only, description from the rom list
35+
bool arcade = false; // Arcade game (naomi, atomiswave, system sp, ...)
3536
bool device = false; // Corresponds to a physical cdrom device
3637
};
3738

3839
class GameScanner
3940
{
4041
std::vector<GameMedia> game_list;
41-
std::vector<GameMedia> arcade_game_list;
4242
std::mutex mutex;
4343
std::mutex threadMutex;
4444
std::unique_ptr<std::thread> scan_thread;
@@ -49,7 +49,7 @@ class GameScanner
4949
using LockGuard = std::lock_guard<std::mutex>;
5050

5151
void insert_game(const GameMedia& game);
52-
void insert_arcade_game(const GameMedia& game);
52+
void insert_arcade_game(GameMedia game);
5353
void add_game_directory(const std::string& path);
5454

5555
public:

0 commit comments

Comments
 (0)