diff --git a/data/data.qrc b/data/data.qrc
index 3027a9f3c..233d07bfc 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -15,6 +15,7 @@
schema/schema-12.sql
schema/schema-13.sql
schema/schema-14.sql
+ schema/schema-15.sql
schema/device-schema.sql
style/strawberry.css
style/smartplaylistsearchterm.css
diff --git a/data/icons.qrc b/data/icons.qrc
index 0b7b484a5..f5c4dfb6d 100644
--- a/data/icons.qrc
+++ b/data/icons.qrc
@@ -92,6 +92,9 @@
icons/128x128/tidal.png
icons/128x128/qobuz.png
icons/128x128/multimedia-player-ipod-standard-black.png
+ icons/128x128/radio.png
+ icons/128x128/somafm.png
+ icons/128x128/radioparadise.png
icons/64x64/albums.png
icons/64x64/alsa.png
icons/64x64/application-exit.png
@@ -185,6 +188,9 @@
icons/64x64/tidal.png
icons/64x64/qobuz.png
icons/64x64/multimedia-player-ipod-standard-black.png
+ icons/64x64/radio.png
+ icons/64x64/somafm.png
+ icons/64x64/radioparadise.png
icons/48x48/albums.png
icons/48x48/alsa.png
icons/48x48/application-exit.png
@@ -282,6 +288,9 @@
icons/48x48/tidal.png
icons/48x48/qobuz.png
icons/48x48/multimedia-player-ipod-standard-black.png
+ icons/48x48/radio.png
+ icons/48x48/somafm.png
+ icons/48x48/radioparadise.png
icons/32x32/albums.png
icons/32x32/alsa.png
icons/32x32/application-exit.png
@@ -379,6 +388,9 @@
icons/32x32/tidal.png
icons/32x32/qobuz.png
icons/32x32/multimedia-player-ipod-standard-black.png
+ icons/32x32/radio.png
+ icons/32x32/somafm.png
+ icons/32x32/radioparadise.png
icons/22x22/albums.png
icons/22x22/alsa.png
icons/22x22/application-exit.png
@@ -476,5 +488,8 @@
icons/22x22/tidal.png
icons/22x22/qobuz.png
icons/22x22/multimedia-player-ipod-standard-black.png
+ icons/22x22/radio.png
+ icons/22x22/somafm.png
+ icons/22x22/radioparadise.png
diff --git a/data/icons/128x128/radio.png b/data/icons/128x128/radio.png
new file mode 100644
index 000000000..eaa960e3c
Binary files /dev/null and b/data/icons/128x128/radio.png differ
diff --git a/data/icons/128x128/radioparadise.png b/data/icons/128x128/radioparadise.png
new file mode 100644
index 000000000..6daac4122
Binary files /dev/null and b/data/icons/128x128/radioparadise.png differ
diff --git a/data/icons/128x128/somafm.png b/data/icons/128x128/somafm.png
new file mode 100644
index 000000000..41aad9938
Binary files /dev/null and b/data/icons/128x128/somafm.png differ
diff --git a/data/icons/22x22/radio.png b/data/icons/22x22/radio.png
new file mode 100644
index 000000000..89bf29255
Binary files /dev/null and b/data/icons/22x22/radio.png differ
diff --git a/data/icons/22x22/radioparadise.png b/data/icons/22x22/radioparadise.png
new file mode 100644
index 000000000..774d54d62
Binary files /dev/null and b/data/icons/22x22/radioparadise.png differ
diff --git a/data/icons/22x22/somafm.png b/data/icons/22x22/somafm.png
new file mode 100644
index 000000000..768ae96d6
Binary files /dev/null and b/data/icons/22x22/somafm.png differ
diff --git a/data/icons/32x32/radio.png b/data/icons/32x32/radio.png
new file mode 100644
index 000000000..8c9a0f27e
Binary files /dev/null and b/data/icons/32x32/radio.png differ
diff --git a/data/icons/32x32/radioparadise.png b/data/icons/32x32/radioparadise.png
new file mode 100644
index 000000000..c86817893
Binary files /dev/null and b/data/icons/32x32/radioparadise.png differ
diff --git a/data/icons/32x32/somafm.png b/data/icons/32x32/somafm.png
new file mode 100644
index 000000000..82d1ac8b7
Binary files /dev/null and b/data/icons/32x32/somafm.png differ
diff --git a/data/icons/48x48/radio.png b/data/icons/48x48/radio.png
new file mode 100644
index 000000000..cbf432dd8
Binary files /dev/null and b/data/icons/48x48/radio.png differ
diff --git a/data/icons/48x48/radioparadise.png b/data/icons/48x48/radioparadise.png
new file mode 100644
index 000000000..e23cd4e3d
Binary files /dev/null and b/data/icons/48x48/radioparadise.png differ
diff --git a/data/icons/48x48/somafm.png b/data/icons/48x48/somafm.png
new file mode 100644
index 000000000..2ac85fafb
Binary files /dev/null and b/data/icons/48x48/somafm.png differ
diff --git a/data/icons/64x64/radio.png b/data/icons/64x64/radio.png
new file mode 100644
index 000000000..8c542b390
Binary files /dev/null and b/data/icons/64x64/radio.png differ
diff --git a/data/icons/64x64/radioparadise.png b/data/icons/64x64/radioparadise.png
new file mode 100644
index 000000000..901e0d92b
Binary files /dev/null and b/data/icons/64x64/radioparadise.png differ
diff --git a/data/icons/64x64/somafm.png b/data/icons/64x64/somafm.png
new file mode 100644
index 000000000..9de62ab0a
Binary files /dev/null and b/data/icons/64x64/somafm.png differ
diff --git a/data/icons/full/radio.png b/data/icons/full/radio.png
new file mode 100644
index 000000000..eaa960e3c
Binary files /dev/null and b/data/icons/full/radio.png differ
diff --git a/data/icons/full/radioparadise.png b/data/icons/full/radioparadise.png
new file mode 100644
index 000000000..10f8a1415
Binary files /dev/null and b/data/icons/full/radioparadise.png differ
diff --git a/data/icons/full/somafm.png b/data/icons/full/somafm.png
new file mode 100644
index 000000000..41aad9938
Binary files /dev/null and b/data/icons/full/somafm.png differ
diff --git a/data/schema/schema-15.sql b/data/schema/schema-15.sql
new file mode 100644
index 000000000..c29613257
--- /dev/null
+++ b/data/schema/schema-15.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS radio_channels (
+ source INTEGER NOT NULL DEFAULT 0,
+ name TEXT DEFAULT '',
+ url TEXT DEFAULT '',
+ thumbnail_url TEXT DEFAULT ''
+);
+
+UPDATE schema_version SET version=15;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index 682493b46..074e4bac8 100644
--- a/data/schema/schema.sql
+++ b/data/schema/schema.sql
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
-INSERT INTO schema_version (version) VALUES (14);
+INSERT INTO schema_version (version) VALUES (15);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL DEFAULT '',
@@ -605,6 +605,13 @@ CREATE TABLE IF NOT EXISTS devices (
transcode_format NOT NULL DEFAULT 5
);
+CREATE TABLE IF NOT EXISTS radio_channels (
+ source INTEGER NOT NULL DEFAULT 0,
+ name TEXT DEFAULT '',
+ url TEXT DEFAULT '',
+ thumbnail_url TEXT DEFAULT ''
+);
+
CREATE INDEX IF NOT EXISTS idx_url ON songs (url);
CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, artist);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 36bababc0..9f0e8ec03 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -228,6 +228,17 @@ set(SOURCES
internet/internetcollectionviewcontainer.cpp
internet/internetsearchview.cpp
+ radios/radioservices.cpp
+ radios/radiobackend.cpp
+ radios/radiomodel.cpp
+ radios/radioview.cpp
+ radios/radioviewcontainer.cpp
+ radios/radioservice.cpp
+ radios/radioplaylistitem.cpp
+ radios/radiochannel.cpp
+ radios/somafmservice.cpp
+ radios/radioparadiseservice.cpp
+
scrobbler/audioscrobbler.cpp
scrobbler/scrobblerservices.cpp
scrobbler/scrobblerservice.cpp
@@ -449,6 +460,16 @@ set(HEADERS
internet/internetcollectionview.h
internet/internetcollectionviewcontainer.h
+ radios/radioservices.h
+ radios/radiobackend.h
+ radios/radiomodel.h
+ radios/radioview.h
+ radios/radioviewcontainer.h
+ radios/radioservice.h
+ radios/radiomimedata.h
+ radios/somafmservice.h
+ radios/radioparadiseservice.h
+
scrobbler/audioscrobbler.h
scrobbler/scrobblerservices.h
scrobbler/scrobblerservice.h
@@ -532,6 +553,8 @@ set(UI
internet/internetcollectionviewcontainer.ui
internet/internetsearchview.ui
+ radios/radioviewcontainer.ui
+
organize/organizedialog.ui
organize/organizeerrordialog.ui
diff --git a/src/context/contextalbumsmodel.cpp b/src/context/contextalbumsmodel.cpp
index 9f26489cc..5c5776d47 100644
--- a/src/context/contextalbumsmodel.cpp
+++ b/src/context/contextalbumsmodel.cpp
@@ -382,4 +382,3 @@ SongList ContextAlbumsModel::GetChildSongs(const QModelIndexList &indexes) const
SongList ContextAlbumsModel::GetChildSongs(const QModelIndex &idx) const {
return GetChildSongs(QModelIndexList() << idx);
}
-
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 3fe594dbd..7f1cc6e97 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -91,6 +91,9 @@
# include "moodbar/moodbarloader.h"
#endif
+#include "radios/radioservices.h"
+#include "radios/radiobackend.h"
+
using namespace std::chrono_literals;
class ApplicationImpl {
@@ -171,15 +174,13 @@ class ApplicationImpl {
#endif
return internet_services;
}),
+ radio_services_([=]() { return new RadioServices(app, app); }),
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
- lastfm_import_([=]() { return new LastFMImport(app); }),
-
#ifdef HAVE_MOODBAR
moodbar_loader_([=]() { return new MoodbarLoader(app, app); }),
moodbar_controller_([=]() { return new MoodbarController(app, app); }),
#endif
- dummy_([=]() { return nullptr; })
-
+ lastfm_import_([=]() { return new LastFMImport(app); })
{}
Lazy tag_reader_client_;
@@ -199,13 +200,13 @@ class ApplicationImpl {
Lazy current_albumcover_loader_;
Lazy lyrics_providers_;
Lazy internet_services_;
+ Lazy radio_services_;
Lazy scrobbler_;
- Lazy lastfm_import_;
#ifdef HAVE_MOODBAR
Lazy moodbar_loader_;
Lazy moodbar_controller_;
#endif
- Lazy dummy_;
+ Lazy lastfm_import_;
};
@@ -265,7 +266,8 @@ void Application::Exit() {
#ifndef Q_OS_WIN
<< device_manager()
#endif
- << internet_services();
+ << internet_services()
+ << radio_services()->radio_backend();
QObject::connect(tag_reader_client(), &TagReaderClient::ExitFinished, this, &Application::ExitReceived);
tag_reader_client()->ExitAsync();
@@ -287,6 +289,9 @@ void Application::Exit() {
QObject::connect(internet_services(), &InternetServices::ExitFinished, this, &Application::ExitReceived);
internet_services()->Exit();
+ QObject::connect(radio_services()->radio_backend(), &RadioBackend::ExitFinished, this, &Application::ExitReceived);
+ radio_services()->radio_backend()->ExitAsync();
+
}
void Application::ExitReceived() {
@@ -328,6 +333,7 @@ LyricsProviders *Application::lyrics_providers() const { return p_->lyrics_provi
PlaylistBackend *Application::playlist_backend() const { return p_->playlist_backend_.get(); }
PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); }
InternetServices *Application::internet_services() const { return p_->internet_services_.get(); }
+RadioServices *Application::radio_services() const { return p_->radio_services_.get(); }
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
LastFMImport *Application::lastfm_import() const { return p_->lastfm_import_.get(); }
#ifdef HAVE_MOODBAR
diff --git a/src/core/application.h b/src/core/application.h
index 139b3028f..ffabfa2ed 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -58,6 +58,7 @@ class LyricsProviders;
class AudioScrobbler;
class LastFMImport;
class InternetServices;
+class RadioServices;
#ifdef HAVE_MOODBAR
class MoodbarController;
class MoodbarLoader;
@@ -94,15 +95,17 @@ class Application : public QObject {
LyricsProviders *lyrics_providers() const;
AudioScrobbler *scrobbler() const;
- LastFMImport *lastfm_import() const;
InternetServices *internet_services() const;
+ RadioServices *radio_services() const;
#ifdef HAVE_MOODBAR
MoodbarController *moodbar_controller() const;
MoodbarLoader *moodbar_loader() const;
#endif
+ LastFMImport *lastfm_import() const;
+
void Exit();
QThread *MoveToNewThread(QObject *object);
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 36b4e46e8..f0c922f4f 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -54,7 +54,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 14;
+const int Database::kSchemaVersion = 15;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index da79a829f..6c6a5250e 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -180,6 +180,9 @@
#include "internet/internetcollectionview.h"
#include "internet/internetsearchview.h"
+#include "radios/radioservices.h"
+#include "radios/radioviewcontainer.h"
+
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
@@ -277,6 +280,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic
#ifdef HAVE_QOBUZ
qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Qobuz), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, this)),
#endif
+ radio_view_(new RadioViewContainer(this)),
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
collection_show_all_(nullptr),
collection_show_duplicates_(nullptr),
@@ -316,8 +320,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic
hidden_(false),
exit_(false),
exit_count_(0),
- delete_files_(false)
- {
+ delete_files_(false) {
qLog(Debug) << "Starting";
@@ -343,6 +346,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic
ui_->tabs->AddTab(playlist_list_, "playlists", IconLoader::Load("view-media-playlist"), tr("Playlists"));
ui_->tabs->AddTab(smartplaylists_view_, "smartplaylists", IconLoader::Load("view-media-playlist"), tr("Smart playlists"));
ui_->tabs->AddTab(file_view_, "files", IconLoader::Load("document-open"), tr("Files"));
+ ui_->tabs->AddTab(radio_view_, "radios", IconLoader::Load("radio"), tr("Radios"));
#ifndef Q_OS_WIN
ui_->tabs->AddTab(device_view_, "devices", IconLoader::Load("device"), tr("Devices"));
#endif
@@ -401,6 +405,8 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic
organize_dialog_->SetDestinationModel(app_->collection()->model()->directory_model());
+ radio_view_->view()->setModel(app_->radio_services()->radio_model());
+
// Icons
qLog(Debug) << "Creating UI";
@@ -675,6 +681,10 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic
QObject::connect(qobuz_view_->search_view(), &InternetSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
#endif
+ 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);
+
// Playlist menu
QObject::connect(playlist_menu_, &QMenu::aboutToHide, this, &MainWindow::PlaylistMenuHidden);
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, &MainWindow::PlaylistPlay);
@@ -1126,6 +1136,9 @@ void MainWindow::ReloadAllSettings() {
queue_view_->ReloadSettings();
playlist_list_->ReloadSettings();
smartplaylists_view_->ReloadSettings();
+ radio_view_->ReloadSettings();
+ app_->internet_services()->ReloadSettings();
+ app_->radio_services()->ReloadSettings();
app_->cover_providers()->ReloadSettings();
app_->lyrics_providers()->ReloadSettings();
#ifdef HAVE_MOODBAR
diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h
index af4db2fcc..a4df4f662 100644
--- a/src/core/mainwindow.h
+++ b/src/core/mainwindow.h
@@ -100,6 +100,7 @@ class Windows7ThumbBar;
#endif
class AddStreamDialog;
class LastFMImportDialog;
+class RadioViewContainer;
class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT
@@ -338,6 +339,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
InternetTabsView *tidal_view_;
InternetTabsView *qobuz_view_;
+ RadioViewContainer *radio_view_;
+
LastFMImportDialog *lastfm_import_dialog_;
QAction *collection_show_all_;
diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp
index 1d72435aa..3290c03ce 100644
--- a/src/core/metatypes.cpp
+++ b/src/core/metatypes.cpp
@@ -72,6 +72,8 @@
#include "smartplaylists/playlistgenerator_fwd.h"
+#include "radios/radiochannel.h"
+
void RegisterMetaTypes() {
qRegisterMetaType("const char*");
@@ -141,4 +143,6 @@ void RegisterMetaTypes() {
qRegisterMetaType("PlaylistGeneratorPtr");
+ qRegisterMetaType("RadioChannelList");
+
}
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 106646817..1012f56ac 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -375,7 +375,13 @@ double Song::rating() const { return d->rating_; }
bool Song::is_collection_song() const { return d->source_ == Source_Collection; }
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
-bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic || d->source_ == Source_Qobuz; }
+bool Song::is_stream() const { return d->source_ == Source_Stream ||
+ d->source_ == Source_Tidal ||
+ d->source_ == Source_Subsonic ||
+ d->source_ == Source_Qobuz ||
+ d->source_ == Source_SomaFM ||
+ d->source_ == Source_RadioParadise;
+}
bool Song::is_cdda() const { return d->source_ == Source_CDDA; }
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
bool Song::stream_url_can_expire() const { return d->source_ == Song::Source_Tidal || d->source_ == Song::Source_Qobuz; }
@@ -490,7 +496,13 @@ Song::Source Song::SourceFromURL(const QUrl &url) {
else if (url.scheme() == "tidal") return Source_Tidal;
else if (url.scheme() == "subsonic") return Source_Subsonic;
else if (url.scheme() == "qobuz") return Source_Qobuz;
- else if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "rtsp") return Source_Stream;
+ else if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "rtsp") {
+ if (url.host().endsWith("tidal.com", Qt::CaseInsensitive)) { return Source_Tidal; }
+ if (url.host().endsWith("qobuz.com", Qt::CaseInsensitive)) { return Source_Qobuz; }
+ if (url.host().endsWith("somafm.com", Qt::CaseInsensitive)) { return Source_SomaFM; }
+ if (url.host().endsWith("radioparadise.com", Qt::CaseInsensitive)) { return Source_RadioParadise; }
+ return Source_Stream;
+ }
else return Source_Unknown;
}
@@ -498,15 +510,36 @@ Song::Source Song::SourceFromURL(const QUrl &url) {
QString Song::TextForSource(Source source) {
switch (source) {
- case Song::Source_LocalFile: return "file";
- case Song::Source_Collection: return "collection";
- case Song::Source_CDDA: return "cd";
- case Song::Source_Device: return "device";
- case Song::Source_Stream: return "stream";
- case Song::Source_Tidal: return "tidal";
- case Song::Source_Subsonic: return "subsonic";
- case Song::Source_Qobuz: return "qobuz";
- case Song::Source_Unknown: return "unknown";
+ case Song::Source_LocalFile: return "file";
+ case Song::Source_Collection: return "collection";
+ case Song::Source_CDDA: return "cd";
+ case Song::Source_Device: return "device";
+ case Song::Source_Stream: return "stream";
+ case Song::Source_Tidal: return "tidal";
+ case Song::Source_Subsonic: return "subsonic";
+ case Song::Source_Qobuz: return "qobuz";
+ case Song::Source_SomaFM: return "somafm";
+ case Song::Source_RadioParadise: return "radioparadise";
+ case Song::Source_Unknown: return "unknown";
+ }
+ return "unknown";
+
+}
+
+QString Song::DescriptionForSource(Source source) {
+
+ switch (source) {
+ case Song::Source_LocalFile: return "File";
+ case Song::Source_Collection: return "Collection";
+ case Song::Source_CDDA: return "CD";
+ case Song::Source_Device: return "Device";
+ case Song::Source_Stream: return "Stream";
+ case Song::Source_Tidal: return "Tidal";
+ case Song::Source_Subsonic: return "Subsonic";
+ case Song::Source_Qobuz: return "Qobuz";
+ case Song::Source_SomaFM: return "SomaFM";
+ case Song::Source_RadioParadise: return "Radio Paradise";
+ case Song::Source_Unknown: return "Unknown";
}
return "unknown";
@@ -522,6 +555,8 @@ Song::Source Song::SourceFromText(const QString &source) {
if (source == "tidal") return Source_Tidal;
if (source == "subsonic") return Source_Subsonic;
if (source == "qobuz") return Source_Qobuz;
+ if (source == "somafm") return Source_SomaFM;
+ if (source == "radioparadise") return Source_RadioParadise;
return Source_Unknown;
@@ -530,15 +565,17 @@ Song::Source Song::SourceFromText(const QString &source) {
QIcon Song::IconForSource(Source source) {
switch (source) {
- case Song::Source_LocalFile: return IconLoader::Load("folder-sound");
- case Song::Source_Collection: return IconLoader::Load("library-music");
- case Song::Source_CDDA: return IconLoader::Load("media-optical");
- case Song::Source_Device: return IconLoader::Load("device");
- case Song::Source_Stream: return IconLoader::Load("applications-internet");
- case Song::Source_Tidal: return IconLoader::Load("tidal");
- case Song::Source_Subsonic: return IconLoader::Load("subsonic");
- case Song::Source_Qobuz: return IconLoader::Load("qobuz");
- case Song::Source_Unknown: return IconLoader::Load("edit-delete");
+ case Song::Source_LocalFile: return IconLoader::Load("folder-sound");
+ case Song::Source_Collection: return IconLoader::Load("library-music");
+ case Song::Source_CDDA: return IconLoader::Load("media-optical");
+ case Song::Source_Device: return IconLoader::Load("device");
+ case Song::Source_Stream: return IconLoader::Load("applications-internet");
+ case Song::Source_Tidal: return IconLoader::Load("tidal");
+ case Song::Source_Subsonic: return IconLoader::Load("subsonic");
+ case Song::Source_Qobuz: return IconLoader::Load("qobuz");
+ case Song::Source_SomaFM: return IconLoader::Load("somafm");
+ case Song::Source_RadioParadise: return IconLoader::Load("radioparadise");
+ case Song::Source_Unknown: return IconLoader::Load("edit-delete");
}
return IconLoader::Load("edit-delete");
@@ -721,6 +758,8 @@ QString Song::ImageCacheDir(const Song::Source source) {
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Stream:
+ case Song::Source_SomaFM:
+ case Song::Source_RadioParadise:
case Song::Source_Unknown:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers";
}
diff --git a/src/core/song.h b/src/core/song.h
index 78a27b9ae..86c6e00b7 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -75,6 +75,8 @@ class Song {
Source_Tidal = 6,
Source_Subsonic = 7,
Source_Qobuz = 8,
+ Source_SomaFM = 9,
+ Source_RadioParadise = 10
};
// Don't change these values - they're stored in the database, and defined in the tag reader protobuf.
@@ -134,6 +136,7 @@ class Song {
static Source SourceFromURL(const QUrl &url);
static QString TextForSource(Source source);
+ static QString DescriptionForSource(Source source);
static Song::Source SourceFromText(const QString &source);
static QIcon IconForSource(Source source);
static QString TextForFiletype(FileType filetype);
@@ -141,6 +144,7 @@ class Song {
static QIcon IconForFiletype(FileType filetype);
QString TextForSource() const { return TextForSource(source()); }
+ QString DescriptionForSource() const { return DescriptionForSource(source()); }
QIcon IconForSource() const { return IconForSource(source()); }
QString TextForFiletype() const { return TextForFiletype(filetype()); }
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp
index d4875a2ef..7d894e7aa 100644
--- a/src/covermanager/albumcoverchoicecontroller.cpp
+++ b/src/covermanager/albumcoverchoicecontroller.cpp
@@ -534,6 +534,8 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
+ case Song::Source_RadioParadise:
+ case Song::Source_SomaFM:
case Song::Source_Unknown:
break;
case Song::Source_Tidal:
diff --git a/src/covermanager/albumcoverloader.cpp b/src/covermanager/albumcoverloader.cpp
index 22957e8f8..0ff014d4a 100644
--- a/src/covermanager/albumcoverloader.cpp
+++ b/src/covermanager/albumcoverloader.cpp
@@ -200,6 +200,8 @@ QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, con
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
+ case Song::Source_SomaFM:
+ case Song::Source_RadioParadise:
case Song::Source_Unknown:
filename = Utilities::Sha1CoverHash(artist, album).toHex();
break;
diff --git a/src/internet/internetservices.cpp b/src/internet/internetservices.cpp
index e873715be..a8de20dcc 100644
--- a/src/internet/internetservices.cpp
+++ b/src/internet/internetservices.cpp
@@ -60,7 +60,7 @@ void InternetServices::RemoveService(InternetService *service) {
}
-InternetService *InternetServices::ServiceBySource(const Song::Source source) {
+InternetService *InternetServices::ServiceBySource(const Song::Source source) const {
if (services_.contains(source)) return services_.value(source);
return nullptr;
diff --git a/src/internet/internetservices.h b/src/internet/internetservices.h
index 75035d723..53029c756 100644
--- a/src/internet/internetservices.h
+++ b/src/internet/internetservices.h
@@ -40,7 +40,7 @@ class InternetServices : public QObject {
explicit InternetServices(QObject *parent = nullptr);
~InternetServices() override;
- InternetService *ServiceBySource(const Song::Source source);
+ InternetService *ServiceBySource(const Song::Source source) const;
template
T *Service() {
return static_cast(this->ServiceBySource(T::kSource));
diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp
index fed92c4b9..495571741 100644
--- a/src/playlist/playlist.cpp
+++ b/src/playlist/playlist.cpp
@@ -93,6 +93,10 @@
#include "internet/internetplaylistitem.h"
#include "internet/internetsongmimedata.h"
+#include "radios/radioservice.h"
+#include "radios/radiomimedata.h"
+#include "radios/radioplaylistitem.h"
+
using namespace std::chrono_literals;
const char *Playlist::kCddaMimeType = "x-content/audio-cdda";
@@ -787,11 +791,14 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
else if (const PlaylistItemMimeData *item_data = qobject_cast(data)) {
InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now);
}
+ else if (const PlaylistGeneratorMimeData *generator_data = qobject_cast(data)) {
+ InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now, enqueue_next_now);
+ }
else if (const InternetSongMimeData *internet_song_data = qobject_cast(data)) {
InsertInternetItems(internet_song_data->service, internet_song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
- else if (const PlaylistGeneratorMimeData *generator_data = qobject_cast(data)) {
- InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now, enqueue_next_now);
+ else if (const RadioMimeData *radio_data = qobject_cast(data)) {
+ InsertRadioItems(radio_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
else if (data->hasFormat(kRowsMimetype)) {
// Dragged from the playlist
@@ -1156,6 +1163,20 @@ void Playlist::InsertInternetItems(InternetService *service, const SongList &son
}
+void Playlist::InsertRadioItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
+
+ PlaylistItemList playlist_items;
+ QList song_urls;
+ playlist_items.reserve(songs.count());
+ for (const Song &song : songs) {
+ playlist_items << std::make_shared(song);
+ song_urls << song.url();
+ }
+
+ InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
+
+}
+
void Playlist::UpdateItems(SongList songs) {
qLog(Debug) << "Updating playlist with new tracks' info";
diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h
index 7c6b51d0b..81649ca54 100644
--- a/src/playlist/playlist.h
+++ b/src/playlist/playlist.h
@@ -58,8 +58,8 @@ class PlaylistBackend;
class PlaylistFilter;
class Queue;
class TaskManager;
-class InternetServices;
class InternetService;
+class RadioService;
namespace PlaylistUndoCommands {
class InsertItems;
@@ -243,8 +243,9 @@ class Playlist : public QAbstractListModel {
void InsertCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongs(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongsOrCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
- void InsertInternetItems(InternetService *service, const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSmartPlaylist(PlaylistGeneratorPtr gen, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
+ void InsertInternetItems(InternetService *service, const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
+ void InsertRadioItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void ReshuffleIndices();
diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp
index 706619d86..32fd127ee 100644
--- a/src/playlist/playlistitem.cpp
+++ b/src/playlist/playlistitem.cpp
@@ -46,6 +46,8 @@ PlaylistItem *PlaylistItem::NewFromSource(const Song::Source source) {
case Song::Source_Subsonic:
case Song::Source_Tidal:
case Song::Source_Qobuz:
+ case Song::Source_RadioParadise:
+ case Song::Source_SomaFM:
case Song::Source_Stream:
return new InternetPlaylistItem(source);
case Song::Source_LocalFile:
@@ -67,6 +69,8 @@ PlaylistItem *PlaylistItem::NewFromSong(const Song &song) {
case Song::Source_Subsonic:
case Song::Source_Tidal:
case Song::Source_Qobuz:
+ case Song::Source_RadioParadise:
+ case Song::Source_SomaFM:
case Song::Source_Stream:
return new InternetPlaylistItem(song);
case Song::Source_LocalFile:
diff --git a/src/radios/radiobackend.cpp b/src/radios/radiobackend.cpp
new file mode 100644
index 000000000..165ff2386
--- /dev/null
+++ b/src/radios/radiobackend.cpp
@@ -0,0 +1,135 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/logging.h"
+#include "core/application.h"
+#include "core/database.h"
+#include "core/song.h"
+#include "radiobackend.h"
+#include "radiochannel.h"
+
+RadioBackend::RadioBackend(Application *app, Database *db, QObject *parent)
+ : QObject(parent),
+ app_(app),
+ db_(db),
+ original_thread_(thread()) {}
+
+void RadioBackend::Close() {
+
+ if (db_) {
+ QMutexLocker l(db_->Mutex());
+ db_->Close();
+ }
+
+}
+
+void RadioBackend::ExitAsync() {
+ metaObject()->invokeMethod(this, "Exit", Qt::QueuedConnection);
+}
+
+void RadioBackend::Exit() {
+
+ assert(QThread::currentThread() == thread());
+
+ moveToThread(original_thread_);
+ emit ExitFinished();
+
+}
+
+void RadioBackend::AddChannelsAsync(const RadioChannelList &channels) {
+ metaObject()->invokeMethod(this, "AddChannels", Qt::QueuedConnection, Q_ARG(RadioChannelList, channels));
+}
+
+void RadioBackend::AddChannels(const RadioChannelList &channels) {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare(QString("INSERT INTO radio_channels (source, name, url, thumbnail_url) VALUES (:source, :name, :url, :thumbnail_url)"));
+
+ RadioChannelList streams;
+ for (const RadioChannel &channel : channels) {
+ q.bindValue(":source", channel.source);
+ q.bindValue(":name", channel.name);
+ q.bindValue(":url", channel.url);
+ q.bindValue(":thumbnail_url", channel.thumbnail_url);
+ if (!q.exec()) {
+ db_->CheckErrors(q);
+ }
+ }
+
+ emit NewChannels(channels);
+
+}
+
+void RadioBackend::GetChannelsAsync() {
+
+ metaObject()->invokeMethod(this, "GetChannels", Qt::QueuedConnection);
+
+}
+
+void RadioBackend::GetChannels() {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare("SELECT source, name, url, thumbnail_url FROM radio_channels");
+
+ if (!q.exec()) {
+ db_->CheckErrors(q);
+ return;
+ }
+
+ RadioChannelList channels;
+ while (q.next()) {
+ RadioChannel channel;
+ channel.source = Song::Source(q.value(0).toInt());
+ channel.name = q.value(1).toString();
+ channel.url.setUrl(q.value(2).toString());
+ channel.thumbnail_url.setUrl(q.value(3).toString());
+ channels << channel;
+ }
+
+ emit NewChannels(channels);
+
+}
+
+void RadioBackend::DeleteChannelsAsync() {
+ metaObject()->invokeMethod(this, "DeleteChannels", Qt::QueuedConnection);
+}
+
+void RadioBackend::DeleteChannels() {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare("DELETE FROM radio_channels");
+
+ if (!q.exec()) {
+ db_->CheckErrors(q);
+ }
+
+}
diff --git a/src/radios/radiobackend.h b/src/radios/radiobackend.h
new file mode 100644
index 000000000..ccc3465eb
--- /dev/null
+++ b/src/radios/radiobackend.h
@@ -0,0 +1,61 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOBACKEND_H
+#define RADIOBACKEND_H
+
+#include
+
+#include "radiochannel.h"
+
+class Application;
+class Database;
+
+class RadioBackend : public QObject {
+ Q_OBJECT
+
+ public:
+ explicit RadioBackend(Application *app, Database *db, QObject *parent = nullptr);
+
+ void Close();
+ void ExitAsync();
+
+ void AddChannelsAsync(const RadioChannelList &channels);
+ void GetChannelsAsync();
+ void DeleteChannelsAsync();
+
+ private slots:
+ void AddChannels(const RadioChannelList &channels);
+ void GetChannels();
+ void DeleteChannels();
+
+ signals:
+ void NewChannels(RadioChannelList);
+ void ExitFinished();
+
+ private slots:
+ void Exit();
+
+ private:
+ Application *app_;
+ Database *db_;
+ QThread *original_thread_;
+};
+
+#endif // RADIOBACKEND_H
diff --git a/src/radios/radiochannel.cpp b/src/radios/radiochannel.cpp
new file mode 100644
index 000000000..41be309c1
--- /dev/null
+++ b/src/radios/radiochannel.cpp
@@ -0,0 +1,36 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/song.h"
+
+#include "radiochannel.h"
+
+Song RadioChannel::ToSong() const {
+
+ Song song(source);
+ song.set_valid(true);
+ song.set_filetype(Song::FileType_Stream);
+ song.set_title(Song::DescriptionForSource(source) + " " + name);
+ song.set_url(url);
+
+ return song;
+
+}
diff --git a/src/radios/radiochannel.h b/src/radios/radiochannel.h
new file mode 100644
index 000000000..734df92d3
--- /dev/null
+++ b/src/radios/radiochannel.h
@@ -0,0 +1,43 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOCHANNEL_H
+#define RADIOCHANNEL_H
+
+#include
+#include
+#include
+
+#include "core/song.h"
+
+struct RadioChannel {
+ explicit RadioChannel(const Song::Source _source = Song::Source_Unknown, const QString &_name = QString(), const QUrl &_url = QUrl(), const QUrl &_thumbnail_url = QUrl()) : source(_source), name(_name), url(_url), thumbnail_url(_thumbnail_url) {}
+
+ Song::Source source;
+ QString name;
+ QUrl url;
+ QUrl thumbnail_url;
+
+ Song ToSong() const;
+};
+typedef QList RadioChannelList;
+
+Q_DECLARE_METATYPE(RadioChannel)
+
+#endif // RADIOCHANNEL_H
diff --git a/src/radios/radioitem.h b/src/radios/radioitem.h
new file mode 100644
index 000000000..0f9e1d303
--- /dev/null
+++ b/src/radios/radioitem.h
@@ -0,0 +1,49 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOITEM_H
+#define RADIOITEM_H
+
+#include "config.h"
+
+#include "core/simpletreeitem.h"
+#include "core/song.h"
+#include "radiochannel.h"
+
+class RadioItem : public SimpleTreeItem {
+ public:
+
+ enum Type {
+ Type_LoadingIndicator,
+ Type_Root,
+ Type_Service,
+ Type_Channel,
+ };
+
+ explicit RadioItem(SimpleTreeModel *_model) : SimpleTreeItem(Type_Root, _model) {}
+ explicit RadioItem(Type _type, RadioItem *_parent = nullptr) : SimpleTreeItem(_type, _parent) {}
+
+ Song::Source source;
+ RadioChannel channel;
+
+ private:
+ Q_DISABLE_COPY(RadioItem)
+};
+
+#endif // RADIOITEM_H
diff --git a/src/radios/radiomimedata.h b/src/radios/radiomimedata.h
new file mode 100644
index 000000000..a7ba928de
--- /dev/null
+++ b/src/radios/radiomimedata.h
@@ -0,0 +1,36 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOMIMEDATA_H
+#define RADIOMIMEDATA_H
+
+#include
+
+#include "core/mimedata.h"
+#include "core/song.h"
+
+class RadioMimeData : public MimeData {
+ Q_OBJECT
+
+ public:
+ explicit RadioMimeData() {}
+ SongList songs;
+};
+
+#endif // RADIOMIMEDATA_H
diff --git a/src/radios/radiomodel.cpp b/src/radios/radiomodel.cpp
new file mode 100644
index 000000000..7aaa93367
--- /dev/null
+++ b/src/radios/radiomodel.cpp
@@ -0,0 +1,344 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/application.h"
+#include "core/simpletreemodel.h"
+#include "playlist/playlistmanager.h"
+#include "covermanager/albumcoverloader.h"
+#include "covermanager/albumcoverloaderoptions.h"
+#include "covermanager/albumcoverloaderresult.h"
+#include "radiomodel.h"
+#include "radioservices.h"
+#include "radioservice.h"
+#include "radiomimedata.h"
+#include "radiochannel.h"
+
+const int RadioModel::kTreeIconSize = 22;
+
+RadioModel::RadioModel(Application *app, QObject *parent)
+ : SimpleTreeModel(new RadioItem(this), parent),
+ app_(app) {
+
+ root_->lazy_loaded = true;
+
+ cover_loader_options_.get_image_data_ = false;
+ cover_loader_options_.get_image_ = true;
+ cover_loader_options_.scale_output_image_ = true;
+ cover_loader_options_.pad_output_image_ = true;
+ cover_loader_options_.desired_height_ = kTreeIconSize;
+
+ if (app_) {
+ QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded);
+ }
+
+}
+
+Qt::ItemFlags RadioModel::flags(const QModelIndex &idx) const {
+
+ switch (IndexToItem(idx)->type) {
+ case RadioItem::Type_Service:
+ case RadioItem::Type_Channel:
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
+ case RadioItem::Type_Root:
+ case RadioItem::Type_LoadingIndicator:
+ default:
+ return Qt::ItemIsEnabled;
+ }
+
+}
+
+QVariant RadioModel::data(const QModelIndex &idx, int role) const {
+
+ if (!idx.isValid()) return QVariant();
+
+ const RadioItem *item = IndexToItem(idx);
+ if (!item) return QVariant();
+
+ if (role == Qt::DecorationRole && item->type == RadioItem::Type_Channel) {
+ return const_cast(this)->ChannelIcon(idx);
+ }
+
+ return data(item, role);
+
+}
+
+QVariant RadioModel::data(const RadioItem *item, int role) const {
+
+ switch(role) {
+ case Qt::DecorationRole:
+ if (item->type == RadioItem::Type_Service) {
+ return Song::IconForSource(item->source);
+ }
+ break;
+ case Qt::DisplayRole:
+ return item->DisplayText();
+ break;
+ case Role_Type:
+ return item->type;
+ break;
+ case Role_SortText:
+ return item->SortText();
+ break;
+ case Role_Source:
+ return item->source;
+ break;
+ case Role_Homepage:{
+ RadioService *service = app_->radio_services()->ServiceBySource(item->source);
+ if (service) return service->Homepage();
+ break;
+ }
+ case Role_Donate:{
+ RadioService *service = app_->radio_services()->ServiceBySource(item->source);
+ if (service) return service->Donate();
+ break;
+ }
+ }
+
+ return QVariant();
+
+}
+
+QStringList RadioModel::mimeTypes() const {
+ return QStringList() << "text/uri-list";
+}
+
+QMimeData *RadioModel::mimeData(const QModelIndexList &indexes) const {
+
+ if (indexes.isEmpty()) return nullptr;
+
+ RadioMimeData *data = new RadioMimeData;
+ QList urls;
+ for (const QModelIndex &idx : indexes) {
+ GetChildSongs(IndexToItem(idx), &urls, &data->songs);
+ }
+
+ data->setUrls(urls);
+ data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
+
+ return data;
+
+}
+
+void RadioModel::Reset() {
+
+ beginResetModel();
+ container_nodes_.clear();
+ items_.clear();
+ pixmap_cache_.clear();
+ pending_art_.clear();
+ pending_cache_keys_.clear();
+ delete root_;
+ root_ = new RadioItem(this);
+ root_->lazy_loaded = true;
+ endResetModel();
+
+}
+
+void RadioModel::AddChannels(const RadioChannelList &channels) {
+
+ for (const RadioChannel &channel : channels) {
+ RadioItem *container = root_;
+ if (container_nodes_.contains(channel.source)) {
+ container = container_nodes_[channel.source];
+ }
+ else {
+ RadioItem *item = new RadioItem(RadioItem::Type_Service, root_);
+ item->source = channel.source;
+ item->display_text = Song::DescriptionForSource(channel.source);
+ item->sort_text = SortText(Song::TextForSource(channel.source));
+ item->lazy_loaded = true;
+ beginInsertRows(ItemToIndex(root_), root_->children.count(), root_->children.count());
+ container_nodes_.insert(channel.source, item);
+ endInsertRows();
+ container = item;
+ }
+ RadioItem *item = new RadioItem(RadioItem::Type_Channel, container);
+ item->source = channel.source;
+ item->display_text = channel.name;
+ item->sort_text = SortText(Song::TextForSource(channel.source) + " - " + channel.name);
+ item->channel = channel;
+ item->lazy_loaded = true;
+ beginInsertRows(ItemToIndex(container), container->children.count(), container->children.count());
+ items_ << item;
+ endInsertRows();
+ }
+
+}
+
+bool RadioModel::IsPlayable(const QModelIndex &idx) const {
+
+ return idx.data(Role_Type) == RadioItem::Type_Channel;
+
+}
+
+bool RadioModel::CompareItems(const RadioItem *a, const RadioItem *b) const {
+
+ QVariant left(data(a, RadioModel::Role_SortText));
+ QVariant right(data(b, RadioModel::Role_SortText));
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ if (left.metaType().id() == QMetaType::Int)
+#else
+ if (left.type() == QVariant::Int)
+#endif
+ return left.toInt() < right.toInt();
+ else return left.toString() < right.toString();
+
+}
+
+void RadioModel::GetChildSongs(RadioItem *item, QList *urls, SongList *songs) const {
+
+ switch (item->type) {
+ case RadioItem::Type_Service:{
+ QList children = item->children;
+ std::sort(children.begin(), children.end(), std::bind(&RadioModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2));
+ for (RadioItem *child : children) {
+ GetChildSongs(child, urls, songs);
+ }
+ break;
+ }
+ case RadioItem::Type_Channel:
+ if (!urls->contains(item->channel.url)) {
+ urls->append(item->channel.url);
+ songs->append(item->channel.ToSong());
+ }
+ break;
+ default:
+ break;
+ }
+
+}
+
+SongList RadioModel::GetChildSongs(const QModelIndexList &indexes) const {
+
+ QList urls;
+ SongList songs;
+ for (const QModelIndex &idx : indexes) {
+ GetChildSongs(IndexToItem(idx), &urls, &songs);
+ }
+ return songs;
+
+}
+
+SongList RadioModel::GetChildSongs(const QModelIndex &idx) const {
+ return GetChildSongs(QModelIndexList() << idx);
+}
+
+QString RadioModel::ChannelIconPixmapCacheKey(const QModelIndex &idx) const {
+
+ QStringList path;
+ QModelIndex idx_copy = idx;
+ while (idx_copy.isValid()) {
+ path.prepend(idx_copy.data().toString());
+ idx_copy = idx_copy.parent();
+ }
+
+ return path.join('/');
+
+}
+
+QPixmap RadioModel::ServiceIcon(const QModelIndex &idx) const {
+ return Song::IconForSource(Song::Source(idx.data(Role_Source).toInt())).pixmap(kTreeIconSize, kTreeIconSize);
+}
+
+QPixmap RadioModel::ServiceIcon(RadioItem *item) const {
+ return Song::IconForSource(item->source).pixmap(kTreeIconSize, kTreeIconSize);
+}
+
+QPixmap RadioModel::ChannelIcon(const QModelIndex &idx) {
+
+ if (!idx.isValid()) return QPixmap();
+
+ RadioItem *item = IndexToItem(idx);
+ if (!item) return ServiceIcon(idx);
+
+ const QString cache_key = ChannelIconPixmapCacheKey(idx);
+
+ QPixmap cached_pixmap;
+ if (pixmap_cache_.find(cache_key, &cached_pixmap)) {
+ return cached_pixmap;
+ }
+
+ if (pending_cache_keys_.contains(cache_key)) {
+ return ServiceIcon(idx);
+ }
+
+ SongList songs = GetChildSongs(idx);
+ if (!songs.isEmpty()) {
+ Song song = songs.first();
+ song.set_art_automatic(item->channel.thumbnail_url);
+ const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, song);
+ pending_art_[id] = ItemAndCacheKey(item, cache_key);
+ pending_cache_keys_.insert(cache_key);
+ }
+
+ return ServiceIcon(idx);
+
+}
+
+void RadioModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
+
+ if (!pending_art_.contains(id)) return;
+
+ ItemAndCacheKey item_and_cache_key = pending_art_.take(id);
+ RadioItem *item = item_and_cache_key.first;
+ if (!item) return;
+
+ const QString &cache_key = item_and_cache_key.second;
+
+ pending_cache_keys_.remove(cache_key);
+
+ if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) {
+ pixmap_cache_.insert(cache_key, ServiceIcon(item));
+ }
+ else {
+ pixmap_cache_.insert(cache_key, QPixmap::fromImage(result.image_scaled));
+ }
+
+ const QModelIndex idx = ItemToIndex(item);
+ if (!idx.isValid()) return;
+
+ emit dataChanged(idx, idx);
+
+}
+
+QString RadioModel::SortText(QString text) {
+
+ if (text.isEmpty()) {
+ text = " unknown";
+ }
+ else {
+ text = text.toLower();
+ }
+ text = text.remove(QRegularExpression("[^\\w ]", QRegularExpression::UseUnicodePropertiesOption));
+
+ return text;
+
+}
diff --git a/src/radios/radiomodel.h b/src/radios/radiomodel.h
new file mode 100644
index 000000000..1d5c00eaa
--- /dev/null
+++ b/src/radios/radiomodel.h
@@ -0,0 +1,102 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOMODEL_H
+#define RADIOMODEL_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "core/simpletreemodel.h"
+#include "covermanager/albumcoverloaderoptions.h"
+#include "covermanager/albumcoverloaderresult.h"
+#include "radioitem.h"
+#include "radiochannel.h"
+
+class QMimeData;
+
+class Application;
+class Database;
+
+class RadioModel : public SimpleTreeModel {
+ Q_OBJECT
+
+ public:
+ explicit RadioModel(Application *app, QObject *parent = nullptr);
+
+ enum Role {
+ Role_Type = Qt::UserRole + 1,
+ Role_SortText,
+ Role_Source,
+ Role_Url,
+ Role_Homepage,
+ Role_Donate,
+ RoleCount,
+ };
+
+ // QAbstractItemModel
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ QVariant data(const QModelIndex &idx, int role) const override;
+ QStringList mimeTypes() const override;
+ QMimeData *mimeData(const QModelIndexList &indexes) const override;
+
+ QVariant data(const RadioItem *item, int role) const;
+
+ void Reset();
+ void AddChannels(const RadioChannelList &channels);
+
+ private:
+ bool IsPlayable(const QModelIndex &idx) const;
+ bool CompareItems(const RadioItem *a, const RadioItem *b) const;
+ void GetChildSongs(RadioItem *item, QList *urls, SongList *songs) const;
+ SongList GetChildSongs(const QModelIndexList &indexes) const;
+ SongList GetChildSongs(const QModelIndex &idx) const;
+ QString ChannelIconPixmapCacheKey(const QModelIndex &idx) const;
+ QPixmap ServiceIcon(const QModelIndex &idx) const;
+ QPixmap ServiceIcon(RadioItem *item) const;
+ QPixmap ChannelIcon(const QModelIndex &idx);
+ QString SortText(QString text);
+
+ private slots:
+ void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
+
+ private:
+ static const int kTreeIconSize;
+ typedef QPair ItemAndCacheKey;
+
+ Application *app_;
+ AlbumCoverLoaderOptions cover_loader_options_;
+ QMap container_nodes_;
+ QList items_;
+ QPixmapCache pixmap_cache_;
+ QMap pending_art_;
+ QSet pending_cache_keys_;
+};
+
+#endif // RADIOMODEL_H
diff --git a/src/radios/radioparadiseservice.cpp b/src/radios/radioparadiseservice.cpp
new file mode 100644
index 000000000..5daad93ff
--- /dev/null
+++ b/src/radios/radioparadiseservice.cpp
@@ -0,0 +1,45 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/iconloader.h"
+#include "radioparadiseservice.h"
+#include "radiochannel.h"
+
+RadioParadiseService::RadioParadiseService(Application *app, NetworkAccessManager *network, QObject *parent) : RadioService(Song::Source_RadioParadise, "Radio Paradise", IconLoader::Load("radioparadise"), app, network, parent) {}
+
+QUrl RadioParadiseService::Homepage() { return QUrl("https://radioparadise.com/"); }
+QUrl RadioParadiseService::Donate() { return QUrl("https://payments.radioparadise.com/rp2s-content.php?name=Support&file=support"); }
+
+void RadioParadiseService::GetChannels() {
+
+ emit NewChannels(RadioChannelList()
+ << RadioChannel(source_, "Main Mix 320k AAC", QUrl("https://stream.radioparadise.com/aac-320"))
+ << RadioChannel(source_, "Mellow Mix 320k AAC", QUrl("https://stream.radioparadise.com/mellow-320"))
+ << RadioChannel(source_, "Rock Mix 320k AAC", QUrl("https://stream.radioparadise.com/rock-320"))
+ << RadioChannel(source_, "World/Etc Mix 320k AAC", QUrl("https://stream.radioparadise.com/world-etc-320"))
+ << RadioChannel(source_, "Main Mix FLAC", QUrl("http://stream.radioparadise.com/flacm"))
+ << RadioChannel(source_, "Mellow Mix FLAC", QUrl("http://stream.radioparadise.com/mellow-flacm"))
+ << RadioChannel(source_, "Rock Mix FLAC", QUrl("http://stream.radioparadise.com/rock-flacm"))
+ << RadioChannel(source_, "World/Etc Mix FLAC", QUrl("http://stream.radioparadise.com/world-etc-flacm"))
+ );
+
+}
diff --git a/src/radios/radioparadiseservice.h b/src/radios/radioparadiseservice.h
new file mode 100644
index 000000000..8ac02e47e
--- /dev/null
+++ b/src/radios/radioparadiseservice.h
@@ -0,0 +1,45 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOPARADISESERVICE_H
+#define RADIOPARADISESERVICE_H
+
+#include
+#include
+
+#include "radioservice.h"
+
+class Application;
+class NetworkAccessManager;
+
+class RadioParadiseService : public RadioService {
+ Q_OBJECT
+
+ public:
+ explicit RadioParadiseService(Application *app, NetworkAccessManager *network, QObject *parent = nullptr);
+
+ QUrl Homepage() override;
+ QUrl Donate() override;
+
+ public slots:
+ void GetChannels() override;
+
+};
+
+#endif // RADIOPARADISESERVICE_H
diff --git a/src/radios/radioplaylistitem.cpp b/src/radios/radioplaylistitem.cpp
new file mode 100644
index 000000000..574983695
--- /dev/null
+++ b/src/radios/radioplaylistitem.cpp
@@ -0,0 +1,77 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "radioplaylistitem.h"
+#include "collection/sqlrow.h"
+
+RadioPlaylistItem::RadioPlaylistItem(const Song::Source source)
+ : PlaylistItem(source), source_(source) {}
+
+RadioPlaylistItem::RadioPlaylistItem(const Song &metadata)
+ : PlaylistItem(metadata.source()),
+ source_(metadata.source()),
+ metadata_(metadata) {
+ InitMetadata();
+}
+
+bool RadioPlaylistItem::InitFromQuery(const SqlRow &query) {
+
+ metadata_.InitFromQuery(query, false, (static_cast(Song::kColumns.count()) + 1) * 1);
+ InitMetadata();
+ return true;
+
+}
+
+QVariant RadioPlaylistItem::DatabaseValue(DatabaseColumn column) const {
+ return PlaylistItem::DatabaseValue(column);
+}
+
+void RadioPlaylistItem::InitMetadata() {
+
+ if (metadata_.title().isEmpty()) metadata_.set_title(metadata_.url().toString());
+ if (metadata_.source() == Song::Source_Unknown) metadata_.set_source(Song::Source_Stream);
+ if (metadata_.filetype() == Song::FileType_Unknown) metadata_.set_filetype(Song::FileType_Stream);
+ metadata_.set_valid(true);
+
+}
+
+Song RadioPlaylistItem::Metadata() const {
+
+ if (HasTemporaryMetadata()) return temp_metadata_;
+ return metadata_;
+
+}
+
+QUrl RadioPlaylistItem::Url() const { return metadata_.url(); }
+
+void RadioPlaylistItem::SetArtManual(const QUrl &cover_url) {
+
+ metadata_.set_art_manual(cover_url);
+ temp_metadata_.set_art_manual(cover_url);
+
+}
diff --git a/src/radios/radioplaylistitem.h b/src/radios/radioplaylistitem.h
new file mode 100644
index 000000000..8a973127e
--- /dev/null
+++ b/src/radios/radioplaylistitem.h
@@ -0,0 +1,62 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOPLAYLISTITEM_H
+#define RADIOPLAYLISTITEM_H
+
+#include "config.h"
+
+#include
+#include
+
+#include "core/song.h"
+#include "collection/sqlrow.h"
+#include "playlist/playlistitem.h"
+
+class RadioService;
+
+class RadioPlaylistItem : public PlaylistItem {
+
+ public:
+ explicit RadioPlaylistItem(const Song::Source source);
+ explicit RadioPlaylistItem(const Song &metadata);
+
+ bool InitFromQuery(const SqlRow &query) override;
+ Song Metadata() const override;
+ Song OriginalMetadata() const override { return metadata_; }
+ QUrl Url() const override;
+
+ void SetMetadata(const Song &metadata) override { metadata_ = metadata; }
+ void SetArtManual(const QUrl &cover_url) override;
+
+ protected:
+ QVariant DatabaseValue(DatabaseColumn) const override;
+ Song DatabaseSongMetadata() const override { return metadata_; }
+
+ private:
+ void InitMetadata();
+
+ private:
+ Song::Source source_;
+ Song metadata_;
+
+ Q_DISABLE_COPY(RadioPlaylistItem)
+};
+
+#endif // INTERNETPLAYLISTITEM_H
diff --git a/src/radios/radioservice.cpp b/src/radios/radioservice.cpp
new file mode 100644
index 000000000..47266e7b1
--- /dev/null
+++ b/src/radios/radioservice.cpp
@@ -0,0 +1,106 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/application.h"
+#include "core/logging.h"
+#include "radioservice.h"
+
+RadioService::RadioService(const Song::Source source, const QString &name, const QIcon &icon, Application *app, NetworkAccessManager *network, QObject *parent)
+ : QObject(parent),
+ app_(app),
+ network_(network),
+ source_(source),
+ name_(name),
+ icon_(icon) {
+
+}
+
+QByteArray RadioService::ExtractData(QNetworkReply *reply) {
+
+ if (reply->error() != QNetworkReply::NoError) {
+ Error(QString("Failed to retrieve data from %1: %2 (%3)").arg(name_).arg(reply->errorString()).arg(reply->error()));
+ if (reply->error() < 200) {
+ return QByteArray();
+ }
+ }
+ else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
+ }
+
+ return reply->readAll();
+
+}
+
+QJsonObject RadioService::ExtractJsonObj(const QByteArray &data) {
+
+ if (data.isEmpty()) {
+ return QJsonObject();
+ }
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ Error(QString("Failed to parse Json data from %1: %2").arg(name_).arg(json_error.errorString()));
+ return QJsonObject();
+ }
+
+ if (json_doc.isEmpty()) {
+ Error(QString("%1: Received empty Json document.").arg(name_), data);
+ return QJsonObject();
+ }
+
+ if (!json_doc.isObject()) {
+ Error(QString("%1: Json document is not an object.").arg(name_), json_doc);
+ return QJsonObject();
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ Error(QString("%1: Received empty Json object.").arg(name_), json_doc);
+ return QJsonObject();
+ }
+
+ return json_obj;
+
+}
+
+QJsonObject RadioService::ExtractJsonObj(QNetworkReply *reply) {
+
+ return ExtractJsonObj(ExtractData(reply));
+
+}
+
+void RadioService::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << name_ << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
diff --git a/src/radios/radioservice.h b/src/radios/radioservice.h
new file mode 100644
index 000000000..61fcf8778
--- /dev/null
+++ b/src/radios/radioservice.h
@@ -0,0 +1,118 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOSERVICE_H
+#define RADIOSERVICE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "playlist/playlistitem.h"
+#include "settings/settingsdialog.h"
+#include "widgets/multiloadingindicator.h"
+#include "radiochannel.h"
+
+class QNetworkReply;
+class QStandardItem;
+
+class Application;
+class NetworkAccessManager;
+class RadioModel;
+
+class RadioService : public QObject {
+ Q_OBJECT
+
+ public:
+ explicit RadioService(const Song::Source source, const QString &name, const QIcon &icon, Application *app, NetworkAccessManager *network, QObject *parent = nullptr);
+
+ Song::Source source() const { return source_; }
+ QString name() const { return name_; }
+
+ virtual void ReloadSettings() {}
+
+ virtual QUrl Homepage() = 0;
+ virtual QUrl Donate() = 0;
+
+ signals:
+ void StreamError(const QString& message);
+ void StreamMetadataFound(const QUrl& original_url, const Song& song);
+
+ void NewChannels(RadioChannelList channels = RadioChannelList());
+
+ public slots:
+ virtual void ShowConfig() {}
+ virtual void GetChannels() = 0;
+
+ private slots:
+
+ protected:
+ // Called once when context menu is created
+ virtual void PopulateContextMenu(){};
+ // Called every time context menu is shown
+ virtual void UpdateContextMenu(){};
+
+ // Returns all the playlist insertion related QActions (see below).
+ QList GetPlaylistActions();
+
+ // Returns the 'append to playlist' QAction.
+ QAction *GetAppendToPlaylistAction();
+ // Returns the 'replace playlist' QAction.
+ QAction *GetReplacePlaylistAction();
+ // Returns the 'open in new playlist' QAction.
+ QAction *GetOpenInNewPlaylistAction();
+
+ QByteArray ExtractData(QNetworkReply *reply);
+ QJsonObject ExtractJsonObj(const QByteArray &data);
+ QJsonObject ExtractJsonObj(QNetworkReply *reply);
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ // Describes how songs should be added to playlist.
+ enum AddMode {
+ // appends songs to the current playlist
+ AddMode_Append,
+ // clears the current playlist and then appends all songs to it
+ AddMode_Replace,
+ // creates a new, empty playlist and then adds all songs to it
+ AddMode_OpenInNew
+ };
+
+ // Adds the 'index' element to playlist using the 'add_mode' mode.
+ void AddItemToPlaylist(const QModelIndex& index, AddMode add_mode);
+ // Adds the 'indexes' elements to playlist using the 'add_mode' mode.
+ void AddItemsToPlaylist(const QModelIndexList& indexes, AddMode add_mode);
+
+ protected:
+ Application *app_;
+ NetworkAccessManager *network_;
+ RadioModel *model_;
+ Song::Source source_;
+ QString name_;
+ QIcon icon_;
+
+};
+
+Q_DECLARE_METATYPE(RadioService*)
+
+#endif // RADIOSERVICE_H
diff --git a/src/radios/radioservices.cpp b/src/radios/radioservices.cpp
new file mode 100644
index 000000000..ac3520d6c
--- /dev/null
+++ b/src/radios/radioservices.cpp
@@ -0,0 +1,138 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/logging.h"
+#include "core/application.h"
+#include "core/database.h"
+#include "core/networkaccessmanager.h"
+#include "radioservices.h"
+#include "radiobackend.h"
+#include "radiomodel.h"
+#include "radioservice.h"
+#include "radiochannel.h"
+#include "somafmservice.h"
+#include "radioparadiseservice.h"
+
+RadioServices::RadioServices(Application *app, QObject *parent)
+ : QObject(parent),
+ network_(new NetworkAccessManager(this)),
+ backend_(nullptr),
+ model_(new RadioModel(app, this)),
+ channels_refresh_(false) {
+
+ backend_ = new RadioBackend(app, app->database());
+ app->MoveToThread(backend_, app->database()->thread());
+
+ QObject::connect(backend_, &RadioBackend::NewChannels, this, &RadioServices::GotChannelsFromBackend);
+
+ AddService(new SomaFMService(app, network_, this));
+ AddService(new RadioParadiseService(app, network_, this));
+
+}
+
+RadioServices::~RadioServices() {
+
+ backend_->deleteLater();
+
+}
+
+void RadioServices::AddService(RadioService *service) {
+
+ qLog(Debug) << "Adding radio service:" << service->name();
+ services_.insert(service->source(), service);
+
+ QObject::connect(service, &RadioService::NewChannels, this, &RadioServices::GotChannelsFromService);
+ QObject::connect(service, &RadioService::destroyed, this, &RadioServices::ServiceDeleted);
+
+}
+
+void RadioServices::RemoveService(RadioService *service) {
+
+ if (!services_.contains(service->source())) return;
+
+ services_.remove(service->source());
+ QObject::disconnect(service, nullptr, this, nullptr);
+
+}
+
+void RadioServices::ServiceDeleted() {
+
+ RadioService *service = qobject_cast(sender());
+ if (service) RemoveService(service);
+
+}
+
+RadioService *RadioServices::ServiceBySource(const Song::Source source) const {
+
+ if (services_.contains(source)) return services_.value(source);
+ return nullptr;
+
+}
+
+void RadioServices::ReloadSettings() {
+
+ QList services = services_.values();
+ for (RadioService *service : services) {
+ service->ReloadSettings();
+ }
+
+}
+
+void RadioServices::GetChannels() {
+
+ model_->Reset();
+ backend_->GetChannelsAsync();
+
+}
+
+void RadioServices::RefreshChannels() {
+
+ channels_refresh_ = true;
+ model_->Reset();
+ backend_->DeleteChannelsAsync();
+
+ for (RadioService *service : services_) {
+ service->GetChannels();
+ }
+
+}
+
+void RadioServices::GotChannelsFromBackend(const RadioChannelList &channels) {
+
+ if (channels.isEmpty()) {
+ if (!channels_refresh_) {
+ RefreshChannels();
+ }
+ }
+ else {
+ model_->AddChannels(channels);
+ }
+
+}
+
+void RadioServices::GotChannelsFromService(const RadioChannelList &channels) {
+
+ RadioService *service = qobject_cast(sender());
+ if (!service) return;
+
+ backend_->AddChannelsAsync(channels);
+
+}
diff --git a/src/radios/radioservices.h b/src/radios/radioservices.h
new file mode 100644
index 000000000..571a8593a
--- /dev/null
+++ b/src/radios/radioservices.h
@@ -0,0 +1,74 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOSERVICES_H
+#define RADIOSERVICES_H
+
+#include
+#include
+
+#include "core/song.h"
+#include "radiochannel.h"
+
+class Application;
+class NetworkAccessManager;
+class RadioBackend;
+class RadioModel;
+class RadioService;
+
+class RadioServices : public QObject {
+ Q_OBJECT
+
+ public:
+ explicit RadioServices(Application *app, QObject *parent = nullptr);
+ ~RadioServices();
+
+ void AddService(RadioService *service);
+ void RemoveService(RadioService *service);
+
+ RadioService *ServiceBySource(const Song::Source source) const;
+
+ template
+ T *Service() {
+ return static_cast(this->ServiceBySource(T::source));
+ }
+
+ void ReloadSettings();
+
+ RadioBackend *radio_backend() const { return backend_; }
+ RadioModel *radio_model() const { return model_; }
+
+ private slots:
+ void ServiceDeleted();
+ void GotChannelsFromBackend(const RadioChannelList &channels);
+ void GotChannelsFromService(const RadioChannelList &channels);
+
+ public slots:
+ void GetChannels();
+ void RefreshChannels();
+
+ private:
+ NetworkAccessManager *network_;
+ RadioBackend *backend_;
+ RadioModel *model_;
+ QMap services_;
+ bool channels_refresh_;
+};
+
+#endif // RADIOSERVICES_H
diff --git a/src/radios/radioview.cpp b/src/radios/radioview.cpp
new file mode 100644
index 000000000..6f559a8d2
--- /dev/null
+++ b/src/radios/radioview.cpp
@@ -0,0 +1,192 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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/mimedata.h"
+#include "core/iconloader.h"
+#include "radiomodel.h"
+#include "radioview.h"
+#include "radioservice.h"
+#include "radiomimedata.h"
+#include "collection/collectionitemdelegate.h"
+
+RadioView::RadioView(QWidget *parent)
+ : AutoExpandingTreeView(parent),
+ menu_(nullptr),
+ action_playlist_append_(nullptr),
+ action_playlist_replace_(nullptr),
+ action_playlist_new_(nullptr),
+ action_homepage_(nullptr),
+ action_donate_(nullptr),
+ initialized_(false) {
+
+ setItemDelegate(new CollectionItemDelegate(this));
+ SetExpandOnReset(false);
+ setAttribute(Qt::WA_MacShowFocusRect, false);
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ QObject::connect(this, &RadioView::doubleClicked, this, &RadioView::DoubleClicked);
+
+}
+
+RadioView::~RadioView() { delete menu_; }
+
+void RadioView::setModel(RadioModel *model) {
+
+ AutoExpandingTreeView::setModel(model);
+
+}
+
+void RadioView::showEvent(QShowEvent*) {
+
+ if (!initialized_) {
+ emit GetChannels();
+ initialized_ = true;
+ }
+
+}
+
+void RadioView::contextMenuEvent(QContextMenuEvent *e) {
+
+ if (!menu_) {
+ menu_ = new QMenu;
+
+ action_playlist_append_ = new QAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this);
+ QObject::connect(action_playlist_append_, &QAction::triggered, this, &RadioView::AddToPlaylist);
+ menu_->addAction(action_playlist_append_);
+
+ action_playlist_replace_ = new QAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this);
+ QObject::connect(action_playlist_replace_, &QAction::triggered, this, &RadioView::ReplacePlaylist);
+ menu_->addAction(action_playlist_replace_);
+
+ action_playlist_new_ = new QAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this);
+ QObject::connect(action_playlist_new_, &QAction::triggered, this, &RadioView::OpenInNewPlaylist);
+ menu_->addAction(action_playlist_new_);
+
+ action_homepage_ = new QAction(IconLoader::Load("download"), tr("Open homepage"), this);
+ QObject::connect(action_homepage_, &QAction::triggered, this, &RadioView::Homepage);
+ menu_->addAction(action_homepage_);
+
+ action_donate_ = new QAction(IconLoader::Load("download"), tr("Donate"), this);
+ QObject::connect(action_donate_, &QAction::triggered, this, &RadioView::Donate);
+ menu_->addAction(action_donate_);
+
+ menu_->addAction(IconLoader::Load("view-refresh"), tr("Refresh channels"), this, &RadioView::GetChannels);
+ }
+
+ const bool channels_selected = !selectedIndexes().isEmpty();
+
+ action_playlist_append_->setVisible(channels_selected);
+ action_playlist_replace_->setVisible(channels_selected);
+ action_playlist_new_->setVisible(channels_selected);
+ action_homepage_->setVisible(channels_selected);
+ action_donate_->setVisible(channels_selected);
+
+ menu_->popup(e->globalPos());
+
+}
+
+void RadioView::AddToPlaylist() {
+
+ emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
+
+}
+
+void RadioView::ReplacePlaylist() {
+
+ QMimeData *qmimedata = model()->mimeData(selectedIndexes());
+ if (MimeData *mimedata = qobject_cast(qmimedata)) {
+ mimedata->clear_first_ = true;
+ }
+
+ emit AddToPlaylistSignal(qmimedata);
+
+}
+
+void RadioView::OpenInNewPlaylist() {
+
+ QMimeData *qmimedata = model()->mimeData(selectedIndexes());
+ if (RadioMimeData *mimedata = qobject_cast(qmimedata)) {
+ mimedata->open_in_new_playlist_ = true;
+ if (!mimedata->songs.isEmpty()) {
+ mimedata->name_for_new_playlist_ = mimedata->songs.first().title();
+ }
+ }
+
+ emit AddToPlaylistSignal(qmimedata);
+
+}
+
+void RadioView::Homepage() {
+
+ const QModelIndexList indexes = selectedIndexes();
+
+ QList urls;
+ for (const QModelIndex &idx : indexes) {
+ QUrl url = idx.data(RadioModel::Role_Homepage).toUrl();
+ if (!urls.contains(url)) {
+ urls << url;
+ }
+ }
+
+ for (const QUrl &url : urls) {
+ QDesktopServices::openUrl(url);
+ }
+
+}
+
+void RadioView::Donate() {
+
+ const QModelIndexList indexes = selectedIndexes();
+
+ QList urls;
+ for (const QModelIndex &idx : indexes) {
+ QUrl url = idx.data(RadioModel::Role_Donate).toUrl();
+ if (!urls.contains(url)) {
+ urls << url;
+ }
+ }
+
+ for (const QUrl &url : urls) {
+ QDesktopServices::openUrl(url);
+ }
+
+}
+
+void RadioView::DoubleClicked() {
+
+ const QModelIndexList indexes = selectedIndexes();
+ if (indexes.isEmpty()) return;
+
+ for (const QModelIndex &idx : indexes) {
+ if (idx.data(RadioModel::Role_Type).toInt() != RadioItem::Type_Channel) {
+ return;
+ }
+ }
+
+ AddToPlaylist();
+
+}
diff --git a/src/radios/radioview.h b/src/radios/radioview.h
new file mode 100644
index 000000000..602469400
--- /dev/null
+++ b/src/radios/radioview.h
@@ -0,0 +1,68 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOVIEW_H
+#define RADIOVIEW_H
+
+#include
+
+#include "widgets/autoexpandingtreeview.h"
+
+class QMimeData;
+class QMenu;
+class QAction;
+class QShowEvent;
+class QContextMenuEvent;
+
+class RadioModel;
+
+class RadioView : public AutoExpandingTreeView {
+ Q_OBJECT
+
+ public:
+ explicit RadioView(QWidget *parent = nullptr);
+ ~RadioView();
+
+ void setModel(RadioModel *model);
+ void showEvent(QShowEvent *e);
+ void contextMenuEvent(QContextMenuEvent *e) override;
+
+ signals:
+ void GetChannels();
+ void AddToPlaylistSignal(QMimeData*);
+
+ private slots:
+ void AddToPlaylist();
+ void ReplacePlaylist();
+ void OpenInNewPlaylist();
+ void Homepage();
+ void Donate();
+ void DoubleClicked();
+
+ private:
+ QMenu *menu_;
+ QAction *action_playlist_append_;
+ QAction *action_playlist_replace_;
+ QAction *action_playlist_new_;
+ QAction *action_homepage_;
+ QAction *action_donate_;
+ bool initialized_;
+};
+
+#endif // RADIOVIEW_H
diff --git a/src/radios/radioviewcontainer.cpp b/src/radios/radioviewcontainer.cpp
new file mode 100644
index 000000000..80803b4d8
--- /dev/null
+++ b/src/radios/radioviewcontainer.cpp
@@ -0,0 +1,54 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/iconloader.h"
+#include "settings/appearancesettingspage.h"
+#include "radioviewcontainer.h"
+#include "ui_radioviewcontainer.h"
+
+RadioViewContainer::RadioViewContainer(QWidget *parent)
+ : QWidget(parent),
+ ui_(new Ui_RadioViewContainer) {
+
+ ui_->setupUi(this);
+
+ QObject::connect(ui_->refresh, &QToolButton::clicked, this, &RadioViewContainer::Refresh);
+
+ ui_->refresh->setIcon(IconLoader::Load("view-refresh"));
+
+ ReloadSettings();
+
+}
+
+RadioViewContainer::~RadioViewContainer() { delete ui_; }
+
+void RadioViewContainer::ReloadSettings() {
+
+ QSettings s;
+ s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
+ int iconsize = s.value(AppearanceSettingsPage::kIconSizeLeftPanelButtons, 22).toInt();
+ s.endGroup();
+
+ ui_->refresh->setIconSize(QSize(iconsize, iconsize));
+
+}
diff --git a/src/radios/radioviewcontainer.h b/src/radios/radioviewcontainer.h
new file mode 100644
index 000000000..11135f73a
--- /dev/null
+++ b/src/radios/radioviewcontainer.h
@@ -0,0 +1,47 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 RADIOVIEWCONTAINER_H
+#define RADIOVIEWCONTAINER_H
+
+#include
+
+#include "ui_radioviewcontainer.h"
+
+class RadioView;
+
+class RadioViewContainer : public QWidget {
+ Q_OBJECT
+
+ public:
+ explicit RadioViewContainer(QWidget *parent = nullptr);
+ ~RadioViewContainer();
+
+ void ReloadSettings();
+
+ RadioView *view() const { return ui_->view; }
+
+ signals:
+ void Refresh();
+
+ private:
+ Ui_RadioViewContainer *ui_;
+};
+
+#endif // RADIOVIEWCONTAINER_H
diff --git a/src/radios/radioviewcontainer.ui b/src/radios/radioviewcontainer.ui
new file mode 100644
index 000000000..de107c201
--- /dev/null
+++ b/src/radios/radioviewcontainer.ui
@@ -0,0 +1,110 @@
+
+
+ RadioViewContainer
+
+
+
+ 0
+ 0
+ 424
+ 395
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ true
+
+
+
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ false
+
+
+ true
+
+
+ QAbstractItemView::DragDrop
+
+
+ true
+
+
+
+ 16
+ 16
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+ RadioView
+ QTreeView
+
+
+
+
+
+
diff --git a/src/radios/somafmservice.cpp b/src/radios/somafmservice.cpp
new file mode 100644
index 000000000..b0ef8ac3e
--- /dev/null
+++ b/src/radios/somafmservice.cpp
@@ -0,0 +1,166 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 "core/application.h"
+#include "core/logging.h"
+#include "core/networkaccessmanager.h"
+#include "core/taskmanager.h"
+#include "core/iconloader.h"
+#include "playlistparsers/playlistparser.h"
+#include "somafmservice.h"
+#include "radiochannel.h"
+
+const char *SomaFMService::kApiChannelsUrl = "https://somafm.com/channels.json";
+
+SomaFMService::SomaFMService(Application *app, NetworkAccessManager *network, QObject *parent) : RadioService(Song::Source_SomaFM, "SomaFM", IconLoader::Load("somafm"), app, network, parent) {}
+
+SomaFMService::~SomaFMService() {
+ Abort();
+}
+
+QUrl SomaFMService::Homepage() { return QUrl("https://somafm.com/"); }
+QUrl SomaFMService::Donate() { return QUrl("https://somafm.com/support/"); }
+
+void SomaFMService::Abort() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ QObject::disconnect(reply, nullptr, this, nullptr);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+ channels_.clear();
+
+}
+
+void SomaFMService::GetChannels() {
+
+ Abort();
+
+ QUrl url(kApiChannelsUrl);
+ QNetworkRequest req(url);
+ QNetworkReply *reply = network_->get(req);
+ replies_ << reply;
+ const int task_id = app_->task_manager()->StartTask(tr("Getting %1 channels").arg(name_));
+ QObject::connect(reply, &QNetworkReply::finished, this, [=]() { GetChannelsReply(reply, task_id); });
+
+}
+
+void SomaFMService::GetChannelsReply(QNetworkReply *reply, const int task_id) {
+
+ if (replies_.contains(reply)) replies_.removeAll(reply);
+ reply->deleteLater();
+
+ QJsonObject object = ExtractJsonObj(reply);
+ if (object.isEmpty()) {
+ app_->task_manager()->SetTaskFinished(task_id);
+ emit NewChannels();
+ return;
+ }
+
+ if (!object.contains("channels") || !object["channels"].isArray()) {
+ Error("Missing JSON channels array.", object);
+ app_->task_manager()->SetTaskFinished(task_id);
+ emit NewChannels();
+ return;
+ }
+ QJsonArray array_channels = object["channels"].toArray();
+
+ RadioChannelList channels;
+ for (const QJsonValueRef value_channel : array_channels) {
+ if (!value_channel.isObject()) continue;
+ QJsonObject obj_channel = value_channel.toObject();
+ if (!obj_channel.contains("title") || !obj_channel.contains("image")) {
+ continue;
+ }
+ QString name = obj_channel["title"].toString();
+ QString image = obj_channel["image"].toString();
+ QJsonArray playlists = obj_channel["playlists"].toArray();
+ for (const QJsonValueRef playlist : playlists) {
+ if (!playlist.isObject()) continue;
+ QJsonObject obj_playlist = playlist.toObject();
+ if (!obj_playlist.contains("url") || !obj_playlist.contains("quality")) {
+ continue;
+ }
+ RadioChannel channel;
+ QString quality = obj_playlist["quality"].toString();
+ if (quality != "highest") continue;
+ channel.source = source_;
+ channel.name = name;
+ channel.url.setUrl(obj_playlist["url"].toString());
+ channel.thumbnail_url.setUrl(image);
+ if (obj_playlist.contains("format")) {
+ channel.name.append(" " + obj_playlist["format"].toString().toUpper());
+ }
+ channels << channel;
+ }
+ }
+
+ if (channels.isEmpty()) {
+ app_->task_manager()->SetTaskFinished(task_id);
+ emit NewChannels();
+ }
+ else {
+ for (const RadioChannel &channel : channels) {
+ GetStreamUrl(task_id, channel);
+ }
+ }
+
+}
+
+void SomaFMService::GetStreamUrl(const int task_id, const RadioChannel &channel) {
+
+ QNetworkRequest req(channel.url);
+ QNetworkReply *reply = network_->get(req);
+ replies_ << reply;
+ QObject::connect(reply, &QNetworkReply::finished, this, [=]() { GetStreamUrlsReply(reply, task_id, channel); });
+
+}
+
+void SomaFMService::GetStreamUrlsReply(QNetworkReply *reply, const int task_id, RadioChannel channel) {
+
+ if (replies_.contains(reply)) replies_.removeAll(reply);
+ reply->deleteLater();
+
+ PlaylistParser parser;
+ QList songs = parser.LoadFromDevice(reply);
+ if (!songs.isEmpty()) {
+ channel.url = songs.first().url();
+ }
+
+ channels_ << channel;
+
+ if (replies_.isEmpty()) {
+ app_->task_manager()->SetTaskFinished(task_id);
+ emit NewChannels(channels_);
+ channels_.clear();
+ }
+
+}
diff --git a/src/radios/somafmservice.h b/src/radios/somafmservice.h
new file mode 100644
index 000000000..c30ab8709
--- /dev/null
+++ b/src/radios/somafmservice.h
@@ -0,0 +1,61 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2021, Jonas Kvinge
+ *
+ * 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 SOMAFMSERVICE_H
+#define SOMAFMSERVICE_H
+
+#include
+#include
+#include
+
+#include "radioservice.h"
+#include "radiochannel.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+
+class SomaFMService : public RadioService {
+ Q_OBJECT
+
+ public:
+ explicit SomaFMService(Application *app, NetworkAccessManager *network, QObject *parent = nullptr);
+ ~SomaFMService();
+
+ QUrl Homepage() override;
+ QUrl Donate() override;
+
+ void Abort();
+
+ public slots:
+ void GetChannels() override;
+
+ private:
+ void GetStreamUrl(const int task_id, const RadioChannel &channel);
+
+ private slots:
+ void GetChannelsReply(QNetworkReply *reply, const int task_id);
+ void GetStreamUrlsReply(QNetworkReply *reply, const int task_id, RadioChannel channel);
+
+ private:
+ static const char *kApiChannelsUrl;
+ QList replies_;
+ RadioChannelList channels_;
+};
+
+#endif // SOMAFMSERVICE_H
diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp
index 160de206f..2f1da4689 100644
--- a/src/settings/qobuzsettingspage.cpp
+++ b/src/settings/qobuzsettingspage.cpp
@@ -121,8 +121,6 @@ void QobuzSettingsPage::Save() {
s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked());
s.endGroup();
- service_->ReloadSettings();
-
}
void QobuzSettingsPage::LoginClicked() {
diff --git a/src/settings/subsonicsettingspage.cpp b/src/settings/subsonicsettingspage.cpp
index a5b04f50c..bbabd1945 100644
--- a/src/settings/subsonicsettingspage.cpp
+++ b/src/settings/subsonicsettingspage.cpp
@@ -102,8 +102,6 @@ void SubsonicSettingsPage::Save() {
s.setValue("serversidescrobbling", ui_->checkbox_server_scrobbling->isChecked());
s.endGroup();
- service_->ReloadSettings();
-
}
void SubsonicSettingsPage::TestClicked() {
diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp
index abe6719d2..bd7701e56 100644
--- a/src/settings/tidalsettingspage.cpp
+++ b/src/settings/tidalsettingspage.cpp
@@ -148,8 +148,6 @@ void TidalSettingsPage::Save() {
s.setValue("album_explicit", ui_->checkbox_album_explicit->isChecked());
s.endGroup();
- service_->ReloadSettings();
-
}
void TidalSettingsPage::LoginClicked() {