Files
bearwave/src/notificationmanager.cpp
2026-05-25 09:37:11 +02:00

266 lines
7.8 KiB
C++

#include "notificationmanager.h"
#include "radiobackend.h"
#include "bearplayer.h"
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusMessage>
#include <QDBusConnection>
#include <QDir>
#include <QFile>
#include <QCryptographicHash>
#include <QStandardPaths>
#include <QDebug>
NotificationManager::NotificationManager(RadioBackend *backend, QObject *parent)
: QObject(parent)
, m_backend(backend)
, m_player(backend->player())
, m_notificationShown(false)
{
m_coversDir = QDir::homePath() + QStringLiteral("/.cache/bearwave/covers");
QDir().mkpath(m_coversDir);
m_networkManager = new QNetworkAccessManager(this);
m_notifyTimer.setSingleShot(true);
connect(&m_notifyTimer, &QTimer::timeout, this, &NotificationManager::onNotifyTimeout);
connect(m_player, &BearPlayer::trackInfoChanged, this, &NotificationManager::onTrackInfoChanged);
connect(m_player, &BearPlayer::currentStationChanged, this, &NotificationManager::onTrackInfoChanged);
}
NotificationManager::~NotificationManager()
{
closeNotification();
}
void NotificationManager::onTrackInfoChanged()
{
const QString station = m_player->currentStationName();
const QString artist = m_player->currentTrackArtist();
const QString title = m_player->currentTrackTitle();
const QString coverUrl = m_player->currentCoverArtUrl();
if (artist.isEmpty() && title.isEmpty()) {
m_notifyTimer.stop();
if (m_currentReply) {
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
closeNotification();
m_lastArtist.clear();
m_lastTitle.clear();
m_lastStation.clear();
m_notifiedCoverUrl.clear();
m_activeCoverPath.clear();
m_notificationShown = false;
return;
}
bool trackChanged = (artist != m_lastArtist || title != m_lastTitle || station != m_lastStation);
if (trackChanged) {
m_notifyTimer.stop();
if (m_currentReply) {
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
m_lastArtist = artist;
m_lastTitle = title;
m_lastStation = station;
m_notifiedCoverUrl.clear();
m_activeCoverPath.clear();
m_notificationShown = false;
}
if (m_notificationShown) {
return;
}
QString targetUrl = coverUrl;
if (targetUrl.isEmpty() && m_backend) {
QObject *stationObj = m_backend->currentStation();
if (stationObj) {
targetUrl = stationObj->property("favicon").toString();
if (!targetUrl.startsWith(QLatin1String("https://"))) {
targetUrl.clear();
}
}
}
if (targetUrl.isEmpty()) {
if (!m_notifyTimer.isActive()) {
m_notifyTimer.start(800);
}
} else {
m_notifiedCoverUrl = targetUrl;
const QString hash = QString(QCryptographicHash::hash(targetUrl.toUtf8(), QCryptographicHash::Md5).toHex());
const QString filePath = m_coversDir + "/" + hash + ".jpg";
if (QFile::exists(filePath)) {
m_activeCoverPath = filePath;
triggerNotification();
} else {
downloadCover(targetUrl);
if (!m_notifyTimer.isActive()) {
m_notifyTimer.start(800);
}
}
}
}
void NotificationManager::downloadCover(const QString &url)
{
if (m_currentReply) {
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
m_pendingCoverUrl = url;
QNetworkRequest request(url);
m_currentReply = m_networkManager->get(request);
connect(m_currentReply, &QNetworkReply::finished, this, &NotificationManager::onDownloadFinished);
}
void NotificationManager::onDownloadFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (!reply) return;
if (reply == m_currentReply) {
m_currentReply = nullptr;
}
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
const QString hash = QString(QCryptographicHash::hash(m_pendingCoverUrl.toUtf8(), QCryptographicHash::Md5).toHex());
const QString filePath = m_coversDir + "/" + hash + ".jpg";
QFile file(filePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
m_activeCoverPath = filePath;
triggerNotification();
}
} else {
if (reply->error() != QNetworkReply::OperationCanceledError) {
qDebug() << "Cover download failed:" << reply->errorString();
}
}
reply->deleteLater();
}
void NotificationManager::onNotifyTimeout()
{
triggerNotification();
}
void NotificationManager::triggerNotification()
{
m_notifyTimer.stop();
qDebug() << "Triggering notification - station:" << m_lastStation << "artist:" << m_lastArtist << "title:" << m_lastTitle << "cover:" << m_activeCoverPath;
showNotification(m_lastStation, m_lastArtist, m_lastTitle, m_activeCoverPath);
m_notificationShown = true;
}
void NotificationManager::showNotification(const QString &station, const QString &artist, const QString &title, const QString &coverPath)
{
QDBusInterface notifyInterface(
QStringLiteral("org.freedesktop.Notifications"),
QStringLiteral("/org/freedesktop/Notifications"),
QStringLiteral("org.freedesktop.Notifications"),
QDBusConnection::sessionBus()
);
if (!notifyInterface.isValid()) {
qWarning() << "DBus Notification interface is invalid";
return;
}
QString summary;
QString body;
if (!title.isEmpty()) {
summary = title;
if (!artist.isEmpty()) {
body = artist;
}
if (!station.isEmpty()) {
if (!body.isEmpty()) body += QStringLiteral("\n");
body += tr("Station: %1").arg(station);
}
} else {
if (!station.isEmpty()) {
summary = station;
} else {
summary = QStringLiteral("BearWave");
}
if (!artist.isEmpty()) {
body = artist;
}
}
QVariantMap hints;
hints.insert(QStringLiteral("desktop-entry"), QStringLiteral("de.nerdbear.bearwave"));
if (!coverPath.isEmpty()) {
hints.insert(QStringLiteral("image-path"), coverPath);
hints.insert(QStringLiteral("image_path"), coverPath);
}
QList<QVariant> args;
args << QStringLiteral("BearWave")
<< m_lastNotificationId
<< QStringLiteral("de.nerdbear.bearwave")
<< summary
<< body
<< QStringList()
<< hints
<< -1;
QDBusReply<uint> dbusReply = notifyInterface.callWithArgumentList(
QDBus::Block,
QStringLiteral("Notify"),
args
);
if (dbusReply.isValid()) {
m_lastNotificationId = dbusReply.value();
} else {
qWarning() << "Failed to send notification via DBus:" << dbusReply.error().message();
}
}
void NotificationManager::closeNotification()
{
if (m_lastNotificationId == 0) {
return;
}
qDebug() << "Closing notification with ID:" << m_lastNotificationId;
QDBusInterface notifyInterface(
QStringLiteral("org.freedesktop.Notifications"),
QStringLiteral("/org/freedesktop/Notifications"),
QStringLiteral("org.freedesktop.Notifications"),
QDBusConnection::sessionBus()
);
if (notifyInterface.isValid()) {
notifyInterface.call(QStringLiteral("CloseNotification"), m_lastNotificationId);
}
m_lastNotificationId = 0;
}