~ubuntu-branches/ubuntu/saucy/clementine/saucy

« back to all changes in this revision

Viewing changes to src/internet/jamendoservice.cpp

  • Committer: Package Import Robot
  • Author(s): Thomas PIERSON
  • Date: 2012-01-01 20:43:39 UTC
  • mfrom: (1.1.1)
  • Revision ID: package-import@ubuntu.com-20120101204339-lsb6nndwhfy05sde
Tags: 1.0.1+dfsg-1
New upstream release. (Closes: #653926, #651611, #657391)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* This file is part of Clementine.
 
2
   Copyright 2010, David Sansome <me@davidsansome.com>
 
3
 
 
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.
 
8
 
 
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.
 
13
 
 
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/>.
 
16
*/
 
17
 
 
18
#include "jamendoservice.h"
 
19
 
 
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"
 
38
 
 
39
#include <QDesktopServices>
 
40
#include <QFutureWatcher>
 
41
#include <QMenu>
 
42
#include <QMessageBox>
 
43
#include <QNetworkReply>
 
44
#include <QSortFilterProxyModel>
 
45
#include <QtConcurrentRun>
 
46
#include <QXmlStreamReader>
 
47
#include "qtiocompressor.h"
 
48
 
 
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";
 
61
 
 
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";
 
66
 
 
67
const char* JamendoService::kSettingsGroup = "Jamendo";
 
68
 
 
69
const int JamendoService::kBatchSize = 10000;
 
70
const int JamendoService::kApproxDatabaseSize = 300000;
 
71
 
 
72
JamendoService::JamendoService(InternetModel* parent)
 
73
    : InternetService(kServiceName, parent, parent),
 
74
      network_(new NetworkAccessManager(this)),
 
75
      context_menu_(NULL),
 
76
      library_backend_(NULL),
 
77
      library_filter_(NULL),
 
78
      library_model_(NULL),
 
79
      library_sort_model_(new QSortFilterProxyModel(this)),
 
80
      load_database_task_id_(0),
 
81
      total_song_count_(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)));
 
89
 
 
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;
 
95
 
 
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))
 
109
    )
 
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))
 
114
    )
 
115
  );
 
116
 
 
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);
 
121
 
 
122
  model()->global_search()->AddProvider(new LibrarySearchProvider(
 
123
      library_backend_,
 
124
      tr("Jamendo"),
 
125
      "jamendo",
 
126
      QIcon(":/providers/jamendo.png"),
 
127
      false,
 
128
      this));
 
129
}
 
130
 
 
131
JamendoService::~JamendoService() {
 
132
}
 
133
 
 
134
QStandardItem* JamendoService::CreateRootItem() {
 
135
  QStandardItem* item = new QStandardItem(QIcon(":providers/jamendo.png"), kServiceName);
 
136
  item->setData(true, InternetModel::Role_CanLazyLoad);
 
137
  return item;
 
138
}
 
139
 
 
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_);
 
145
      break;
 
146
    }
 
147
    default:
 
148
      break;
 
149
  }
 
150
}
 
151
 
 
152
void JamendoService::UpdateTotalSongCount(int count) {
 
153
  total_song_count_ = count;
 
154
  if (total_song_count_ == 0 && !load_database_task_id_) {
 
155
    DownloadDirectory();
 
156
  }
 
157
  else {
 
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
 
161
  }
 
162
}
 
163
 
 
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)
 
169
      return;
 
170
  }
 
171
  accepted_download_ = true;
 
172
  QNetworkRequest req = QNetworkRequest(QUrl(kDirectoryUrl));
 
173
  req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
 
174
                   QNetworkRequest::AlwaysNetwork);
 
175
 
 
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)));
 
180
 
 
181
  if (!load_database_task_id_) {
 
182
    load_database_task_id_ = model()->task_manager()->StartTask(
 
183
        tr("Downloading Jamendo catalogue"));
 
184
  }
 
185
}
 
186
 
 
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);
 
191
}
 
192
 
 
193
void JamendoService::DownloadDirectoryFinished() {
 
194
  QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
 
195
  Q_ASSERT(reply);
 
196
 
 
197
  model()->task_manager()->SetTaskFinished(load_database_task_id_);
 
198
  load_database_task_id_ = 0;
 
199
 
 
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";
 
205
    delete gzip;
 
206
    return;
 
207
  }
 
208
 
 
209
  load_database_task_id_ = model()->task_manager()->StartTask(
 
210
      tr("Parsing Jamendo catalogue"));
 
211
 
 
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()));
 
217
}
 
218
 
 
219
void JamendoService::ParseDirectory(QIODevice* device) const {
 
220
  int total_count = 0;
 
221
 
 
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)));
 
227
 
 
228
  // Delete the database and recreate it.  This is faster than dropping tables
 
229
  // or removing rows.
 
230
  library_backend_->db()->RecreateAttachedDb("jamendo");
 
231
 
 
232
  TrackIdList track_ids;
 
233
  SongList songs;
 
234
  QXmlStreamReader reader(device);
 
235
  while (!reader.atEnd()) {
 
236
    reader.readNext();
 
237
    if (reader.tokenType() == QXmlStreamReader::StartElement &&
 
238
        reader.name() == "artist") {
 
239
      songs << ReadArtist(&reader, &track_ids);
 
240
    }
 
241
 
 
242
    if (songs.count() >= kBatchSize) {
 
243
      // Add the songs to the database in batches
 
244
      library_backend_->AddOrUpdateSongs(songs);
 
245
      InsertTrackIds(track_ids);
 
246
 
 
247
      total_count += songs.count();
 
248
      songs.clear();
 
249
      track_ids.clear();
 
250
 
 
251
      // Update progress info
 
252
      model()->task_manager()->SetTaskProgress(
 
253
            load_database_task_id_, total_count, kApproxDatabaseSize);
 
254
    }
 
255
  }
 
256
 
 
257
  library_backend_->AddOrUpdateSongs(songs);
 
258
  InsertTrackIds(track_ids);
 
259
 
 
260
  connect(library_backend_, SIGNAL(SongsDiscovered(SongList)),
 
261
          library_model_, SLOT(SongsDiscovered(SongList)));
 
262
  connect(library_backend_, SIGNAL(TotalSongCountUpdated(int)),
 
263
          SLOT(UpdateTotalSongCount(int)));
 
264
 
 
265
  library_backend_->UpdateTotalSongCount();
 
266
}
 
267
 
 
268
void JamendoService::InsertTrackIds(const TrackIdList& ids) const {
 
269
  QMutexLocker l(library_backend_->db()->Mutex());
 
270
  QSqlDatabase db(library_backend_->db()->Connect());
 
271
 
 
272
  ScopedTransaction t(&db);
 
273
 
 
274
  QSqlQuery insert(QString("INSERT INTO %1 (%2) VALUES (:id)")
 
275
                   .arg(kTrackIdsTable, kTrackIdsColumn), db);
 
276
 
 
277
  foreach (int id, ids) {
 
278
    insert.bindValue(":id", id);
 
279
    if (!insert.exec()) {
 
280
      qLog(Warning) << "Query failed" << insert.lastQuery();
 
281
    }
 
282
  }
 
283
 
 
284
  t.Commit();
 
285
}
 
286
 
 
287
SongList JamendoService::ReadArtist(QXmlStreamReader* reader,
 
288
                                    TrackIdList* track_ids) const {
 
289
  SongList ret;
 
290
  QString current_artist;
 
291
 
 
292
  while (!reader->atEnd()) {
 
293
    reader->readNext();
 
294
 
 
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);
 
301
      }
 
302
    } else if (reader->isEndElement() && reader->name() == "artist") {
 
303
      break;
 
304
    }
 
305
  }
 
306
 
 
307
  return ret;
 
308
}
 
309
 
 
310
SongList JamendoService::ReadAlbum(
 
311
    const QString& artist, QXmlStreamReader* reader, TrackIdList* track_ids) const {
 
312
  SongList ret;
 
313
  QString current_album;
 
314
  QString cover;
 
315
  int current_album_id = 0;
 
316
 
 
317
  while (!reader->atEnd()) {
 
318
    reader->readNext();
 
319
 
 
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,
 
329
                         reader, track_ids);
 
330
      }
 
331
    } else if (reader->isEndElement() && reader->name() == "album") {
 
332
      break;
 
333
    }
 
334
  }
 
335
  return ret;
 
336
}
 
337
 
 
338
Song JamendoService::ReadTrack(const QString& artist,
 
339
                               const QString& album,
 
340
                               const QString& album_cover,
 
341
                               int album_id,
 
342
                               QXmlStreamReader* reader,
 
343
                               TrackIdList* track_ids) const {
 
344
  Song song;
 
345
  song.set_artist(artist);
 
346
  song.set_album(album);
 
347
  song.set_filetype(Song::Type_Stream);
 
348
  song.set_directory_id(0);
 
349
  song.set_mtime(0);
 
350
  song.set_ctime(0);
 
351
  song.set_filesize(0);
 
352
 
 
353
  // Shoehorn the album ID into the comment field
 
354
  song.set_comment(QString::number(album_id));
 
355
 
 
356
  while (!reader->atEnd()) {
 
357
    reader->readNext();
 
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.
 
368
        if (genre_id != 0) {
 
369
          song.set_genre_id3(genre_id);
 
370
        }
 
371
      } else if (name == "id") {
 
372
        QString id_text = reader->readElementText();
 
373
        int id = id_text.toInt();
 
374
        if (id == 0)
 
375
          continue;
 
376
 
 
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);
 
381
 
 
382
        // Rely on songs getting added in this exact order
 
383
        track_ids->append(id);
 
384
      }
 
385
    } else if (reader->isEndElement() && reader->name() == "track") {
 
386
      break;
 
387
    }
 
388
  }
 
389
  return song;
 
390
}
 
391
 
 
392
void JamendoService::ParseDirectoryFinished() {
 
393
  QFutureWatcher<void>* watcher = static_cast<QFutureWatcher<void>*>(sender());
 
394
  delete watcher;
 
395
 
 
396
  //show smart playlists
 
397
  library_model_->set_show_smart_playlists(true);
 
398
  library_model_->Reset();
 
399
 
 
400
  model()->task_manager()->SetTaskFinished(load_database_task_id_);
 
401
  load_database_task_id_ = 0;
 
402
}
 
403
 
 
404
void JamendoService::EnsureMenuCreated() {
 
405
  if (library_filter_)
 
406
    return;
 
407
 
 
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()));
 
417
 
 
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);
 
424
 
 
425
    context_menu_->addSeparator();
 
426
    context_menu_->addMenu(library_filter_->menu());
 
427
  }
 
428
}
 
429
 
 
430
void JamendoService::ShowContextMenu(const QModelIndex& index, const QPoint& global_pos) {
 
431
  EnsureMenuCreated();
 
432
 
 
433
  if (index.model() == library_sort_model_) {
 
434
    context_item_ = index;
 
435
  } else {
 
436
    context_item_ = QModelIndex();
 
437
  }
 
438
 
 
439
  const bool enabled = accepted_download_ && context_item_.isValid();
 
440
 
 
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_);
 
452
 
 
453
  context_menu_->popup(global_pos);
 
454
}
 
455
 
 
456
QWidget* JamendoService::HeaderWidget() const {
 
457
  const_cast<JamendoService*>(this)->EnsureMenuCreated();
 
458
  return library_filter_;
 
459
}
 
460
 
 
461
QModelIndex JamendoService::GetCurrentIndex() {
 
462
  return context_item_;
 
463
}
 
464
 
 
465
void JamendoService::AlbumInfo() {
 
466
  SongList songs(library_model_->GetChildSongs(
 
467
      library_sort_model_->mapToSource(context_item_)));
 
468
  if (songs.isEmpty())
 
469
    return;
 
470
 
 
471
  // We put the album ID into the comment field
 
472
  int id = songs.first().comment().toInt();
 
473
  if (!id)
 
474
    return;
 
475
 
 
476
  QDesktopServices::openUrl(QUrl(QString(kAlbumInfoUrl).arg(id)));
 
477
}
 
478
 
 
479
void JamendoService::DownloadAlbum() {
 
480
  SongList songs(library_model_->GetChildSongs(
 
481
      library_sort_model_->mapToSource(context_item_)));
 
482
  if (songs.isEmpty())
 
483
    return;
 
484
 
 
485
  // We put the album ID into the comment field
 
486
  int id = songs.first().comment().toInt();
 
487
  if (!id)
 
488
    return;
 
489
 
 
490
  QDesktopServices::openUrl(QUrl(QString(kDownloadAlbumUrl).arg(id)));
 
491
}
 
492
 
 
493
void JamendoService::Homepage() {
 
494
  QDesktopServices::openUrl(QUrl(kHomepage));
 
495
}