Skip to content

Commit 856d07c

Browse files
committed
preserve values from existing playlist, plus:
- known limitiations extended (doc) - testcases updated - inputfolder for RA always absolute - moved some path logic from skyscraper.cpp to retroarch.cpp
1 parent df834d8 commit 856d07c

11 files changed

Lines changed: 227 additions & 2311 deletions

File tree

docs/FRONTENDS.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,21 @@ skip existing entries, Skyscraper will use the old game list as a reference.
347347

348348
#### Known Limitations
349349

350-
1. Initial generation of a RetroArch playlist does not work for folders resp.
350+
1. RetroArch's [deprecated playlist
351+
format](https://docs.libretro.com/guides/roms-playlists-thumbnails/#6-line-playlist-format-deprecated)
352+
with plain six lines per game is not supported.
353+
2. Initial generation of a RetroArch playlist does not work for folders resp.
351354
platforms which may require a different RetroArch core per game (for example
352355
`arcade/` on RetroPie). However, updating an existing playlist file for such
353356
folder works, as the existing entry of `"db_name"` per each game is
354357
preserved.
355-
2. Any RetroArch configuration is not evaluated, thus if you changed your
358+
3. Any RetroArch configuration is not evaluated, thus if you changed your
356359
RetroArch configuration (e.g., by using non-default file paths) the produced
357-
JSON/lpl file might not display correctly in RetroArch frontend.
360+
JSON/lpl file might not display correctly in RetroArch frontend.
361+
4. Existing compressed playlists are not supported yet and will not be read by
362+
Skyscraper to preserver any data, thus you may lose previous changes. Before
363+
you start using Skyscraper's RetroArch playlist output you should disable the
364+
playlist compression in RetroArch and save the playlist from RetroArch as
365+
plain JSON.
366+
5. The settings of "scan_content_dir" in an existing playlist are not yet
367+
preserved.

skyscraper.pro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ unix:config.files=aliasMap.csv hints.xml mameMap.csv \
5454
unix:examples.path=$${SYSCONFDIR}/skyscraper
5555
unix:examples.files=config.ini.example README.md artwork.xml \
5656
artwork.xml.example1 artwork.xml.example2 artwork.xml.example3 \
57-
artwork.xml.example4 batocera-artwork.xml docs/ARTWORK.md docs/CACHE.md
57+
artwork.xml.example4 batocera-artwork.xml retroarch-artwork.xml \
58+
docs/ARTWORK.md docs/CACHE.md
5859

5960
unix:cacheexamples.path=$${SYSCONFDIR}/skyscraper/cache
6061
unix:cacheexamples.files=cache/priorities.xml.example docs/CACHE.md

src/retroarch.cpp

Lines changed: 128 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@
2020
#include "retroarch.h"
2121

2222
#include "gameentry.h"
23-
#include "pathtools.h"
2423
#include "platform.h"
25-
#include "strtools.h"
2624

27-
#include <QDate>
2825
#include <QDebug>
2926
#include <QDir>
3027
#include <QFile>
@@ -33,10 +30,27 @@
3330
#include <QJsonObject>
3431
#include <QRegularExpression>
3532
#include <QStringBuilder>
36-
#include <QTextStream>
3733

38-
static const QString RA_LPL_VERSION = "1.5";
39-
static const QString RA_DETECT = "DETECT";
34+
static const QString LPL_VERSION_VAL = "1.5";
35+
static const QString DETECT_VAL = "DETECT";
36+
37+
const QString META_VERSION = "version";
38+
const QString META_DFLT_CORE_PATH = "default_core_path";
39+
const QString META_DFLT_CORE_NAME = "default_core_name";
40+
41+
const QStringList LPL_META_PROPS = {
42+
META_DFLT_CORE_PATH, META_DFLT_CORE_NAME, META_VERSION,
43+
"label_display_mode", "left_display_mode", "right_display_mode",
44+
"thumbnail_match_mode", "sort_mode", "base_content_directory"};
45+
46+
const QString ITEMS_ARRAY = "items";
47+
48+
const QString ITEM_CORE_NAME = "core_name";
49+
const QString ITEM_CORE_PATH = "core_path";
50+
const QString ITEM_CRC = "crc32";
51+
const QString ITEM_DB_NAME = "db_name";
52+
const QString ITEM_LABEL = "label";
53+
const QString ITEM_PATH = "path";
4054

4155
RetroArch::RetroArch() {}
4256

@@ -47,16 +61,6 @@ QString RetroArch::sanitizeForFilename(const QString &name) {
4761
return sanitized;
4862
}
4963

50-
QString RetroArch::jsonEscape(const QString &str) {
51-
QString escaped = str;
52-
escaped.replace("\\", "\\\\");
53-
escaped.replace("\"", "\\\"");
54-
escaped.replace("\n", "\\n");
55-
escaped.replace("\r", "\\r");
56-
escaped.replace("\t", "\\t");
57-
return escaped;
58-
}
59-
6064
const QString RetroArch::getPlatformOutputName() {
6165
// Look up the RetroArch db_name from peas.json
6266
QString dbName = Platform::get().getRetroArchDbName(config->platform);
@@ -72,47 +76,35 @@ const QString RetroArch::getPlatformOutputName() {
7276
}
7377

7478
bool RetroArch::loadOldGameList(const QString &gameListFileString) {
75-
QFile gameListFile(gameListFileString);
76-
if (!gameListFile.exists() || !gameListFile.open(QIODevice::ReadOnly)) {
77-
return false;
78-
}
79-
80-
QByteArray jsonData = gameListFile.readAll();
81-
gameListFile.close();
82-
83-
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
84-
if (!doc.isObject()) {
79+
QJsonDocument doc;
80+
if (QFile gameListFile(gameListFileString);
81+
!gameListFile.open(QIODevice::ReadOnly)) {
8582
return false;
83+
} else {
84+
QByteArray jsonData = gameListFile.readAll();
85+
gameListFile.close();
86+
doc = QJsonDocument::fromJson(jsonData);
87+
if (!doc.isObject()) {
88+
return false;
89+
}
8690
}
8791

88-
QJsonObject root = doc.object();
89-
QJsonArray items = root.value("items").toArray();
92+
existingPlaylist = doc.object();
93+
QJsonArray items = existingPlaylist.value(ITEMS_ARRAY).toArray();
9094

9195
for (const QJsonValue &item : items) {
92-
if (!item.isObject()) {
93-
continue;
96+
if (item.isObject()) {
97+
QJsonObject itemObj = item.toObject();
98+
GameEntry oldEntry;
99+
// always absolute path with Retroarch
100+
// path might be contain backslashes on Windows
101+
oldEntry.path = itemObj.value(ITEM_PATH).toString();
102+
oldEntry.title = itemObj.value(ITEM_LABEL).toString();
103+
// remaining properties of an item are held in existingPlaylist
104+
oldEntries.append(oldEntry);
94105
}
95-
96-
QJsonObject itemObj = item.toObject();
97-
GameEntry newEntry;
98-
newEntry.path = itemObj.value("path").toString();
99-
newEntry.title = itemObj.value("label").toString();
100-
// FIXME: preserve also db_name if present for game and core_name,
101-
// core_path if not "DETECT"
102-
oldEntries.append(newEntry);
103106
}
104107

105-
// FIXME: preserve also "preamble" (=everything, except version sibling to
106-
// items) e.g.:
107-
// "default_core_path": "", # overwrite only iff -e / raExtra is provided
108-
// "default_core_name": "", # overwrite only iff -e / raExtra is provided
109-
// "base_content_directory": "yadda_yadda",
110-
// "label_display_mode": 2,
111-
// "right_thumbnail_mode": 0,
112-
// "left_thumbnail_mode": 0,
113-
// "thumbnail_match_mode": 0,
114-
// "sort_mode": 2,
115-
116108
return true;
117109
}
118110

@@ -145,19 +137,6 @@ void RetroArch::skipExisting(QList<GameEntry> &gameEntries,
145137
}
146138
}
147139

148-
void RetroArch::preserveFromOld(GameEntry &entry) {
149-
QString fn = entry.baseName;
150-
for (const auto &oldEntry : oldEntries) {
151-
if (QFileInfo(oldEntry.path).fileName() == fn) {
152-
if (entry.title.isEmpty()) {
153-
entry.title = oldEntry.title;
154-
}
155-
// FIXME: restore also other values (see FIXME in loadOldGameList())
156-
break;
157-
}
158-
}
159-
}
160-
161140
void RetroArch::assembleList(QString &finalOutput,
162141
QList<GameEntry> &gameEntries) {
163142
if (gameEntries.isEmpty())
@@ -169,71 +148,101 @@ void RetroArch::assembleList(QString &finalOutput,
169148
baseNameToTitle[entry.baseName] = entry.title;
170149
}
171150

172-
QJsonObject root;
173-
root.insert("version", RA_LPL_VERSION);
174-
175-
// Parse default_core_path and default_core_name from frontendExtra
176-
// Format: "<CORE_PATH>;<CORE_NAME>" or leave as DETECT
177-
QString corePathStr = RA_DETECT;
178-
QString coreNameStr = RA_DETECT;
179-
180-
if (!config->frontendExtra.isEmpty()) {
181-
// frontendExtra is used for default_core_path and default_core_name
182-
// Format: "<CORE_PATH>;<CORE_NAME>"
183-
QStringList parts = config->frontendExtra.split(";");
184-
corePathStr = parts[0];
185-
coreNameStr = parts[1];
186-
}
151+
QJsonObject newPlaylist = createMetaProps();
187152

188-
// FIXME: if values from preamble exist from existing playlist use these
189-
// instead of the defaults
190-
root.insert("default_core_path", corePathStr);
191-
root.insert("default_core_name", coreNameStr);
192-
root.insert("label_display_mode", "0"); // show full labels
193-
root.insert("left_display_mode", "0"); // system default
194-
root.insert("right_display_mode", "0"); // system default
195-
root.insert("thumbnail_match_mode", "0"); // system default
196-
root.insert("sort_mode", "0"); // system default
153+
QJsonArray exitsingItems = existingPlaylist.value(ITEMS_ARRAY).toArray();
154+
QJsonObject eitemObj;
197155

198156
QJsonArray items;
157+
QString gameFn;
199158

200159
int dots = -1;
201160
int dotMod = gameEntries.length() * 0.1 + 1;
202-
203-
for (const auto &entry : gameEntries) {
161+
for (auto const &entry : gameEntries) {
204162
if (++dots % dotMod == 0) {
205163
printf(".");
206164
fflush(stdout);
207165
}
208-
166+
gameFn = QFileInfo(entry.path).fileName();
209167
// TODO: unpack support for CRC and inter-zip reference
210168
// "path": "/storage/emulated/0/ROMs/virtualboy/Game.zip#Game.vb",
211169
// "crc32": "133E9372|crc",
212-
213-
QJsonObject itemObj;
214170
QString absPath = entry.absoluteFilePath;
215171
#ifdef Q_OS_WIN
216172
absPath = absPath.replace("/", "\\\\");
217173
#endif
218-
itemObj.insert("path", absPath);
219-
itemObj.insert("label", entry.title);
220-
itemObj.insert("core_path", RA_DETECT);
221-
itemObj.insert("core_name", RA_DETECT);
222-
itemObj.insert("crc32", RA_DETECT);
223-
itemObj.insert("db_name", QString(getPlatformOutputName() % ".lpl"));
174+
bool hasExisting = false;
175+
QJsonObject itemObj;
176+
for (const QJsonValue &eit : exitsingItems) {
177+
if (eit.isObject()) {
178+
eitemObj = eit.toObject();
179+
if (eitemObj[ITEM_PATH].toString().endsWith(gameFn)) {
180+
hasExisting = true;
181+
itemObj = eitemObj;
182+
break;
183+
}
184+
}
185+
}
186+
187+
if (!hasExisting) {
188+
itemObj.insert(ITEM_CORE_PATH, DETECT_VAL);
189+
itemObj.insert(ITEM_CORE_NAME, DETECT_VAL);
190+
itemObj.insert(ITEM_CRC, DETECT_VAL);
191+
itemObj.insert(ITEM_DB_NAME, getGameListFileName());
192+
}
193+
194+
itemObj.insert(ITEM_PATH, absPath);
195+
itemObj.insert(ITEM_LABEL, entry.title);
224196

225197
items.append(itemObj);
226198
}
227199

228-
root.insert("items", items);
200+
newPlaylist.insert(ITEMS_ARRAY, items);
229201

230-
QJsonDocument doc(root);
202+
QJsonDocument doc(newPlaylist);
231203
finalOutput = doc.toJson(QJsonDocument::Indented);
232204
}
233205

206+
QJsonObject RetroArch::createMetaProps() {
207+
QJsonObject newPlaylist;
208+
newPlaylist.insert(META_VERSION, LPL_VERSION_VAL);
209+
210+
QString corePathStr = DETECT_VAL;
211+
QString coreNameStr = DETECT_VAL;
212+
213+
// Parse default_core_path and default_core_name from frontendExtra
214+
// (raExtra= or -e)
215+
if (!config->frontendExtra.isEmpty()) {
216+
QStringList parts = config->frontendExtra.split(";");
217+
corePathStr = parts[0];
218+
coreNameStr = parts[1];
219+
}
220+
221+
// create or restore meta properties
222+
for (const auto &k : LPL_META_PROPS) {
223+
QString v = existingPlaylist[k].toString();
224+
if (v.isEmpty()) {
225+
if (k == META_VERSION)
226+
newPlaylist.insert(k, LPL_VERSION_VAL);
227+
else if (k == META_DFLT_CORE_NAME)
228+
newPlaylist.insert(k, coreNameStr);
229+
else if (k == META_DFLT_CORE_PATH)
230+
newPlaylist.insert(k, corePathStr);
231+
else if (k == "base_content_directory")
232+
; // don't set default "base_content_directory"
233+
else
234+
newPlaylist.insert(k, "0");
235+
} else {
236+
newPlaylist.insert(k, v);
237+
}
238+
}
239+
return newPlaylist;
240+
}
241+
234242
QString RetroArch::getTargetFileName(GameEntry::Types t,
235243
const QString &baseName) {
236-
(void)t; // Suppress unused parameter warning
244+
(void)t;
245+
// for media files use sanitized title as filename stem
237246
QString title = baseNameToTitle.value(baseName, baseName);
238247
return sanitizeForFilename(title);
239248
}
@@ -242,7 +251,7 @@ bool RetroArch::canSkip() { return true; }
242251

243252
QString RetroArch::getGameListFileName() {
244253
return config->gameListFilename.isEmpty()
245-
? (getPlatformOutputName() + ".lpl")
254+
? (getPlatformOutputName() % ".lpl")
246255
: config->gameListFilename;
247256
}
248257

@@ -251,11 +260,27 @@ QString RetroArch::getInputFolder() {
251260
}
252261

253262
QString RetroArch::getGameListFolder() {
254-
return QDir::homePath() % "/.config/retroarch/playlists";
263+
if (config->gameListFolder.isEmpty()) {
264+
return QDir::homePath() % "/.config/retroarch/playlists";
265+
} else {
266+
if (config->gameListFolder.endsWith("/" % config->platform)) {
267+
return config->gameListFolder.replace("/" % config->platform, "");
268+
}
269+
return config->gameListFolder;
270+
}
255271
}
256272

257273
QString RetroArch::getMediaFolder() {
258-
return QDir::homePath() % "/.config/retroarch/thumbnails";
274+
if (config->mediaFolder.isEmpty()) {
275+
return QDir::homePath() % "/.config/retroarch/thumbnails/" %
276+
getPlatformOutputName();
277+
} else {
278+
if (config->mediaFolder.endsWith("/" % config->platform)) {
279+
return config->mediaFolder.replace("/" % config->platform,
280+
"/" % getPlatformOutputName());
281+
}
282+
return config->mediaFolder;
283+
}
259284
}
260285

261286
QString RetroArch::getCoversFolder() {

0 commit comments

Comments
 (0)