diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55d8b73231..73fb66f069 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -708,6 +708,7 @@ set(SOURCES
src/settings/coverssettingspage.cpp
src/settings/lyricssettingspage.cpp
src/settings/networkproxysettingspage.cpp
+ src/settings/radiosettingspage.cpp
src/settings/appearancesettingspage.cpp
src/settings/contextsettingspage.cpp
src/settings/notificationssettingspage.cpp
@@ -780,6 +781,9 @@ set(SOURCES
src/radios/radiochannel.cpp
src/radios/somafmservice.cpp
src/radios/radioparadiseservice.cpp
+ src/radios/radiobrowserservice.cpp
+ src/radios/radiobrowsersearchview.cpp
+ src/radios/radiobrowsersearchmodel.cpp
src/radios/radiomimedata.cpp
src/scrobbler/audioscrobbler.cpp
@@ -1007,6 +1011,7 @@ set(HEADERS
src/settings/coverssettingspage.h
src/settings/lyricssettingspage.h
src/settings/networkproxysettingspage.h
+ src/settings/radiosettingspage.h
src/settings/appearancesettingspage.h
src/settings/contextsettingspage.h
src/settings/notificationssettingspage.h
@@ -1077,6 +1082,9 @@ set(HEADERS
src/radios/radiomimedata.h
src/radios/somafmservice.h
src/radios/radioparadiseservice.h
+ src/radios/radiobrowserservice.h
+ src/radios/radiobrowsersearchview.h
+ src/radios/radiobrowsersearchmodel.h
src/scrobbler/audioscrobbler.h
src/scrobbler/scrobblersettingsservice.h
@@ -1161,6 +1169,7 @@ set(UI
src/settings/coverssettingspage.ui
src/settings/lyricssettingspage.ui
src/settings/networkproxysettingspage.ui
+ src/settings/radiosettingspage.ui
src/settings/appearancesettingspage.ui
src/settings/notificationssettingspage.ui
src/settings/transcodersettingspage.ui
@@ -1189,6 +1198,7 @@ set(UI
src/streaming/streamingsearchview.ui
src/radios/radioviewcontainer.ui
+ src/radios/radiobrowsersearchview.ui
src/organize/organizedialog.ui
src/organize/organizeerrordialog.ui
diff --git a/data/icons.qrc b/data/icons.qrc
index ebbfe2f3b2..798e0e2e94 100644
--- a/data/icons.qrc
+++ b/data/icons.qrc
@@ -97,6 +97,7 @@
icons/128x128/radio.png
icons/128x128/somafm.png
icons/128x128/radioparadise.png
+ icons/128x128/radiobrowser.png
icons/128x128/musicbrainz.png
icons/64x64/albums.png
icons/64x64/alsa.png
@@ -196,6 +197,7 @@
icons/64x64/radio.png
icons/64x64/somafm.png
icons/64x64/radioparadise.png
+ icons/64x64/radiobrowser.png
icons/64x64/musicbrainz.png
icons/48x48/albums.png
icons/48x48/alsa.png
@@ -299,6 +301,7 @@
icons/48x48/radio.png
icons/48x48/somafm.png
icons/48x48/radioparadise.png
+ icons/48x48/radiobrowser.png
icons/48x48/musicbrainz.png
icons/32x32/albums.png
icons/32x32/alsa.png
@@ -402,6 +405,7 @@
icons/32x32/radio.png
icons/32x32/somafm.png
icons/32x32/radioparadise.png
+ icons/32x32/radiobrowser.png
icons/32x32/musicbrainz.png
icons/22x22/albums.png
icons/22x22/alsa.png
@@ -505,6 +509,7 @@
icons/22x22/radio.png
icons/22x22/somafm.png
icons/22x22/radioparadise.png
+ icons/22x22/radiobrowser.png
icons/22x22/musicbrainz.png
diff --git a/data/icons/128x128/radiobrowser.png b/data/icons/128x128/radiobrowser.png
new file mode 100644
index 0000000000..eaa960e3c0
Binary files /dev/null and b/data/icons/128x128/radiobrowser.png differ
diff --git a/data/icons/22x22/radiobrowser.png b/data/icons/22x22/radiobrowser.png
new file mode 100644
index 0000000000..89bf292559
Binary files /dev/null and b/data/icons/22x22/radiobrowser.png differ
diff --git a/data/icons/32x32/radiobrowser.png b/data/icons/32x32/radiobrowser.png
new file mode 100644
index 0000000000..8c9a0f27e8
Binary files /dev/null and b/data/icons/32x32/radiobrowser.png differ
diff --git a/data/icons/48x48/radiobrowser.png b/data/icons/48x48/radiobrowser.png
new file mode 100644
index 0000000000..cbf432dd8b
Binary files /dev/null and b/data/icons/48x48/radiobrowser.png differ
diff --git a/data/icons/64x64/radiobrowser.png b/data/icons/64x64/radiobrowser.png
new file mode 100644
index 0000000000..8c542b390a
Binary files /dev/null and b/data/icons/64x64/radiobrowser.png differ
diff --git a/data/icons/full/radiobrowser.png b/data/icons/full/radiobrowser.png
new file mode 100644
index 0000000000..eaa960e3c0
Binary files /dev/null and b/data/icons/full/radiobrowser.png differ
diff --git a/src/constants/radiobrowsersettings.h b/src/constants/radiobrowsersettings.h
new file mode 100644
index 0000000000..813aa3e5a4
--- /dev/null
+++ b/src/constants/radiobrowsersettings.h
@@ -0,0 +1,34 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOBROWSERSETTINGS_H
+#define RADIOBROWSERSETTINGS_H
+
+namespace RadioBrowserSettings {
+
+constexpr char kSettingsGroup[] = "RadioBrowser";
+constexpr char kServerUrl[] = "server_url";
+constexpr char kSearchLimit[] = "search_limit";
+constexpr int kSearchLimitDefault = 100;
+constexpr char kHideBroken[] = "hide_broken";
+constexpr bool kHideBrokenDefault = true;
+
+} // namespace RadioBrowserSettings
+
+#endif // RADIOBROWSERSETTINGS_H
diff --git a/src/constants/radioparadisesettings.h b/src/constants/radioparadisesettings.h
new file mode 100644
index 0000000000..c71dc82ed7
--- /dev/null
+++ b/src/constants/radioparadisesettings.h
@@ -0,0 +1,29 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOPARADISESETTINGS_H
+#define RADIOPARADISESETTINGS_H
+
+namespace RadioParadiseSettings {
+
+constexpr char kSettingsGroup[] = "RadioParadise";
+
+} // namespace RadioParadiseSettings
+
+#endif // RADIOPARADISESETTINGS_H
diff --git a/src/constants/somafmsettings.h b/src/constants/somafmsettings.h
new file mode 100644
index 0000000000..69b32117e5
--- /dev/null
+++ b/src/constants/somafmsettings.h
@@ -0,0 +1,31 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef SOMAFMSETTINGS_H
+#define SOMAFMSETTINGS_H
+
+namespace SomaFMSettings {
+
+constexpr char kSettingsGroup[] = "SomaFM";
+constexpr char kQuality[] = "quality";
+constexpr char kQualityDefault[] = "highest";
+
+} // namespace SomaFMSettings
+
+#endif // SOMAFMSETTINGS_H
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 2103d413a3..ae0210ef6b 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -191,6 +191,8 @@
#include "radios/radioservices.h"
#include "radios/radioviewcontainer.h"
+#include "radios/radiobrowserservice.h"
+#include "radios/radiobrowsersearchview.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
@@ -492,6 +494,11 @@ MainWindow::MainWindow(Application *app,
radio_view_->view()->setModel(app_->radio_services()->sort_model());
+ RadioBrowserService *radio_browser_service = qobject_cast(app_->radio_services()->ServiceBySource(Song::Source::RadioBrowser));
+ if (radio_browser_service) {
+ radio_view_->search_view()->Init(radio_browser_service);
+ }
+
// Icons
qLog(Debug) << "Creating UI";
@@ -794,6 +801,7 @@ MainWindow::MainWindow(Application *app,
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
QObject::connect(radio_view_->view(), &RadioView::GetChannels, &*app_->radio_services(), &RadioServices::GetChannels);
QObject::connect(radio_view_->view(), &RadioView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
+ QObject::connect(radio_view_->search_view(), &RadioBrowserSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
// Playlist menu
QObject::connect(playlist_menu_, &QMenu::aboutToHide, this, &MainWindow::PlaylistMenuHidden);
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 5f0652ad26..1e2fa5ffe2 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -686,7 +686,7 @@ const QString &Song::playlist_effective_albumartistsort() const { return is_comp
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
-bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
+bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise || d->source_ == Source::RadioBrowser; }
bool Song::is_stream_service() const { return d->source_ == Source::Subsonic || d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
bool Song::is_stream() const { return is_radio() || is_stream_service(); }
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
@@ -1164,6 +1164,7 @@ QString Song::TextForSource(const Source source) {
case Source::Qobuz: return u"qobuz"_s;
case Source::SomaFM: return u"somafm"_s;
case Source::RadioParadise: return u"radioparadise"_s;
+ case Source::RadioBrowser: return u"radiobrowser"_s;
case Source::Unknown: return u"unknown"_s;
}
return u"unknown"_s;
@@ -1184,6 +1185,7 @@ QString Song::DescriptionForSource(const Source source) {
case Source::Qobuz: return u"Qobuz"_s;
case Source::SomaFM: return u"SomaFM"_s;
case Source::RadioParadise: return u"Radio Paradise"_s;
+ case Source::RadioBrowser: return u"Radio Browser"_s;
case Source::Unknown: return u"Unknown"_s;
}
return u"unknown"_s;
@@ -1203,7 +1205,7 @@ Song::Source Song::SourceFromText(const QString &source) {
if (source.compare("qobuz"_L1, Qt::CaseInsensitive) == 0) return Source::Qobuz;
if (source.compare("somafm"_L1, Qt::CaseInsensitive) == 0) return Source::SomaFM;
if (source.compare("radioparadise"_L1, Qt::CaseInsensitive) == 0) return Source::RadioParadise;
-
+ if (source.compare("radiobrowser"_L1, Qt::CaseInsensitive) == 0) return Source::RadioBrowser;
return Source::Unknown;
}
@@ -1222,6 +1224,7 @@ QIcon Song::IconForSource(const Source source) {
case Source::Qobuz: return IconLoader::Load(u"qobuz"_s);
case Source::SomaFM: return IconLoader::Load(u"somafm"_s);
case Source::RadioParadise: return IconLoader::Load(u"radioparadise"_s);
+ case Source::RadioBrowser: return IconLoader::Load(u"radiobrowser"_s);
case Source::Unknown: return IconLoader::Load(u"edit-delete"_s);
}
return IconLoader::Load(u"edit-delete"_s);
@@ -1238,6 +1241,7 @@ QString Song::DomainForSource(const Source source) {
case Song::Source::Qobuz: return u"qobuz.com"_s;
case Song::Source::SomaFM: return u"somafm.com"_s;
case Song::Source::RadioParadise: return u"radioparadise.com"_s;
+ case Song::Source::RadioBrowser: return u"radio-browser.info"_s;
case Song::Source::Spotify: return u"spotify.com"_s;
default: return QString();
}
@@ -1352,7 +1356,8 @@ QString Song::ShareURL() const {
switch (source()) {
case Song::Source::Stream:
- case Song::Source::SomaFM: return url().toString();
+ case Song::Source::SomaFM:
+ case Song::Source::RadioBrowser: return url().toString();
case Song::Source::Tidal: return "https://tidal.com/track/%1"_L1.arg(song_id());
case Song::Source::Qobuz: return "https://open.qobuz.com/track/%1"_L1.arg(song_id());
case Song::Source::Spotify: return "https://open.spotify.com/track/%1"_L1.arg(song_id());
@@ -1495,6 +1500,7 @@ QString Song::ImageCacheDir(const Source source) {
case Source::Stream:
case Source::SomaFM:
case Source::RadioParadise:
+ case Source::RadioBrowser:
case Source::Unknown:
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/albumcovers"_s;
}
diff --git a/src/core/song.h b/src/core/song.h
index d028952ae8..4c7f82fff0 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -76,7 +76,8 @@ class Song {
Qobuz = 8,
SomaFM = 9,
RadioParadise = 10,
- Spotify = 11
+ Spotify = 11,
+ RadioBrowser = 12
};
static const int kSourceCount = 16;
diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp
index a61ec9bc08..0db89bf6af 100644
--- a/src/covermanager/albumcoverchoicecontroller.cpp
+++ b/src/covermanager/albumcoverchoicecontroller.cpp
@@ -583,6 +583,7 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art
case Song::Source::Stream:
case Song::Source::RadioParadise:
case Song::Source::SomaFM:
+ case Song::Source::RadioBrowser:
case Song::Source::Unknown:
break;
case Song::Source::Subsonic:
diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp
index fffb69f67b..8e18cda0b1 100644
--- a/src/playlist/playlistitem.cpp
+++ b/src/playlist/playlistitem.cpp
@@ -57,6 +57,7 @@ PlaylistItemPtr PlaylistItem::NewFromSource(const Song::Source source, const QUu
case Song::Source::Stream:
case Song::Source::RadioParadise:
case Song::Source::SomaFM:
+ case Song::Source::RadioBrowser:
return make_shared(source, uuid);
case Song::Source::LocalFile:
case Song::Source::CDDA:
@@ -82,6 +83,7 @@ PlaylistItemPtr PlaylistItem::NewFromSong(const Song &song) {
case Song::Source::Stream:
case Song::Source::RadioParadise:
case Song::Source::SomaFM:
+ case Song::Source::RadioBrowser:
return make_shared(song);
case Song::Source::LocalFile:
case Song::Source::CDDA:
diff --git a/src/radios/radiobrowsersearchmodel.cpp b/src/radios/radiobrowsersearchmodel.cpp
new file mode 100644
index 0000000000..c95d33ba9e
--- /dev/null
+++ b/src/radios/radiobrowsersearchmodel.cpp
@@ -0,0 +1,136 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+#include
+
+#include "radiobrowsersearchmodel.h"
+#include "radiochannel.h"
+#include "radiomimedata.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+RadioBrowserSearchModel::RadioBrowserSearchModel(QObject *parent) : QAbstractTableModel(parent) {}
+
+int RadioBrowserSearchModel::rowCount(const QModelIndex &parent) const {
+ return parent.isValid() ? 0 : static_cast(channels_.size());
+}
+
+int RadioBrowserSearchModel::columnCount(const QModelIndex &parent) const {
+ return parent.isValid() ? 0 : static_cast(Column::ColumnCount);
+}
+
+QVariant RadioBrowserSearchModel::data(const QModelIndex &idx, const int role) const {
+
+ if (!idx.isValid() || idx.row() >= channels_.size()) return QVariant();
+
+ const Column column = static_cast(idx.column());
+ const RadioChannel &channel = channels_.at(idx.row());
+
+ if (role == Qt::DisplayRole) {
+ switch (column) {
+ case Column::Name: return channel.name;
+ case Column::Country: return channel.country;
+ case Column::Tags: return channel.tags;
+ case Column::Codec: return channel.codec;
+ default: return QVariant();
+ }
+ }
+ else if (role == Qt::ToolTipRole && column == Column::Name) {
+ return channel.name;
+ }
+
+ return QVariant();
+
+}
+
+QVariant RadioBrowserSearchModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
+
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant();
+
+ const Column column = static_cast(section);
+ switch (column) {
+ case Column::Name: return tr("Name");
+ case Column::Country: return tr("Country");
+ case Column::Tags: return tr("Tags");
+ case Column::Codec: return tr("Codec");
+ default: return QVariant();
+ }
+
+}
+
+Qt::ItemFlags RadioBrowserSearchModel::flags(const QModelIndex &idx) const {
+
+ if (!idx.isValid()) return Qt::NoItemFlags;
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
+
+}
+
+QMimeData *RadioBrowserSearchModel::mimeData(const QModelIndexList &indexes) const {
+
+ RadioMimeData *mimedata = new RadioMimeData;
+ QList urls;
+ for (const QModelIndex &idx : indexes) {
+ if (idx.column() != 0) continue;
+ const RadioChannel &channel = channels_.at(idx.row());
+ if (!channel.url.isEmpty()) {
+ Song song = channel.ToSong();
+ urls << song.url();
+ mimedata->songs << song;
+ }
+ }
+ if (mimedata->songs.isEmpty()) {
+ delete mimedata;
+ return nullptr;
+ }
+ mimedata->setUrls(urls);
+ return mimedata;
+
+}
+
+QStringList RadioBrowserSearchModel::mimeTypes() const {
+ return QStringList() << u"text/uri-list"_s;
+}
+
+void RadioBrowserSearchModel::AddChannels(const RadioChannelList &channels) {
+
+ if (channels.isEmpty()) return;
+
+ const int first = static_cast(channels_.size());
+ const int last = first + static_cast(channels.size()) - 1;
+ beginInsertRows(QModelIndex(), first, last);
+ channels_.append(channels);
+ endInsertRows();
+
+}
+
+RadioChannel RadioBrowserSearchModel::ChannelForRow(const int row) const {
+ if (row < 0 || row >= channels_.size()) return RadioChannel();
+ return channels_.at(row);
+}
+
+void RadioBrowserSearchModel::Clear() {
+
+ if (channels_.isEmpty()) return;
+
+ beginResetModel();
+ channels_.clear();
+ endResetModel();
+
+}
diff --git a/src/radios/radiobrowsersearchmodel.h b/src/radios/radiobrowsersearchmodel.h
new file mode 100644
index 0000000000..e289f10097
--- /dev/null
+++ b/src/radios/radiobrowsersearchmodel.h
@@ -0,0 +1,61 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOBROWSERSEARCHMODEL_H
+#define RADIOBROWSERSEARCHMODEL_H
+
+#include
+#include
+#include
+#include
+
+#include "radiochannel.h"
+
+class RadioBrowserSearchModel : public QAbstractTableModel {
+ Q_OBJECT
+
+ public:
+ explicit RadioBrowserSearchModel(QObject *parent = nullptr);
+
+ enum class Column {
+ Name = 0,
+ Country,
+ Tags,
+ Codec,
+ ColumnCount
+ };
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override;
+ QVariant headerData(const int section, const Qt::Orientation orientation, const int role = Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex &idx) const override;
+
+ QMimeData *mimeData(const QModelIndexList &indexes) const override;
+ QStringList mimeTypes() const override;
+
+ void AddChannels(const RadioChannelList &channels);
+ RadioChannel ChannelForRow(const int row) const;
+ void Clear();
+
+ private:
+ RadioChannelList channels_;
+};
+
+#endif // RADIOBROWSERSEARCHMODEL_H
diff --git a/src/radios/radiobrowsersearchview.cpp b/src/radios/radiobrowsersearchview.cpp
new file mode 100644
index 0000000000..e69ca71897
--- /dev/null
+++ b/src/radios/radiobrowsersearchview.cpp
@@ -0,0 +1,280 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/iconloader.h"
+#include "core/settings.h"
+#include "constants/radiobrowsersettings.h"
+#include "widgets/stretchheaderview.h"
+#include "radiobrowserservice.h"
+#include "radiobrowsersearchview.h"
+#include "radiobrowsersearchmodel.h"
+#include "radiomimedata.h"
+#include "ui_radiobrowsersearchview.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+RadioBrowserSearchView::RadioBrowserSearchView(QWidget *parent)
+ : QWidget(parent),
+ ui_(new Ui_RadioBrowserSearchView),
+ service_(nullptr),
+ model_(new RadioBrowserSearchModel(this)),
+ search_timer_(new QTimer(this)),
+ context_menu_(nullptr),
+ action_add_to_playlist_(nullptr),
+ current_offset_(0),
+ search_limit_(100),
+ has_more_(false),
+ initialized_(false) {
+
+ ui_->setupUi(this);
+
+ ui_->results->setModel(model_);
+
+ StretchHeaderView *header = new StretchHeaderView(Qt::Horizontal, this);
+ ui_->results->setHeader(header);
+ header->SetStretchEnabled(true);
+ header->SetColumnWidth(static_cast(RadioBrowserSearchModel::Column::Name), 0.5);
+ header->SetColumnWidth(static_cast(RadioBrowserSearchModel::Column::Country), 0.2);
+ header->SetColumnWidth(static_cast(RadioBrowserSearchModel::Column::Tags), 0.2);
+ header->SetColumnWidth(static_cast(RadioBrowserSearchModel::Column::Codec), 0.1);
+
+ ui_->search->setPlaceholderText(tr("Search radio stations..."));
+
+ // Country filter - starts with "All countries", populated dynamically after API fetch
+ ui_->combo_country->addItem(tr("All countries"), QString());
+
+ // Sort order
+ ui_->combo_sort->addItem(tr("By votes"), u"votes"_s);
+ ui_->combo_sort->addItem(tr("By clicks"), u"clickcount"_s);
+ ui_->combo_sort->addItem(tr("By name"), u"name"_s);
+ ui_->combo_sort->addItem(tr("By bitrate"), u"bitrate"_s);
+
+ search_timer_->setSingleShot(true);
+ search_timer_->setInterval(300);
+
+ QObject::connect(ui_->search, &SearchField::textChanged, this, &RadioBrowserSearchView::TextChanged);
+ QObject::connect(search_timer_, &QTimer::timeout, this, &RadioBrowserSearchView::SearchTriggered);
+ QObject::connect(ui_->button_loadmore, &QPushButton::clicked, this, &RadioBrowserSearchView::LoadMore);
+ QObject::connect(ui_->combo_country, QOverload::of(&QComboBox::currentIndexChanged), this, &RadioBrowserSearchView::CountryChanged);
+ QObject::connect(ui_->combo_sort, QOverload::of(&QComboBox::currentIndexChanged), this, &RadioBrowserSearchView::SortChanged);
+ QObject::connect(ui_->results, &QTreeView::doubleClicked, this, &RadioBrowserSearchView::ItemDoubleClicked);
+ ui_->results->setContextMenuPolicy(Qt::CustomContextMenu);
+ QObject::connect(ui_->results, &QTreeView::customContextMenuRequested, this, &RadioBrowserSearchView::ShowContextMenu);
+
+}
+
+RadioBrowserSearchView::~RadioBrowserSearchView() {
+
+ if (service_) {
+ QObject::disconnect(service_, nullptr, this, nullptr);
+ }
+ delete ui_;
+
+}
+
+void RadioBrowserSearchView::showEvent(QShowEvent *e) {
+
+ Q_UNUSED(e)
+
+ if (!initialized_ && service_) {
+ service_->FetchCountries();
+ initialized_ = true;
+ }
+
+}
+
+void RadioBrowserSearchView::Init(RadioBrowserService *service) {
+
+ service_ = service;
+ QObject::connect(service_, &RadioBrowserService::SearchFinished, this, &RadioBrowserSearchView::SearchFinished);
+ QObject::connect(service_, &RadioBrowserService::SearchError, this, &RadioBrowserSearchView::SearchError);
+ QObject::connect(service_, &RadioBrowserService::CountriesLoaded, this, &RadioBrowserSearchView::CountriesLoaded);
+
+ // Load defaults from settings
+ Settings s;
+ s.beginGroup(QLatin1String(RadioBrowserSettings::kSettingsGroup));
+ search_limit_ = s.value(QLatin1String(RadioBrowserSettings::kSearchLimit), RadioBrowserSettings::kSearchLimitDefault).toInt();
+
+ const QString default_sort = s.value(u"default_sort"_s, u"votes"_s).toString();
+ for (int i = 0; i < ui_->combo_sort->count(); ++i) {
+ if (ui_->combo_sort->itemData(i).toString() == default_sort) {
+ ui_->combo_sort->setCurrentIndex(i);
+ break;
+ }
+ }
+
+ default_country_ = s.value(u"default_country"_s).toString();
+ s.endGroup();
+
+}
+
+void RadioBrowserSearchView::TextChanged(const QString &text) {
+
+ Q_UNUSED(text)
+ search_timer_->start();
+
+}
+
+void RadioBrowserSearchView::SearchTriggered() {
+
+ current_offset_ = 0;
+ model_->Clear();
+ DoSearch();
+
+}
+
+void RadioBrowserSearchView::DoSearch() {
+
+ if (!service_) return;
+
+ const QString query = ui_->search->text().trimmed();
+ const QString country = ui_->combo_country->currentData().toString();
+ const QString order = ui_->combo_sort->currentData().toString();
+
+ ui_->label_status->setText(tr("Searching..."));
+ ui_->stacked->setCurrentWidget(ui_->page_results);
+
+ service_->Search(query, country, QString(), QString(), order, search_limit_, current_offset_);
+
+}
+
+void RadioBrowserSearchView::SearchFinished(const RadioChannelList &channels, const bool has_more) {
+
+ has_more_ = has_more;
+ ui_->button_loadmore->setVisible(has_more);
+
+ if (channels.isEmpty() && current_offset_ == 0) {
+ ui_->label_status->setText(tr("No stations found."));
+ return;
+ }
+
+ ui_->label_status->setText(tr("%1 stations found").arg(model_->rowCount() + channels.size()));
+
+ model_->AddChannels(channels);
+
+}
+
+void RadioBrowserSearchView::SearchError(const QString &error) {
+
+ ui_->label_status->setText(error);
+
+}
+
+void RadioBrowserSearchView::CountriesLoaded(const QList> &countries) {
+
+ ui_->combo_country->clear();
+ ui_->combo_country->addItem(tr("All countries"), QString());
+
+ for (const QPair &entry : countries) {
+ ui_->combo_country->addItem(entry.first, entry.second);
+ }
+
+ // Restore saved default country
+ if (!default_country_.isEmpty()) {
+ for (int i = 0; i < ui_->combo_country->count(); ++i) {
+ if (ui_->combo_country->itemData(i).toString() == default_country_) {
+ ui_->combo_country->setCurrentIndex(i);
+ break;
+ }
+ }
+ }
+
+}
+
+void RadioBrowserSearchView::LoadMore() {
+
+ current_offset_ += search_limit_;
+ DoSearch();
+
+}
+
+void RadioBrowserSearchView::CountryChanged(const int index) {
+
+ Q_UNUSED(index)
+
+ if (service_) SearchTriggered();
+
+}
+
+void RadioBrowserSearchView::SortChanged(const int index) {
+
+ Q_UNUSED(index)
+
+ if (service_) SearchTriggered();
+
+}
+
+void RadioBrowserSearchView::ItemDoubleClicked(const QModelIndex &index) {
+
+ if (!index.isValid()) return;
+
+ const RadioChannel channel = model_->ChannelForRow(index.row());
+ if (channel.url.isEmpty()) return;
+
+ RadioMimeData *mimedata = new RadioMimeData;
+ mimedata->songs << channel.ToSong();
+ Q_EMIT AddToPlaylist(mimedata);
+
+}
+
+void RadioBrowserSearchView::AddSelectedToPlaylist() {
+
+ const QModelIndexList selected = ui_->results->selectionModel()->selectedRows(static_cast(RadioBrowserSearchModel::Column::Name));
+ if (selected.isEmpty()) return;
+
+ RadioMimeData *mimedata = new RadioMimeData;
+ for (const QModelIndex &idx : selected) {
+ const RadioChannel channel = model_->ChannelForRow(idx.row());
+ if (!channel.url.isEmpty()) {
+ mimedata->songs << channel.ToSong();
+ }
+ }
+ if (!mimedata->songs.isEmpty()) {
+ Q_EMIT AddToPlaylist(mimedata);
+ }
+ else {
+ delete mimedata;
+ }
+
+}
+
+void RadioBrowserSearchView::ShowContextMenu(const QPoint &pos) {
+
+ if (!context_menu_) {
+ context_menu_ = new QMenu(this);
+
+ action_add_to_playlist_ = new QAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this);
+ QObject::connect(action_add_to_playlist_, &QAction::triggered, this, &RadioBrowserSearchView::AddSelectedToPlaylist);
+ context_menu_->addAction(action_add_to_playlist_);
+ }
+
+ const bool has_selection = !ui_->results->selectionModel()->selectedRows().isEmpty();
+ action_add_to_playlist_->setEnabled(has_selection);
+
+ context_menu_->popup(ui_->results->viewport()->mapToGlobal(pos));
+
+}
diff --git a/src/radios/radiobrowsersearchview.h b/src/radios/radiobrowsersearchview.h
new file mode 100644
index 0000000000..ca85b3b2e9
--- /dev/null
+++ b/src/radios/radiobrowsersearchview.h
@@ -0,0 +1,86 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOBROWSERSEARCHVIEW_H
+#define RADIOBROWSERSEARCHVIEW_H
+
+#include
+#include
+#include
+
+#include "radiochannel.h"
+
+class QTimer;
+class QMimeData;
+class QMenu;
+class QAction;
+class QShowEvent;
+class QContextMenuEvent;
+
+class RadioBrowserSearchModel;
+class RadioBrowserService;
+
+class Ui_RadioBrowserSearchView;
+
+class RadioBrowserSearchView : public QWidget {
+ Q_OBJECT
+
+ public:
+ explicit RadioBrowserSearchView(QWidget *parent = nullptr);
+ ~RadioBrowserSearchView();
+
+ void Init(RadioBrowserService *service);
+
+ protected:
+ void showEvent(QShowEvent *e) override;
+
+ Q_SIGNALS:
+ void AddToPlaylist(QMimeData *mimedata);
+
+ private Q_SLOTS:
+ void TextChanged(const QString &text);
+ void SearchTriggered();
+ void SearchFinished(const RadioChannelList &channels, const bool has_more);
+ void SearchError(const QString &error);
+ void LoadMore();
+ void CountryChanged(const int index);
+ void SortChanged(const int index);
+ void CountriesLoaded(const QList> &countries);
+ void AddSelectedToPlaylist();
+ void ItemDoubleClicked(const QModelIndex &index);
+ void ShowContextMenu(const QPoint &pos);
+
+ private:
+ void DoSearch();
+
+ Ui_RadioBrowserSearchView *ui_;
+ RadioBrowserService *service_;
+ RadioBrowserSearchModel *model_;
+ QTimer *search_timer_;
+ QMenu *context_menu_;
+ QAction *action_add_to_playlist_;
+
+ QString default_country_;
+ int current_offset_;
+ int search_limit_;
+ bool has_more_;
+ bool initialized_;
+};
+
+#endif // RADIOBROWSERSEARCHVIEW_H
diff --git a/src/radios/radiobrowsersearchview.ui b/src/radios/radiobrowsersearchview.ui
new file mode 100644
index 0000000000..3accce60ca
--- /dev/null
+++ b/src/radios/radiobrowsersearchview.ui
@@ -0,0 +1,181 @@
+
+
+ RadioBrowserSearchView
+
+
+
+ 0
+ 0
+ 400
+ 500
+
+
+
+
+ 2
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
+
+
+ -
+
+
+ 4
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 1
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ true
+
+
+ QAbstractItemView::DragOnly
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Load more...
+
+
+ false
+
+
+
+
+
+
+
+ -
+
+
+ Search for radio stations using radio-browser.info
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ SearchField
+ QWidget
+
+
+
+
+
+
diff --git a/src/radios/radiobrowserservice.cpp b/src/radios/radiobrowserservice.cpp
new file mode 100644
index 0000000000..78a7c13d24
--- /dev/null
+++ b/src/radios/radiobrowserservice.cpp
@@ -0,0 +1,339 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/networkaccessmanager.h"
+#include "core/taskmanager.h"
+#include "core/iconloader.h"
+#include "settings/radiosettingspage.h"
+#include "radiobrowserservice.h"
+#include "radiochannel.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+const QStringList RadioBrowserService::kFallbackServers = {
+ u"de1.api.radio-browser.info"_s,
+ u"de2.api.radio-browser.info"_s,
+ u"nl1.api.radio-browser.info"_s,
+ u"at1.api.radio-browser.info"_s
+};
+
+RadioBrowserService::RadioBrowserService(const SharedPtr task_manager, const SharedPtr network, QObject *parent)
+ : RadioService(Song::Source::RadioBrowser, u"Radio Browser"_s, IconLoader::Load(u"radiobrowser"_s), task_manager, network, parent),
+ dns_lookup_(nullptr),
+ server_discovered_(false),
+ has_pending_search_(false),
+ has_pending_countries_(false),
+ fallback_index_(0) {}
+
+RadioBrowserService::~RadioBrowserService() {
+ Abort();
+}
+
+QUrl RadioBrowserService::Homepage() { return QUrl(u"https://www.radio-browser.info/"_s); }
+QUrl RadioBrowserService::Donate() { return QUrl(u"https://www.radio-browser.info/"_s); }
+
+void RadioBrowserService::Abort() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ QObject::disconnect(reply, nullptr, this, nullptr);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+ if (dns_lookup_) {
+ dns_lookup_->abort();
+ dns_lookup_->deleteLater();
+ dns_lookup_ = nullptr;
+ }
+
+ has_pending_search_ = false;
+ has_pending_countries_ = false;
+
+}
+
+void RadioBrowserService::GetChannels() {
+ // RadioBrowser has 50,000+ stations. We don't load them all.
+ // Only emit empty list so the model doesn't break on refresh.
+ Q_EMIT NewChannels();
+}
+
+void RadioBrowserService::DiscoverServer() {
+
+ if (dns_lookup_) {
+ dns_lookup_->abort();
+ dns_lookup_->deleteLater();
+ dns_lookup_ = nullptr;
+ }
+
+ fallback_index_ = 0;
+
+ dns_lookup_ = new QDnsLookup(QDnsLookup::A, u"all.api.radio-browser.info"_s, this);
+ QObject::connect(dns_lookup_, &QDnsLookup::finished, this, &RadioBrowserService::DnsLookupFinished);
+ dns_lookup_->lookup();
+
+}
+
+void RadioBrowserService::DnsLookupFinished() {
+
+ if (!dns_lookup_) return;
+
+ if (dns_lookup_->error() != QDnsLookup::NoError || dns_lookup_->hostAddressRecords().isEmpty()) {
+ // DNS failed, try fallback servers
+ dns_lookup_->deleteLater();
+ dns_lookup_ = nullptr;
+
+ if (fallback_index_ < kFallbackServers.size()) {
+ TestServer(kFallbackServers.at(fallback_index_));
+ ++fallback_index_;
+ }
+ else {
+ Q_EMIT SearchError(tr("Failed to discover Radio Browser server."));
+ }
+ return;
+ }
+
+ // Use first resolved hostname
+ const auto records = dns_lookup_->hostAddressRecords();
+ dns_lookup_->deleteLater();
+ dns_lookup_ = nullptr;
+
+ // Try fallback servers since we can't easily do reverse DNS in Qt
+ // The DNS lookup confirms the service exists, now use a known hostname
+ fallback_index_ = 0;
+ TestServer(kFallbackServers.at(fallback_index_));
+
+}
+
+void RadioBrowserService::TestServer(const QString &hostname) {
+
+ QUrl url;
+ url.setScheme(u"https"_s);
+ url.setHost(hostname);
+ url.setPath(u"/json/stats"_s);
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
+ QNetworkReply *reply = network_->get(request);
+ replies_ << reply;
+ QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { ServerTestReply(reply); });
+
+}
+
+void RadioBrowserService::ServerTestReply(QNetworkReply *reply) {
+
+ if (replies_.contains(reply)) replies_.removeAll(reply);
+ reply->deleteLater();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ // Try next fallback
+ ++fallback_index_;
+ if (fallback_index_ < kFallbackServers.size()) {
+ TestServer(kFallbackServers.at(fallback_index_));
+ }
+ else {
+ Q_EMIT SearchError(tr("No Radio Browser server available."));
+ }
+ return;
+ }
+
+ // Server works
+ QUrl url;
+ url.setScheme(u"https"_s);
+ url.setHost(reply->url().host());
+ server_url_ = url;
+ server_discovered_ = true;
+
+ // Execute pending search if any
+ if (has_pending_search_) {
+ has_pending_search_ = false;
+ Search(pending_search_.query, pending_search_.country, pending_search_.tag, pending_search_.language, pending_search_.order, pending_search_.limit, pending_search_.offset);
+ }
+
+ // Execute pending countries fetch if any
+ if (has_pending_countries_) {
+ has_pending_countries_ = false;
+ FetchCountries();
+ }
+
+}
+
+void RadioBrowserService::Search(const QString &query,
+ const QString &country,
+ const QString &tag,
+ const QString &language,
+ const QString &order,
+ const int limit,
+ const int offset) {
+
+ if (!server_discovered_) {
+ // Save search and discover server first
+ pending_search_ = {query, country, tag, language, order, limit, offset};
+ has_pending_search_ = true;
+ DiscoverServer();
+ return;
+ }
+
+ QUrl url(server_url_);
+ url.setPath(u"/json/stations/search"_s);
+
+ QUrlQuery url_query;
+ if (!query.isEmpty()) url_query.addQueryItem(u"name"_s, query);
+ if (!country.isEmpty()) url_query.addQueryItem(u"countrycode"_s, country);
+ if (!tag.isEmpty()) url_query.addQueryItem(u"tag"_s, tag);
+ if (!language.isEmpty()) url_query.addQueryItem(u"language"_s, language);
+ url_query.addQueryItem(u"limit"_s, QString::number(limit));
+ url_query.addQueryItem(u"offset"_s, QString::number(offset));
+ url_query.addQueryItem(u"hidebroken"_s, u"true"_s);
+
+ if (!order.isEmpty()) {
+ url_query.addQueryItem(u"order"_s, order);
+ if (order != u"name"_s) {
+ url_query.addQueryItem(u"reverse"_s, u"true"_s);
+ }
+ }
+ else {
+ url_query.addQueryItem(u"order"_s, u"votes"_s);
+ url_query.addQueryItem(u"reverse"_s, u"true"_s);
+ }
+
+ url.setQuery(url_query);
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
+ QNetworkReply *reply = network_->get(request);
+ replies_ << reply;
+ const int task_id = task_manager_->StartTask(tr("Searching Radio Browser"));
+ QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, task_id, limit]() { SearchReply(reply, task_id, limit); });
+
+}
+
+void RadioBrowserService::SearchReply(QNetworkReply *reply, const int task_id, const int limit) {
+
+ if (replies_.contains(reply)) replies_.removeAll(reply);
+ reply->deleteLater();
+
+ const QJsonArray array = ExtractJsonArray(reply);
+ task_manager_->SetTaskFinished(task_id);
+
+ if (array.isEmpty()) {
+ Q_EMIT SearchFinished(RadioChannelList(), false);
+ return;
+ }
+
+ RadioChannelList channels;
+ for (const QJsonValue &value : array) {
+ if (!value.isObject()) continue;
+ const QJsonObject obj = value.toObject();
+
+ const QString name = obj["name"_L1].toString().trimmed();
+ if (name.isEmpty()) continue;
+
+ // Prefer url_resolved over url
+ QString stream_url = obj["url_resolved"_L1].toString();
+ if (stream_url.isEmpty()) stream_url = obj["url"_L1].toString();
+ if (stream_url.isEmpty()) continue;
+
+ RadioChannel channel;
+ channel.source = source_;
+ channel.name = name;
+ channel.url.setUrl(stream_url);
+ channel.country = obj["country"_L1].toString().trimmed();
+ channel.tags = obj["tags"_L1].toString().trimmed();
+ channel.codec = obj["codec"_L1].toString().trimmed();
+
+ const QString favicon = obj["favicon"_L1].toString();
+ if (!favicon.isEmpty()) {
+ channel.thumbnail_url.setUrl(favicon);
+ }
+
+ channels << channel;
+ }
+
+ const bool has_more = (array.size() == limit);
+ Q_EMIT SearchFinished(channels, has_more);
+
+}
+
+void RadioBrowserService::FetchCountries() {
+
+ if (!server_discovered_) {
+ has_pending_countries_ = true;
+ if (!has_pending_search_) {
+ DiscoverServer();
+ }
+ return;
+ }
+
+ QUrl url(server_url_);
+ url.setPath(u"/json/countrycodes"_s);
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
+ QNetworkReply *reply = network_->get(request);
+ replies_ << reply;
+ QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { CountriesReply(reply); });
+
+}
+
+void RadioBrowserService::CountriesReply(QNetworkReply *reply) {
+
+ if (replies_.contains(reply)) replies_.removeAll(reply);
+ reply->deleteLater();
+
+ const QJsonArray array = ExtractJsonArray(reply);
+
+ // Collect country codes that have stations
+ QSet codes_with_stations;
+ for (const QJsonValue &value : array) {
+ if (!value.isObject()) continue;
+ const QJsonObject obj = value.toObject();
+ const QString code = obj["name"_L1].toString().toUpper();
+ const int count = obj["stationcount"_L1].toInt();
+ if (!code.isEmpty() && count > 0) {
+ codes_with_stations.insert(code);
+ }
+ }
+
+ // Filter the full country list to only include countries with stations
+ const QList> all_countries = RadioSettingsPage::CountryList();
+ QList> countries;
+ for (const QPair &entry : all_countries) {
+ if (codes_with_stations.contains(entry.second)) {
+ countries << entry;
+ }
+ }
+
+ Q_EMIT CountriesLoaded(countries);
+
+}
diff --git a/src/radios/radiobrowserservice.h b/src/radios/radiobrowserservice.h
new file mode 100644
index 0000000000..77c02845a1
--- /dev/null
+++ b/src/radios/radiobrowserservice.h
@@ -0,0 +1,101 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOBROWSERSERVICE_H
+#define RADIOBROWSERSERVICE_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include "radioservice.h"
+#include "radiochannel.h"
+
+class QNetworkReply;
+class QDnsLookup;
+
+class TaskManager;
+class NetworkAccessManager;
+
+class RadioBrowserService : public RadioService {
+ Q_OBJECT
+
+ public:
+ explicit RadioBrowserService(const SharedPtr task_manager, const SharedPtr network, QObject *parent = nullptr);
+ ~RadioBrowserService();
+
+ QUrl Homepage() override;
+ QUrl Donate() override;
+
+ void Abort();
+
+ void Search(const QString &query,
+ const QString &country = QString(),
+ const QString &tag = QString(),
+ const QString &language = QString(),
+ const QString &order = QString(),
+ const int limit = 100,
+ const int offset = 0);
+
+ void FetchCountries();
+
+ Q_SIGNALS:
+ void SearchFinished(const RadioChannelList &channels, const bool has_more);
+ void SearchError(const QString &error);
+ void CountriesLoaded(const QList> &countries);
+
+ public Q_SLOTS:
+ void GetChannels() override;
+
+ private Q_SLOTS:
+ void DnsLookupFinished();
+ void ServerTestReply(QNetworkReply *reply);
+ void SearchReply(QNetworkReply *reply, const int task_id, const int limit);
+ void CountriesReply(QNetworkReply *reply);
+
+ private:
+ void DiscoverServer();
+ void TestServer(const QString &hostname);
+
+ QList replies_;
+ QDnsLookup *dns_lookup_;
+ QUrl server_url_;
+ bool server_discovered_;
+
+ // Pending search to execute after server discovery
+ struct PendingSearch {
+ QString query;
+ QString country;
+ QString tag;
+ QString language;
+ QString order;
+ int limit;
+ int offset;
+ };
+ PendingSearch pending_search_;
+ bool has_pending_search_;
+ bool has_pending_countries_;
+
+ static const QStringList kFallbackServers;
+ int fallback_index_;
+};
+
+#endif // RADIOBROWSERSERVICE_H
diff --git a/src/radios/radiochannel.h b/src/radios/radiochannel.h
index 55ad45a776..c0e146332e 100644
--- a/src/radios/radiochannel.h
+++ b/src/radios/radiochannel.h
@@ -21,6 +21,7 @@
#define RADIOCHANNEL_H
#include
+#include
#include
#include
#include
@@ -34,11 +35,26 @@ struct RadioChannel {
QString name;
QUrl url;
QUrl thumbnail_url;
+ QString country;
+ QString tags;
+ QString codec;
Song ToSong() const;
};
using RadioChannelList = QList;
+inline QDataStream &operator<<(QDataStream &stream, const RadioChannel &channel) {
+ stream << static_cast(channel.source) << channel.name << channel.url << channel.thumbnail_url << channel.country << channel.tags << channel.codec;
+ return stream;
+}
+
+inline QDataStream &operator>>(QDataStream &stream, RadioChannel &channel) {
+ int source = 0;
+ stream >> source >> channel.name >> channel.url >> channel.thumbnail_url >> channel.country >> channel.tags >> channel.codec;
+ channel.source = static_cast(source);
+ return stream;
+}
+
Q_DECLARE_METATYPE(RadioChannel)
Q_DECLARE_METATYPE(RadioChannelList)
diff --git a/src/radios/radiomodel.h b/src/radios/radiomodel.h
index 68b4ec52f6..129bc2ae39 100644
--- a/src/radios/radiomodel.h
+++ b/src/radios/radiomodel.h
@@ -70,6 +70,7 @@ class RadioModel : public SimpleTreeModel {
void Reset();
void AddChannels(const RadioChannelList &channels);
+ QList sources() const { return container_nodes_.keys(); }
private:
bool IsPlayable(const QModelIndex &idx) const;
diff --git a/src/radios/radioservice.cpp b/src/radios/radioservice.cpp
index 3195d85c95..6b6f7429d4 100644
--- a/src/radios/radioservice.cpp
+++ b/src/radios/radioservice.cpp
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#include "includes/shared_ptr.h"
#include "core/logging.h"
@@ -99,6 +100,40 @@ QJsonObject RadioService::ExtractJsonObj(QNetworkReply *reply) {
}
+QJsonArray RadioService::ExtractJsonArray(const QByteArray &data) {
+
+ if (data.isEmpty()) {
+ return QJsonArray();
+ }
+
+ QJsonParseError json_error;
+ const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ Error(QStringLiteral("Failed to parse Json data from %1: %2").arg(name_, json_error.errorString()));
+ return QJsonArray();
+ }
+
+ if (json_document.isEmpty()) {
+ Error(QStringLiteral("%1: Received empty Json document.").arg(name_), data);
+ return QJsonArray();
+ }
+
+ if (!json_document.isArray()) {
+ Error(QStringLiteral("%1: Json document is not an array.").arg(name_), json_document);
+ return QJsonArray();
+ }
+
+ return json_document.array();
+
+}
+
+QJsonArray RadioService::ExtractJsonArray(QNetworkReply *reply) {
+
+ return ExtractJsonArray(ExtractData(reply));
+
+}
+
void RadioService::Error(const QString &error, const QVariant &debug) {
qLog(Error) << name_ << error;
diff --git a/src/radios/radioservice.h b/src/radios/radioservice.h
index 34eba9d85d..87592854dc 100644
--- a/src/radios/radioservice.h
+++ b/src/radios/radioservice.h
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#include "includes/shared_ptr.h"
#include "core/song.h"
@@ -68,6 +69,8 @@ class RadioService : public QObject {
QByteArray ExtractData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
QJsonObject ExtractJsonObj(QNetworkReply *reply);
+ QJsonArray ExtractJsonArray(const QByteArray &data);
+ QJsonArray ExtractJsonArray(QNetworkReply *reply);
void Error(const QString &error, const QVariant &debug = QVariant());
protected:
diff --git a/src/radios/radioservices.cpp b/src/radios/radioservices.cpp
index a86481db71..dd9f634407 100644
--- a/src/radios/radioservices.cpp
+++ b/src/radios/radioservices.cpp
@@ -33,6 +33,7 @@
#include "radiochannel.h"
#include "somafmservice.h"
#include "radioparadiseservice.h"
+#include "radiobrowserservice.h"
using std::make_shared;
@@ -61,6 +62,7 @@ RadioServices::RadioServices(const SharedPtr task_manager,
AddService(new SomaFMService(task_manager, network_, this));
AddService(new RadioParadiseService(task_manager, network_, this));
+ AddService(new RadioBrowserService(task_manager, network_, this));
}
@@ -135,6 +137,7 @@ void RadioServices::GotChannelsFromBackend(const RadioChannelList &channels) {
}
else {
model_->AddChannels(channels);
+ channels_refresh_ = false;
}
}
diff --git a/src/radios/radioview.cpp b/src/radios/radioview.cpp
index aaa0285ece..e39f8fb139 100644
--- a/src/radios/radioview.cpp
+++ b/src/radios/radioview.cpp
@@ -25,11 +25,13 @@
#include
#include
#include
+#include
#include
#include "core/mimedata.h"
#include "core/iconloader.h"
#include "radiomodel.h"
+#include "radioitem.h"
#include "radioview.h"
#include "radioservice.h"
#include "radiomimedata.h"
@@ -107,6 +109,23 @@ void RadioView::contextMenuEvent(QContextMenuEvent *e) {
}
+void RadioView::mouseDoubleClickEvent(QMouseEvent *event) {
+
+ const QModelIndex idx = indexAt(event->pos());
+ if (idx.isValid()) {
+ const RadioItem::Type type = idx.data(RadioModel::Role_Type).value();
+ if (type == RadioItem::Type::Service) {
+ // Service node: only expand/collapse, don't add all channels to playlist
+ setExpanded(idx, !isExpanded(idx));
+ event->accept();
+ return;
+ }
+ }
+
+ AutoExpandingTreeView::mouseDoubleClickEvent(event);
+
+}
+
void RadioView::AddToPlaylist() {
const QModelIndexList selected_indexes = selectedIndexes();
diff --git a/src/radios/radioview.h b/src/radios/radioview.h
index 6e8b9513a7..1187276ff7 100644
--- a/src/radios/radioview.h
+++ b/src/radios/radioview.h
@@ -28,6 +28,7 @@ class QMimeData;
class QMenu;
class QAction;
class QShowEvent;
+class QMouseEvent;
class QContextMenuEvent;
class RadioModel;
@@ -41,6 +42,7 @@ class RadioView : public AutoExpandingTreeView {
void showEvent(QShowEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
Q_SIGNALS:
void GetChannels();
diff --git a/src/radios/radioviewcontainer.h b/src/radios/radioviewcontainer.h
index 3f2a4c7b84..b4069619c3 100644
--- a/src/radios/radioviewcontainer.h
+++ b/src/radios/radioviewcontainer.h
@@ -25,6 +25,7 @@
#include "ui_radioviewcontainer.h"
class RadioView;
+class RadioBrowserSearchView;
class RadioViewContainer : public QWidget {
Q_OBJECT
@@ -36,6 +37,7 @@ class RadioViewContainer : public QWidget {
void ReloadSettings();
RadioView *view() const { return ui_->view; }
+ RadioBrowserSearchView *search_view() const { return ui_->search_view; }
Q_SIGNALS:
void Refresh();
diff --git a/src/radios/radioviewcontainer.ui b/src/radios/radioviewcontainer.ui
index de107c201d..b7131a1cbb 100644
--- a/src/radios/radioviewcontainer.ui
+++ b/src/radios/radioviewcontainer.ui
@@ -30,70 +30,104 @@
0
-
-
-
-
-
-
- true
-
-
-
+
+
+ 0
+
+
+
+ Channels
+
+
+
+ 0
-
-
- 22
- 22
-
+
+ 0
-
- true
+
+ 0
-
-
- -
-
-
- Qt::Horizontal
+
+ 0
-
-
- 40
- 20
-
+
+ 0
-
-
-
-
- -
-
-
- false
-
-
- true
-
-
- QAbstractItemView::DragDrop
-
-
- true
-
-
-
- 16
- 16
-
-
-
- true
-
-
- true
-
-
- true
-
+
-
+
+
-
+
+
+ true
+
+
+
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ false
+
+
+ true
+
+
+ QAbstractItemView::DragDrop
+
+
+ true
+
+
+
+ 16
+ 16
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+ Radio Browser
+
+
@@ -104,6 +138,11 @@
QTreeView
+
+ RadioBrowserSearchView
+ QWidget
+ radios/radiobrowsersearchview.h
+
diff --git a/src/radios/somafmservice.cpp b/src/radios/somafmservice.cpp
index 8c63081ee9..a8ce1bce23 100644
--- a/src/radios/somafmservice.cpp
+++ b/src/radios/somafmservice.cpp
@@ -31,6 +31,8 @@
#include "core/networkaccessmanager.h"
#include "core/taskmanager.h"
#include "core/iconloader.h"
+#include "core/settings.h"
+#include "constants/somafmsettings.h"
#include "playlistparsers/playlistparser.h"
#include "somafmservice.h"
#include "radiochannel.h"
@@ -97,6 +99,11 @@ void SomaFMService::GetChannelsReply(QNetworkReply *reply, const int task_id) {
}
const QJsonArray array_channels = object["channels"_L1].toArray();
+ Settings s;
+ s.beginGroup(QLatin1String(SomaFMSettings::kSettingsGroup));
+ const QString preferred_quality = s.value(QLatin1String(SomaFMSettings::kQuality), QLatin1String(SomaFMSettings::kQualityDefault)).toString();
+ s.endGroup();
+
RadioChannelList channels;
for (const QJsonValue &value_channel : array_channels) {
if (!value_channel.isObject()) continue;
@@ -115,7 +122,7 @@ void SomaFMService::GetChannelsReply(QNetworkReply *reply, const int task_id) {
}
RadioChannel channel;
QString quality = obj_playlist["quality"_L1].toString();
- if (quality != "highest"_L1) continue;
+ if (quality != preferred_quality) continue;
channel.source = source_;
channel.name = name;
channel.url.setUrl(obj_playlist["url"_L1].toString());
diff --git a/src/settings/radiosettingspage.cpp b/src/settings/radiosettingspage.cpp
new file mode 100644
index 0000000000..9916400a3e
--- /dev/null
+++ b/src/settings/radiosettingspage.cpp
@@ -0,0 +1,152 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "settingsdialog.h"
+#include "radiosettingspage.h"
+#include "ui_radiosettingspage.h"
+#include "core/iconloader.h"
+#include "core/settings.h"
+#include "constants/somafmsettings.h"
+#include "constants/radiobrowsersettings.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+QList> RadioSettingsPage::CountryList() {
+
+ QSet seen;
+ QList> countries;
+
+ const QList locales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyTerritory);
+ for (const QLocale &locale : locales) {
+ const QLocale::Territory territory = locale.territory();
+ if (territory == QLocale::AnyTerritory || seen.contains(territory)) continue;
+ seen.insert(territory);
+
+ const QString locale_name = locale.name();
+ const int underscore = locale_name.lastIndexOf(u'_');
+ if (underscore < 0) continue;
+ const QString code = locale_name.mid(underscore + 1);
+ if (code.length() != 2) continue;
+
+ countries << qMakePair(QLocale::territoryToString(territory), code);
+ }
+
+ std::sort(countries.begin(), countries.end(), [](const QPair &a, const QPair &b) {
+ return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
+ });
+
+ return countries;
+
+}
+
+void RadioSettingsPage::PopulateCountries(QComboBox *combo) {
+
+ combo->addItem(QCoreApplication::translate("RadioSettingsPage", "All countries"), QString());
+
+ const QList> countries = CountryList();
+ for (const QPair &entry : countries) {
+ combo->addItem(entry.first, entry.second);
+ }
+
+}
+
+RadioSettingsPage::RadioSettingsPage(SettingsDialog *dialog, QWidget *parent)
+ : SettingsPage(dialog, parent),
+ ui_(new Ui_RadioSettingsPage) {
+
+ ui_->setupUi(this);
+ setWindowIcon(IconLoader::Load(u"radio"_s, true, 0, 32));
+
+ // SomaFM quality options
+ ui_->combo_somafm_quality->addItem(tr("Highest"), u"highest"_s);
+ ui_->combo_somafm_quality->addItem(tr("High"), u"high"_s);
+ ui_->combo_somafm_quality->addItem(tr("Low"), u"low"_s);
+
+ // Radio Browser sort options
+ ui_->combo_default_sort->addItem(tr("By votes"), u"votes"_s);
+ ui_->combo_default_sort->addItem(tr("By clicks"), u"clickcount"_s);
+ ui_->combo_default_sort->addItem(tr("By name"), u"name"_s);
+ ui_->combo_default_sort->addItem(tr("By bitrate"), u"bitrate"_s);
+
+ // Radio Browser country options
+ PopulateCountries(ui_->combo_default_country);
+
+}
+
+RadioSettingsPage::~RadioSettingsPage() { delete ui_; }
+
+void RadioSettingsPage::Load() {
+
+ // SomaFM
+ {
+ Settings s;
+ s.beginGroup(QLatin1String(SomaFMSettings::kSettingsGroup));
+ ComboBoxLoadFromSettings(s, ui_->combo_somafm_quality, QLatin1String(SomaFMSettings::kQuality), QLatin1String(SomaFMSettings::kQualityDefault));
+ s.endGroup();
+ }
+
+ // Radio Browser
+ {
+ Settings s;
+ s.beginGroup(QLatin1String(RadioBrowserSettings::kSettingsGroup));
+ ui_->spin_search_limit->setValue(s.value(QLatin1String(RadioBrowserSettings::kSearchLimit), RadioBrowserSettings::kSearchLimitDefault).toInt());
+ ui_->check_hide_broken->setChecked(s.value(QLatin1String(RadioBrowserSettings::kHideBroken), RadioBrowserSettings::kHideBrokenDefault).toBool());
+ ComboBoxLoadFromSettings(s, ui_->combo_default_sort, u"default_sort"_s, u"votes"_s);
+ ComboBoxLoadFromSettings(s, ui_->combo_default_country, u"default_country"_s, QString());
+ s.endGroup();
+ }
+
+ Init(ui_->layout_radiosettingspage->parentWidget());
+
+}
+
+void RadioSettingsPage::Save() {
+
+ // SomaFM
+ {
+ Settings s;
+ s.beginGroup(QLatin1String(SomaFMSettings::kSettingsGroup));
+ s.setValue(QLatin1String(SomaFMSettings::kQuality), ui_->combo_somafm_quality->currentData().toString());
+ s.endGroup();
+ }
+
+ // Radio Browser
+ {
+ Settings s;
+ s.beginGroup(QLatin1String(RadioBrowserSettings::kSettingsGroup));
+ s.setValue(QLatin1String(RadioBrowserSettings::kSearchLimit), ui_->spin_search_limit->value());
+ s.setValue(QLatin1String(RadioBrowserSettings::kHideBroken), ui_->check_hide_broken->isChecked());
+ s.setValue(u"default_sort"_s, ui_->combo_default_sort->currentData().toString());
+ s.setValue(u"default_country"_s, ui_->combo_default_country->currentData().toString());
+ s.endGroup();
+ }
+
+}
diff --git a/src/settings/radiosettingspage.h b/src/settings/radiosettingspage.h
new file mode 100644
index 0000000000..2e835192d1
--- /dev/null
+++ b/src/settings/radiosettingspage.h
@@ -0,0 +1,51 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2026, Malte Zilinski
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef RADIOSETTINGSPAGE_H
+#define RADIOSETTINGSPAGE_H
+
+#include
+#include
+#include
+#include
+
+#include "settings/settingspage.h"
+
+class QComboBox;
+class SettingsDialog;
+class Ui_RadioSettingsPage;
+
+class RadioSettingsPage : public SettingsPage {
+ Q_OBJECT
+
+ public:
+ explicit RadioSettingsPage(SettingsDialog *dialog, QWidget *parent = nullptr);
+ ~RadioSettingsPage() override;
+
+ static QList> CountryList();
+ static void PopulateCountries(QComboBox *combo);
+
+ void Load() override;
+ void Save() override;
+
+ private:
+ Ui_RadioSettingsPage *ui_;
+};
+
+#endif // RADIOSETTINGSPAGE_H
diff --git a/src/settings/radiosettingspage.ui b/src/settings/radiosettingspage.ui
new file mode 100644
index 0000000000..5e36635386
--- /dev/null
+++ b/src/settings/radiosettingspage.ui
@@ -0,0 +1,115 @@
+
+
+ RadioSettingsPage
+
+
+
+ 0
+ 0
+ 450
+ 500
+
+
+
+ Radios
+
+
+ -
+
+
+ SomaFM
+
+
+
-
+
+
+ Stream quality:
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Radio Browser
+
+
+
-
+
+
+ Search results limit:
+
+
+
+ -
+
+
+ 10
+
+
+ 500
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Hide broken stations
+
+
+ true
+
+
+
+ -
+
+
+ Default sort order:
+
+
+
+ -
+
+
+ -
+
+
+ Default country:
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index 6eaf5587ad..6ef23d6ad2 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -91,6 +91,8 @@
# include "qobuzsettingspage.h"
#endif
+#include "radiosettingspage.h"
+
#include "ui_settingsdialog.h"
using namespace Qt::Literals::StringLiterals;
@@ -162,6 +164,8 @@ SettingsDialog::SettingsDialog(const SharedPtr player,
AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service(), this), streaming);
#endif
+ AddPage(Page::Radio, new RadioSettingsPage(this, this), streaming);
+
// List box
QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged);
ui_->list->setCurrentItem(pages_[Page::Behaviour].item_);
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index 8723ca193c..9adfcf6ed5 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -93,6 +93,7 @@ class SettingsDialog : public QDialog {
Tidal,
Qobuz,
Spotify,
+ Radio,
};
enum Role {
diff --git a/src/utilities/coverutils.cpp b/src/utilities/coverutils.cpp
index 4b3deaf4e9..d44088dbcf 100644
--- a/src/utilities/coverutils.cpp
+++ b/src/utilities/coverutils.cpp
@@ -142,6 +142,7 @@ QString CoverUtils::CoverFilenameFromSource(const Song::Source source, const QUr
case Song::Source::Stream:
case Song::Source::SomaFM:
case Song::Source::RadioParadise:
+ case Song::Source::RadioBrowser:
case Song::Source::Unknown:
filename = QString::fromLatin1(Sha1CoverHash(artist, album).toHex());
break;