From baf5b2341cd6791df815ece2dda174ab906159af Mon Sep 17 00:00:00 2001 From: dipendra Date: Sat, 25 Apr 2026 23:44:20 +0545 Subject: [PATCH] feat(playlist): add M3U parser with UTF-8/Cyrillic filename support Adds a new M3UParser class that reads .m3u and .m3u8 playlist files and loads presets into the playlist. Handles UTF-8 encoded filenames including Cyrillic and other non-ASCII characters correctly by: - Opening files in binary mode to preserve UTF-8 bytes - Stripping Windows CR line endings safely - Using direct char comparison for '#' (0x23) instead of ctype functions that would misinterpret high-byte UTF-8 sequences Fixes #962 --- src/playlist/CMakeLists.txt | 3 ++ src/playlist/M3UParser.cpp | 64 +++++++++++++++++++++++++++++++++++++ src/playlist/M3UParser.hpp | 33 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/playlist/M3UParser.cpp create mode 100644 src/playlist/M3UParser.hpp diff --git a/src/playlist/CMakeLists.txt b/src/playlist/CMakeLists.txt index d7deb44d5c..d8fde28155 100644 --- a/src/playlist/CMakeLists.txt +++ b/src/playlist/CMakeLists.txt @@ -26,6 +26,8 @@ add_library(projectM_playlist_main OBJECT Playlist.hpp PlaylistCWrapper.cpp PlaylistCWrapper.hpp + M3UParser.cpp + M3UParser.hpp ) target_include_directories(projectM_playlist_main @@ -188,3 +190,4 @@ if(ENABLE_INSTALL) endif() endif() + diff --git a/src/playlist/M3UParser.cpp b/src/playlist/M3UParser.cpp new file mode 100644 index 0000000000..91c4e488eb --- /dev/null +++ b/src/playlist/M3UParser.cpp @@ -0,0 +1,64 @@ +#include "M3UParser.hpp" +#include "Playlist.hpp" + +#include +#include + +namespace libprojectM { +namespace Playlist { + +auto M3UParser::LoadM3U(const std::string& filename, + Playlist& playlist, + bool allowDuplicates) -> uint32_t +{ + // Open in binary mode to avoid any platform newline translation + // that could corrupt multi-byte UTF-8 sequences + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) + { + return 0; + } + + uint32_t addedCount{0}; + std::string line; + + while (std::getline(file, line)) + { + // Strip Windows-style \r (CR) from line endings. + // Must be done before any other checks so we don't treat + // "\r" as a non-empty path. + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + + // Skip empty lines + if (line.empty()) + { + continue; + } + + // Safe ASCII check: '#' is 0x23, always a single byte in UTF-8. + // We compare the raw char value directly — no ctype functions — + // so high-byte UTF-8 characters (e.g. Cyrillic 0xD0-0xBF) are + // never misidentified as comments or skipped. + if (line[0] == '#') + { + // M3U metadata line (e.g. #EXTM3U, #EXTINF) — skip it. + continue; + } + + // Everything else is treated as a preset file path. + // std::string handles UTF-8 bytes transparently, so Cyrillic + // and other non-ASCII paths are passed through unchanged. + if (playlist.AddItem(line, Playlist::InsertAtEnd, allowDuplicates)) + { + addedCount++; + } + } + + return addedCount; +} + +} // namespace Playlist +} // namespace libprojectM diff --git a/src/playlist/M3UParser.hpp b/src/playlist/M3UParser.hpp new file mode 100644 index 0000000000..3e51fe7cb7 --- /dev/null +++ b/src/playlist/M3UParser.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace libprojectM { +namespace Playlist { + +class Playlist; + +/** + * @brief Parses M3U and extended M3U (M3U8) playlist files. + * + * Supports UTF-8 encoded filenames, including non-ASCII characters + * such as Cyrillic, CJK, and other Unicode scripts. + */ +class M3UParser +{ +public: + /** + * @brief Loads presets from an M3U file into the given playlist. + * @param filename Path to the .m3u or .m3u8 file. + * @param playlist The playlist to add items to. + * @param allowDuplicates If true, duplicate entries are allowed. + * @return Number of presets successfully added. + */ + static auto LoadM3U(const std::string& filename, + Playlist& playlist, + bool allowDuplicates) -> uint32_t; +}; + +} // namespace Playlist +} // namespace libprojectM