Initial RadioBrowser support: implemented search, groups by category, top 100
This commit is contained in:
parent
e7768948e0
commit
3e31094227
|
@ -193,7 +193,6 @@ set(SOURCES
|
|||
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
|
||||
|
@ -478,6 +477,7 @@ set(HEADERS
|
|||
globalsearch/searchprovider.h
|
||||
globalsearch/simplesearchprovider.h
|
||||
globalsearch/suggestionwidget.h
|
||||
globalsearch/radiobrowsersearchprovider.h
|
||||
|
||||
internet/core/cloudfileservice.h
|
||||
internet/digitally/digitallyimportedclient.h
|
||||
|
@ -510,7 +510,6 @@ set(HEADERS
|
|||
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
|
||||
|
|
|
@ -99,8 +99,7 @@ void RegisterMetaTypes() {
|
|||
qRegisterMetaType<SomaFMService::Stream>("SomaFMService::Stream");
|
||||
qRegisterMetaType<IntergalacticFMService::Stream>(
|
||||
"IntergalacticFMService::Stream");
|
||||
qRegisterMetaType<RadioBrowserService::Stream>(
|
||||
"RadioBrowserService::Stream");
|
||||
qRegisterMetaType<RadioBrowserService::Stream>("RadioBrowserService::Stream");
|
||||
qRegisterMetaType<SongList>("SongList");
|
||||
qRegisterMetaType<Song>("Song");
|
||||
qRegisterMetaTypeStreamOperators<DigitallyImportedClient::Channel>(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2021, Fabio Bas <ctrlaltca@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,36 +17,50 @@
|
|||
|
||||
#include "radiobrowsersearchprovider.h"
|
||||
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
namespace {
|
||||
const int kSearchStationLimit = 10;
|
||||
} // namespace
|
||||
|
||||
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();
|
||||
Application* app, RadioBrowserService* service, QObject* parent)
|
||||
: SearchProvider(app, parent), service_(service) {
|
||||
Init(RadioBrowserService::kServiceName, "radiobrowser",
|
||||
IconLoader::Load("radiobrowser", IconLoader::Provider),
|
||||
WantsDelayedQueries);
|
||||
connect(service_,
|
||||
SIGNAL(SearchFinished(int, RadioBrowserService::StreamList)),
|
||||
SLOT(SearchFinishedSlot(int, RadioBrowserService::StreamList)));
|
||||
}
|
||||
|
||||
void RadioBrowserSearchProvider::LoadArtAsync(int id, const Result& result) {
|
||||
emit ArtLoaded(id, icon_);
|
||||
void RadioBrowserSearchProvider::SearchAsync(int id, const QString& query) {
|
||||
PendingState state;
|
||||
state.orig_id_ = id;
|
||||
state.tokens_ = TokenizeQuery(query);
|
||||
|
||||
const QString query_string = state.tokens_.join(" ");
|
||||
service_->Search(id, query_string, kSearchStationLimit);
|
||||
}
|
||||
|
||||
void RadioBrowserSearchProvider::RecreateItems() {
|
||||
QList<Item> items;
|
||||
void RadioBrowserSearchProvider::SearchFinishedSlot(
|
||||
int search_id, RadioBrowserService::StreamList streams) {
|
||||
ResultList ret;
|
||||
|
||||
for (const RadioBrowserService::Stream& stream : service_->Streams()) {
|
||||
Item item;
|
||||
item.metadata_ = stream.ToSong(service_->name());
|
||||
item.keyword_ = stream.name_;
|
||||
items << item;
|
||||
for (auto stream : streams) {
|
||||
Result result(this);
|
||||
result.group_automatically_ = false;
|
||||
result.metadata_ = stream.ToSong(QString());
|
||||
ret << result;
|
||||
}
|
||||
|
||||
SetItems(items);
|
||||
emit ResultsAvailable(search_id, ret);
|
||||
emit SearchFinished(search_id);
|
||||
}
|
||||
/*
|
||||
void RadioBrowserSearchProvider::ShowConfig() {
|
||||
if (service_) {
|
||||
return service_->ShowConfig();
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2021, Fabio Bas <ctrlaltca@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,23 +19,24 @@
|
|||
#define RADIOBROWSERSEARCHPROVIDER_H
|
||||
|
||||
#include "internet/radiobrowser/radiobrowserservice.h"
|
||||
#include "simplesearchprovider.h"
|
||||
#include "searchprovider.h"
|
||||
|
||||
class RadioBrowserSearchProvider : public SearchProvider {
|
||||
Q_OBJECT
|
||||
|
||||
class RadioBrowserSearchProvider : public SimpleSearchProvider {
|
||||
public:
|
||||
RadioBrowserSearchProvider(RadioBrowserServiceBase* service,
|
||||
Application* app, QObject* parent);
|
||||
// SearchProvider
|
||||
RadioBrowserSearchProvider(Application* app, RadioBrowserService* service,
|
||||
QObject* parent = nullptr);
|
||||
void SearchAsync(int id, const QString& query) override;
|
||||
// void ShowConfig() override;
|
||||
InternetService* internet_service() override { return service_; }
|
||||
|
||||
void LoadArtAsync(int id, const Result& result) override;
|
||||
|
||||
protected:
|
||||
void RecreateItems() override;
|
||||
public slots:
|
||||
void SearchFinishedSlot(int search_id,
|
||||
RadioBrowserService::StreamList streams);
|
||||
|
||||
private:
|
||||
RadioBrowserServiceBase* service_;
|
||||
QImage icon_;
|
||||
RadioBrowserService* service_;
|
||||
};
|
||||
|
||||
#endif // RADIOBROWSERSEARCHPROVIDER_H
|
||||
|
|
|
@ -332,6 +332,11 @@ QMimeData* InternetModel::mimeData(const QModelIndexList& indexes) const {
|
|||
|
||||
if (urls.isEmpty()) return nullptr;
|
||||
|
||||
for (const QModelIndex& index : new_indexes) {
|
||||
InternetModel::ServiceForIndex(index)
|
||||
->ItemNowPlaying(itemFromIndex(index));
|
||||
}
|
||||
|
||||
InternetMimeData* data = new InternetMimeData(this);
|
||||
data->setUrls(urls);
|
||||
data->indexes = new_indexes;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010-2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
|
||||
Copyright 2011, Paweł Bara <keirangtp@gmail.com>
|
||||
Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2021, Fabio Bas <ctrlaltca@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -39,63 +35,140 @@
|
|||
#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) {
|
||||
bool operator<(const RadioBrowserService::Stream& a,
|
||||
const RadioBrowserService::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)),
|
||||
const char* RadioBrowserService::kServiceName = "Radio-Browser.info";
|
||||
QString RadioBrowserService::SearchUrl =
|
||||
"%1/json/stations/byname/%2?limit=%3";
|
||||
QString RadioBrowserService::PlayClickUrl = "%1/json/url/%2";
|
||||
|
||||
QList<RadioBrowserService::Branch> RadioBrowserService::BranchList = {
|
||||
{"By Country", "%1/json/countries",
|
||||
"%1/json/stations/bycountryexact/%2?hidebroken=true", Type_Category},
|
||||
{"By Language", "%1/json/languages",
|
||||
"%1/json/stations/bylanguageexact/%2?hidebroken=true", Type_Category},
|
||||
{"By Tag", "%1/json/tags", "%1/json/stations/bytagexact/%2?hidebroken=true",
|
||||
Type_Category},
|
||||
{"By Codec", "%1/json/codecs",
|
||||
"%1/json/stations/bycodecexact/%2?hidebroken=true", Type_Category},
|
||||
{"Top 100 Clicked", "",
|
||||
"%1/json/stations/search?order=clickcount&reverse=true&limit=100",
|
||||
Type_Top100},
|
||||
{"Top 100 Voted", "",
|
||||
"%1/json/stations/search?order=votes&reverse=true&limit=100", Type_Top100},
|
||||
{"Top 100 Trending", "",
|
||||
"%1/json/stations/search?order=clicktrend&reverse=true&limit=100",
|
||||
Type_Top100}};
|
||||
|
||||
RadioBrowserService::RadioBrowserService(Application* app,
|
||||
InternetModel* parent)
|
||||
: InternetService(kServiceName, app, parent, parent),
|
||||
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) {
|
||||
name_(kServiceName),
|
||||
main_server_url_(QStringLiteral("http://all.api.radio-browser.info")),
|
||||
homepage_url_(QUrl("https://www.radio-browser.info")),
|
||||
icon_(IconLoader::Load("radiobrowser", IconLoader::Provider)) {
|
||||
ReloadSettings();
|
||||
|
||||
app_->player()->RegisterUrlHandler(url_handler_);
|
||||
app_->global_search()->AddProvider(
|
||||
new RadioBrowserSearchProvider(this, app_, this));
|
||||
new RadioBrowserSearchProvider(app_, this, this));
|
||||
}
|
||||
|
||||
RadioBrowserServiceBase::~RadioBrowserServiceBase() {
|
||||
delete context_menu_;
|
||||
}
|
||||
RadioBrowserService::~RadioBrowserService() { delete context_menu_; }
|
||||
|
||||
QStandardItem* RadioBrowserServiceBase::CreateRootItem() {
|
||||
QStandardItem* RadioBrowserService::CreateRootItem() {
|
||||
root_ = new QStandardItem(icon_, name_);
|
||||
root_->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
return root_;
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::LazyPopulate(QStandardItem* item) {
|
||||
void RadioBrowserService::LazyPopulate(QStandardItem* item) {
|
||||
switch (item->data(InternetModel::Role_Type).toInt()) {
|
||||
case InternetModel::Type_Service:
|
||||
RefreshStreams();
|
||||
RefreshRootItem();
|
||||
break;
|
||||
case RadioBrowserService::Type_Category:
|
||||
RefreshCategory(item);
|
||||
break;
|
||||
case RadioBrowserService::Type_CategoryItem:
|
||||
RefreshCategoryItem(item);
|
||||
break;
|
||||
case RadioBrowserService::Type_Top100:
|
||||
RefreshTop100(item);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::ShowContextMenu(const QPoint& global_pos) {
|
||||
void RadioBrowserService::RefreshRootItem() {
|
||||
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
|
||||
for (auto branch : RadioBrowserService::BranchList) {
|
||||
QStandardItem* item = new QStandardItem(
|
||||
IconLoader::Load("icon_radio", IconLoader::Lastfm), QString());
|
||||
item->setText(branch.name);
|
||||
item->setData(branch.type, InternetModel::Role_Type);
|
||||
item->setData(branch.listUrl, RadioBrowserService::Role_ListUrl);
|
||||
item->setData(branch.itemsUrl, RadioBrowserService::Role_ItemsUrl);
|
||||
item->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
root_->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
void RadioBrowserService::RefreshCategory(QStandardItem* item) {
|
||||
QString determinedUrl = item->data(RadioBrowserService::Role_ListUrl)
|
||||
.toString()
|
||||
.arg(main_server_url_);
|
||||
QUrl url(determinedUrl);
|
||||
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
int task_id = app_->task_manager()->StartTask(tr("Getting channels"));
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(RefreshCategoryFinished(QNetworkReply*, int, QStandardItem*)),
|
||||
reply, task_id, item);
|
||||
}
|
||||
|
||||
void RadioBrowserService::RefreshCategoryItem(QStandardItem* item) {
|
||||
QStandardItem* parent = item->parent();
|
||||
QString determinedUrl = parent->data(RadioBrowserService::Role_ItemsUrl)
|
||||
.toString()
|
||||
.arg(main_server_url_, item->text());
|
||||
QUrl url(determinedUrl);
|
||||
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
int task_id = app_->task_manager()->StartTask(tr("Getting channels"));
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(RefreshStreamsFinished(QNetworkReply*, int, QStandardItem*)),
|
||||
reply, task_id, item);
|
||||
}
|
||||
|
||||
void RadioBrowserService::RefreshTop100(QStandardItem* item) {
|
||||
QString determinedUrl = item->data(RadioBrowserService::Role_ItemsUrl)
|
||||
.toString()
|
||||
.arg(main_server_url_);
|
||||
QUrl url(determinedUrl);
|
||||
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
int task_id = app_->task_manager()->StartTask(tr("Getting channels"));
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(RefreshStreamsFinished(QNetworkReply*, int, QStandardItem*)),
|
||||
reply, task_id, item);
|
||||
}
|
||||
|
||||
void RadioBrowserService::ShowContextMenu(const QPoint& global_pos) {
|
||||
if (!model()->current_index().isValid()) return;
|
||||
QStandardItem* item = model()->itemFromIndex(model()->current_index());
|
||||
if (!item) return;
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu;
|
||||
context_menu_->addActions(GetPlaylistActions());
|
||||
|
@ -103,48 +176,40 @@ void RadioBrowserServiceBase::ShowContextMenu(const QPoint& global_pos) {
|
|||
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()));
|
||||
SLOT(LazyPopulate(item)));
|
||||
}
|
||||
|
||||
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) {
|
||||
void RadioBrowserService::RefreshCategoryFinished(QNetworkReply* reply,
|
||||
int task_id,
|
||||
QStandardItem* item) {
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
reply->deleteLater();
|
||||
QJsonDocument document = ParseJsonReply(reply);
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
app_->AddError(
|
||||
tr("Failed to get channel list:\n%1").arg(reply->errorString()));
|
||||
return;
|
||||
QStringList list;
|
||||
QJsonArray contents = document.array();
|
||||
qLog(Debug) << "RadioBrowser station list found:" << contents.size();
|
||||
for (const QJsonValue& c : contents) {
|
||||
QJsonObject item = c.toObject();
|
||||
list << item["name"].toString();
|
||||
}
|
||||
|
||||
PopulateCategory(item, list);
|
||||
}
|
||||
|
||||
void RadioBrowserService::RefreshStreamsFinished(QNetworkReply* reply,
|
||||
int task_id,
|
||||
QStandardItem* item) {
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
reply->deleteLater();
|
||||
QJsonDocument document = ParseJsonReply(reply);
|
||||
|
||||
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) {
|
||||
|
@ -152,25 +217,19 @@ void RadioBrowserServiceBase::RefreshStreamsFinished(QNetworkReply* reply,
|
|||
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();
|
||||
PopulateStreams(item, list);
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::ReadStation(QJsonObject& item,
|
||||
StreamList* ret) {
|
||||
void RadioBrowserService::ReadStation(QJsonObject& item, StreamList* ret) {
|
||||
Stream stream;
|
||||
stream.name_ = item["name"].toString();
|
||||
stream.uuid_ = item["stationuuid"].toString();
|
||||
QUrl url(item["url"].toString());
|
||||
stream.url_ = url;
|
||||
ret->append(stream);
|
||||
}
|
||||
|
||||
Song RadioBrowserServiceBase::Stream::ToSong(const QString& prefix) const {
|
||||
Song RadioBrowserService::Stream::ToSong(const QString& prefix) const {
|
||||
QString song_title = name_.trimmed();
|
||||
if (!song_title.startsWith(prefix)) {
|
||||
song_title = prefix + " " + song_title;
|
||||
|
@ -184,38 +243,36 @@ Song RadioBrowserServiceBase::Stream::ToSong(const QString& prefix) const {
|
|||
return ret;
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::Homepage() {
|
||||
void RadioBrowserService::Homepage() {
|
||||
QDesktopServices::openUrl(homepage_url_);
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::Donate() {
|
||||
QDesktopServices::openUrl(donate_page_url_);
|
||||
PlaylistItem::Options RadioBrowserService::playlistitem_options() const {
|
||||
return PlaylistItem::PauseDisabled | PlaylistItem::SeekDisabled;
|
||||
}
|
||||
|
||||
PlaylistItem::Options RadioBrowserServiceBase::playlistitem_options() const {
|
||||
return PlaylistItem::PauseDisabled;
|
||||
}
|
||||
void RadioBrowserService::PopulateCategory(QStandardItem* parentItem,
|
||||
QStringList& elements) {
|
||||
if (parentItem->hasChildren())
|
||||
parentItem->removeRows(0, parentItem->rowCount());
|
||||
|
||||
RadioBrowserServiceBase::StreamList RadioBrowserServiceBase::Streams() {
|
||||
if (IsStreamListStale()) {
|
||||
metaObject()->invokeMethod(this, "ForceRefreshStreams",
|
||||
Qt::QueuedConnection);
|
||||
for (const QString& element : elements) {
|
||||
QStandardItem* item = new QStandardItem(
|
||||
IconLoader::Load("icon_radio", IconLoader::Lastfm), QString());
|
||||
item->setText(element);
|
||||
item->setData(RadioBrowserService::Type_CategoryItem,
|
||||
InternetModel::Role_Type);
|
||||
item->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
parentItem->appendRow(item);
|
||||
}
|
||||
return streams_;
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::RefreshStreams() {
|
||||
if (IsStreamListStale()) {
|
||||
ForceRefreshStreams();
|
||||
return;
|
||||
}
|
||||
PopulateStreams();
|
||||
}
|
||||
void RadioBrowserService::PopulateStreams(QStandardItem* parentItem,
|
||||
StreamList& streams) {
|
||||
if (parentItem->hasChildren())
|
||||
parentItem->removeRows(0, parentItem->rowCount());
|
||||
|
||||
void RadioBrowserServiceBase::PopulateStreams() {
|
||||
if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
|
||||
|
||||
for (const Stream& stream : streams_) {
|
||||
for (const Stream& stream : streams) {
|
||||
QStandardItem* item = new QStandardItem(
|
||||
IconLoader::Load("icon_radio", IconLoader::Lastfm), QString());
|
||||
item->setText(stream.name_);
|
||||
|
@ -223,32 +280,73 @@ void RadioBrowserServiceBase::PopulateStreams() {
|
|||
InternetModel::Role_SongMetadata);
|
||||
item->setData(InternetModel::PlayBehaviour_SingleItem,
|
||||
InternetModel::Role_PlayBehaviour);
|
||||
item->setData(stream.uuid_,
|
||||
RadioBrowserService::Role_StationUuid);
|
||||
|
||||
root_->appendRow(item);
|
||||
parentItem->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream& out,
|
||||
const RadioBrowserServiceBase::Stream& stream) {
|
||||
const RadioBrowserService::Stream& stream) {
|
||||
out << stream.name_ << stream.url_;
|
||||
return out;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& in,
|
||||
RadioBrowserServiceBase::Stream& stream) {
|
||||
RadioBrowserService::Stream& stream) {
|
||||
in >> stream.name_ >> stream.url_;
|
||||
return in;
|
||||
}
|
||||
|
||||
void RadioBrowserServiceBase::ReloadSettings() {
|
||||
streams_.Load();
|
||||
streams_.Sort();
|
||||
void RadioBrowserService::ReloadSettings() {}
|
||||
|
||||
void RadioBrowserService::Search(int search_id, const QString& query,
|
||||
const int limit) {
|
||||
QString determinedUrl =
|
||||
RadioBrowserService::SearchUrl.arg(main_server_url_, query)
|
||||
.arg(limit);
|
||||
QUrl url(determinedUrl);
|
||||
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
int task_id = app_->task_manager()->StartTask(tr("Getting channels"));
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(SearchFinishedInternal(QNetworkReply*, int, int)), reply,
|
||||
task_id, search_id);
|
||||
}
|
||||
|
||||
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)) {}
|
||||
void RadioBrowserService::SearchFinishedInternal(QNetworkReply* reply,
|
||||
int task_id,
|
||||
int search_id) {
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
reply->deleteLater();
|
||||
QJsonDocument document = ParseJsonReply(reply);
|
||||
|
||||
StreamList list;
|
||||
QJsonArray contents = document.array();
|
||||
qLog(Debug) << "RadioBrowser station list found:" << contents.size();
|
||||
for (const QJsonValue& c : contents) {
|
||||
QJsonObject item = c.toObject();
|
||||
ReadStation(item, &list);
|
||||
}
|
||||
|
||||
emit SearchFinished(search_id, list);
|
||||
}
|
||||
|
||||
void RadioBrowserService::ItemNowPlaying(QStandardItem* item) {
|
||||
QString station_uuid = item->data(RadioBrowserService::Role_StationUuid).toString();
|
||||
if(station_uuid.isEmpty())
|
||||
return;
|
||||
|
||||
QString determinedUrl =
|
||||
RadioBrowserService::PlayClickUrl.arg(main_server_url_, station_uuid);
|
||||
QUrl url(determinedUrl);
|
||||
|
||||
qLog(Debug) << "RadioBrowser station played:" << determinedUrl;
|
||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
||||
connect(reply, &QNetworkReply::finished, [this, reply]()
|
||||
{
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010-2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2021, Fabio Bas <ctrlaltca@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -23,6 +21,7 @@
|
|||
#include <QJsonObject>
|
||||
|
||||
#include "core/cachedlist.h"
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "internet/core/internetservice.h"
|
||||
|
||||
class RadioBrowserUrlHandler;
|
||||
|
@ -31,87 +30,103 @@ class QNetworkAccessManager;
|
|||
class QNetworkReply;
|
||||
class QMenu;
|
||||
|
||||
class RadioBrowserServiceBase : public InternetService {
|
||||
class RadioBrowserService : 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();
|
||||
RadioBrowserService(Application* app, InternetModel* parent);
|
||||
~RadioBrowserService();
|
||||
|
||||
enum ItemType {
|
||||
Type_Stream = 2000,
|
||||
};
|
||||
|
||||
enum Type {
|
||||
Type_Category = InternetModel::TypeCount,
|
||||
Type_CategoryItem,
|
||||
Type_Top100,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Role_ListUrl = InternetModel::RoleCount,
|
||||
Role_ItemsUrl,
|
||||
Role_StationUuid,
|
||||
};
|
||||
|
||||
struct Stream {
|
||||
QString name_;
|
||||
QUrl url_;
|
||||
QString uuid_;
|
||||
|
||||
Song ToSong(const QString& prefix) const;
|
||||
};
|
||||
|
||||
struct Branch {
|
||||
QString name;
|
||||
QString listUrl;
|
||||
QString itemsUrl;
|
||||
Type type;
|
||||
};
|
||||
|
||||
static QList<Branch> BranchList;
|
||||
static QString SearchUrl;
|
||||
static QString PlayClickUrl;
|
||||
|
||||
typedef QList<Stream> StreamList;
|
||||
|
||||
static const int kStreamsCacheDurationSecs;
|
||||
static const char* kServiceName;
|
||||
|
||||
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();
|
||||
void Search(int search_id, const QString& query, const int limit);
|
||||
void ItemNowPlaying(QStandardItem* item) override;
|
||||
|
||||
signals:
|
||||
void StreamsChanged();
|
||||
void SearchFinished(int search_id,
|
||||
RadioBrowserService::StreamList streams);
|
||||
|
||||
private slots:
|
||||
void ForceRefreshStreams();
|
||||
void RefreshStreams();
|
||||
void RefreshStreamsFinished(QNetworkReply* reply, int task_id);
|
||||
void LazyPopulate(QStandardItem* item);
|
||||
|
||||
void RefreshRootItem();
|
||||
void RefreshCategory(QStandardItem* item);
|
||||
void RefreshCategoryItem(QStandardItem* item);
|
||||
void RefreshTop100(QStandardItem* item);
|
||||
|
||||
void RefreshCategoryFinished(QNetworkReply* reply, int task_id,
|
||||
QStandardItem* item);
|
||||
void RefreshStreamsFinished(QNetworkReply* reply, int task_id,
|
||||
QStandardItem* item);
|
||||
void SearchFinishedInternal(QNetworkReply* reply, int task_id, int search_id);
|
||||
|
||||
void Homepage();
|
||||
void Donate();
|
||||
|
||||
private:
|
||||
void ReadStation(QJsonObject& value, StreamList* ret);
|
||||
void PopulateStreams();
|
||||
void PopulateCategory(QStandardItem* parentItem, QStringList& elements);
|
||||
void PopulateStreams(QStandardItem* parentItem, StreamList& streams);
|
||||
|
||||
private:
|
||||
const QString url_scheme_;
|
||||
RadioBrowserUrlHandler* url_handler_;
|
||||
|
||||
QStandardItem* root_;
|
||||
QMenu* context_menu_;
|
||||
|
||||
QNetworkAccessManager* network_;
|
||||
|
||||
CachedList<Stream> streams_;
|
||||
|
||||
const QString name_;
|
||||
const QUrl channel_list_url_;
|
||||
const QString main_server_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);
|
||||
QDataStream& operator>>(QDataStream& in, RadioBrowserService::Stream& stream);
|
||||
Q_DECLARE_METATYPE(RadioBrowserService::Stream)
|
||||
|
||||
#endif // INTERNET_RADIOBROWSER_RADIOBROWSERSERVICE_H_
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2012, Olaf Christ <olafc81@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "radiobrowserurlhandler.h"
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#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<QNetworkReply*>(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<Song> 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()));
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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_
|
Loading…
Reference in New Issue