Skip to content

Commit baf5b23

Browse files
committed
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
1 parent 64fe364 commit baf5b23

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

src/playlist/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ add_library(projectM_playlist_main OBJECT
2626
Playlist.hpp
2727
PlaylistCWrapper.cpp
2828
PlaylistCWrapper.hpp
29+
M3UParser.cpp
30+
M3UParser.hpp
2931
)
3032

3133
target_include_directories(projectM_playlist_main
@@ -188,3 +190,4 @@ if(ENABLE_INSTALL)
188190
endif()
189191

190192
endif()
193+

src/playlist/M3UParser.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#include "M3UParser.hpp"
2+
#include "Playlist.hpp"
3+
4+
#include <fstream>
5+
#include <string>
6+
7+
namespace libprojectM {
8+
namespace Playlist {
9+
10+
auto M3UParser::LoadM3U(const std::string& filename,
11+
Playlist& playlist,
12+
bool allowDuplicates) -> uint32_t
13+
{
14+
// Open in binary mode to avoid any platform newline translation
15+
// that could corrupt multi-byte UTF-8 sequences
16+
std::ifstream file(filename, std::ios::binary);
17+
if (!file.is_open())
18+
{
19+
return 0;
20+
}
21+
22+
uint32_t addedCount{0};
23+
std::string line;
24+
25+
while (std::getline(file, line))
26+
{
27+
// Strip Windows-style \r (CR) from line endings.
28+
// Must be done before any other checks so we don't treat
29+
// "\r" as a non-empty path.
30+
if (!line.empty() && line.back() == '\r')
31+
{
32+
line.pop_back();
33+
}
34+
35+
// Skip empty lines
36+
if (line.empty())
37+
{
38+
continue;
39+
}
40+
41+
// Safe ASCII check: '#' is 0x23, always a single byte in UTF-8.
42+
// We compare the raw char value directly — no ctype functions —
43+
// so high-byte UTF-8 characters (e.g. Cyrillic 0xD0-0xBF) are
44+
// never misidentified as comments or skipped.
45+
if (line[0] == '#')
46+
{
47+
// M3U metadata line (e.g. #EXTM3U, #EXTINF) — skip it.
48+
continue;
49+
}
50+
51+
// Everything else is treated as a preset file path.
52+
// std::string handles UTF-8 bytes transparently, so Cyrillic
53+
// and other non-ASCII paths are passed through unchanged.
54+
if (playlist.AddItem(line, Playlist::InsertAtEnd, allowDuplicates))
55+
{
56+
addedCount++;
57+
}
58+
}
59+
60+
return addedCount;
61+
}
62+
63+
} // namespace Playlist
64+
} // namespace libprojectM

src/playlist/M3UParser.hpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <string>
5+
6+
namespace libprojectM {
7+
namespace Playlist {
8+
9+
class Playlist;
10+
11+
/**
12+
* @brief Parses M3U and extended M3U (M3U8) playlist files.
13+
*
14+
* Supports UTF-8 encoded filenames, including non-ASCII characters
15+
* such as Cyrillic, CJK, and other Unicode scripts.
16+
*/
17+
class M3UParser
18+
{
19+
public:
20+
/**
21+
* @brief Loads presets from an M3U file into the given playlist.
22+
* @param filename Path to the .m3u or .m3u8 file.
23+
* @param playlist The playlist to add items to.
24+
* @param allowDuplicates If true, duplicate entries are allowed.
25+
* @return Number of presets successfully added.
26+
*/
27+
static auto LoadM3U(const std::string& filename,
28+
Playlist& playlist,
29+
bool allowDuplicates) -> uint32_t;
30+
};
31+
32+
} // namespace Playlist
33+
} // namespace libprojectM

0 commit comments

Comments
 (0)