Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/playlist/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -188,3 +190,4 @@ if(ENABLE_INSTALL)
endif()

endif()

64 changes: 64 additions & 0 deletions src/playlist/M3UParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "M3UParser.hpp"
#include "Playlist.hpp"

#include <fstream>
#include <string>

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
33 changes: 33 additions & 0 deletions src/playlist/M3UParser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

#include <cstdint>
#include <string>

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