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