mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
Fix ICY metadata stream parsing for Qt6
This commit is contained in:
@@ -11,6 +11,7 @@ set(SOURCES
|
||||
mprisadaptor.cpp
|
||||
bearwavecontroladaptor.cpp
|
||||
notificationmanager.cpp
|
||||
icyreader.cpp
|
||||
)
|
||||
|
||||
# Create executable
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include <QMediaPlayer>
|
||||
#include <QAudioOutput>
|
||||
#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;
|
||||
};
|
||||
|
||||
135
src/icyreader.cpp
Normal file
135
src/icyreader.cpp
Normal 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
44
src/icyreader.h
Normal 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
|
||||
Reference in New Issue
Block a user