1 /* This file is part of Clementine.
2 Copyright 2010-2012, David Sansome <me@davidsansome.com>
3 Copyright 2010, 2012, 2014, John Maguire <john.maguire@gmail.com>
4 Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
5 Copyright 2011, Paweł Bara <keirangtp@gmail.com>
6 Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
7
8 Clementine is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 Clementine is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 #include "internet/icecast/icecastservice.h"
23
24 #include <algorithm>
25
26 #include <QDesktopServices>
27 #include <QMenu>
28 #include <QMultiHash>
29 #include <QNetworkReply>
30 #include <QRegExp>
31 #include <QtConcurrentRun>
32
33 #include "core/application.h"
34 #include "core/closure.h"
35 #include "core/database.h"
36 #include "core/mergedproxymodel.h"
37 #include "core/network.h"
38 #include "core/taskmanager.h"
39 #include "globalsearch/globalsearch.h"
40 #include "globalsearch/icecastsearchprovider.h"
41 #include "internet/core/internetmodel.h"
42 #include "internet/icecast/icecastbackend.h"
43 #include "internet/icecast/icecastfilterwidget.h"
44 #include "internet/icecast/icecastmodel.h"
45 #include "playlist/songplaylistitem.h"
46 #include "ui/iconloader.h"
47
48 using std::sort;
49 using std::unique;
50
51 const char* IcecastService::kServiceName = "Icecast";
52 const char* IcecastService::kDirectoryUrl =
53 "http://data.clementine-player.org/icecast-directory";
54 const char* IcecastService::kHomepage = "http://dir.xiph.org/";
55
IcecastService(Application * app,InternetModel * parent)56 IcecastService::IcecastService(Application* app, InternetModel* parent)
57 : InternetService(kServiceName, app, parent, parent),
58 network_(new NetworkAccessManager(this)),
59 context_menu_(nullptr),
60 backend_(nullptr),
61 model_(nullptr),
62 filter_(new IcecastFilterWidget(0)) {
63 backend_ = new IcecastBackend;
64 backend_->moveToThread(app_->database()->thread());
65 backend_->Init(app_->database());
66
67 model_ = new IcecastModel(backend_, this);
68 filter_->SetIcecastModel(model_);
69
70 app_->global_search()->AddProvider(
71 new IcecastSearchProvider(backend_, app_, this));
72 }
73
~IcecastService()74 IcecastService::~IcecastService() {}
75
CreateRootItem()76 QStandardItem* IcecastService::CreateRootItem() {
77 root_ = new QStandardItem(IconLoader::Load("icon_radio", IconLoader::Lastfm),
78 kServiceName);
79 root_->setData(true, InternetModel::Role_CanLazyLoad);
80 return root_;
81 }
82
LazyPopulate(QStandardItem * item)83 void IcecastService::LazyPopulate(QStandardItem* item) {
84 switch (item->data(InternetModel::Role_Type).toInt()) {
85 case InternetModel::Type_Service:
86 model_->Init();
87 model()->merged_model()->AddSubModel(model()->indexFromItem(item),
88 model_);
89
90 if (backend_->IsEmpty()) {
91 LoadDirectory();
92 }
93
94 break;
95 default:
96 break;
97 }
98 }
99
LoadDirectory()100 void IcecastService::LoadDirectory() {
101 int task_id =
102 app_->task_manager()->StartTask(tr("Downloading Icecast directory"));
103 RequestDirectory(QUrl(kDirectoryUrl), task_id);
104 }
105
RequestDirectory(const QUrl & url,int task_id)106 void IcecastService::RequestDirectory(const QUrl& url, int task_id) {
107 QNetworkRequest req = QNetworkRequest(url);
108 req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
109 QNetworkRequest::AlwaysNetwork);
110
111 QNetworkReply* reply = network_->get(req);
112 NewClosure(reply, SIGNAL(finished()), this,
113 SLOT(DownloadDirectoryFinished(QNetworkReply*, int)), reply,
114 task_id);
115 }
116
DownloadDirectoryFinished(QNetworkReply * reply,int task_id)117 void IcecastService::DownloadDirectoryFinished(QNetworkReply* reply,
118 int task_id) {
119 if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
120 // Discard the old reply and follow the redirect
121 reply->deleteLater();
122 RequestDirectory(
123 reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(),
124 task_id);
125 return;
126 }
127
128 QFuture<IcecastBackend::StationList> future =
129 QtConcurrent::run(this, &IcecastService::ParseDirectory, reply);
130 NewClosure(future, this, SLOT(ParseDirectoryFinished(
131 QFuture<IcecastBackend::StationList>, int)),
132 future, task_id);
133 }
134
135 namespace {
136 template <typename T>
137 struct GenreSorter {
GenreSorter__anonba87eee70111::GenreSorter138 explicit GenreSorter(const QMultiHash<QString, T>& genres)
139 : genres_(genres) {}
140
operator ()__anonba87eee70111::GenreSorter141 bool operator()(const QString& a, const QString& b) const {
142 return genres_.count(a) > genres_.count(b);
143 }
144
145 private:
146 const QMultiHash<QString, T>& genres_;
147 };
148
149 template <typename T>
150 struct StationSorter {
operator ()__anonba87eee70111::StationSorter151 bool operator()(const T& a, const T& b) const {
152 return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
153 }
154 };
155
156 template <typename T>
157 struct StationSorter<T*> {
operator ()__anonba87eee70111::StationSorter158 bool operator()(const T* a, const T* b) const {
159 return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
160 }
161 };
162
163 template <typename T>
164 struct StationEquality {
operator ()__anonba87eee70111::StationEquality165 bool operator()(T a, T b) const { return a.name == b.name; }
166 };
167
FilterGenres(const QStringList & genres)168 QStringList FilterGenres(const QStringList& genres) {
169 QStringList ret;
170 for (const QString& genre : genres) {
171 if (genre.length() < 2) continue;
172 if (genre.contains("ÃÂ")) continue; // Broken unicode.
173 if (genre.contains(QRegExp("^#x[0-9a-f][0-9a-f]")))
174 continue; // Broken XML entities.
175
176 // Convert 80 -> 80s.
177 if (genre.contains(QRegExp("^[0-9]0$"))) {
178 ret << genre + 's';
179 } else {
180 ret << genre;
181 }
182 }
183
184 if (ret.empty()) {
185 ret << "other";
186 }
187 return ret;
188 }
189 } // namespace
190
ParseDirectoryFinished(QFuture<IcecastBackend::StationList> future,int task_id)191 void IcecastService::ParseDirectoryFinished(
192 QFuture<IcecastBackend::StationList> future, int task_id) {
193 IcecastBackend::StationList all_stations = future.result();
194 sort(all_stations.begin(), all_stations.end(),
195 StationSorter<IcecastBackend::Station>());
196 // Remove duplicates by name. These tend to be multiple URLs for the same
197 // station.
198 IcecastBackend::StationList::iterator it =
199 unique(all_stations.begin(), all_stations.end(),
200 StationEquality<IcecastBackend::Station>());
201 all_stations.erase(it, all_stations.end());
202
203 // Cluster stations by genre.
204 QMultiHash<QString, IcecastBackend::Station*> genres;
205
206 // Add stations.
207 for (int i = 0; i < all_stations.count(); ++i) {
208 IcecastBackend::Station& s = all_stations[i];
209 genres.insert(s.genre, &s);
210 }
211
212 QSet<QString> genre_set = genres.keys().toSet();
213
214 // Merge genres with only 1 or 2 stations into "Other".
215 for (const QString& genre : genre_set) {
216 if (genres.count(genre) < 3) {
217 const QList<IcecastBackend::Station*>& small_genre = genres.values(genre);
218 for (IcecastBackend::Station* s : small_genre) {
219 s->genre = "Other";
220 }
221 }
222 }
223
224 backend_->ClearAndAddStations(all_stations);
225
226 app_->task_manager()->SetTaskFinished(task_id);
227 }
228
ParseDirectory(QIODevice * device) const229 IcecastBackend::StationList IcecastService::ParseDirectory(
230 QIODevice* device) const {
231 QXmlStreamReader reader(device);
232 IcecastBackend::StationList stations;
233 while (!reader.atEnd()) {
234 reader.readNext();
235 if (reader.tokenType() == QXmlStreamReader::StartElement &&
236 reader.name() == "entry") {
237 stations << ReadStation(&reader);
238 }
239 }
240 device->deleteLater();
241 return stations;
242 }
243
ReadStation(QXmlStreamReader * reader) const244 IcecastBackend::Station IcecastService::ReadStation(
245 QXmlStreamReader* reader) const {
246 IcecastBackend::Station station;
247 while (!reader->atEnd()) {
248 reader->readNext();
249 if (reader->tokenType() == QXmlStreamReader::EndElement) break;
250
251 if (reader->tokenType() == QXmlStreamReader::StartElement) {
252 QStringRef name = reader->name();
253 QString value =
254 reader->readElementText(QXmlStreamReader::SkipChildElements);
255
256 if (name == "server_name") station.name = value;
257 if (name == "listen_url") station.url = QUrl(value);
258 if (name == "server_type") station.mime_type = value;
259 if (name == "bitrate") station.bitrate = value.toInt();
260 if (name == "channels") station.channels = value.toInt();
261 if (name == "samplerate") station.samplerate = value.toInt();
262 if (name == "genre")
263 station.genre =
264 FilterGenres(value.split(' ', QString::SkipEmptyParts))[0];
265 }
266 }
267
268 // Title case the genre
269 if (!station.genre.isEmpty()) {
270 station.genre[0] = station.genre[0].toUpper();
271 }
272
273 return station;
274 }
275
HeaderWidget() const276 QWidget* IcecastService::HeaderWidget() const { return filter_; }
277
ShowContextMenu(const QPoint & global_pos)278 void IcecastService::ShowContextMenu(const QPoint& global_pos) {
279 EnsureMenuCreated();
280
281 const bool can_play = model()->current_index().isValid() &&
282 model()->current_index().model() == model_ &&
283 model_->GetSong(model()->current_index()).is_valid();
284
285 GetAppendToPlaylistAction()->setEnabled(can_play);
286 GetReplacePlaylistAction()->setEnabled(can_play);
287 GetOpenInNewPlaylistAction()->setEnabled(can_play);
288 context_menu_->popup(global_pos);
289 }
290
EnsureMenuCreated()291 void IcecastService::EnsureMenuCreated() {
292 if (context_menu_) return;
293
294 context_menu_ = new QMenu;
295
296 context_menu_->addActions(GetPlaylistActions());
297 context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
298 tr("Open %1 in browser").arg("dir.xiph.org"), this,
299 SLOT(Homepage()));
300 context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base),
301 tr("Refresh station list"), this,
302 SLOT(LoadDirectory()));
303
304 context_menu_->addSeparator();
305 context_menu_->addMenu(filter_->menu());
306 }
307
Homepage()308 void IcecastService::Homepage() { QDesktopServices::openUrl(QUrl(kHomepage)); }
309