1
/* This file is part of Clementine.
2
Copyright 2010, David Sansome <me@davidsansome.com>
4
Clementine is free software: you can redistribute it and/or modify
5
it under the terms of the GNU General Public License as published by
6
the Free Software Foundation, either version 3 of the License, or
7
(at your option) any later version.
9
Clementine is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
GNU General Public License for more details.
14
You should have received a copy of the GNU General Public License
15
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
18
#include "jamendoservice.h"
20
#include "jamendodynamicplaylist.h"
21
#include "jamendoplaylistitem.h"
22
#include "internetmodel.h"
23
#include "core/database.h"
24
#include "core/logging.h"
25
#include "core/mergedproxymodel.h"
26
#include "core/network.h"
27
#include "core/scopedtransaction.h"
28
#include "core/taskmanager.h"
29
#include "core/timeconstants.h"
30
#include "globalsearch/globalsearch.h"
31
#include "globalsearch/librarysearchprovider.h"
32
#include "library/librarybackend.h"
33
#include "library/libraryfilterwidget.h"
34
#include "library/librarymodel.h"
35
#include "smartplaylists/generator.h"
36
#include "smartplaylists/querygenerator.h"
37
#include "ui/iconloader.h"
39
#include <QDesktopServices>
40
#include <QFutureWatcher>
42
#include <QMessageBox>
43
#include <QNetworkReply>
44
#include <QSortFilterProxyModel>
45
#include <QtConcurrentRun>
46
#include <QXmlStreamReader>
47
#include "qtiocompressor.h"
49
const char* JamendoService::kServiceName = "Jamendo";
50
const char* JamendoService::kDirectoryUrl =
51
"http://img.jamendo.com/data/dbdump_artistalbumtrack.xml.gz";
52
const char* JamendoService::kMp3StreamUrl =
53
"http://api.jamendo.com/get2/stream/track/redirect/?id=%1&streamencoding=mp31";
54
const char* JamendoService::kOggStreamUrl =
55
"http://api.jamendo.com/get2/stream/track/redirect/?id=%1&streamencoding=ogg2";
56
const char* JamendoService::kAlbumCoverUrl =
57
"http://api.jamendo.com/get2/image/album/redirect/?id=%1&imagesize=300";
58
const char* JamendoService::kHomepage = "http://www.jamendo.com/";
59
const char* JamendoService::kAlbumInfoUrl = "http://www.jamendo.com/album/%1";
60
const char* JamendoService::kDownloadAlbumUrl = "http://www.jamendo.com/download/album/%1";
62
const char* JamendoService::kSongsTable = "jamendo.songs";
63
const char* JamendoService::kFtsTable = "jamendo.songs_fts";
64
const char* JamendoService::kTrackIdsTable = "jamendo.track_ids";
65
const char* JamendoService::kTrackIdsColumn = "track_id";
67
const char* JamendoService::kSettingsGroup = "Jamendo";
69
const int JamendoService::kBatchSize = 10000;
70
const int JamendoService::kApproxDatabaseSize = 300000;
72
JamendoService::JamendoService(InternetModel* parent)
73
: InternetService(kServiceName, parent, parent),
74
network_(new NetworkAccessManager(this)),
76
library_backend_(NULL),
77
library_filter_(NULL),
79
library_sort_model_(new QSortFilterProxyModel(this)),
80
load_database_task_id_(0),
82
accepted_download_(false) {
83
library_backend_ = new LibraryBackend;
84
library_backend_->moveToThread(parent->db_thread());
85
library_backend_->Init(parent->db_thread()->Worker(), kSongsTable,
86
QString::null, QString::null, kFtsTable);
87
connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
88
SLOT(UpdateTotalSongCount(int)));
90
using smart_playlists::Generator;
91
using smart_playlists::GeneratorPtr;
92
using smart_playlists::QueryGenerator;
93
using smart_playlists::Search;
94
using smart_playlists::SearchTerm;
96
library_model_ = new LibraryModel(library_backend_, parent->task_manager(), this);
97
library_model_->set_show_various_artists(false);
98
library_model_->set_show_smart_playlists(false);
99
library_model_->set_default_smart_playlists(LibraryModel::DefaultGenerators()
100
<< (LibraryModel::GeneratorList()
101
<< GeneratorPtr(new JamendoDynamicPlaylist(tr("Jamendo Top Tracks of the Month"),
102
JamendoDynamicPlaylist::OrderBy_RatingMonth))
103
<< GeneratorPtr(new JamendoDynamicPlaylist(tr("Jamendo Top Tracks of the Week"),
104
JamendoDynamicPlaylist::OrderBy_RatingWeek))
105
<< GeneratorPtr(new JamendoDynamicPlaylist(tr("Jamendo Top Tracks"),
106
JamendoDynamicPlaylist::OrderBy_Rating))
107
<< GeneratorPtr(new JamendoDynamicPlaylist(tr("Jamendo Most Listened Tracks"),
108
JamendoDynamicPlaylist::OrderBy_Listened))
110
<< (LibraryModel::GeneratorList()
111
<< GeneratorPtr(new QueryGenerator(tr("Dynamic random mix"), Search(
112
Search::Type_All, Search::TermList(),
113
Search::Sort_Random, SearchTerm::Field_Title), true))
117
library_sort_model_->setSourceModel(library_model_);
118
library_sort_model_->setSortRole(LibraryModel::Role_SortText);
119
library_sort_model_->setDynamicSortFilter(true);
120
library_sort_model_->sort(0);
122
model()->global_search()->AddProvider(new LibrarySearchProvider(
126
QIcon(":/providers/jamendo.png"),
131
JamendoService::~JamendoService() {
134
QStandardItem* JamendoService::CreateRootItem() {
135
QStandardItem* item = new QStandardItem(QIcon(":providers/jamendo.png"), kServiceName);
136
item->setData(true, InternetModel::Role_CanLazyLoad);
140
void JamendoService::LazyPopulate(QStandardItem* item) {
141
switch (item->data(InternetModel::Role_Type).toInt()) {
142
case InternetModel::Type_Service: {
143
library_model_->Init();
144
model()->merged_model()->AddSubModel(item->index(), library_sort_model_);
152
void JamendoService::UpdateTotalSongCount(int count) {
153
total_song_count_ = count;
154
if (total_song_count_ == 0 && !load_database_task_id_) {
158
//show smart playlist in song count if the db is loaded
159
library_model_->set_show_smart_playlists(true);
160
accepted_download_ = true; //the user has previously accepted
164
void JamendoService::DownloadDirectory() {
165
//don't ask if we're refreshing the database
166
if (total_song_count_ == 0) {
167
if (QMessageBox::question(context_menu_, tr("Jamendo database"), tr("This action will create a database which could be as big as 150 MB.\n"
168
"Do you want to continue anyway?"), QMessageBox::Ok | QMessageBox::Cancel) != QMessageBox::Ok)
171
accepted_download_ = true;
172
QNetworkRequest req = QNetworkRequest(QUrl(kDirectoryUrl));
173
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
174
QNetworkRequest::AlwaysNetwork);
176
QNetworkReply* reply = network_->get(req);
177
connect(reply, SIGNAL(finished()), SLOT(DownloadDirectoryFinished()));
178
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
179
SLOT(DownloadDirectoryProgress(qint64,qint64)));
181
if (!load_database_task_id_) {
182
load_database_task_id_ = model()->task_manager()->StartTask(
183
tr("Downloading Jamendo catalogue"));
187
void JamendoService::DownloadDirectoryProgress(qint64 received, qint64 total) {
188
float progress = float(received) / total;
189
model()->task_manager()->SetTaskProgress(load_database_task_id_,
190
int(progress * 100), 100);
193
void JamendoService::DownloadDirectoryFinished() {
194
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
197
model()->task_manager()->SetTaskFinished(load_database_task_id_);
198
load_database_task_id_ = 0;
200
// TODO: Not leak reply.
201
QtIOCompressor* gzip = new QtIOCompressor(reply);
202
gzip->setStreamFormat(QtIOCompressor::GzipFormat);
203
if (!gzip->open(QIODevice::ReadOnly)) {
204
qLog(Warning) << "Jamendo library not in gzip format";
209
load_database_task_id_ = model()->task_manager()->StartTask(
210
tr("Parsing Jamendo catalogue"));
212
QFuture<void> future = QtConcurrent::run(
213
this, &JamendoService::ParseDirectory, gzip);
214
QFutureWatcher<void>* watcher = new QFutureWatcher<void>();
215
watcher->setFuture(future);
216
connect(watcher, SIGNAL(finished()), SLOT(ParseDirectoryFinished()));
219
void JamendoService::ParseDirectory(QIODevice* device) const {
222
// Bit of a hack: don't update the model while we're parsing the xml
223
disconnect(library_backend_, SIGNAL(SongsDiscovered(SongList)),
224
library_model_, SLOT(SongsDiscovered(SongList)));
225
disconnect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
226
this, SLOT(UpdateTotalSongCount(int)));
228
// Delete the database and recreate it. This is faster than dropping tables
230
library_backend_->db()->RecreateAttachedDb("jamendo");
232
TrackIdList track_ids;
234
QXmlStreamReader reader(device);
235
while (!reader.atEnd()) {
237
if (reader.tokenType() == QXmlStreamReader::StartElement &&
238
reader.name() == "artist") {
239
songs << ReadArtist(&reader, &track_ids);
242
if (songs.count() >= kBatchSize) {
243
// Add the songs to the database in batches
244
library_backend_->AddOrUpdateSongs(songs);
245
InsertTrackIds(track_ids);
247
total_count += songs.count();
251
// Update progress info
252
model()->task_manager()->SetTaskProgress(
253
load_database_task_id_, total_count, kApproxDatabaseSize);
257
library_backend_->AddOrUpdateSongs(songs);
258
InsertTrackIds(track_ids);
260
connect(library_backend_, SIGNAL(SongsDiscovered(SongList)),
261
library_model_, SLOT(SongsDiscovered(SongList)));
262
connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
263
SLOT(UpdateTotalSongCount(int)));
265
library_backend_->UpdateTotalSongCount();
268
void JamendoService::InsertTrackIds(const TrackIdList& ids) const {
269
QMutexLocker l(library_backend_->db()->Mutex());
270
QSqlDatabase db(library_backend_->db()->Connect());
272
ScopedTransaction t(&db);
274
QSqlQuery insert(QString("INSERT INTO %1 (%2) VALUES (:id)")
275
.arg(kTrackIdsTable, kTrackIdsColumn), db);
277
foreach (int id, ids) {
278
insert.bindValue(":id", id);
279
if (!insert.exec()) {
280
qLog(Warning) << "Query failed" << insert.lastQuery();
287
SongList JamendoService::ReadArtist(QXmlStreamReader* reader,
288
TrackIdList* track_ids) const {
290
QString current_artist;
292
while (!reader->atEnd()) {
295
if (reader->tokenType() == QXmlStreamReader::StartElement) {
296
QStringRef name = reader->name();
297
if (name == "name") {
298
current_artist = reader->readElementText().trimmed();
299
} else if (name == "album") {
300
ret << ReadAlbum(current_artist, reader, track_ids);
302
} else if (reader->isEndElement() && reader->name() == "artist") {
310
SongList JamendoService::ReadAlbum(
311
const QString& artist, QXmlStreamReader* reader, TrackIdList* track_ids) const {
313
QString current_album;
315
int current_album_id = 0;
317
while (!reader->atEnd()) {
320
if (reader->tokenType() == QXmlStreamReader::StartElement) {
321
if (reader->name() == "name") {
322
current_album = reader->readElementText().trimmed();
323
} else if (reader->name() == "id") {
324
QString id = reader->readElementText();
325
cover = QString(kAlbumCoverUrl).arg(id);
326
current_album_id = id.toInt();
327
} else if (reader->name() == "track") {
328
ret << ReadTrack(artist, current_album, cover, current_album_id,
331
} else if (reader->isEndElement() && reader->name() == "album") {
338
Song JamendoService::ReadTrack(const QString& artist,
339
const QString& album,
340
const QString& album_cover,
342
QXmlStreamReader* reader,
343
TrackIdList* track_ids) const {
345
song.set_artist(artist);
346
song.set_album(album);
347
song.set_filetype(Song::Type_Stream);
348
song.set_directory_id(0);
351
song.set_filesize(0);
353
// Shoehorn the album ID into the comment field
354
song.set_comment(QString::number(album_id));
356
while (!reader->atEnd()) {
358
if (reader->isStartElement()) {
359
QStringRef name = reader->name();
360
if (name == "name") {
361
song.set_title(reader->readElementText().trimmed());
362
} else if (name == "duration") {
363
const int length = reader->readElementText().toFloat();
364
song.set_length_nanosec(length * kNsecPerSec);
365
} else if (name == "id3genre") {
366
int genre_id = reader->readElementText().toInt();
367
// In theory, genre 0 is "blues"; in practice it's invalid.
369
song.set_genre_id3(genre_id);
371
} else if (name == "id") {
372
QString id_text = reader->readElementText();
373
int id = id_text.toInt();
377
QString mp3_url = QString(kMp3StreamUrl).arg(id_text);
378
song.set_url(QUrl(mp3_url));
379
song.set_art_automatic(album_cover);
380
song.set_valid(true);
382
// Rely on songs getting added in this exact order
383
track_ids->append(id);
385
} else if (reader->isEndElement() && reader->name() == "track") {
392
void JamendoService::ParseDirectoryFinished() {
393
QFutureWatcher<void>* watcher = static_cast<QFutureWatcher<void>*>(sender());
396
//show smart playlists
397
library_model_->set_show_smart_playlists(true);
398
library_model_->Reset();
400
model()->task_manager()->SetTaskFinished(load_database_task_id_);
401
load_database_task_id_ = 0;
404
void JamendoService::EnsureMenuCreated() {
408
context_menu_ = new QMenu;
409
context_menu_->addActions(GetPlaylistActions());
410
album_info_ = context_menu_->addAction(IconLoader::Load("view-media-lyrics"),
411
tr("Album info on jamendo.com..."), this, SLOT(AlbumInfo()));
412
download_album_ = context_menu_->addAction(IconLoader::Load("download"),
413
tr("Download this album..."), this, SLOT(DownloadAlbum()));
414
context_menu_->addSeparator();
415
context_menu_->addAction(IconLoader::Load("download"), tr("Open %1 in browser").arg("jamendo.com"), this, SLOT(Homepage()));
416
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Refresh catalogue"), this, SLOT(DownloadDirectory()));
418
if (accepted_download_) {
419
library_filter_ = new LibraryFilterWidget(0);
420
library_filter_->SetSettingsGroup(kSettingsGroup);
421
library_filter_->SetLibraryModel(library_model_);
422
library_filter_->SetFilterHint(tr("Search Jamendo"));
423
library_filter_->SetAgeFilterEnabled(false);
425
context_menu_->addSeparator();
426
context_menu_->addMenu(library_filter_->menu());
430
void JamendoService::ShowContextMenu(const QModelIndex& index, const QPoint& global_pos) {
433
if (index.model() == library_sort_model_) {
434
context_item_ = index;
436
context_item_ = QModelIndex();
439
const bool enabled = accepted_download_ && context_item_.isValid();
441
//make menu items visible and enabled only when needed
442
GetAppendToPlaylistAction()->setVisible(accepted_download_);
443
GetAppendToPlaylistAction()->setEnabled(enabled);
444
GetReplacePlaylistAction()->setVisible(accepted_download_);
445
GetReplacePlaylistAction()->setEnabled(enabled);
446
GetOpenInNewPlaylistAction()->setEnabled(enabled);
447
GetOpenInNewPlaylistAction()->setVisible(accepted_download_);
448
album_info_->setEnabled(enabled);
449
album_info_->setVisible(accepted_download_);
450
download_album_->setEnabled(enabled);
451
download_album_->setVisible(accepted_download_);
453
context_menu_->popup(global_pos);
456
QWidget* JamendoService::HeaderWidget() const {
457
const_cast<JamendoService*>(this)->EnsureMenuCreated();
458
return library_filter_;
461
QModelIndex JamendoService::GetCurrentIndex() {
462
return context_item_;
465
void JamendoService::AlbumInfo() {
466
SongList songs(library_model_->GetChildSongs(
467
library_sort_model_->mapToSource(context_item_)));
471
// We put the album ID into the comment field
472
int id = songs.first().comment().toInt();
476
QDesktopServices::openUrl(QUrl(QString(kAlbumInfoUrl).arg(id)));
479
void JamendoService::DownloadAlbum() {
480
SongList songs(library_model_->GetChildSongs(
481
library_sort_model_->mapToSource(context_item_)));
485
// We put the album ID into the comment field
486
int id = songs.first().comment().toInt();
490
QDesktopServices::openUrl(QUrl(QString(kDownloadAlbumUrl).arg(id)));
493
void JamendoService::Homepage() {
494
QDesktopServices::openUrl(QUrl(kHomepage));