From e7768948e0f81bdded472bdd6ad5fe238f07a9af Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Fri, 12 Feb 2021 21:11:20 +0100 Subject: [PATCH] Initial radio-browser.info support --- data/data.qrc | 3 + data/providers/22x22/radiobrowser.png | Bin 0 -> 12491 bytes data/providers/32x32/radiobrowser.png | Bin 0 -> 8578 bytes data/providers/48x48/radiobrowser.png | Bin 0 -> 13904 bytes src/CMakeLists.txt | 5 + src/core/metatypes.cpp | 5 + .../radiobrowsersearchprovider.cpp | 52 ++++ src/globalsearch/radiobrowsersearchprovider.h | 42 +++ src/internet/core/internetmodel.cpp | 2 + .../radiobrowser/radiobrowserservice.cpp | 254 ++++++++++++++++++ .../radiobrowser/radiobrowserservice.h | 117 ++++++++ .../radiobrowser/radiobrowserurlhandler.cpp | 91 +++++++ .../radiobrowser/radiobrowserurlhandler.h | 50 ++++ 13 files changed, 621 insertions(+) create mode 100644 data/providers/22x22/radiobrowser.png create mode 100644 data/providers/32x32/radiobrowser.png create mode 100644 data/providers/48x48/radiobrowser.png create mode 100644 src/globalsearch/radiobrowsersearchprovider.cpp create mode 100644 src/globalsearch/radiobrowsersearchprovider.h create mode 100644 src/internet/radiobrowser/radiobrowserservice.cpp create mode 100644 src/internet/radiobrowser/radiobrowserservice.h create mode 100644 src/internet/radiobrowser/radiobrowserurlhandler.cpp create mode 100644 src/internet/radiobrowser/radiobrowserurlhandler.h diff --git a/data/data.qrc b/data/data.qrc index 3ea3e2eaa..c39bd5acb 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -350,6 +350,7 @@ providers/22x22/skydrive.png providers/22x22/somafm.png providers/22x22/intergalacticfm.png + providers/22x22/radiobrowser.png providers/22x22/songkick.png providers/22x22/soundcloud.png providers/22x22/spotify.png @@ -383,6 +384,7 @@ providers/32x32/skydrive.png providers/32x32/somafm.png providers/32x32/intergalacticfm.png + providers/32x32/radiobrowser.png providers/32x32/songkick.png providers/32x32/soundcloud.png providers/32x32/spotify.png @@ -416,6 +418,7 @@ providers/48x48/skydrive.png providers/48x48/somafm.png providers/48x48/intergalacticfm.png + providers/48x48/radiobrowser.png providers/48x48/songkick.png providers/48x48/soundcloud.png providers/48x48/spotify.png diff --git a/data/providers/22x22/radiobrowser.png b/data/providers/22x22/radiobrowser.png new file mode 100644 index 0000000000000000000000000000000000000000..c88fa6be9359fe56b0e202c182f3431375d044d4 GIT binary patch literal 12491 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 literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2d5092d5a..93451d6a2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -155,6 +155,7 @@ set(SOURCES globalsearch/simplesearchprovider.cpp globalsearch/somafmsearchprovider.cpp globalsearch/intergalacticfmsearchprovider.cpp + globalsearch/radiobrowsersearchprovider.cpp globalsearch/suggestionwidget.cpp globalsearch/urlsearchprovider.cpp @@ -191,6 +192,8 @@ set(SOURCES internet/somafm/somafmurlhandler.cpp internet/intergalacticfm/intergalacticfmservice.cpp internet/intergalacticfm/intergalacticfmurlhandler.cpp + internet/radiobrowser/radiobrowserservice.cpp + internet/radiobrowser/radiobrowserurlhandler.cpp internet/subsonic/subsonicservice.cpp internet/subsonic/subsonicsettingspage.cpp internet/subsonic/subsonicurlhandler.cpp @@ -506,6 +509,8 @@ set(HEADERS internet/somafm/somafmurlhandler.h internet/intergalacticfm/intergalacticfmservice.h internet/intergalacticfm/intergalacticfmurlhandler.h + internet/radiobrowser/radiobrowserservice.h + internet/radiobrowser/radiobrowserurlhandler.h internet/subsonic/subsonicservice.h internet/subsonic/subsonicsettingspage.h internet/subsonic/subsonicurlhandler.h diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index 232b4feb2..8312498b5 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -37,6 +37,7 @@ #include "internet/intergalacticfm/intergalacticfmservice.h" #include "internet/podcasts/podcast.h" #include "internet/podcasts/podcastepisode.h" +#include "internet/radiobrowser/radiobrowserservice.h" #include "internet/somafm/somafmservice.h" #include "library/directory.h" #include "playlist/playlist.h" @@ -98,6 +99,8 @@ void RegisterMetaTypes() { qRegisterMetaType("SomaFMService::Stream"); qRegisterMetaType( "IntergalacticFMService::Stream"); + qRegisterMetaType( + "RadioBrowserService::Stream"); qRegisterMetaType("SongList"); qRegisterMetaType("Song"); qRegisterMetaTypeStreamOperators( @@ -108,6 +111,8 @@ void RegisterMetaTypes() { "SomaFMService::Stream"); qRegisterMetaTypeStreamOperators( "IntergalacticFMService::Stream"); + qRegisterMetaTypeStreamOperators( + "RadioBrowserService::Stream"); qRegisterMetaType("SubdirectoryList"); qRegisterMetaType("Subdirectory"); qRegisterMetaType>("QList"); diff --git a/src/globalsearch/radiobrowsersearchprovider.cpp b/src/globalsearch/radiobrowsersearchprovider.cpp new file mode 100644 index 000000000..3bce35360 --- /dev/null +++ b/src/globalsearch/radiobrowsersearchprovider.cpp @@ -0,0 +1,52 @@ +/* This file is part of Clementine. + Copyright 2011, David Sansome + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "radiobrowsersearchprovider.h" + +RadioBrowserSearchProvider::RadioBrowserSearchProvider( + RadioBrowserServiceBase* service, Application* app, QObject* parent) + : SimpleSearchProvider(app, parent), service_(service) { + Init(service->name(), service->url_scheme(), service->icon(), + CanGiveSuggestions); + set_result_limit(3); + set_max_suggestion_count(3); + icon_ = ScaleAndPad( + service->icon().pixmap(service->icon().availableSizes()[0]).toImage()); + + connect(service, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems())); + + // Load the stream list on startup only if it doesn't involve going to update + // info from the server. + if (!service_->IsStreamListStale()) RecreateItems(); +} + +void RadioBrowserSearchProvider::LoadArtAsync(int id, const Result& result) { + emit ArtLoaded(id, icon_); +} + +void RadioBrowserSearchProvider::RecreateItems() { + QList items; + + for (const RadioBrowserService::Stream& stream : service_->Streams()) { + Item item; + item.metadata_ = stream.ToSong(service_->name()); + item.keyword_ = stream.name_; + items << item; + } + + SetItems(items); +} diff --git a/src/globalsearch/radiobrowsersearchprovider.h b/src/globalsearch/radiobrowsersearchprovider.h new file mode 100644 index 000000000..479e67ba9 --- /dev/null +++ b/src/globalsearch/radiobrowsersearchprovider.h @@ -0,0 +1,42 @@ + +/* This file is part of Clementine. + Copyright 2011, David Sansome + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef RADIOBROWSERSEARCHPROVIDER_H +#define RADIOBROWSERSEARCHPROVIDER_H + +#include "internet/radiobrowser/radiobrowserservice.h" +#include "simplesearchprovider.h" + +class RadioBrowserSearchProvider : public SimpleSearchProvider { + public: + RadioBrowserSearchProvider(RadioBrowserServiceBase* service, + Application* app, QObject* parent); + // SearchProvider + InternetService* internet_service() override { return service_; } + + void LoadArtAsync(int id, const Result& result) override; + + protected: + void RecreateItems() override; + + private: + RadioBrowserServiceBase* service_; + QImage icon_; +}; + +#endif // RADIOBROWSERSEARCHPROVIDER_H diff --git a/src/internet/core/internetmodel.cpp b/src/internet/core/internetmodel.cpp index bba5ff400..8c1275c92 100644 --- a/src/internet/core/internetmodel.cpp +++ b/src/internet/core/internetmodel.cpp @@ -40,6 +40,7 @@ #include "internet/jamendo/jamendoservice.h" #include "internet/magnatune/magnatuneservice.h" #include "internet/podcasts/podcastservice.h" +#include "internet/radiobrowser/radiobrowserservice.h" #include "internet/somafm/somafmservice.h" #include "internet/subsonic/subsonicservice.h" #include "smartplaylists/generatormimedata.h" @@ -96,6 +97,7 @@ InternetModel::InternetModel(Application* app, QObject* parent) AddService(new RadioTunesService(app, this)); AddService(new SomaFMService(app, this)); AddService(new IntergalacticFMService(app, this)); + AddService(new RadioBrowserService(app, this)); #ifdef HAVE_SPOTIFY AddService(new SpotifyService(app, this)); #endif diff --git a/src/internet/radiobrowser/radiobrowserservice.cpp b/src/internet/radiobrowser/radiobrowserservice.cpp new file mode 100644 index 000000000..cb1a56648 --- /dev/null +++ b/src/internet/radiobrowser/radiobrowserservice.cpp @@ -0,0 +1,254 @@ +/* This file is part of Clementine. + Copyright 2010-2013, David Sansome + Copyright 2011, Tyler Rhodes + Copyright 2011, Paweł Bara + Copyright 2012, 2014, John Maguire + Copyright 2014, Krzysztof Sobiecki + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "radiobrowserservice.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/closure.h" +#include "core/logging.h" +#include "core/network.h" +#include "core/player.h" +#include "core/taskmanager.h" +#include "core/utilities.h" +#include "globalsearch/globalsearch.h" +#include "globalsearch/radiobrowsersearchprovider.h" +#include "radiobrowserurlhandler.h" +#include "internet/core/internetmodel.h" +#include "ui/iconloader.h" + +const int RadioBrowserServiceBase::kStreamsCacheDurationSecs = + 60 * 60 * 24 * 28; // 4 weeks + +bool operator<(const RadioBrowserServiceBase::Stream& a, + const RadioBrowserServiceBase::Stream& b) { + return a.name_.compare(b.name_, Qt::CaseInsensitive) < 0; +} + +RadioBrowserServiceBase::RadioBrowserServiceBase( + Application* app, InternetModel* parent, const QString& name, + const QUrl& channel_list_url, const QUrl& homepage_url, + const QUrl& donate_page_url, const QIcon& icon) + : InternetService(name, app, parent, parent), + url_scheme_(name.toLower().remove(' ')), + url_handler_(new RadioBrowserUrlHandler(app, this, this)), + root_(nullptr), + context_menu_(nullptr), + network_(new NetworkAccessManager(this)), + streams_(name, "streams", kStreamsCacheDurationSecs), + name_(name), + channel_list_url_(channel_list_url), + homepage_url_(homepage_url), + donate_page_url_(donate_page_url), + icon_(icon) { + ReloadSettings(); + + app_->player()->RegisterUrlHandler(url_handler_); + app_->global_search()->AddProvider( + new RadioBrowserSearchProvider(this, app_, this)); +} + +RadioBrowserServiceBase::~RadioBrowserServiceBase() { + delete context_menu_; +} + +QStandardItem* RadioBrowserServiceBase::CreateRootItem() { + root_ = new QStandardItem(icon_, name_); + root_->setData(true, InternetModel::Role_CanLazyLoad); + return root_; +} + +void RadioBrowserServiceBase::LazyPopulate(QStandardItem* item) { + switch (item->data(InternetModel::Role_Type).toInt()) { + case InternetModel::Type_Service: + RefreshStreams(); + break; + + default: + break; + } +} + +void RadioBrowserServiceBase::ShowContextMenu(const QPoint& global_pos) { + if (!context_menu_) { + context_menu_ = new QMenu; + context_menu_->addActions(GetPlaylistActions()); + context_menu_->addAction(IconLoader::Load("download", IconLoader::Base), + tr("Open %1 in browser").arg(homepage_url_.host()), + this, SLOT(Homepage())); + + if (!donate_page_url_.isEmpty()) { + context_menu_->addAction(IconLoader::Load("download", IconLoader::Base), + tr("Donate"), this, SLOT(Donate())); + } + + context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base), + tr("Refresh channels"), this, + SLOT(ForceRefreshStreams())); + } + + context_menu_->popup(global_pos); +} + +void RadioBrowserServiceBase::ForceRefreshStreams() { + QNetworkReply* reply = network_->get(QNetworkRequest(channel_list_url_)); + int task_id = app_->task_manager()->StartTask(tr("Getting channels")); + + NewClosure(reply, SIGNAL(finished()), this, + SLOT(RefreshStreamsFinished(QNetworkReply*, int)), reply, task_id); +} + +void RadioBrowserServiceBase::RefreshStreamsFinished(QNetworkReply* reply, + int task_id) { + app_->task_manager()->SetTaskFinished(task_id); + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + app_->AddError( + tr("Failed to get channel list:\n%1").arg(reply->errorString())); + return; + } + + StreamList list; + + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + app_->AddError( + tr("Failed to parse channel list:\n%1").arg(error.errorString())); + return; + } + + QJsonArray contents = document.array(); + qLog(Debug) << "RadioBrowser station list found:" << contents.size(); + for (const QJsonValue& c : contents) { + QJsonObject item = c.toObject(); + ReadStation(item, &list); + } + + streams_.Update(list); + streams_.Sort(); + + // Only update the item's children if it's already been populated + if (!root_->data(InternetModel::Role_CanLazyLoad).toBool()) PopulateStreams(); + + emit StreamsChanged(); +} + +void RadioBrowserServiceBase::ReadStation(QJsonObject& item, + StreamList* ret) { + Stream stream; + stream.name_ = item["name"].toString(); + QUrl url(item["url"].toString()); + stream.url_ = url; + ret->append(stream); +} + +Song RadioBrowserServiceBase::Stream::ToSong(const QString& prefix) const { + QString song_title = name_.trimmed(); + if (!song_title.startsWith(prefix)) { + song_title = prefix + " " + song_title; + } + + Song ret; + ret.set_valid(true); + ret.set_title(song_title); + ret.set_artist(name_); + ret.set_url(url_); + return ret; +} + +void RadioBrowserServiceBase::Homepage() { + QDesktopServices::openUrl(homepage_url_); +} + +void RadioBrowserServiceBase::Donate() { + QDesktopServices::openUrl(donate_page_url_); +} + +PlaylistItem::Options RadioBrowserServiceBase::playlistitem_options() const { + return PlaylistItem::PauseDisabled; +} + +RadioBrowserServiceBase::StreamList RadioBrowserServiceBase::Streams() { + if (IsStreamListStale()) { + metaObject()->invokeMethod(this, "ForceRefreshStreams", + Qt::QueuedConnection); + } + return streams_; +} + +void RadioBrowserServiceBase::RefreshStreams() { + if (IsStreamListStale()) { + ForceRefreshStreams(); + return; + } + PopulateStreams(); +} + +void RadioBrowserServiceBase::PopulateStreams() { + if (root_->hasChildren()) root_->removeRows(0, root_->rowCount()); + + for (const Stream& stream : streams_) { + QStandardItem* item = new QStandardItem( + IconLoader::Load("icon_radio", IconLoader::Lastfm), QString()); + item->setText(stream.name_); + item->setData(QVariant::fromValue(stream.ToSong(name_)), + InternetModel::Role_SongMetadata); + item->setData(InternetModel::PlayBehaviour_SingleItem, + InternetModel::Role_PlayBehaviour); + + root_->appendRow(item); + } +} + +QDataStream& operator<<(QDataStream& out, + const RadioBrowserServiceBase::Stream& stream) { + out << stream.name_ << stream.url_; + return out; +} + +QDataStream& operator>>(QDataStream& in, + RadioBrowserServiceBase::Stream& stream) { + in >> stream.name_ >> stream.url_; + return in; +} + +void RadioBrowserServiceBase::ReloadSettings() { + streams_.Load(); + streams_.Sort(); +} + +RadioBrowserService::RadioBrowserService(Application* app, + InternetModel* parent) + : RadioBrowserServiceBase( + app, parent, "Radio-Browser.info", + QUrl("http://all.api.radio-browser.info/json/stations"), + QUrl("https://www.radio-browser.info"), QUrl(), + IconLoader::Load("radiobrowser", IconLoader::Provider)) {} diff --git a/src/internet/radiobrowser/radiobrowserservice.h b/src/internet/radiobrowser/radiobrowserservice.h new file mode 100644 index 000000000..153b882e4 --- /dev/null +++ b/src/internet/radiobrowser/radiobrowserservice.h @@ -0,0 +1,117 @@ +/* This file is part of Clementine. + Copyright 2010-2013, David Sansome + Copyright 2010, 2014, John Maguire + Copyright 2014, Krzysztof Sobiecki + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef INTERNET_RADIOBROWSER_RADIOBROWSERSERVICE_H_ +#define INTERNET_RADIOBROWSER_RADIOBROWSERSERVICE_H_ + +#include + +#include "core/cachedlist.h" +#include "internet/core/internetservice.h" + +class RadioBrowserUrlHandler; + +class QNetworkAccessManager; +class QNetworkReply; +class QMenu; + +class RadioBrowserServiceBase : public InternetService { + Q_OBJECT + + public: + RadioBrowserServiceBase(Application* app, InternetModel* parent, + const QString& name, const QUrl& channel_list_url, + const QUrl& homepage_url, + const QUrl& donate_page_url, const QIcon& icon); + ~RadioBrowserServiceBase(); + + enum ItemType { + Type_Stream = 2000, + }; + + struct Stream { + QString name_; + QUrl url_; + + Song ToSong(const QString& prefix) const; + }; + typedef QList StreamList; + + static const int kStreamsCacheDurationSecs; + + const QString& url_scheme() const { return url_scheme_; } + const QIcon& icon() const { return icon_; } + + QStandardItem* CreateRootItem(); + void LazyPopulate(QStandardItem* item); + void ShowContextMenu(const QPoint& global_pos); + + PlaylistItem::Options playlistitem_options() const; + QNetworkAccessManager* network() const { return network_; } + + void ReloadSettings(); + + bool IsStreamListStale() const { return streams_.IsStale(); } + StreamList Streams(); + + signals: + void StreamsChanged(); + + private slots: + void ForceRefreshStreams(); + void RefreshStreams(); + void RefreshStreamsFinished(QNetworkReply* reply, int task_id); + + void Homepage(); + void Donate(); + + private: + void ReadStation(QJsonObject& value, StreamList* ret); + void PopulateStreams(); + + private: + const QString url_scheme_; + RadioBrowserUrlHandler* url_handler_; + + QStandardItem* root_; + QMenu* context_menu_; + + QNetworkAccessManager* network_; + + CachedList streams_; + + const QString name_; + const QUrl channel_list_url_; + const QUrl homepage_url_; + const QUrl donate_page_url_; + const QIcon icon_; +}; + +class RadioBrowserService : public RadioBrowserServiceBase { + public: + RadioBrowserService(Application* app, InternetModel* parent); +}; + +QDataStream& operator<<(QDataStream& out, + const RadioBrowserService::Stream& stream); +QDataStream& operator>>(QDataStream& in, + RadioBrowserService::Stream& stream); +Q_DECLARE_METATYPE(RadioBrowserService::Stream) + +#endif // INTERNET_RADIOBROWSER_RADIOBROWSERSERVICE_H_ diff --git a/src/internet/radiobrowser/radiobrowserurlhandler.cpp b/src/internet/radiobrowser/radiobrowserurlhandler.cpp new file mode 100644 index 000000000..408e37397 --- /dev/null +++ b/src/internet/radiobrowser/radiobrowserurlhandler.cpp @@ -0,0 +1,91 @@ +/* This file is part of Clementine. + Copyright 2011-2013, David Sansome + Copyright 2012, Olaf Christ + Copyright 2014, Krzysztof Sobiecki + Copyright 2014, John Maguire + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "radiobrowserurlhandler.h" + +#include +#include +#include +#include + +#include "core/application.h" +#include "core/logging.h" +#include "core/taskmanager.h" +#include "radiobrowserservice.h" +#include "internet/core/internetmodel.h" +#include "playlistparsers/playlistparser.h" + +RadioBrowserUrlHandler::RadioBrowserUrlHandler( + Application* app, RadioBrowserServiceBase* service, QObject* parent) + : UrlHandler(parent), app_(app), service_(service), task_id_(0) {} + +QString RadioBrowserUrlHandler::scheme() const { + return service_->url_scheme(); +} + +QIcon RadioBrowserUrlHandler::icon() const { return service_->icon(); } + +UrlHandler::LoadResult RadioBrowserUrlHandler::StartLoading( + const QUrl& url) { + QUrl playlist_url = url; + playlist_url.setScheme("https"); + + // Load the playlist + QNetworkReply* reply = + service_->network()->get(QNetworkRequest(playlist_url)); + connect(reply, SIGNAL(finished()), SLOT(LoadPlaylistFinished())); + + if (!task_id_) + task_id_ = app_->task_manager()->StartTask(tr("Loading stream")); + + return LoadResult(url, LoadResult::WillLoadAsynchronously); +} + +void RadioBrowserUrlHandler::LoadPlaylistFinished() { + QNetworkReply* reply = qobject_cast(sender()); + app_->task_manager()->SetTaskFinished(task_id_); + task_id_ = 0; + + QUrl original_url(reply->url()); + original_url.setScheme(scheme()); + + if (reply->error() != QNetworkReply::NoError) { + // TODO((David Sansome): Error handling + qLog(Error) << reply->errorString(); + emit AsyncLoadComplete(LoadResult(original_url, LoadResult::NoMoreTracks)); + return; + } + + // Parse the playlist + PlaylistParser parser(nullptr); + QList songs = parser.LoadFromDevice(reply); + + qLog(Info) << "Loading station finished, got" << songs.count() << "songs"; + + // Failed to get playlist? + if (songs.count() == 0) { + qLog(Error) << "Error loading" << scheme() << "playlist"; + emit AsyncLoadComplete(LoadResult(original_url, LoadResult::NoMoreTracks)); + return; + } + + emit AsyncLoadComplete( + LoadResult(original_url, LoadResult::TrackAvailable, songs[0].url())); +} diff --git a/src/internet/radiobrowser/radiobrowserurlhandler.h b/src/internet/radiobrowser/radiobrowserurlhandler.h new file mode 100644 index 000000000..855636565 --- /dev/null +++ b/src/internet/radiobrowser/radiobrowserurlhandler.h @@ -0,0 +1,50 @@ +/* This file is part of Clementine. + Copyright 2011-2013, David Sansome + Copyright 2014, Krzysztof Sobiecki + Copyright 2014, John Maguire + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef INTERNET_RADIOBROWSER_RADIOBROWSERURLHANDLER_H_ +#define INTERNET_RADIOBROWSER_RADIOBROWSERURLHANDLER_H_ + +#include "core/urlhandler.h" + +class Application; +class RadioBrowserServiceBase; + +class RadioBrowserUrlHandler : public UrlHandler { + Q_OBJECT + + public: + RadioBrowserUrlHandler(Application* app, + RadioBrowserServiceBase* service, + QObject* parent); + + QString scheme() const; + QIcon icon() const; + LoadResult StartLoading(const QUrl& url); + + private slots: + void LoadPlaylistFinished(); + + private: + Application* app_; + RadioBrowserServiceBase* service_; + + int task_id_; +}; + +#endif // INTERNET_RADIOBROWSER_RADIOBROWSERURLHANDLER_H_