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
|
mprisadaptor.cpp
|
||||||
bearwavecontroladaptor.cpp
|
bearwavecontroladaptor.cpp
|
||||||
notificationmanager.cpp
|
notificationmanager.cpp
|
||||||
|
icyreader.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create executable
|
# Create executable
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
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