diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa6ac3ab3..14f9a2169 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -119,6 +119,7 @@ set(SOURCES playlistparsers/xspfparser.cpp radio/fixlastfm.cpp + radio/icecastservice.cpp radio/lastfmconfig.cpp radio/lastfmservice.cpp radio/lastfmstationdialog.cpp @@ -282,6 +283,7 @@ set(HEADERS playlistparsers/plsparser.h playlistparsers/xspfparser.h + radio/icecastservice.h radio/lastfmconfig.h radio/lastfmservice.h radio/lastfmstationdialog.h diff --git a/src/core/player.cpp b/src/core/player.cpp index dce1e983c..33f7157ca 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -398,8 +398,9 @@ void Player::EngineMetadataReceived(const Engine::SimpleMetaBundle& bundle) { bundle_copy.title = bundle_copy.title.left(dash_pos).trimmed(); } - // Hack as SomaFM's artist/title descriptions are backwards. - if (item->Url().host().contains("somafm.com")) { + // Hack as SomaFM's and icecast's artist/title descriptions are backwards. + if (item->Url().host().contains("somafm.com") || + item->Url().fragment() == "icecast") { qSwap(bundle_copy.artist, bundle_copy.title); } diff --git a/src/radio/icecastservice.cpp b/src/radio/icecastservice.cpp new file mode 100644 index 000000000..5db53f18e --- /dev/null +++ b/src/radio/icecastservice.cpp @@ -0,0 +1,170 @@ +#include "icecastservice.h" + +#include +using std::sort; +using std::unique; + +#include +#include +#include +#include + +#include "core/network.h" + +const char* IcecastService::kServiceName = "Icecast"; +const char* IcecastService::kDirectoryUrl = "http://dir.xiph.org/yp.xml"; + +IcecastService::IcecastService(RadioModel* parent) + : RadioService(kServiceName, parent), + network_(new NetworkAccessManager(this)) { +} + +IcecastService::~IcecastService() { +} + +RadioItem* IcecastService::CreateRootItem(RadioItem* parent) { + root_ = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent); + root_->icon = QIcon(":last.fm/icon_radio.png"); + return root_; +} + +void IcecastService::LazyPopulate(RadioItem* item) { + switch (item->type) { + case RadioItem::Type_Service: + LoadDirectory(); + break; + default: + break; + } +} + +void IcecastService::LoadDirectory() { + QNetworkRequest req = QNetworkRequest(QUrl(kDirectoryUrl)); + QNetworkReply* reply = network_->get(req); + connect(reply, SIGNAL(finished()), SLOT(DownloadDirectoryFinished())); +} + +void IcecastService::DownloadDirectoryFinished() { + QNetworkReply* reply = qobject_cast(sender()); + Q_ASSERT(reply); + + QFuture > future = + QtConcurrent::run(this, &IcecastService::ParseDirectory, reply); + QFutureWatcher >* watcher = + new QFutureWatcher >(this); + watcher->setFuture(future); + connect(watcher, SIGNAL(finished()), SLOT(ParseDirectoryFinished())); +} + +namespace { +template +struct GenreSorter { + GenreSorter(const QMultiHash& genres) + : genres_(genres) { + } + + bool operator() (const QString& a, const QString& b) const { + return genres_.count(a) > genres_.count(b); + } + + private: + const QMultiHash& genres_; +}; + +template +struct StationSorter { + bool operator() (T a, T b) const { + return a->name.compare(b->name, Qt::CaseInsensitive) < 0; + } +}; + +template +struct StationEquality { + bool operator() (T a, T b) const { + return a->name == b->name; + } +}; +} + +void IcecastService::ParseDirectoryFinished() { + QFutureWatcher >* watcher = + static_cast >*>(sender()); + const QList& all_stations = watcher->result(); + + // Cluster stations by genre. + QMultiHash genres; + foreach (const Station& s, all_stations) { + foreach (const QString& genre, s.genres) { + genres.insert(genre, &s); + } + } + + // Sort genres by station count. + QList genre_names = genres.keys().toSet().toList(); + sort(genre_names.begin(), genre_names.end(), GenreSorter(genres)); + + foreach (const QString& genre, genre_names) { + RadioItem* genre_item = new RadioItem(this, Type_Genre, genre); + genre_item->icon = QIcon(":last.fm/icon_tag.png"); + + QList stations = genres.values(genre); + sort(stations.begin(), stations.end(), StationSorter()); + // Remove duplicates by name. These tend to be multiple URLs for the same station. + QList::iterator it = + unique(stations.begin(), stations.end(), StationEquality()); + stations.erase(it, stations.end()); + foreach (const Station* station, stations) { + RadioItem* radio = new RadioItem( + this, Type_Stream, station->url.toString(), genre_item); + radio->lazy_loaded = true; + radio->playable = true; + radio->icon = QIcon(":last.fm/icon_radio.png"); + radio->display_text = station->name; + } + genre_item->InsertNotify(root_); + } + + root_->lazy_loaded = true; + delete watcher; +} + +QList IcecastService::ParseDirectory(QIODevice* device) const { + QXmlStreamReader reader(device); + QList stations; + while (!reader.atEnd()) { + reader.readNext(); + if (reader.tokenType() == QXmlStreamReader::StartElement && + reader.name() == "entry") { + stations << ReadStation(&reader); + } + } + device->deleteLater(); + return stations; +} + +IcecastService::Station IcecastService::ReadStation(QXmlStreamReader* reader) const { + Station station; + 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(); + if (name == "genre") station.genres = value.split(' ', QString::SkipEmptyParts); + } + } + + // HACK: This hints to the player that the artist and title metadata needs swapping. + station.url.setFragment("icecast"); + + return station; +} diff --git a/src/radio/icecastservice.h b/src/radio/icecastservice.h new file mode 100644 index 000000000..522372f51 --- /dev/null +++ b/src/radio/icecastservice.h @@ -0,0 +1,55 @@ +#ifndef ICECASTSERVICE_H +#define ICECASTSERVICE_H + +#include "radioservice.h" + +#include + +class NetworkAccessManager; + +class IcecastService : public RadioService { + Q_OBJECT + public: + IcecastService(RadioModel* parent); + ~IcecastService(); + + RadioItem* CreateRootItem(RadioItem* parent); + void LazyPopulate(RadioItem* item); + + static const char* kServiceName; + static const char* kDirectoryUrl; + + enum ItemType { + Type_Stream = 3000, + Type_Genre, + }; + + private: + struct Station { + Station() + : bitrate(0), + channels(0), + samplerate(0) { + } + QString name; + QUrl url; + QString mime_type; + int bitrate; + int channels; + int samplerate; + QStringList genres; + }; + + void LoadDirectory(); + QList ParseDirectory(QIODevice* device) const; + Station ReadStation(QXmlStreamReader* reader) const; + + RadioItem* root_; + NetworkAccessManager* network_; + + private slots: + void DownloadDirectoryFinished(); + void ParseDirectoryFinished(); +}; + +#endif diff --git a/src/radio/radiomodel.cpp b/src/radio/radiomodel.cpp index 9110409e2..f8ee201df 100644 --- a/src/radio/radiomodel.cpp +++ b/src/radio/radiomodel.cpp @@ -17,6 +17,7 @@ #include "radiomodel.h" #include "radioservice.h" +#include "icecastservice.h" #include "lastfmservice.h" #include "somafmservice.h" #include "radiomimedata.h" @@ -44,6 +45,7 @@ RadioModel::RadioModel(BackgroundThread* db_thread, AddService(new LastFMService(this)); AddService(new SomaFMService(this)); AddService(new MagnatuneService(this)); + AddService(new IcecastService(this)); AddService(new SavedRadio(this)); }