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 +
radios/radioview.h
+
+
+ + +
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() {