2010-11-23 12:42:19 +01:00
|
|
|
/* This file is part of Clementine.
|
|
|
|
Copyright 2010, David Sansome <me@davidsansome.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/>.
|
|
|
|
*/
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
#include "icecastservice.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
using std::sort;
|
|
|
|
using std::unique;
|
|
|
|
|
2010-11-24 20:41:17 +01:00
|
|
|
#include <QDesktopServices>
|
2010-11-22 17:57:26 +01:00
|
|
|
#include <QFutureWatcher>
|
2010-11-24 20:41:17 +01:00
|
|
|
#include <QMenu>
|
2010-11-22 17:57:26 +01:00
|
|
|
#include <QMultiHash>
|
|
|
|
#include <QNetworkReply>
|
2010-11-22 21:36:16 +01:00
|
|
|
#include <QRegExp>
|
2010-11-22 17:57:26 +01:00
|
|
|
#include <QtConcurrentRun>
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
#include "icecastbackend.h"
|
|
|
|
#include "icecastfilterwidget.h"
|
|
|
|
#include "icecastmodel.h"
|
2011-07-15 15:27:50 +02:00
|
|
|
#include "internetmodel.h"
|
2010-11-23 23:36:00 +01:00
|
|
|
#include "core/mergedproxymodel.h"
|
2010-11-22 17:57:26 +01:00
|
|
|
#include "core/network.h"
|
2010-11-23 19:53:08 +01:00
|
|
|
#include "core/taskmanager.h"
|
2010-11-24 20:41:17 +01:00
|
|
|
#include "playlist/songplaylistitem.h"
|
|
|
|
#include "ui/iconloader.h"
|
2010-11-22 17:57:26 +01:00
|
|
|
|
|
|
|
const char* IcecastService::kServiceName = "Icecast";
|
2011-03-22 21:41:31 +01:00
|
|
|
const char* IcecastService::kDirectoryUrl = "http://data.clementine-player.org/icecast-directory";
|
2010-11-24 20:41:17 +01:00
|
|
|
const char* IcecastService::kHomepage = "http://dir.xiph.org/";
|
2010-11-22 17:57:26 +01:00
|
|
|
|
2011-07-15 15:27:50 +02:00
|
|
|
IcecastService::IcecastService(InternetModel* parent)
|
|
|
|
: InternetService(kServiceName, parent, parent),
|
2010-11-23 19:53:08 +01:00
|
|
|
network_(new NetworkAccessManager(this)),
|
2010-11-24 20:41:17 +01:00
|
|
|
context_menu_(NULL),
|
2010-11-23 23:36:00 +01:00
|
|
|
backend_(NULL),
|
|
|
|
model_(NULL),
|
|
|
|
filter_(new IcecastFilterWidget(0)),
|
2010-11-23 19:53:08 +01:00
|
|
|
load_directory_task_id_(0)
|
|
|
|
{
|
2010-11-23 23:36:00 +01:00
|
|
|
backend_ = new IcecastBackend;
|
|
|
|
backend_->moveToThread(parent->db_thread());
|
|
|
|
backend_->Init(parent->db_thread()->Worker());
|
|
|
|
|
|
|
|
model_ = new IcecastModel(backend_, this);
|
|
|
|
filter_->SetIcecastModel(model_);
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
IcecastService::~IcecastService() {
|
|
|
|
}
|
|
|
|
|
2011-01-09 19:27:41 +01:00
|
|
|
QStandardItem* IcecastService::CreateRootItem() {
|
|
|
|
root_ = new QStandardItem(QIcon(":last.fm/icon_radio.png"), kServiceName);
|
2011-07-15 15:27:50 +02:00
|
|
|
root_->setData(true, InternetModel::Role_CanLazyLoad);
|
2010-11-22 17:57:26 +01:00
|
|
|
return root_;
|
|
|
|
}
|
|
|
|
|
2011-01-09 19:27:41 +01:00
|
|
|
void IcecastService::LazyPopulate(QStandardItem* item) {
|
2011-07-15 15:27:50 +02:00
|
|
|
switch (item->data(InternetModel::Role_Type).toInt()) {
|
|
|
|
case InternetModel::Type_Service:
|
2010-11-23 23:36:00 +01:00
|
|
|
model_->Init();
|
2011-01-09 19:27:41 +01:00
|
|
|
model()->merged_model()->AddSubModel(model()->indexFromItem(item), model_);
|
2010-11-23 23:36:00 +01:00
|
|
|
|
|
|
|
if (backend_->IsEmpty()) {
|
|
|
|
LoadDirectory();
|
|
|
|
}
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void IcecastService::LoadDirectory() {
|
2011-03-22 21:41:31 +01:00
|
|
|
RequestDirectory(QUrl(kDirectoryUrl));
|
2010-11-23 19:53:08 +01:00
|
|
|
|
2010-11-23 23:37:00 +01:00
|
|
|
if (!load_directory_task_id_) {
|
2010-11-23 19:53:08 +01:00
|
|
|
load_directory_task_id_ = model()->task_manager()->StartTask(
|
|
|
|
tr("Downloading Icecast directory"));
|
2010-11-23 23:37:00 +01:00
|
|
|
}
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
|
2011-03-22 21:41:31 +01:00
|
|
|
void IcecastService::RequestDirectory(const QUrl& url) {
|
|
|
|
QNetworkRequest req = QNetworkRequest(url);
|
|
|
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
|
|
|
QNetworkRequest::AlwaysNetwork);
|
|
|
|
|
|
|
|
QNetworkReply* reply = network_->get(req);
|
|
|
|
connect(reply, SIGNAL(finished()), SLOT(DownloadDirectoryFinished()));
|
|
|
|
}
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
void IcecastService::DownloadDirectoryFinished() {
|
|
|
|
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
|
|
|
|
Q_ASSERT(reply);
|
|
|
|
|
2011-03-22 21:41:31 +01:00
|
|
|
if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
|
|
|
|
// Discard the old reply and follow the redirect
|
|
|
|
reply->deleteLater();
|
|
|
|
RequestDirectory(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
QFuture<IcecastBackend::StationList> future =
|
2010-11-22 17:57:26 +01:00
|
|
|
QtConcurrent::run(this, &IcecastService::ParseDirectory, reply);
|
2010-11-23 23:36:00 +01:00
|
|
|
QFutureWatcher<IcecastBackend::StationList>* watcher =
|
|
|
|
new QFutureWatcher<IcecastBackend::StationList>(this);
|
2010-11-22 17:57:26 +01:00
|
|
|
watcher->setFuture(future);
|
|
|
|
connect(watcher, SIGNAL(finished()), SLOT(ParseDirectoryFinished()));
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
template <typename T>
|
|
|
|
struct GenreSorter {
|
|
|
|
GenreSorter(const QMultiHash<QString, T>& genres)
|
|
|
|
: genres_(genres) {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool operator() (const QString& a, const QString& b) const {
|
|
|
|
return genres_.count(a) > genres_.count(b);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const QMultiHash<QString, T>& genres_;
|
|
|
|
};
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
struct StationSorter {
|
2010-11-22 22:14:06 +01:00
|
|
|
bool operator() (const T& a, const T& b) const {
|
2010-11-22 21:47:53 +01:00
|
|
|
return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-11-22 22:14:06 +01:00
|
|
|
template <typename T>
|
|
|
|
struct StationSorter<T*> {
|
|
|
|
bool operator() (const T* a, const T* b) const {
|
|
|
|
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
template <typename T>
|
|
|
|
struct StationEquality {
|
|
|
|
bool operator() (T a, T b) const {
|
2010-11-22 21:47:53 +01:00
|
|
|
return a.name == b.name;
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
};
|
2010-11-22 21:36:16 +01:00
|
|
|
|
|
|
|
QStringList FilterGenres(const QStringList& genres) {
|
|
|
|
QStringList ret;
|
|
|
|
foreach (const QString& genre, genres) {
|
|
|
|
if (genre.length() < 2) continue;
|
|
|
|
if (genre.contains("ÃÂ")) continue; // Broken unicode.
|
|
|
|
if (genre.contains(QRegExp("^#x[0-9a-f][0-9a-f]"))) continue; // Broken XML entities.
|
|
|
|
|
|
|
|
// Convert 80 -> 80s.
|
|
|
|
if (genre.contains(QRegExp("^[0-9]0$"))) {
|
|
|
|
ret << genre + 's';
|
|
|
|
} else {
|
|
|
|
ret << genre;
|
|
|
|
}
|
|
|
|
}
|
2010-11-22 21:49:09 +01:00
|
|
|
|
|
|
|
if (ret.empty()) {
|
|
|
|
ret << "other";
|
|
|
|
}
|
2010-11-22 21:36:16 +01:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void IcecastService::ParseDirectoryFinished() {
|
2010-11-23 23:36:00 +01:00
|
|
|
QFutureWatcher<IcecastBackend::StationList >* watcher =
|
|
|
|
static_cast<QFutureWatcher<IcecastBackend::StationList>*>(sender());
|
|
|
|
IcecastBackend::StationList all_stations = watcher->result();
|
|
|
|
sort(all_stations.begin(), all_stations.end(), StationSorter<IcecastBackend::Station>());
|
2010-11-22 21:47:53 +01:00
|
|
|
// Remove duplicates by name. These tend to be multiple URLs for the same station.
|
2010-11-23 23:36:00 +01:00
|
|
|
IcecastBackend::StationList::iterator it =
|
|
|
|
unique(all_stations.begin(), all_stations.end(), StationEquality<IcecastBackend::Station>());
|
2010-11-22 21:47:53 +01:00
|
|
|
all_stations.erase(it, all_stations.end());
|
2010-11-22 17:57:26 +01:00
|
|
|
|
|
|
|
// Cluster stations by genre.
|
2010-11-23 23:36:00 +01:00
|
|
|
QMultiHash<QString, IcecastBackend::Station*> genres;
|
|
|
|
|
|
|
|
// Add stations.
|
|
|
|
for (int i=0 ; i<all_stations.count() ; ++i) {
|
|
|
|
IcecastBackend::Station& s = all_stations[i];
|
|
|
|
genres.insert(s.genre, &s);
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
|
2010-11-22 22:14:06 +01:00
|
|
|
QSet<QString> genre_set = genres.keys().toSet();
|
|
|
|
|
|
|
|
// Merge genres with only 1 or 2 stations into "Other".
|
|
|
|
foreach (const QString& genre, genre_set) {
|
|
|
|
if (genres.count(genre) < 3) {
|
2010-11-23 23:36:00 +01:00
|
|
|
const QList<IcecastBackend::Station*>& small_genre = genres.values(genre);
|
|
|
|
foreach (IcecastBackend::Station* s, small_genre) {
|
|
|
|
s->genre = "Other";
|
2010-11-22 22:14:06 +01:00
|
|
|
}
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
backend_->ClearAndAddStations(all_stations);
|
2010-11-22 17:57:26 +01:00
|
|
|
delete watcher;
|
2010-11-23 19:53:08 +01:00
|
|
|
|
|
|
|
model()->task_manager()->SetTaskFinished(load_directory_task_id_);
|
|
|
|
load_directory_task_id_ = 0;
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
IcecastBackend::StationList IcecastService::ParseDirectory(QIODevice* device) const {
|
2010-11-22 17:57:26 +01:00
|
|
|
QXmlStreamReader reader(device);
|
2010-11-23 23:36:00 +01:00
|
|
|
IcecastBackend::StationList stations;
|
2010-11-22 17:57:26 +01:00
|
|
|
while (!reader.atEnd()) {
|
|
|
|
reader.readNext();
|
|
|
|
if (reader.tokenType() == QXmlStreamReader::StartElement &&
|
|
|
|
reader.name() == "entry") {
|
|
|
|
stations << ReadStation(&reader);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
device->deleteLater();
|
|
|
|
return stations;
|
|
|
|
}
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
IcecastBackend::Station IcecastService::ReadStation(QXmlStreamReader* reader) const {
|
|
|
|
IcecastBackend::Station station;
|
2010-11-22 17:57:26 +01:00
|
|
|
while (!reader->atEnd()) {
|
|
|
|
reader->readNext();
|
|
|
|
if (reader->tokenType() == QXmlStreamReader::EndElement)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (reader->tokenType() == QXmlStreamReader::StartElement) {
|
|
|
|
QStringRef name = reader->name();
|
|
|
|
QString value = reader->readElementText(QXmlStreamReader::SkipChildElements);
|
|
|
|
|
|
|
|
if (name == "server_name") station.name = value;
|
|
|
|
if (name == "listen_url") station.url = QUrl(value);
|
|
|
|
if (name == "server_type") station.mime_type = value;
|
|
|
|
if (name == "bitrate") station.bitrate = value.toInt();
|
|
|
|
if (name == "channels") station.channels = value.toInt();
|
|
|
|
if (name == "samplerate") station.samplerate = value.toInt();
|
2010-11-23 23:36:00 +01:00
|
|
|
if (name == "genre") station.genre =
|
|
|
|
FilterGenres(value.split(' ', QString::SkipEmptyParts))[0];
|
2010-11-22 17:57:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-23 23:36:00 +01:00
|
|
|
// Title case the genre
|
|
|
|
if (!station.genre.isEmpty()) {
|
|
|
|
station.genre[0] = station.genre[0].toUpper();
|
|
|
|
}
|
|
|
|
|
2010-11-22 17:57:26 +01:00
|
|
|
// HACK: This hints to the player that the artist and title metadata needs swapping.
|
|
|
|
station.url.setFragment("icecast");
|
|
|
|
|
|
|
|
return station;
|
|
|
|
}
|
2010-11-23 23:36:00 +01:00
|
|
|
|
|
|
|
QWidget* IcecastService::HeaderWidget() const {
|
|
|
|
return filter_;
|
|
|
|
}
|
2010-11-24 20:41:17 +01:00
|
|
|
|
2011-01-09 19:27:41 +01:00
|
|
|
void IcecastService::ShowContextMenu(const QModelIndex& index,
|
2010-11-24 20:41:17 +01:00
|
|
|
const QPoint& global_pos) {
|
|
|
|
EnsureMenuCreated();
|
|
|
|
|
|
|
|
if (index.model() == model_)
|
|
|
|
context_item_ = index;
|
|
|
|
else
|
|
|
|
context_item_ = QModelIndex();
|
|
|
|
|
2010-12-04 16:49:43 +01:00
|
|
|
const bool can_play = context_item_.isValid() &&
|
|
|
|
model_->GetSong(context_item_).is_valid();
|
|
|
|
|
2011-02-10 23:24:17 +01:00
|
|
|
GetAppendToPlaylistAction()->setEnabled(can_play);
|
|
|
|
GetReplacePlaylistAction()->setEnabled(can_play);
|
|
|
|
GetOpenInNewPlaylistAction()->setEnabled(can_play);
|
2010-11-24 20:41:17 +01:00
|
|
|
context_menu_->popup(global_pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
void IcecastService::EnsureMenuCreated() {
|
|
|
|
if (context_menu_)
|
|
|
|
return;
|
|
|
|
|
|
|
|
context_menu_ = new QMenu;
|
|
|
|
|
2011-02-10 23:24:17 +01:00
|
|
|
context_menu_->addActions(GetPlaylistActions());
|
2010-11-24 20:41:17 +01:00
|
|
|
context_menu_->addAction(IconLoader::Load("download"), tr("Open dir.xiph.org in browser"), this, SLOT(Homepage()));
|
|
|
|
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Refresh station list"), this, SLOT(LoadDirectory()));
|
|
|
|
}
|
|
|
|
|
|
|
|
void IcecastService::Homepage() {
|
|
|
|
QDesktopServices::openUrl(QUrl(kHomepage));
|
|
|
|
}
|
|
|
|
|
2011-02-10 23:24:17 +01:00
|
|
|
QModelIndex IcecastService::GetCurrentIndex() {
|
|
|
|
return context_item_;
|
2011-02-09 18:51:59 +01:00
|
|
|
}
|