1 /* This file is part of Clementine.
2    Copyright 2010-2013, David Sansome <me@davidsansome.com>
3    Copyright 2011, Arnaud Bienner <arnaud.bienner@gmail.com>
4    Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
5    Copyright 2011, Paweł Bara <keirangtp@gmail.com>
6    Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
7    Copyright 2013, Alan Briolat <alan.briolat@gmail.com>
8    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
9 
10    Clementine is free software: you can redistribute it and/or modify
11    it under the terms of the GNU General Public License as published by
12    the Free Software Foundation, either version 3 of the License, or
13    (at your option) any later version.
14 
15    Clementine is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19 
20    You should have received a copy of the GNU General Public License
21    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
22 */
23 
24 #include "magnatuneservice.h"
25 
26 #include <QNetworkAccessManager>
27 #include <QNetworkRequest>
28 #include <QNetworkReply>
29 #include <QXmlStreamReader>
30 #include <QSortFilterProxyModel>
31 #include <QMenu>
32 #include <QDesktopServices>
33 #include <QCoreApplication>
34 #include <QSettings>
35 
36 #include <QtDebug>
37 
38 #include "qtiocompressor.h"
39 
40 
41 #include "magnatunedownloaddialog.h"
42 #include "magnatuneplaylistitem.h"
43 #include "magnatuneurlhandler.h"
44 #include "internet/core/internetmodel.h"
45 #include "core/application.h"
46 #include "core/database.h"
47 #include "core/logging.h"
48 #include "core/mergedproxymodel.h"
49 #include "core/network.h"
50 #include "core/player.h"
51 #include "core/song.h"
52 #include "core/taskmanager.h"
53 #include "core/timeconstants.h"
54 #include "globalsearch/globalsearch.h"
55 #include "globalsearch/librarysearchprovider.h"
56 #include "library/librarymodel.h"
57 #include "library/librarybackend.h"
58 #include "library/libraryfilterwidget.h"
59 #include "ui/iconloader.h"
60 #include "ui/settingsdialog.h"
61 
62 const char* MagnatuneService::kServiceName = "Magnatune";
63 const char* MagnatuneService::kSettingsGroup = "Magnatune";
64 const char* MagnatuneService::kSongsTable = "magnatune_songs";
65 const char* MagnatuneService::kFtsTable = "magnatune_songs_fts";
66 
67 const char* MagnatuneService::kHomepage = "http://magnatune.com";
68 const char* MagnatuneService::kDatabaseUrl =
69     "http://magnatune.com/info/song_info_xml.gz";
70 const char* MagnatuneService::kStreamingHostname = "streaming.magnatune.com";
71 const char* MagnatuneService::kDownloadHostname = "download.magnatune.com";
72 
73 const char* MagnatuneService::kPartnerId = "clementine";
74 const char* MagnatuneService::kDownloadUrl =
75     "http://download.magnatune.com/buy/membership_free_dl_xml";
76 
MagnatuneService(Application * app,InternetModel * parent)77 MagnatuneService::MagnatuneService(Application* app, InternetModel* parent)
78     : InternetService(kServiceName, app, parent, parent),
79       url_handler_(new MagnatuneUrlHandler(this, this)),
80       context_menu_(nullptr),
81       root_(nullptr),
82       library_backend_(nullptr),
83       library_model_(nullptr),
84       library_filter_(nullptr),
85       library_sort_model_(new QSortFilterProxyModel(this)),
86       load_database_task_id_(0),
87       membership_(Membership_None),
88       format_(Format_Ogg),
89       total_song_count_(0),
90       network_(new NetworkAccessManager(this)) {
91   // Create the library backend in the database thread
92   library_backend_ = new LibraryBackend;
93   library_backend_->moveToThread(app_->database()->thread());
94   library_backend_->Init(app_->database(), kSongsTable, QString(), QString(),
95                          kFtsTable);
96   library_model_ = new LibraryModel(library_backend_, app_, this);
97 
98   connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
99           SLOT(UpdateTotalSongCount(int)));
100 
101   library_sort_model_->setSourceModel(library_model_);
102   library_sort_model_->setSortRole(LibraryModel::Role_SortText);
103   library_sort_model_->setDynamicSortFilter(true);
104   library_sort_model_->setSortLocaleAware(true);
105   library_sort_model_->sort(0);
106 
107   app_->player()->RegisterUrlHandler(url_handler_);
108   app_->global_search()->AddProvider(new LibrarySearchProvider(
109       library_backend_, tr("Magnatune"), "magnatune",
110       IconLoader::Load("magnatune", IconLoader::Provider),
111       true, app_, this));
112 }
113 
~MagnatuneService()114 MagnatuneService::~MagnatuneService() { delete context_menu_; }
115 
ReloadSettings()116 void MagnatuneService::ReloadSettings() {
117   QSettings s;
118   s.beginGroup(kSettingsGroup);
119 
120   membership_ = MembershipType(s.value("membership", Membership_None).toInt());
121   username_ = s.value("username").toString();
122   password_ = s.value("password").toString();
123   format_ = PreferredFormat(s.value("format", Format_Ogg).toInt());
124 }
125 
CreateRootItem()126 QStandardItem* MagnatuneService::CreateRootItem() {
127   root_ = new QStandardItem(IconLoader::Load("magnatune", IconLoader::Provider),
128                             kServiceName);
129   root_->setData(true, InternetModel::Role_CanLazyLoad);
130   return root_;
131 }
132 
LazyPopulate(QStandardItem * item)133 void MagnatuneService::LazyPopulate(QStandardItem* item) {
134   switch (item->data(InternetModel::Role_Type).toInt()) {
135     case InternetModel::Type_Service:
136       library_model_->Init();
137       if (total_song_count_ == 0 && !load_database_task_id_) {
138         ReloadDatabase();
139       }
140       model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
141       break;
142 
143     default:
144       break;
145   }
146 }
147 
UpdateTotalSongCount(int count)148 void MagnatuneService::UpdateTotalSongCount(int count) {
149   total_song_count_ = count;
150 }
151 
ReloadDatabase()152 void MagnatuneService::ReloadDatabase() {
153   QNetworkRequest request = QNetworkRequest(QUrl(kDatabaseUrl));
154   request.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
155                        QNetworkRequest::AlwaysNetwork);
156 
157   QNetworkReply* reply = network_->get(request);
158   connect(reply, SIGNAL(finished()), SLOT(ReloadDatabaseFinished()));
159 
160   if (!load_database_task_id_)
161     load_database_task_id_ =
162         app_->task_manager()->StartTask(tr("Downloading Magnatune catalogue"));
163 }
164 
ReloadDatabaseFinished()165 void MagnatuneService::ReloadDatabaseFinished() {
166   QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
167 
168   app_->task_manager()->SetTaskFinished(load_database_task_id_);
169   load_database_task_id_ = 0;
170 
171   if (reply->error() != QNetworkReply::NoError) {
172     // TODO(David Sansome): Error handling
173     qLog(Error) << reply->errorString();
174     return;
175   }
176 
177   if (root_->hasChildren()) root_->removeRows(0, root_->rowCount());
178 
179   // The XML file is compressed
180   QtIOCompressor gzip(reply);
181   gzip.setStreamFormat(QtIOCompressor::GzipFormat);
182   if (!gzip.open(QIODevice::ReadOnly)) {
183     qLog(Warning) << "Error opening gzip stream";
184     return;
185   }
186 
187   // Remove all existing songs in the database
188   library_backend_->DeleteAll();
189 
190   // Parse the XML we got from Magnatune
191   QXmlStreamReader reader(&gzip);
192   SongList songs;
193   while (!reader.atEnd()) {
194     reader.readNext();
195 
196     if (reader.tokenType() == QXmlStreamReader::StartElement &&
197         reader.name() == "Track") {
198       songs << ReadTrack(reader);
199     }
200   }
201 
202   // Add the songs to the database
203   library_backend_->AddOrUpdateSongs(songs);
204   library_model_->Reset();
205 }
206 
ReadTrack(QXmlStreamReader & reader)207 Song MagnatuneService::ReadTrack(QXmlStreamReader& reader) {
208   Song song;
209 
210   while (!reader.atEnd()) {
211     reader.readNext();
212 
213     if (reader.tokenType() == QXmlStreamReader::EndElement) break;
214 
215     if (reader.tokenType() == QXmlStreamReader::StartElement) {
216       QStringRef name = reader.name();
217       QString value = ReadElementText(reader);
218 
219       if (name == "artist") song.set_artist(value);
220       if (name == "albumname") song.set_album(value);
221       if (name == "trackname") song.set_title(value);
222       if (name == "tracknum") song.set_track(value.toInt());
223       if (name == "year") song.set_year(value.toInt());
224       if (name == "magnatunegenres") song.set_genre(value.section(',', 0, 0));
225       if (name == "seconds")
226         song.set_length_nanosec(value.toInt() * kNsecPerSec);
227       if (name == "cover_small") song.set_art_automatic(value);
228       if (name == "albumsku") song.set_comment(value);
229       if (name == "url") {
230         QUrl url;
231         // Magnatune's URLs are already encoded
232         url.setUrl(value.toLocal8Bit());
233         url.setScheme("magnatune");
234         song.set_url(url);
235       }
236     }
237   }
238 
239   song.set_valid(true);
240   song.set_filetype(Song::Type_Stream);
241 
242   // We need to set these to satisfy the database constraints
243   song.set_directory_id(0);
244   song.set_mtime(0);
245   song.set_ctime(0);
246   song.set_filesize(0);
247 
248   return song;
249 }
250 
251 // TODO(David Sansome): Replace with readElementText(SkipChildElements) in Qt 4.6
ReadElementText(QXmlStreamReader & reader)252 QString MagnatuneService::ReadElementText(QXmlStreamReader& reader) {
253   int level = 1;
254   QString ret;
255   while (!reader.atEnd()) {
256     switch (reader.readNext()) {
257       case QXmlStreamReader::StartElement:
258         level++;
259         break;
260       case QXmlStreamReader::EndElement:
261         level--;
262         break;
263       case QXmlStreamReader::Characters:
264         ret += reader.text().toString().trimmed();
265         break;
266       default:
267         break;
268     }
269 
270     if (level == 0) break;
271   }
272   return ret;
273 }
274 
EnsureMenuCreated()275 void MagnatuneService::EnsureMenuCreated() {
276   if (context_menu_) return;
277 
278   context_menu_ = new QMenu;
279 
280   context_menu_->addActions(GetPlaylistActions());
281   download_ = context_menu_->addAction(IconLoader::Load("download",
282                                        IconLoader::Base),
283                                        tr("Download this album"), this,
284                                        SLOT(Download()));
285   context_menu_->addSeparator();
286   context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
287                            tr("Open %1 in browser").arg("magnatune.com"), this,
288                            SLOT(Homepage()));
289   context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base),
290                            tr("Refresh catalogue"), this,
291                            SLOT(ReloadDatabase()));
292   QAction* config_action = context_menu_->addAction(
293       IconLoader::Load("configure", IconLoader::Base), tr("Configure Magnatune..."),
294       this, SLOT(ShowConfig()));
295 
296   library_filter_ = new LibraryFilterWidget(0);
297   library_filter_->SetSettingsGroup(kSettingsGroup);
298   library_filter_->SetLibraryModel(library_model_);
299   library_filter_->SetFilterHint(tr("Search Magnatune"));
300   library_filter_->SetAgeFilterEnabled(false);
301   library_filter_->AddMenuAction(config_action);
302 
303   context_menu_->addSeparator();
304   context_menu_->addMenu(library_filter_->menu());
305 }
306 
ShowContextMenu(const QPoint & global_pos)307 void MagnatuneService::ShowContextMenu(const QPoint& global_pos) {
308   EnsureMenuCreated();
309 
310   const bool is_valid = model()->current_index().model() == library_sort_model_;
311 
312   GetAppendToPlaylistAction()->setEnabled(is_valid);
313   GetReplacePlaylistAction()->setEnabled(is_valid);
314   GetOpenInNewPlaylistAction()->setEnabled(is_valid);
315   download_->setEnabled(is_valid && membership_ == Membership_Download);
316   context_menu_->popup(global_pos);
317 }
318 
Homepage()319 void MagnatuneService::Homepage() {
320   QDesktopServices::openUrl(QUrl(kHomepage));
321 }
322 
ModifyUrl(const QUrl & url) const323 QUrl MagnatuneService::ModifyUrl(const QUrl& url) const {
324   QUrl ret(url);
325   ret.setScheme("http");
326 
327   switch (membership_) {
328     case Membership_None:
329       return ret;  // Use the URL as-is
330 
331     // Otherwise add the hostname
332     case Membership_Streaming:
333       ret.setHost(kStreamingHostname);
334       break;
335     case Membership_Download:
336       ret.setHost(kDownloadHostname);
337       break;
338   }
339 
340   // Add the credentials
341   ret.setUserName(username_);
342   ret.setPassword(password_);
343 
344   // And remove the commercial
345   QString path = ret.path();
346   path.insert(path.lastIndexOf('.'), "_nospeech");
347   ret.setPath(path);
348 
349   return ret;
350 }
351 
ShowConfig()352 void MagnatuneService::ShowConfig() {
353   app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Magnatune);
354 }
355 
Download()356 void MagnatuneService::Download() {
357   QModelIndex index =
358       library_sort_model_->mapToSource(model()->current_index());
359   SongList songs = library_model_->GetChildSongs(index);
360 
361   MagnatuneDownloadDialog* dialog = new MagnatuneDownloadDialog(this, 0);
362   dialog->setAttribute(Qt::WA_DeleteOnClose);
363   dialog->Show(songs);
364 
365   connect(dialog, SIGNAL(Finished(QStringList)),
366           SIGNAL(DownloadFinished(QStringList)));
367 }
368 
HeaderWidget() const369 QWidget* MagnatuneService::HeaderWidget() const {
370   const_cast<MagnatuneService*>(this)->EnsureMenuCreated();
371   return library_filter_;
372 }
373