2
* \file discogsimporter.cpp
9
* Copyright (C) 2006-2013 Urs Fleisch
11
* This file is part of Kid3.
13
* Kid3 is free software; you can redistribute it and/or modify
14
* it under the terms of the GNU General Public License as published by
15
* the Free Software Foundation; either version 2 of the License, or
16
* (at your option) any later version.
18
* Kid3 is distributed in the hope that it will be useful,
19
* but WITHOUT ANY WARRANTY; without even the implied warranty of
20
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
* GNU General Public License for more details.
23
* You should have received a copy of the GNU General Public License
24
* along with this program. If not, see <http://www.gnu.org/licenses/>.
27
#include "discogsimporter.h"
29
#include "serverimporterconfig.h"
30
#include "trackdatamodel.h"
31
#include "configstore.h"
33
#include "jsonparser.h"
34
#include "qtcompatmac.h"
37
* Stores information about extra artists.
38
* The information can be used to add frames to the appropriate tracks.
44
* @param varMap variant map containing extra artist information
46
explicit ExtraArtist(const QVariantMap& varMap);
49
* Add extra artist information to frames.
50
* @param frames frame collection
51
* @param trackPos optional position, the extra artist information will
52
* only be added if this track position is listed in the
53
* track restrictions or is empty
55
void addToFrames(FrameCollection& frames,
56
const QString& trackPos = QString()) const;
59
* Check if extra artist information is only valid for a subset of the tracks.
60
* @return true if extra artist has track restriction.
62
bool hasTrackRestriction() const { return !m_tracks.isEmpty(); }
73
const char discogsServer[] = "api.discogs.com:80";
76
* Replace unicode escape sequences (e.g. "\u2022") by unicode characters.
77
* @param str string containing unicode escape sequences
78
* @return string with replaced unicode escape sequences.
80
QString replaceEscapedUnicodeCharacters(QString str)
82
QRegExp unicodeRe(QLatin1String("\\\\u([0-9a-fA-F]{4})"));
85
offset = unicodeRe.indexIn(str, offset);
87
str.replace(offset, unicodeRe.matchedLength(),
88
QChar(unicodeRe.cap(1).toUInt(0, 16)));
96
* Remove trailing stars and numbers like (2) from a string.
100
* @return fixed up string.
102
QString fixUpArtist(QString str)
104
str.remove(QRegExp(QLatin1String("[*\\s]*\\(\\d+\\)")));
105
str.replace(QRegExp(QLatin1String("\\*($| - |, | / )")), QLatin1String("\\1"));
111
* Create a string with artists contained in an artist list.
112
* @param artists list containing artist maps
113
* @return string with artists joined appropriately.
115
QString getArtistString(const QVariantList& artists)
118
if (!artists.isEmpty()) {
120
foreach (const QVariant& var, artists) {
121
QVariantMap varMap = var.toMap();
122
if (!artist.isEmpty()) {
125
artist += fixUpArtist(varMap.value(QLatin1String("name")).toString());
126
join = varMap.value(QLatin1String("join")).toString();
127
if (join.isEmpty() || join == QLatin1String(",")) {
128
join = QLatin1String(", ");
130
join = QLatin1Char(' ') + join + QLatin1Char(' ');
138
* Add involved people to a frame.
139
* The format used is (should be converted according to tag specifications):
140
* involvee 1 (involvement 1)\n
141
* involvee 2 (involvement 2)\n
143
* involvee n (involvement n)
145
* @param frames frame collection
146
* @param type type of frame
147
* @param involvement involvement (e.g. instrument)
148
* @param involvee name of involvee (e.g. musician)
150
void addInvolvedPeople(
151
FrameCollection& frames, Frame::Type type,
152
const QString& involvement, const QString& involvee)
154
QString value = frames.getValue(type);
155
if (!value.isEmpty()) value += Frame::stringListSeparator();
156
value += involvement;
157
value += Frame::stringListSeparator();
159
frames.setValue(type, value);
163
* Add name to frame with credits.
164
* @param frames frame collection
165
* @param type type of frame
166
* @param name name of person to credit
168
void addCredit(FrameCollection& frames, Frame::Type type, const QString& name)
170
QString value = frames.getValue(type);
171
if (!value.isEmpty()) value += QLatin1String(", ");
173
frames.setValue(type, value);
180
* @param varMap variant map containing extra artist information
182
ExtraArtist::ExtraArtist(const QVariantMap& varMap) :
183
m_name(fixUpArtist(varMap.value(QLatin1String("name")).toString())),
184
m_role(varMap.value(QLatin1String("role")).toString())
186
static const QRegExp tracksSepRe(QLatin1String(",\\s*"));
187
QString tracks = varMap.value(QLatin1String("tracks")).toString();
188
if (!tracks.isEmpty()) {
189
m_tracks = tracks.split(tracksSepRe);
194
* Add extra artist information to frames.
195
* @param frames frame collection
196
* @param trackPos optional position, the extra artist information will
197
* only be added if this track position is listed in the
198
* track restrictions or is empty
200
void ExtraArtist::addToFrames(FrameCollection& frames,
201
const QString& trackPos) const
203
if (!trackPos.isEmpty() && !m_tracks.contains(trackPos))
206
if (m_role.contains(QLatin1String("Composed By")) || m_role.contains(QLatin1String("Music By")) ||
207
m_role.contains(QLatin1String("Songwriter"))) {
208
addCredit(frames, Frame::FT_Composer, m_name);
210
if (m_role.contains(QLatin1String("Written-By")) || m_role.contains(QLatin1String("Written By"))) {
211
addCredit(frames, Frame::FT_Author, m_name);
213
if (m_role.contains(QLatin1String("Lyrics By"))) {
214
addCredit(frames, Frame::FT_Lyricist, m_name);
216
if (m_role.contains(QLatin1String("Conductor"))) {
217
addCredit(frames, Frame::FT_Conductor, m_name);
219
if (m_role.contains(QLatin1String("Orchestra"))) {
220
addCredit(frames, Frame::FT_AlbumArtist, m_name);
222
if (m_role.contains(QLatin1String("Remix"))) {
223
addCredit(frames, Frame::FT_Remixer, m_name);
226
if (m_role.contains(QLatin1String("Arranged By"))) {
227
addInvolvedPeople(frames, Frame::FT_Arranger,
228
QLatin1String("Arranger"), m_name);
230
if (m_role.contains(QLatin1String("Mixed By"))) {
231
addInvolvedPeople(frames, Frame::FT_Arranger,
232
QLatin1String("Mixer"), m_name);
234
if (m_role.contains(QLatin1String("DJ Mix")) || m_role.contains(QLatin1String("Dj Mix"))) {
235
addInvolvedPeople(frames, Frame::FT_Arranger,
236
QLatin1String("DJMixer"), m_name);
238
if (m_role.contains(QLatin1String("Engineer")) || m_role.contains(QLatin1String("Mastered By"))) {
239
addInvolvedPeople(frames, Frame::FT_Arranger,
240
QLatin1String("Engineer"), m_name);
242
if (m_role.contains(QLatin1String("Producer")) || m_role.contains(QLatin1String("Co-producer")) ||
243
m_role.contains(QLatin1String("Executive Producer"))) {
244
addInvolvedPeople(frames, Frame::FT_Arranger,
245
QLatin1String("Producer"), m_name);
248
static const char* const instruments[] = {
249
"Performer", "Vocals", "Voice", "Featuring", "Choir", "Chorus",
250
"Baritone", "Tenor", "Rap", "Scratches", "Drums", "Percussion",
251
"Keyboards", "Cello", "Piano", "Organ", "Synthesizer", "Keys",
252
"Wurlitzer", "Rhodes", "Harmonica", "Xylophone", "Guitar", "Bass",
253
"Strings", "Violin", "Viola", "Banjo", "Harp", "Mandolin",
254
"Clarinet", "Horn", "Cornet", "Flute", "Oboe", "Saxophone",
255
"Trumpet", "Tuba", "Trombone"
258
i < sizeof(instruments) / sizeof(instruments[0]);
260
if (m_role.contains(QString::fromLatin1(instruments[i]))) {
261
addInvolvedPeople(frames, Frame::FT_Performer, m_role, m_name);
271
* @param netMgr network access manager
272
* @param trackDataModel track data to be filled with imported values
274
DiscogsImporter::DiscogsImporter(QNetworkAccessManager* netMgr,
275
TrackDataModel* trackDataModel) :
276
ServerImporter(netMgr, trackDataModel)
278
setObjectName(QLatin1String("DiscogsImporter"));
279
m_discogsHeaders["User-Agent"] = "Kid3/" VERSION
280
" +http://kid3.sourceforge.net";
286
DiscogsImporter::~DiscogsImporter()
291
* Name of import source.
294
const char* DiscogsImporter::name() const { return I18N_NOOP("Discogs"); }
296
/** anchor to online help, 0 to disable */
297
const char* DiscogsImporter::helpAnchor() const { return "import-discogs"; }
299
/** configuration, 0 if not used */
300
ServerImporterConfig* DiscogsImporter::config() const { return &ConfigStore::s_discogsCfg; }
302
/** additional tags option, false if not used */
303
bool DiscogsImporter::additionalTags() const { return true; }
306
* Process finished findCddbAlbum request.
308
* @param searchStr search data received
310
void DiscogsImporter::parseFindResults(const QByteArray& searchStr)
312
// search results have the format (JSON, simplified):
313
// {"results": [{"style": ["Heavy Metal"], "title": "Wizard (23) - Odin",
314
// "type": "release", "id": 2487778}]}
315
QString str = replaceEscapedUnicodeCharacters(QString::fromUtf8(searchStr));
317
QVariantMap map = JsonParser::deserialize(str).toMap();
318
m_albumListModel->clear();
319
foreach (const QVariant& var, map.value(QLatin1String("results")).toList()) {
320
QVariantMap result = var.toMap();
321
QString title = fixUpArtist(result.value(QLatin1String("title")).toString());
322
if (!title.isEmpty()) {
323
m_albumListModel->appendRow(new AlbumListItem(
325
QLatin1String("releases"),
326
QString::number(result.value(QLatin1String("id")).toInt())));
332
* Parse result of album request and populate m_trackDataModel with results.
334
* @param albumStr album data received
336
void DiscogsImporter::parseAlbumResults(const QByteArray& albumStr)
338
// releases have the format (JSON, simplified):
339
// { "styles": ["Heavy Metal"],
340
// "labels": [{"name": "LMP"}],
342
// "artists": [{"name": "Wizard (23)"}],
344
// { "uri": "http://api.discogs.com/image/R-2487778-1293847958.jpeg",
345
// "type": "primary" },
346
// { "uri": "http://api.discogs.com/image/R-2487778-1293847967.jpeg",
347
// "type": "secondary" }],
349
// "genres": ["Rock"],
350
// "thumb": "http://api.discogs.com/image/R-150-2487778-1293847958.jpeg",
351
// "extraartists": [],
354
// {"duration": "5:19", "position": "1", "title": "The Prophecy"},
355
// {"duration": "", "position": "Video", "title": "Betrayer"}
357
// "released": "2003",
358
// "formats": [{"name": "CD"}]
360
QRegExp discTrackPosRe(QLatin1String("(\\d+)-(\\d+)"));
361
QRegExp yearRe(QLatin1String("^\\d{4}-\\d{2}"));
362
QString str = replaceEscapedUnicodeCharacters(QString::fromUtf8(albumStr));
363
QVariantMap map = JsonParser::deserialize(str).toMap();
365
QList<ExtraArtist> trackExtraArtists;
366
ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData());
367
FrameCollection framesHdr;
368
const bool standardTags = getStandardTags();
370
framesHdr.setAlbum(map.value(QLatin1String("title")).toString());
371
framesHdr.setArtist(getArtistString(map.value(QLatin1String("artists")).toList()));
373
// The year can be found in "released".
374
QString released(map.value(QLatin1String("released")).toString());
375
if (yearRe.indexIn(released) == 0) {
376
released.truncate(4);
378
framesHdr.setYear(released.toInt());
380
// The genre can be found in "genre" or "style".
381
// All genres found are checked for an ID3v1 number, starting with those
382
// in the style field.
383
QVariantList genreList(map.value(QLatin1String("styles")).toList() +
384
map.value(QLatin1String("genres")).toList());
386
foreach (const QVariant& var, genreList) {
387
genreNum = Genres::getNumber(var.toString());
388
if (genreNum != 255) {
392
if (genreNum != 255) {
393
framesHdr.setGenre(QString::fromLatin1(Genres::getName(genreNum)));
394
} else if (!genreList.isEmpty()) {
395
framesHdr.setGenre(genreList.first().toString());
399
trackDataVector.setCoverArtUrl(QString());
400
const bool coverArt = getCoverArt();
402
// Cover art can be found in "images"
403
QVariantList images = map.value(QLatin1String("images")).toList();
404
if (!images.isEmpty()) {
405
trackDataVector.setCoverArtUrl(images.first().toMap().value(QLatin1String("uri")).
410
const bool additionalTags = getAdditionalTags();
411
if (additionalTags) {
412
// Publisher can be found in "label"
413
QVariantList labels = map.value(QLatin1String("labels")).toList();
414
if (!labels.isEmpty()) {
415
QVariantMap firstLabelMap = labels.first().toMap();
416
framesHdr.setValue(Frame::FT_Publisher,
417
fixUpArtist(firstLabelMap.value(QLatin1String("name")).toString()));
418
QString catNo = firstLabelMap.value(QLatin1String("catno")).toString();
419
if (!catNo.isEmpty() && catNo.toLower() != QLatin1String("none")) {
420
framesHdr.setValue(Frame::FT_CatalogNumber, catNo);
423
// Media can be found in "formats"
424
QVariantList formats = map.value(QLatin1String("formats")).toList();
425
if (!formats.isEmpty()) {
426
framesHdr.setValue(Frame::FT_Media,
427
formats.first().toMap().value(QLatin1String("name")).toString());
429
// Credits can be found in "extraartists"
430
QVariantList extraartists = map.value(QLatin1String("extraartists")).toList();
431
if (!extraartists.isEmpty()) {
432
foreach (const QVariant& var, extraartists) {
433
ExtraArtist extraArtist(var.toMap());
434
if (extraArtist.hasTrackRestriction()) {
435
trackExtraArtists.append(extraArtist);
437
extraArtist.addToFrames(framesHdr);
441
// Release country can be found in "country"
442
QString country(map.value(QLatin1String("country")).toString());
443
if (!country.isEmpty()) {
444
framesHdr.setValue(Frame::FT_ReleaseCountry, country);
448
FrameCollection frames(framesHdr);
449
ImportTrackDataVector::iterator it = trackDataVector.begin();
450
bool atTrackDataListEnd = (it == trackDataVector.end());
452
QVariantList trackList = map.value(QLatin1String("tracklist")).toList();
454
// Check if all positions are empty.
455
bool allPositionsEmpty = true;
456
foreach (const QVariant& var, trackList) {
457
if (!var.toMap().value(QLatin1String("position")).toString().isEmpty()) {
458
allPositionsEmpty = false;
463
foreach (const QVariant& var, trackList) {
464
QVariantMap track = var.toMap();
466
QString position(track.value(QLatin1String("position")).toString());
468
int pos = position.toInt(&ok);
470
if (discTrackPosRe.exactMatch(position)) {
471
if (additionalTags) {
472
frames.setValue(Frame::FT_Disc, discTrackPosRe.cap(1));
474
pos = discTrackPosRe.cap(2).toInt();
479
QString title(track.value(QLatin1String("title")).toString());
481
QStringList durationHms = track.value(QLatin1String("duration")).toString().split(QLatin1Char(':'));
483
foreach (const QString& var, durationHms) {
485
duration += var.toInt();
487
if (!allPositionsEmpty && position.isEmpty()) {
488
if (additionalTags) {
489
framesHdr.setValue(Frame::FT_Part, title);
491
} else if (!title.isEmpty() || duration != 0) {
493
frames.setTrack(pos);
494
frames.setTitle(title);
496
QVariantList artists(track.value(QLatin1String("artists")).toList());
497
if (!artists.isEmpty()) {
499
frames.setArtist(getArtistString(artists));
501
if (additionalTags) {
502
frames.setValue(Frame::FT_AlbumArtist, framesHdr.getArtist());
505
if (additionalTags) {
506
QVariantList extraartists(track.value(QLatin1String("extraartists")).toList());
507
if (!extraartists.isEmpty()) {
508
foreach (const QVariant& var, extraartists) {
509
ExtraArtist extraArtist(var.toMap());
510
extraArtist.addToFrames(frames);
514
foreach (const ExtraArtist& extraArtist, trackExtraArtists) {
515
extraArtist.addToFrames(frames, position);
518
if (atTrackDataListEnd) {
519
ImportTrackData trackData;
520
trackData.setFrameCollection(frames);
521
trackData.setImportDuration(duration);
522
trackDataVector.append(trackData);
524
while (!atTrackDataListEnd && !it->isEnabled()) {
526
atTrackDataListEnd = (it == trackDataVector.end());
528
if (!atTrackDataListEnd) {
529
(*it).setFrameCollection(frames);
530
(*it).setImportDuration(duration);
532
atTrackDataListEnd = (it == trackDataVector.end());
539
// handle redundant tracks
541
while (!atTrackDataListEnd) {
542
if (it->isEnabled()) {
543
if ((*it).getFileDuration() == 0) {
544
it = trackDataVector.erase(it);
546
(*it).setFrameCollection(frames);
547
(*it).setImportDuration(0);
553
atTrackDataListEnd = (it == trackDataVector.end());
555
m_trackDataModel->setTrackData(trackDataVector);
559
* Send a query command to search on the server.
561
* @param cfg import source configuration
562
* @param artist artist to search
563
* @param album album to search
565
void DiscogsImporter::sendFindQuery(
566
const ServerImporterConfig*,
567
const QString& artist, const QString& album)
570
* Query looks like this:
571
* http://api.discogs.com//database/search?type=release&title&q=amon+amarth+avenger
573
sendRequest(QString::fromLatin1(discogsServer),
574
QLatin1String("/database/search?type=release&title&q=") +
575
encodeUrlQuery(artist + QLatin1Char(' ') + album), m_discogsHeaders);
579
* Send a query command to fetch the track list
582
* @param cfg import source configuration
583
* @param cat category
586
void DiscogsImporter::sendTrackListQuery(
587
const ServerImporterConfig*, const QString& cat, const QString& id)
590
* Query looks like this:
591
* http://api.discogs.com/releases/761529
593
sendRequest(QString::fromLatin1(discogsServer), QLatin1Char('/') + QString::fromLatin1(QUrl::toPercentEncoding(cat)) + QLatin1Char('/')
594
+ id, m_discogsHeaders);