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