Fix ICY metadata stream parsing for Qt6

This commit is contained in:
Sebastian Palencsar
2026-05-25 14:29:38 +02:00
parent 3bb157bac8
commit 44234cc234
5 changed files with 201 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ set(SOURCES
mprisadaptor.cpp mprisadaptor.cpp
bearwavecontroladaptor.cpp bearwavecontroladaptor.cpp
notificationmanager.cpp notificationmanager.cpp
icyreader.cpp
) )
# Create executable # Create executable

View File

@@ -16,6 +16,9 @@ BearPlayer::BearPlayer(QObject *parent)
m_coverArtFetcher = new CoverArtFetcher(this); m_coverArtFetcher = new CoverArtFetcher(this);
connect(m_coverArtFetcher, &CoverArtFetcher::coverUrlReady, this, &BearPlayer::onCoverUrlReady); 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, connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged,
this, &BearPlayer::onPlaybackStateChanged); this, &BearPlayer::onPlaybackStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, 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->setSource(QUrl(url));
m_mediaPlayer->play(); m_mediaPlayer->play();
m_icyReader->start(url);
qDebug() << "Playing:" << name << url; qDebug() << "Playing:" << name << url;
} }
@@ -63,6 +67,7 @@ void BearPlayer::playUrl(const QString &url, const QString &name)
void BearPlayer::stop() void BearPlayer::stop()
{ {
m_mediaPlayer->stop(); m_mediaPlayer->stop();
m_icyReader->stop();
m_currentStationName.clear(); m_currentStationName.clear();
m_lastName.clear(); m_lastName.clear();
m_lastUrl.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 QString BearPlayer::currentNowPlaying() const
{ {
if (!m_currentTrackArtist.isEmpty() && !m_currentTrackTitle.isEmpty()) { if (!m_currentTrackArtist.isEmpty() && !m_currentTrackTitle.isEmpty()) {

View File

@@ -7,7 +7,7 @@
#include <QMediaPlayer> #include <QMediaPlayer>
#include <QAudioOutput> #include <QAudioOutput>
#include "coverartfetcher.h" #include "coverartfetcher.h"
#include "icyreader.h"
class BearPlayer : public QObject class BearPlayer : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -48,6 +48,7 @@ private slots:
void onMediaStatusChanged(QMediaPlayer::MediaStatus status); void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
void onMetaDataChanged(); void onMetaDataChanged();
void onCoverUrlReady(const QString &url); void onCoverUrlReady(const QString &url);
void onIcyMetaDataReceived(const QString &artist, const QString &title);
private: private:
void clearTrackInfo(); void clearTrackInfo();
@@ -63,6 +64,7 @@ private:
QString m_lastName; QString m_lastName;
QString m_currentCoverArtUrl; QString m_currentCoverArtUrl;
CoverArtFetcher *m_coverArtFetcher = nullptr; CoverArtFetcher *m_coverArtFetcher = nullptr;
IcyReader *m_icyReader = nullptr;
int m_retryAttempts = 0; int m_retryAttempts = 0;
bool m_playing = false; bool m_playing = false;
}; };

135
src/icyreader.cpp Normal file
View File

@@ -0,0 +1,135 @@
#include "icyreader.h"
#include <QNetworkRequest>
#include <QDebug>
#include <QRegularExpression>
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<unsigned char>(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);
}
}

44
src/icyreader.h Normal file
View File

@@ -0,0 +1,44 @@
#ifndef ICYREADER_H
#define ICYREADER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
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