Migrate to QtMultimedia and prepare for Flathub

This commit is contained in:
Sebastian Palencsar
2026-05-25 09:28:20 +02:00
parent f05c234fb1
commit 05e2fbbdbe
11 changed files with 179 additions and 85 deletions

View File

@@ -10,15 +10,14 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Qt6
find_package(Qt6 REQUIRED COMPONENTS Core DBus Quick QuickControls2 Widgets LinguistTools)
find_package(Qt6 REQUIRED COMPONENTS Core DBus Quick QuickControls2 Widgets LinguistTools Multimedia)
# KDE Frameworks
find_package(KF6Kirigami REQUIRED)
find_package(KF6I18n REQUIRED)
find_package(KF6CoreAddons REQUIRED)
# Phonon (Qt6)
find_package(Phonon4Qt6 REQUIRED)
# Add src directory
add_subdirectory(src)
@@ -33,3 +32,6 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.bearwave.desktop DESTINATION s
# Install app icon
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/app/bearwave.png DESTINATION share/icons/hicolor/256x256/apps RENAME org.kde.bearwave.png)
# Install AppStream metadata
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.bearwave.metainfo.xml DESTINATION share/metainfo)

View File

@@ -5,7 +5,7 @@ pkgdesc="A KDE-focused desktop internet radio app"
arch=('x86_64')
url="https://github.com/bearwave/bearwave" # Replace with actual URL if different
license=('MIT')
depends=('qt6-base' 'qt6-declarative' 'kirigami' 'ki18n' 'kcoreaddons' 'phonon-qt6')
depends=('qt6-base' 'qt6-declarative' 'kirigami' 'ki18n' 'kcoreaddons' 'qt6-multimedia')
makedepends=('cmake' 'extra-cmake-modules' 'git')
provides=('bearwave')
conflicts=('bearwave')

View File

@@ -1,6 +1,6 @@
# BearWave
KDE-focused desktop internet radio app for Linux, built with Qt 6, QML, KDE Frameworks, and Phonon.
KDE-focused desktop internet radio app for Linux, built with Qt 6, QML, KDE Frameworks, and QtMultimedia.
BearWave is designed for fast station browsing, simple playback controls, favorites, resume support, tray behavior, and clean Plasma integration without turning into a heavy media suite.
@@ -48,6 +48,16 @@ Then launch:
~/.local/bin/bearwave
```
### Option C: Flatpak (Local Build)
If you have `flatpak` and `flatpak-builder` installed, you can build and install BearWave via Flatpak:
```bash
flatpak-builder build-dir org.kde.bearwave.json --force-clean --user --install
flatpak run org.kde.bearwave
```
## What BearWave Is
BearWave focuses on:
@@ -98,8 +108,7 @@ Current priorities:
Current distribution status:
- source repository is the primary delivery format
- Arch Linux is the best-supported packaging path at the moment
- no official Flatpak, AppImage, or broad distro release pipeline yet
- experimental Flatpak manifest included for local sandbox building
## Platform And Support
@@ -118,7 +127,7 @@ BearWave is currently aligned with and tested primarily against:
- KDE Plasma 6
- Qt 6
- KDE Frameworks 6
- Phonon4Qt6
- Qt6 Multimedia
If support for other distributions becomes reliable, they should be added here explicitly instead of implied.
@@ -139,7 +148,7 @@ BearWave should currently be understood as:
- officially documented for source builds
- most naturally aligned with Arch Linux packaging
- likely portable to other Linux distributions with Qt 6 / KF6 / Phonon packages available
- likely portable to other Linux distributions with Qt 6 / KF6 / QtMultimedia packages available
- not yet positioned as a broadly packaged consumer desktop app
If you want the least surprising path today, use either:
@@ -153,14 +162,14 @@ If you want the least surprising path today, use either:
```bash
sudo pacman -S cmake extra-cmake-modules qt6-base qt6-declarative qt6-tools \
kirigami phonon-qt6 phonon-qt6-vlc vlc
kirigami qt6-multimedia qt6-multimedia-ffmpeg
```
### KDE Neon / Ubuntu-based
```bash
sudo apt install cmake ninja-build qt6-base-dev qt6-declarative-dev qt6-tools-dev \
libkf6kirigami-dev libkf6i18n-dev libkf6coreaddons-dev phonon4qt6-dev
libkf6kirigami-dev libkf6i18n-dev libkf6coreaddons-dev qt6-multimedia-dev
```
Note: exact package names can vary between distro releases, and non-Arch dependency sets should currently be treated as best-effort guidance rather than a guaranteed tested path.
@@ -203,7 +212,7 @@ cmake --build build -j"$(nproc)"
- Linux desktop session
- KDE Plasma recommended
- Qt 6 runtime libraries
- Phonon4Qt6 with a working backend
- Qt6 Multimedia backend (e.g. ffmpeg or gstreamer)
- network access to Radio Browser instances
## Usage
@@ -266,8 +275,7 @@ rm -f "$HOME/.config/autostart/org.kde.bearwave.desktop"
### No audio playback
- ensure `phonon-qt6` and `phonon-qt6-vlc` or equivalent are installed
- ensure `vlc` is installed
- ensure `qt6-multimedia` and a backend like `qt6-multimedia-ffmpeg` or `gst-plugins-good` are installed
- test another station URL because some streams go offline
### App icon or launcher entry not updating

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 90 KiB

29
org.kde.bearwave.json Normal file
View File

@@ -0,0 +1,29 @@
{
"app-id": "org.kde.bearwave",
"runtime": "org.kde.Platform",
"runtime-version": "6.6",
"sdk": "org.kde.Sdk",
"command": "bearwave",
"finish-args": [
"--share=network",
"--socket=wayland",
"--socket=fallback-x11",
"--socket=pulseaudio",
"--own-name=org.mpris.MediaPlayer2.bearwave",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.StatusNotifierWatcher"
],
"modules": [
{
"name": "bearwave",
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>org.kde.bearwave</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<name>BearWave</name>
<summary>KDE-focused desktop internet radio app</summary>
<description>
<p>
BearWave is designed for fast station browsing, simple playback controls, favorites, resume support, tray behavior, and clean Plasma integration without turning into a heavy media suite.
</p>
<p>Features:</p>
<ul>
<li>Internet radio via the Radio Browser API with local JSON caching</li>
<li>Station pages for Top, Germany, Netherlands, and quick world/genre filters</li>
<li>Local search and filtering by name, genre, and country</li>
<li>Sorting by name, bitrate, and votes</li>
<li>Favorites with persistent local storage</li>
<li>Playback metadata display when streams provide it</li>
<li>Resume support for last station and volume</li>
<li>MPRIS integration for Plasma media controls and media keys</li>
<li>System tray integration for background playback</li>
<li>Desktop notifications for song/track changes with local cover art caching</li>
</ul>
</description>
<launchable type="desktop-id">org.kde.bearwave.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen01.png</image>
<caption>Main window displaying radio categories</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen02.png</image>
<caption>Station browser and search filters</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen03.png</image>
<caption>The About dialog showing application info</caption>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/spalencsar/bearwave</url>
<url type="bugtracker">https://github.com/spalencsar/bearwave/issues</url>
<developer_name>Sebastian (spalencsar)</developer_name>
<provides>
<binary>bearwave</binary>
</provides>
<releases>
<release version="1.0.1" date="2026-05-23">
<description>
<ul>
<li>Fixed NotificationManager crash (segfault)</li>
<li>Migrated to modern QtMultimedia engine from legacy Phonon</li>
<li>Added Flatpak manifest and build support</li>
<li>Added debounced online search</li>
</ul>
</description>
</release>
</releases>
</component>

View File

@@ -19,7 +19,7 @@ add_executable(bearwave ${SOURCES})
# Qt Resources
target_sources(bearwave PRIVATE qml.qrc)
qt_add_lrelease(
qt_add_lrelease(bearwave
TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/../translations/bearwave_de.ts
QM_FILES_OUTPUT_VARIABLE bearwave_qm_files
@@ -43,10 +43,10 @@ target_link_libraries(bearwave PRIVATE
Qt6::Quick
Qt6::QuickControls2
Qt6::Widgets
Qt6::Multimedia
KF6::Kirigami
KF6::I18n
KF6::CoreAddons
Phonon::phonon4qt6
)
# Install

View File

@@ -2,36 +2,25 @@
#include <QUrl>
#include <QDebug>
#include <QMediaMetaData>
namespace {
QString firstMetaValue(const QMultiMap<QString, QString> &meta, const QStringList &keys)
{
for (const QString &key : keys) {
if (meta.contains(key)) {
const QString value = meta.value(key).trimmed();
if (!value.isEmpty()) {
return value;
}
}
}
return QString();
}
}
BearPlayer::BearPlayer(QObject *parent)
: QObject(parent)
{
m_mediaObject = new Phonon::MediaObject(this);
m_audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this);
m_audioOutput = new QAudioOutput(this);
m_mediaPlayer = new QMediaPlayer(this);
m_mediaPlayer->setAudioOutput(m_audioOutput);
m_coverArtFetcher = new CoverArtFetcher(this);
connect(m_coverArtFetcher, &CoverArtFetcher::coverUrlReady, this, &BearPlayer::onCoverUrlReady);
Phonon::createPath(m_mediaObject, m_audioOutput);
connect(m_mediaObject, &Phonon::MediaObject::stateChanged,
this, &BearPlayer::onStateChanged);
connect(m_mediaObject, &Phonon::MediaObject::metaDataChanged,
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged,
this, &BearPlayer::onPlaybackStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged,
this, &BearPlayer::onMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::metaDataChanged,
this, &BearPlayer::onMetaDataChanged);
m_retryTimer.setSingleShot(true);
@@ -41,8 +30,8 @@ BearPlayer::BearPlayer(QObject *parent)
return;
}
++m_retryAttempts;
m_mediaObject->setCurrentSource(QUrl(m_lastUrl));
m_mediaObject->play();
m_mediaPlayer->setSource(QUrl(m_lastUrl));
m_mediaPlayer->play();
qDebug() << "Retry stream" << m_retryAttempts << m_lastName;
});
}
@@ -65,15 +54,15 @@ void BearPlayer::playUrl(const QString &url, const QString &name)
emit currentStationChanged(m_currentStationName);
clearTrackInfo();
m_mediaObject->setCurrentSource(QUrl(url));
m_mediaObject->play();
m_mediaPlayer->setSource(QUrl(url));
m_mediaPlayer->play();
qDebug() << "Playing:" << name << url;
}
void BearPlayer::stop()
{
m_mediaObject->stop();
m_mediaPlayer->stop();
m_currentStationName.clear();
m_lastName.clear();
m_lastUrl.clear();
@@ -85,9 +74,9 @@ void BearPlayer::stop()
void BearPlayer::togglePlayPause()
{
if (m_playing) {
m_mediaObject->pause();
m_mediaPlayer->pause();
} else {
m_mediaObject->play();
m_mediaPlayer->play();
}
}
@@ -97,18 +86,23 @@ void BearPlayer::setVolume(qreal vol)
emit volumeChanged(m_audioOutput->volume());
}
void BearPlayer::onStateChanged(int state)
void BearPlayer::onPlaybackStateChanged(QMediaPlayer::PlaybackState state)
{
switch (state) {
case 2:
m_playing = true;
break;
default:
m_playing = false;
scheduleRetry();
break;
}
m_playing = (state == QMediaPlayer::PlayingState);
emit playingChanged(m_playing);
if (state == QMediaPlayer::StoppedState) {
scheduleRetry();
}
}
void BearPlayer::onMediaStatusChanged(QMediaPlayer::MediaStatus status)
{
if (status == QMediaPlayer::InvalidMedia) {
m_playing = false;
emit playingChanged(m_playing);
scheduleRetry();
}
}
void BearPlayer::scheduleRetry()
@@ -123,28 +117,15 @@ void BearPlayer::scheduleRetry()
void BearPlayer::onMetaDataChanged()
{
const QMultiMap<QString, QString> meta = m_mediaObject->metaData();
QMediaMetaData meta = m_mediaPlayer->metaData();
const QString artist = firstMetaValue(meta, {
QStringLiteral("ARTIST"),
QStringLiteral("artist"),
QStringLiteral("Artist")
});
QString title = firstMetaValue(meta, {
QStringLiteral("TITLE"),
QStringLiteral("title"),
QStringLiteral("Title")
});
if (title.isEmpty()) {
title = firstMetaValue(meta, {
QStringLiteral("icy-title"),
QStringLiteral("Icy-Title"),
QStringLiteral("StreamTitle")
});
QString artist = meta.stringValue(QMediaMetaData::ContributingArtist);
if (artist.isEmpty()) {
artist = meta.stringValue(QMediaMetaData::Author);
}
QString title = meta.stringValue(QMediaMetaData::Title);
if (m_currentTrackArtist == artist && m_currentTrackTitle == title) {
return;
}

View File

@@ -4,8 +4,8 @@
#include <QObject>
#include <QTimer>
#include <phonon4qt6/phonon/MediaObject>
#include <phonon4qt6/phonon/AudioOutput>
#include <QMediaPlayer>
#include <QAudioOutput>
#include "coverartfetcher.h"
class BearPlayer : public QObject
@@ -44,7 +44,8 @@ signals:
void trackInfoChanged();
private slots:
void onStateChanged(int state);
void onPlaybackStateChanged(QMediaPlayer::PlaybackState state);
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
void onMetaDataChanged();
void onCoverUrlReady(const QString &url);
@@ -52,8 +53,8 @@ private:
void clearTrackInfo();
void scheduleRetry();
Phonon::MediaObject *m_mediaObject = nullptr;
Phonon::AudioOutput *m_audioOutput = nullptr;
QMediaPlayer *m_mediaPlayer = nullptr;
QAudioOutput *m_audioOutput = nullptr;
QTimer m_retryTimer;
QString m_currentStationName;
QString m_currentTrackTitle;

View File

@@ -15,7 +15,7 @@
int main(int argc, char *argv[])
{
QApplication::setApplicationName(QStringLiteral("BearWave"));
QApplication::setDesktopFileName(QStringLiteral("org.kde.bearwave.desktop"));
QApplication::setDesktopFileName(QStringLiteral("org.kde.bearwave"));
QApplication::setOrganizationName(QStringLiteral("BearWave"));
QApplication::setApplicationVersion(QStringLiteral("1.0.1"));

View File

@@ -45,9 +45,11 @@ void NotificationManager::onTrackInfoChanged()
if (artist.isEmpty() && title.isEmpty()) {
m_notifyTimer.stop();
if (m_currentReply) {
m_currentReply->abort();
m_currentReply->deleteLater();
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
closeNotification();
m_lastArtist.clear();
@@ -64,9 +66,11 @@ void NotificationManager::onTrackInfoChanged()
if (trackChanged) {
m_notifyTimer.stop();
if (m_currentReply) {
m_currentReply->abort();
m_currentReply->deleteLater();
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
m_lastArtist = artist;
m_lastTitle = title;
@@ -115,9 +119,11 @@ void NotificationManager::onTrackInfoChanged()
void NotificationManager::downloadCover(const QString &url)
{
if (m_currentReply) {
m_currentReply->abort();
m_currentReply->deleteLater();
QNetworkReply *reply = m_currentReply;
m_currentReply = nullptr;
reply->disconnect(this);
reply->abort();
reply->deleteLater();
}
m_pendingCoverUrl = url;