mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 14:24:14 +02:00
Migrate to QtMultimedia and prepare for Flathub
This commit is contained in:
@@ -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)
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -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')
|
||||
|
||||
28
README.md
28
README.md
@@ -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
29
org.kde.bearwave.json
Normal 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": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
67
org.kde.bearwave.metainfo.xml
Normal file
67
org.kde.bearwave.metainfo.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user