diff --git a/CMakeLists.txt b/CMakeLists.txt index ac2d141..6cfb7f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/PKGBUILD b/PKGBUILD index c1e7174..b662bd2 100644 --- a/PKGBUILD +++ b/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') diff --git a/README.md b/README.md index 0e5f8cf..8e73874 100644 --- a/README.md +++ b/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 diff --git a/assets/app/bearwave.png b/assets/app/bearwave.png index 15b1335..9c05731 100755 Binary files a/assets/app/bearwave.png and b/assets/app/bearwave.png differ diff --git a/org.kde.bearwave.json b/org.kde.bearwave.json new file mode 100644 index 0000000..4980997 --- /dev/null +++ b/org.kde.bearwave.json @@ -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": "." + } + ] + } + ] +} diff --git a/org.kde.bearwave.metainfo.xml b/org.kde.bearwave.metainfo.xml new file mode 100644 index 0000000..16ac471 --- /dev/null +++ b/org.kde.bearwave.metainfo.xml @@ -0,0 +1,67 @@ + + + org.kde.bearwave + CC0-1.0 + MIT + + BearWave + KDE-focused desktop internet radio app + + +

+ 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. +

+

Features:

+ +
+ + org.kde.bearwave.desktop + + + + https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen01.png + Main window displaying radio categories + + + https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen02.png + Station browser and search filters + + + https://raw.githubusercontent.com/spalencsar/bearwave/main/screenshots/screen03.png + The About dialog showing application info + + + + https://github.com/spalencsar/bearwave + https://github.com/spalencsar/bearwave/issues + + Sebastian (spalencsar) + + + bearwave + + + + + +
    +
  • Fixed NotificationManager crash (segfault)
  • +
  • Migrated to modern QtMultimedia engine from legacy Phonon
  • +
  • Added Flatpak manifest and build support
  • +
  • Added debounced online search
  • +
+
+
+
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3e21586..91aa24a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/bearplayer.cpp b/src/bearplayer.cpp index 6718896..5313691 100644 --- a/src/bearplayer.cpp +++ b/src/bearplayer.cpp @@ -2,36 +2,25 @@ #include #include +#include + -namespace { -QString firstMetaValue(const QMultiMap &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 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; } diff --git a/src/bearplayer.h b/src/bearplayer.h index b31fca0..d33cdd8 100644 --- a/src/bearplayer.h +++ b/src/bearplayer.h @@ -4,8 +4,8 @@ #include #include -#include -#include +#include +#include #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; diff --git a/src/main.cpp b/src/main.cpp index 23b0391..24637b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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")); diff --git a/src/notificationmanager.cpp b/src/notificationmanager.cpp index 21dd378..b8bd523 100644 --- a/src/notificationmanager.cpp +++ b/src/notificationmanager.cpp @@ -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;