From 1fb54f2aa2c48b77f0bd5875af047797393d79b3 Mon Sep 17 00:00:00 2001
From: victorwon2001 <192616110+victorwon2001@users.noreply.github.com>
Date: Tue, 1 Sep 2026 13:55:53 +0900
Subject: [PATCH 1/2] fix: enable locale-aware cJSON parsing
---
build/foo_openlyrics.vcxproj | 1 +
src/sources/lrclib.cpp | 124 +++++++++++++++++++++++++++++++++++
2 files changed, 125 insertions(+)
diff --git a/build/foo_openlyrics.vcxproj b/build/foo_openlyrics.vcxproj
index 8bb955b3..ed969c35 100644
--- a/build/foo_openlyrics.vcxproj
+++ b/build/foo_openlyrics.vcxproj
@@ -208,6 +208,7 @@
NotUsing
NotUsing
NotUsing
+ ENABLE_LOCALES;%(PreprocessorDefinitions)
diff --git a/src/sources/lrclib.cpp b/src/sources/lrclib.cpp
index 08f0c113..92afb78f 100644
--- a/src/sources/lrclib.cpp
+++ b/src/sources/lrclib.cpp
@@ -1,5 +1,7 @@
#include "stdafx.h"
+#include
+
#include "cJSON.h"
// Don't recompile every time in dev/debug builds
@@ -550,6 +552,128 @@ void LrclibLyricsSource::upload(LyricData lyrics, abort_callback& abort)
// Tests
// ============
#if MVTF_TESTS_ENABLED
+class ScopedNumericLocale
+{
+public:
+ ScopedNumericLocale()
+ {
+ const char* current_locale = setlocale(LC_NUMERIC, nullptr);
+ if(current_locale != nullptr)
+ {
+ m_previous_locale = current_locale;
+ }
+ }
+
+ ~ScopedNumericLocale()
+ {
+ if(!m_previous_locale.empty())
+ {
+ (void)setlocale(LC_NUMERIC, m_previous_locale.c_str());
+ }
+ }
+
+ bool select(const char* locale_name)
+ {
+ return !m_previous_locale.empty() && (setlocale(LC_NUMERIC, locale_name) != nullptr);
+ }
+
+ const std::string& previous_locale() const
+ {
+ return m_previous_locale;
+ }
+
+private:
+ std::string m_previous_locale;
+};
+
+MVTF_TEST(cjson_parses_source_responses_with_comma_decimal_locale)
+{
+ bool genius_response_is_valid = false;
+ bool musixmatch_response_is_valid = false;
+ bool lrclib_response_is_valid = false;
+ std::string previous_locale;
+
+ {
+ ScopedNumericLocale numeric_locale;
+ previous_locale = numeric_locale.previous_locale();
+ ASSERT(!previous_locale.empty());
+ ASSERT(numeric_locale.select("French_France.1252"));
+
+ const lconv* current_locale = localeconv();
+ ASSERT(current_locale != nullptr);
+ ASSERT(current_locale->decimal_point != nullptr);
+ ASSERT(current_locale->decimal_point[0] == ',');
+
+ constexpr std::string_view genius_response =
+ R"json({"meta":{"status":200},"response":{"song":{"duration":183.5,)json"
+ R"json("lyrics":{"plain":"genius lyrics"}}}})json";
+ cJSON* genius_json = cJSON_Parse(genius_response.data());
+ if(cJSON_IsObject(genius_json))
+ {
+ const cJSON* song_response = cJSON_GetObjectItem(genius_json, "response");
+ const cJSON* song = cJSON_GetObjectItem(song_response, "song");
+ const cJSON* duration = cJSON_GetObjectItem(song, "duration");
+ const cJSON* lyrics = cJSON_GetObjectItem(song, "lyrics");
+ const cJSON* plain_lyrics = cJSON_GetObjectItem(lyrics, "plain");
+ genius_response_is_valid = cJSON_IsNumber(duration) && duration->valuedouble == 183.5
+ && cJSON_IsString(plain_lyrics) && plain_lyrics->valuestring != nullptr
+ && std::string_view(plain_lyrics->valuestring) == "genius lyrics";
+ }
+ cJSON_Delete(genius_json);
+
+ constexpr std::string_view musixmatch_response =
+ R"json({"message":{"body":{"track_list":[{"track":{"artist_name":"The Lonely Island",)json"
+ R"json("album_name":"Incredibad","track_name":"Like a Boss","has_lyrics":1,"has_subtitles":1,)json"
+ R"json("commontrack_id":1839,"track_length":183.5}}]}}})json";
+ cJSON* musixmatch_json = cJSON_ParseWithLength(musixmatch_response.data(), musixmatch_response.length());
+ if(cJSON_IsObject(musixmatch_json))
+ {
+ const cJSON* message = cJSON_GetObjectItem(musixmatch_json, "message");
+ const cJSON* body = cJSON_GetObjectItem(message, "body");
+ const cJSON* track_list = cJSON_GetObjectItem(body, "track_list");
+ if(cJSON_IsArray(track_list) && cJSON_GetArraySize(track_list) == 1)
+ {
+ const cJSON* track_entry = cJSON_GetArrayItem(track_list, 0);
+ const cJSON* track = cJSON_GetObjectItem(track_entry, "track");
+ const cJSON* has_lyrics = cJSON_GetObjectItem(track, "has_lyrics");
+ const cJSON* has_subtitles = cJSON_GetObjectItem(track, "has_subtitles");
+ const cJSON* commontrack_id = cJSON_GetObjectItem(track, "commontrack_id");
+ const cJSON* track_length = cJSON_GetObjectItem(track, "track_length");
+ musixmatch_response_is_valid = cJSON_IsNumber(has_lyrics) && has_lyrics->valueint == 1
+ && cJSON_IsNumber(has_subtitles) && has_subtitles->valueint == 1
+ && cJSON_IsNumber(commontrack_id) && commontrack_id->valueint == 1839
+ && cJSON_IsNumber(track_length) && track_length->valuedouble == 183.5;
+ }
+ }
+ cJSON_Delete(musixmatch_json);
+
+ constexpr std::string_view lrclib_response =
+ R"json([{"trackName":"Like a Boss","artistName":"The Lonely Island",)json"
+ R"json("albumName":"Incredibad","id":1839,"duration":183.5,)json"
+ R"json("plainLyrics":"lrclib lyrics"}])json";
+ cJSON* lrclib_json = cJSON_ParseWithLength(lrclib_response.data(), lrclib_response.length());
+ if(cJSON_IsArray(lrclib_json) && cJSON_GetArraySize(lrclib_json) == 1)
+ {
+ const cJSON* result = cJSON_GetArrayItem(lrclib_json, 0);
+ const cJSON* id = cJSON_GetObjectItem(result, "id");
+ const cJSON* duration = cJSON_GetObjectItem(result, "duration");
+ const cJSON* plain_lyrics = cJSON_GetObjectItem(result, "plainLyrics");
+ lrclib_response_is_valid = cJSON_IsNumber(id) && id->valueint == 1839
+ && cJSON_IsNumber(duration) && duration->valuedouble == 183.5
+ && cJSON_IsString(plain_lyrics) && plain_lyrics->valuestring != nullptr
+ && std::string_view(plain_lyrics->valuestring) == "lrclib lyrics";
+ }
+ cJSON_Delete(lrclib_json);
+ }
+
+ const char* restored_locale = setlocale(LC_NUMERIC, nullptr);
+ ASSERT(restored_locale != nullptr);
+ ASSERT(previous_locale == restored_locale);
+ ASSERT(genius_response_is_valid);
+ ASSERT(musixmatch_response_is_valid);
+ ASSERT(lrclib_response_is_valid);
+}
+
MVTF_TEST(lrclib_equal_hashes_are_less_or_equal)
{
uint8_t value[32] = { 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
From e005eb27e02257ababb64d5aded6ffc258e96596 Mon Sep 17 00:00:00 2001
From: victorwon2001 <192616110+victorwon2001@users.noreply.github.com>
Date: Mon, 7 Sep 2026 10:38:16 +0900
Subject: [PATCH 2/2] Fix locale regression test formatting
---
src/main.cpp | 1 +
src/sources/lrclib.cpp | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/main.cpp b/src/main.cpp
index 5bf9f7c4..e9439c3c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -28,6 +28,7 @@ namespace
// "\n";
out += "Version " OPENLYRICS_VERSION " (" __DATE__ "):\n"
"- Fix auto-search triggering for missing local files\n"
+ "- Fix Genius, Musixmatch and LRCLIB parsing with comma-decimal locales\n"
"\n";
out += "Version 1.13 (2026-01-17):\n"
"- Enable searching local sources with no visible panels by default\n"
diff --git a/src/sources/lrclib.cpp b/src/sources/lrclib.cpp
index 92afb78f..42d69a4e 100644
--- a/src/sources/lrclib.cpp
+++ b/src/sources/lrclib.cpp
@@ -658,10 +658,10 @@ MVTF_TEST(cjson_parses_source_responses_with_comma_decimal_locale)
const cJSON* id = cJSON_GetObjectItem(result, "id");
const cJSON* duration = cJSON_GetObjectItem(result, "duration");
const cJSON* plain_lyrics = cJSON_GetObjectItem(result, "plainLyrics");
- lrclib_response_is_valid = cJSON_IsNumber(id) && id->valueint == 1839
- && cJSON_IsNumber(duration) && duration->valuedouble == 183.5
- && cJSON_IsString(plain_lyrics) && plain_lyrics->valuestring != nullptr
- && std::string_view(plain_lyrics->valuestring) == "lrclib lyrics";
+ lrclib_response_is_valid = cJSON_IsNumber(id) && id->valueint == 1839 && cJSON_IsNumber(duration)
+ && duration->valuedouble == 183.5 && cJSON_IsString(plain_lyrics)
+ && plain_lyrics->valuestring != nullptr
+ && std::string_view(plain_lyrics->valuestring) == "lrclib lyrics";
}
cJSON_Delete(lrclib_json);
}