From 44234cc234c8ac27dc50f800a14f02942e5b1df7 Mon Sep 17 00:00:00 2001 From: Sebastian Palencsar Date: Mon, 25 May 2026 14:29:38 +0200 Subject: [PATCH] Fix ICY metadata stream parsing for Qt6 --- src/CMakeLists.txt | 1 + src/bearplayer.cpp | 18 ++++++ src/bearplayer.h | 4 +- src/icyreader.cpp | 135 +++++++++++++++++++++++++++++++++++++++++++++ src/icyreader.h | 44 +++++++++++++++ 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/icyreader.cpp create mode 100644 src/icyreader.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91aa24a..23b8447 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ set(SOURCES mprisadaptor.cpp bearwavecontroladaptor.cpp notificationmanager.cpp + icyreader.cpp ) # Create executable diff --git a/src/bearplayer.cpp b/src/bearplayer.cpp index 5313691..3aa8370 100644 --- a/src/bearplayer.cpp +++ b/src/bearplayer.cpp @@ -16,6 +16,9 @@ BearPlayer::BearPlayer(QObject *parent) m_coverArtFetcher = new CoverArtFetcher(this); connect(m_coverArtFetcher, &CoverArtFetcher::coverUrlReady, this, &BearPlayer::onCoverUrlReady); + m_icyReader = new IcyReader(this); + connect(m_icyReader, &IcyReader::metaDataReceived, this, &BearPlayer::onIcyMetaDataReceived); + connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, &BearPlayer::onPlaybackStateChanged); connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, @@ -56,6 +59,7 @@ void BearPlayer::playUrl(const QString &url, const QString &name) m_mediaPlayer->setSource(QUrl(url)); m_mediaPlayer->play(); + m_icyReader->start(url); qDebug() << "Playing:" << name << url; } @@ -63,6 +67,7 @@ void BearPlayer::playUrl(const QString &url, const QString &name) void BearPlayer::stop() { m_mediaPlayer->stop(); + m_icyReader->stop(); m_currentStationName.clear(); m_lastName.clear(); m_lastUrl.clear(); @@ -145,6 +150,19 @@ void BearPlayer::onCoverUrlReady(const QString &url) } } +void BearPlayer::onIcyMetaDataReceived(const QString &artist, const QString &title) +{ + if (m_currentTrackArtist == artist && m_currentTrackTitle == title) { + return; + } + + m_currentTrackArtist = artist; + m_currentTrackTitle = title; + m_currentCoverArtUrl.clear(); + m_coverArtFetcher->fetch(artist, title); + emit trackInfoChanged(); +} + QString BearPlayer::currentNowPlaying() const { if (!m_currentTrackArtist.isEmpty() && !m_currentTrackTitle.isEmpty()) { diff --git a/src/bearplayer.h b/src/bearplayer.h index d33cdd8..1637b4b 100644 --- a/src/bearplayer.h +++ b/src/bearplayer.h @@ -7,7 +7,7 @@ #include #include #include "coverartfetcher.h" - +#include "icyreader.h" class BearPlayer : public QObject { Q_OBJECT @@ -48,6 +48,7 @@ private slots: void onMediaStatusChanged(QMediaPlayer::MediaStatus status); void onMetaDataChanged(); void onCoverUrlReady(const QString &url); + void onIcyMetaDataReceived(const QString &artist, const QString &title); private: void clearTrackInfo(); @@ -63,6 +64,7 @@ private: QString m_lastName; QString m_currentCoverArtUrl; CoverArtFetcher *m_coverArtFetcher = nullptr; + IcyReader *m_icyReader = nullptr; int m_retryAttempts = 0; bool m_playing = false; }; diff --git a/src/icyreader.cpp b/src/icyreader.cpp new file mode 100644 index 0000000..7f947c2 --- /dev/null +++ b/src/icyreader.cpp @@ -0,0 +1,135 @@ +#include "icyreader.h" +#include +#include +#include + +IcyReader::IcyReader(QObject *parent) + : QObject(parent) +{ + m_nam = new QNetworkAccessManager(this); +} + +IcyReader::~IcyReader() +{ + stop(); +} + +void IcyReader::start(const QString &url) +{ + stop(); + + if (url.isEmpty()) + return; + + QNetworkRequest request((QUrl(url))); + request.setRawHeader("Icy-MetaData", "1"); + // Some streams require a User-Agent to send ICY metadata + request.setRawHeader("User-Agent", "VLC/3.0.16 LibVLC/3.0.16"); + + m_reply = m_nam->get(request); + + connect(m_reply, &QNetworkReply::readyRead, this, &IcyReader::onReadyRead); + connect(m_reply, &QNetworkReply::finished, this, &IcyReader::onFinished); +} + +void IcyReader::stop() +{ + if (m_reply) { + m_reply->abort(); + m_reply->deleteLater(); + m_reply = nullptr; + } + m_state = ReadingAudio; + m_metaInt = 0; + m_audioBytesRead = 0; + m_metaLength = 0; +} + +void IcyReader::onReadyRead() +{ + if (!m_reply) + return; + + // Check if we just received headers and parse icy-metaint + if (m_metaInt == 0) { + if (m_reply->hasRawHeader("icy-metaint")) { + m_metaInt = m_reply->rawHeader("icy-metaint").toInt(); + } else { + // No ICY metadata supported by stream + return; + } + } + + if (m_metaInt <= 0) + return; + + while (m_reply && m_reply->bytesAvailable() > 0) { + if (m_state == ReadingAudio) { + qint64 bytesToRead = m_metaInt - m_audioBytesRead; + if (m_reply->bytesAvailable() >= bytesToRead) { + m_reply->read(bytesToRead); // Discard audio data + m_audioBytesRead = 0; + m_state = ReadingMetaLength; + } else { + m_audioBytesRead += m_reply->readAll().size(); + break; // Wait for more data + } + } else if (m_state == ReadingMetaLength) { + char lengthByte; + if (m_reply->read(&lengthByte, 1) == 1) { + m_metaLength = static_cast(lengthByte) * 16; + if (m_metaLength > 0) { + m_state = ReadingMetaData; + } else { + m_state = ReadingAudio; + } + } + } else if (m_state == ReadingMetaData) { + if (m_reply->bytesAvailable() >= m_metaLength) { + QByteArray metaData = m_reply->read(m_metaLength); + parseMetaData(metaData); + m_state = ReadingAudio; + } else { + break; // Wait for more metadata bytes + } + } + } +} + +void IcyReader::onFinished() +{ + if (m_reply) { + m_reply->deleteLater(); + m_reply = nullptr; + } +} + +void IcyReader::parseMetaData(const QByteArray &metaData) +{ + QString metaString = QString::fromUtf8(metaData).trimmed(); + if (metaString.isEmpty()) + return; + + // Format usually is: StreamTitle='Artist - Song';StreamUrl=''; + QRegularExpression re("StreamTitle='([^']*)';"); + QRegularExpressionMatch match = re.match(metaString); + + if (match.hasMatch()) { + QString fullTitle = match.captured(1).trimmed(); + if (fullTitle.isEmpty()) + return; + + QString artist; + QString title; + + int dashIndex = fullTitle.indexOf(" - "); + if (dashIndex != -1) { + artist = fullTitle.left(dashIndex).trimmed(); + title = fullTitle.mid(dashIndex + 3).trimmed(); + } else { + title = fullTitle; + } + + emit metaDataReceived(artist, title); + } +} diff --git a/src/icyreader.h b/src/icyreader.h new file mode 100644 index 0000000..23a2e3e --- /dev/null +++ b/src/icyreader.h @@ -0,0 +1,44 @@ +#ifndef ICYREADER_H +#define ICYREADER_H + +#include +#include +#include +#include + +class IcyReader : public QObject +{ + Q_OBJECT +public: + explicit IcyReader(QObject *parent = nullptr); + ~IcyReader(); + + void start(const QString &url); + void stop(); + +signals: + void metaDataReceived(const QString &artist, const QString &title); + +private slots: + void onReadyRead(); + void onFinished(); + +private: + void parseMetaData(const QByteArray &metaData); + + QNetworkAccessManager *m_nam = nullptr; + QNetworkReply *m_reply = nullptr; + + enum State { + ReadingAudio, + ReadingMetaLength, + ReadingMetaData + }; + + State m_state = ReadingAudio; + int m_metaInt = 0; + int m_audioBytesRead = 0; + int m_metaLength = 0; +}; + +#endif // ICYREADER_H