mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Library: a name edited in the WebUI reaches RetroArch's playlists and every ES-DE rename
A rename in the WebUI was written only to the game's metadata (issue 55). RetroArch's menus (XMB, RGUI) show the playlist label, which never changed, and ES-DE took the new name only while its own was still the label, so a second rename was dropped. src/scraper.cpp now rewrites the game's entry in every playlist that lists its file (edit_playlist_entries, also for removals), with Json::dump in src/scraper_json.h; ES-DE's adapter lets every field the owner edited win (frontends/es-de/ps5/library_ps5.cpp). Tests: python3 -m unittest tests.test_esde_library tests.test_scraper: 15 OK (the label changes, the other entries and the playlist's fields stay; two renames in a row both reach ES-DE's gamelist, its favourite kept). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
cc17179253
commit
67b7fed0ac
5 files changed
+134
-3
No files matched your search
@@ -240,7 +240,8 @@ std::map<std::string, std::string> read_meta(const std::string &path)
|
||||
}
|
||||
|
||||
/* The scraped fields into a game's list entry: a field ES-DE has is kept, unless it is
|
||||
* still the library's default (the name as the label). */
|
||||
* still the library's default (the name as the label) or the owner edited it in the
|
||||
* WebUI, which lists it in "edited" (issue 55: a second rename was dropped). */
|
||||
void add_meta(pugi::xml_node node, const char *media, const char *system,
|
||||
const struct ps5_library_game &game)
|
||||
{
|
||||
@@ -252,6 +253,10 @@ void add_meta(pugi::xml_node node, const char *media, const char *system,
|
||||
{"name", "name"}, {"description", "desc"}, {"developer", "developer"},
|
||||
{"publisher", "publisher"}, {"genre", "genre"}, {"players", "players"},
|
||||
{"rating", "rating"}, {"released", "releasedate"}};
|
||||
const auto list = meta.find("edited");
|
||||
const std::string edited_list = list == meta.end() ? "" : "," + list->second + ",";
|
||||
const auto edited = [&](const char *name)
|
||||
{ return edited_list.find(std::string(",") + name + ",") != std::string::npos; };
|
||||
for (const auto &field : fields)
|
||||
{
|
||||
const auto value = meta.find(field[0]);
|
||||
@@ -273,7 +278,8 @@ void add_meta(pugi::xml_node node, const char *media, const char *system,
|
||||
text = digits + "T000000";
|
||||
}
|
||||
const std::string current = node.child_value(field[1]);
|
||||
if (current.empty() || (std::strcmp(field[1], "name") == 0 && current == game.label))
|
||||
if (current.empty() || edited(field[0]) ||
|
||||
(std::strcmp(field[1], "name") == 0 && current == game.label))
|
||||
set_child(node, field[1], text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2793,6 +2793,52 @@ bool game_identity(const std::string &system, const std::string &key, const std:
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Every RetroArch playlist entry for a game's file: edit changes it in place, or
|
||||
* returns false to remove it. A playlist is written only when it held the game; false
|
||||
* when one could not be written. RetroArch's menus read the label (issues 55, 57). */
|
||||
bool edit_playlist_entries(const std::string &path, const std::function<bool(Json &)> &edit)
|
||||
{
|
||||
const std::string folder = root_path + "/playlists";
|
||||
std::vector<std::string> files;
|
||||
if (DIR *dir = opendir(folder.c_str()))
|
||||
{
|
||||
while (const dirent *entry = readdir(dir))
|
||||
{
|
||||
const std::string name = entry->d_name;
|
||||
if (name.size() > 4 && name.compare(name.size() - 4, 4, ".lpl") == 0)
|
||||
files.push_back(folder + '/' + name);
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
bool ok = true;
|
||||
for (const auto &file : files)
|
||||
{
|
||||
Json playlist = Json::parse(read_text(file));
|
||||
const auto items = playlist.fields.find("items");
|
||||
if (playlist.kind != Json::object || items == playlist.fields.end() ||
|
||||
items->second.kind != Json::array)
|
||||
continue;
|
||||
auto &entries = items->second.items;
|
||||
bool changed = false;
|
||||
for (size_t i = 0; i < entries.size();)
|
||||
{
|
||||
if (entries[i]["path"].str() != path)
|
||||
{
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
if (edit(entries[i]))
|
||||
++i;
|
||||
else
|
||||
entries.erase(entries.begin() + long(i));
|
||||
}
|
||||
if (changed && !write_atomic(file, playlist.dump()))
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool edit_game(const std::string &system, const std::string &key, const std::string &body,
|
||||
std::string &why)
|
||||
{
|
||||
@@ -2853,7 +2899,18 @@ bool edit_game(const std::string &system, const std::string &key, const std::str
|
||||
meta["edited"] = kinds_text(edited);
|
||||
make_folders(library_root() + '/' + system + "/metadata");
|
||||
if (write_atomic(meta_path(system, key), fields_text(meta)))
|
||||
{
|
||||
// The new name in RetroArch's menus too (issue 55); the metadata is the record.
|
||||
if (input["name"].kind == Json::string)
|
||||
edit_playlist_entries(game.path,
|
||||
[&](Json &entry)
|
||||
{
|
||||
entry.fields["label"] =
|
||||
Json{Json::string, false, 0, meta["name"]};
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
why = "Details could not be saved. Check console storage.";
|
||||
return false;
|
||||
}
|
||||
|
||||
+47
-1
@@ -1,6 +1,6 @@
|
||||
/* Copyright (C) 2026 Mihawk; SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
/* A small JSON reader for the scraper's sources (ScreenScraper answers in JSON): values
|
||||
* are read whole into a tree; nothing is written. Malformed input reads as null. */
|
||||
* are read whole into a tree, and dump() writes one back. Malformed input reads as null. */
|
||||
#pragma once
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -49,6 +49,52 @@ struct Json
|
||||
return "";
|
||||
}
|
||||
|
||||
/* The value as JSON text, for files the WebUI edits (RetroArch's playlists): fields
|
||||
* come out in name order, which RetroArch's reader does not mind. */
|
||||
std::string dump() const
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case boolean:
|
||||
return flag ? "true" : "false";
|
||||
case number:
|
||||
return str();
|
||||
case string:
|
||||
{
|
||||
std::string out = "\"";
|
||||
for (unsigned char c : text)
|
||||
if (c == '"' || c == '\\')
|
||||
out += '\\', out += char(c);
|
||||
else if (c < 0x20)
|
||||
{
|
||||
char escaped[8];
|
||||
std::snprintf(escaped, sizeof escaped, "\\u%04x", c);
|
||||
out += escaped;
|
||||
}
|
||||
else
|
||||
out += char(c);
|
||||
return out + '"';
|
||||
}
|
||||
case array:
|
||||
{
|
||||
std::string out = "[";
|
||||
for (const auto &item : items)
|
||||
out += (out.size() > 1 ? "," : "") + item.dump();
|
||||
return out + ']';
|
||||
}
|
||||
case object:
|
||||
{
|
||||
std::string out = "{";
|
||||
for (const auto &field : fields)
|
||||
out += (out.size() > 1 ? "," : "") + Json{string, false, 0, field.first}.dump() +
|
||||
':' + field.second.dump();
|
||||
return out + '}';
|
||||
}
|
||||
default:
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
static Json parse(const std::string &input)
|
||||
{
|
||||
size_t at = 0;
|
||||
|
||||
@@ -154,6 +154,21 @@ int main(int argc, char **argv)
|
||||
assert(path != "./Gone (USA).zip");
|
||||
}
|
||||
assert(count == 2 && kept && added);
|
||||
/* A name the owner edited in the WebUI wins over ES-DE's, every time (issue 55). */
|
||||
for (const char *name : {"Jungle Run Two", "Jungle Run, again"})
|
||||
{
|
||||
write_text(media + "/snes/metadata/Jungle Run 2 (USA).meta",
|
||||
std::string("edited = \"name\"\nname = \"") + name + "\"\n");
|
||||
assert(ps5_esde_write_library_to(&paths, summary, sizeof(summary)) == 2);
|
||||
pugi::xml_document renamed;
|
||||
assert(renamed.load_file((data + "/gamelists/snes/gamelist.xml").c_str()));
|
||||
bool found = false;
|
||||
for (pugi::xml_node game : renamed.child("gameList").children("game"))
|
||||
if (std::string(game.child_value("path")) == "./Jungle Run 2 (USA).zip")
|
||||
found = std::string(game.child_value("name")) == name &&
|
||||
std::string(game.child_value("favorite")) == "true";
|
||||
assert(found);
|
||||
}
|
||||
pugi::xml_document homebrew;
|
||||
assert(homebrew.load_file((data + "/gamelists/homebrew/gamelist.xml").c_str()));
|
||||
assert(std::string(homebrew.child("gameList").child("game").child_value("path")) ==
|
||||
|
||||
@@ -533,6 +533,13 @@ class Scraper(unittest.TestCase):
|
||||
self.assertEqual(next(g for system in json.loads(self.request('GET', '/api/library')[1])['systems'] for g in system['games'] if g['path'] == path)['name'], 'My game')
|
||||
meta = (self.root / 'library/snes/metadata' / (key + '.meta')).read_text()
|
||||
self.assertIn('edited = "name,description,developer,publisher,genre,players,rating,released"', meta)
|
||||
# RetroArch's menus show the playlist's label: the new name reaches it, the other
|
||||
# entries and the playlist's own fields stay (issue 55).
|
||||
playlist = json.loads((self.root / f'playlists/{DB}.lpl').read_text())
|
||||
self.assertEqual(playlist['version'], '1.5')
|
||||
labels = {item['path']: item['label'] for item in playlist['items']}
|
||||
self.assertEqual(labels, {p: 'My game' if p == path else l for l, p in self.games.items()})
|
||||
self.assertEqual({item['core_path'] for item in playlist['items']}, {'DETECT'})
|
||||
metadata['rating'] = 'NaN'
|
||||
self.assertEqual(self.request('POST', '/api/library/game?' + query, json.dumps(metadata).encode())[0], 400)
|
||||
self.assertEqual(rom.read_bytes(), b'replacement')
|
||||
|
||||
Reference in new issue
Block a user