File indexing completed on 2024-05-05 04:48:34
0001 /**************************************************************************************** 0002 * Copyright (c) 2010 Sergey Ivanov <123kash@gmail.com> * 0003 * Copyright (c) 2013 Alberto Villa <avilla@FreeBSD.org> * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify it under * 0006 * the terms of the GNU General Public License as published by the Free Software * 0007 * Foundation; either version 2 of the License, or (at your option) any later * 0008 * version. * 0009 * * 0010 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0012 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0013 * * 0014 * You should have received a copy of the GNU General Public License along with * 0015 * this program. If not, see <http://www.gnu.org/licenses/>. * 0016 ****************************************************************************************/ 0017 0018 #define DEBUG_PREFIX "MusicBrainzFinder" 0019 0020 #include "MusicBrainzFinder.h" 0021 0022 #include "core/meta/Meta.h" 0023 #include "core/meta/support/MetaConstants.h" 0024 #include "core/meta/support/MetaUtility.h" 0025 #include "core/support/Debug.h" 0026 #include "MusicBrainzMeta.h" 0027 #include "TagsFromFileNameGuesser.h" 0028 0029 #include <ThreadWeaver/Queue> 0030 #include <ThreadWeaver/Job> 0031 0032 #include <QAuthenticator> 0033 #include <QNetworkAccessManager> 0034 #include <QTimer> 0035 #include <QUrlQuery> 0036 0037 /* 0038 * Levenshtein distance algorithm implementation carefully pirated from Wikibooks 0039 * (http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance) 0040 * and modified (a bit) to return similarity measure instead of distance. 0041 */ 0042 float 0043 similarity( const QString &s1, const QString &s2 ) 0044 { 0045 const size_t len1 = s1.length(), len2 = s2.length(); 0046 QVector<uint> col( len2 + 1 ), prevCol( len2 + 1 ); 0047 0048 for( uint i = 0; i <= len2; i++ ) 0049 prevCol[i] = i; 0050 for( uint i = 0; i < len1; i++ ) 0051 { 0052 col[0] = i + 1; 0053 for( uint j = 0; j < len2; j++ ) 0054 col[j + 1] = qMin( qMin( 1 + col[j], 1 + prevCol[1 + j] ), 0055 prevCol[j] + ( s1[i] == s2[j] ? 0 : 1 ) ); 0056 col.swap( prevCol ); 0057 } 0058 0059 return 1.0 - ( float )prevCol[len2] / ( len1 + len2 ); 0060 } 0061 0062 MusicBrainzFinder::MusicBrainzFinder( QObject *parent, const QString &host, 0063 const int port, const QString &pathPrefix, 0064 const QString &username, const QString &password ) 0065 : QObject( parent ) 0066 , mb_host( host ) 0067 , mb_port( port ) 0068 , mb_pathPrefix( pathPrefix ) 0069 , mb_username( username ) 0070 , mb_password( password ) 0071 { 0072 DEBUG_BLOCK 0073 0074 debug() << "Initiating MusicBrainz search:"; 0075 debug() << "\thost:\t\t" << mb_host; 0076 debug() << "\tport:\t\t" << mb_port; 0077 debug() << "\tpath prefix:\t" << mb_pathPrefix; 0078 debug() << "\tusername:\t" << mb_username; 0079 debug() << "\tpassword:\t" << mb_password; 0080 0081 net = The::networkAccessManager(); 0082 0083 m_timer = new QTimer( this ); 0084 m_timer->setInterval( 1000 ); 0085 0086 connect( net, &QNetworkAccessManager::authenticationRequired, 0087 this, &MusicBrainzFinder::gotAuthenticationRequest ); 0088 connect( net, &QNetworkAccessManager::finished, 0089 this, &MusicBrainzFinder::gotReply ); 0090 connect( m_timer, &QTimer::timeout, this, &MusicBrainzFinder::sendNewRequest ); 0091 } 0092 0093 bool 0094 MusicBrainzFinder::isRunning() const 0095 { 0096 return !( m_requests.isEmpty() && m_replies.isEmpty() && 0097 m_parsers.isEmpty() ) || m_timer->isActive(); 0098 } 0099 0100 void 0101 MusicBrainzFinder::run( const Meta::TrackList &tracks ) 0102 { 0103 foreach( const Meta::TrackPtr &track, tracks ) 0104 m_requests.append( qMakePair( track, compileTrackRequest( track ) ) ); 0105 0106 m_timer->start(); 0107 } 0108 0109 void 0110 MusicBrainzFinder::lookUpByPUID( const Meta::TrackPtr &track, const QString &puid ) 0111 { 0112 m_requests.append( qMakePair( track, compilePUIDRequest( puid ) ) ); 0113 0114 if( !m_timer->isActive() ) 0115 m_timer->start(); 0116 } 0117 0118 void 0119 MusicBrainzFinder::sendNewRequest() 0120 { 0121 DEBUG_BLOCK 0122 if( m_requests.isEmpty() ) 0123 { 0124 checkDone(); 0125 return; 0126 } 0127 QPair<Meta::TrackPtr, QNetworkRequest> req = m_requests.takeFirst(); 0128 QNetworkReply *reply = net->get( req.second ); 0129 m_replies.insert( reply, req.first ); 0130 connect( reply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred), 0131 this, &MusicBrainzFinder::gotReplyError ); 0132 0133 debug() << "Request sent:" << req.second.url().toString(); 0134 } 0135 0136 void 0137 MusicBrainzFinder::gotAuthenticationRequest( const QNetworkReply *reply, QAuthenticator *authenticator ) 0138 { 0139 if( reply->url().host() == mb_host ) 0140 { 0141 authenticator->setUser( mb_username ); 0142 authenticator->setPassword( mb_password ); 0143 } 0144 } 0145 0146 void 0147 MusicBrainzFinder::gotReplyError( QNetworkReply::NetworkError code ) 0148 { 0149 DEBUG_BLOCK 0150 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() ); 0151 if( !reply ) 0152 return; 0153 0154 if( !m_replies.contains( reply ) || code == QNetworkReply::NoError ) 0155 return; 0156 0157 debug() << "Error occurred during network request:" << reply->errorString(); 0158 disconnect( reply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred), 0159 this, &MusicBrainzFinder::gotReplyError ); 0160 0161 // Send an empty result to populate the tagger. 0162 sendTrack( m_replies.value( reply ), QVariantMap() ); 0163 0164 m_replies.remove( reply ); 0165 reply->deleteLater(); 0166 checkDone(); 0167 } 0168 0169 void 0170 MusicBrainzFinder::gotReply( QNetworkReply *reply ) 0171 { 0172 DEBUG_BLOCK 0173 if( m_replies.contains( reply ) ) 0174 { 0175 if( reply->error() == QNetworkReply::NoError ) 0176 { 0177 QString document( reply->readAll() ); 0178 MusicBrainzXmlParser *parser = new MusicBrainzXmlParser( document ); 0179 m_parsers.insert( parser, m_replies.value( reply ) ); 0180 0181 connect( parser, &MusicBrainzXmlParser::done, 0182 this, &MusicBrainzFinder::parsingDone ); 0183 ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(parser) ); 0184 } 0185 else 0186 /* 0187 * Send an empty result to populate the tagger. In theory, this part should 0188 * never be reachable, but you never know. 0189 */ 0190 sendTrack( m_replies.value( reply ), QVariantMap() ); 0191 } 0192 0193 m_replies.remove( reply ); 0194 reply->deleteLater(); 0195 checkDone(); 0196 } 0197 0198 void 0199 MusicBrainzFinder::parsingDone( ThreadWeaver::JobPointer _parser ) 0200 { 0201 DEBUG_BLOCK 0202 MusicBrainzXmlParser *parser = dynamic_cast<MusicBrainzXmlParser*>( _parser.data() ); 0203 disconnect( parser, &MusicBrainzXmlParser::done, 0204 this, &MusicBrainzFinder::parsingDone ); 0205 0206 if( m_parsers.contains( parser ) && !m_parsers.value( parser ).isNull() ) 0207 { 0208 // When m_parsers.value( parser ) is not empty, we've been parsing tracks. 0209 Meta::TrackPtr trackPtr = m_parsers.value( parser ); 0210 bool found = false; 0211 0212 Q_EMIT progressStep(); 0213 if( parser->type() == MusicBrainzXmlParser::TrackList && 0214 !parser->tracks.isEmpty() ) 0215 { 0216 QVariantMap metadata = m_parsedMetadata.value( trackPtr ); 0217 0218 QString scoreType = MusicBrainz::MUSICBRAINZ; 0219 // Maximum allowed error in track length (seconds). 0220 qlonglong lengthTolerance = 30; 0221 0222 // If there is no parsed metadata, a fingerprint lookup was done. 0223 if( !m_parsedMetadata.contains( trackPtr ) ) 0224 { 0225 scoreType = MusicBrainz::MUSICDNS; 0226 lengthTolerance = 10; 0227 } 0228 0229 lengthTolerance *= 1000; 0230 foreach( QVariantMap track, parser->tracks.values() ) 0231 { 0232 #define SIMILARITY( k ) similarity( metadata.value( k ).toString().toLower(), \ 0233 track.value( k ).toString().toLower() ) 0234 if( track.value( Meta::Field::SCORE ).toInt() < 50 ) 0235 continue; 0236 0237 QString title = track.value( Meta::Field::TITLE ).toString(); 0238 qlonglong length = track.value( Meta::Field::LENGTH ).toLongLong(); 0239 float score = 0; 0240 int maxScore = 0; 0241 0242 /* 0243 * We don't check for the entry to exist because a result without an 0244 * artist deserves a bad score. 0245 */ 0246 if( metadata.contains( Meta::Field::ARTIST ) ) 0247 { 0248 score += 6.0 * SIMILARITY( Meta::Field::ARTIST ); 0249 maxScore += 6; 0250 } 0251 0252 if( track.contains( MusicBrainz::RELEASELIST ) ) 0253 { 0254 // We try to send as many tracks as are the related releases. 0255 foreach( const QString &releaseID, 0256 track.value( MusicBrainz::RELEASELIST ).toStringList() ) 0257 { 0258 /* 0259 * The album artist could be parsed and inserted here, but since 0260 * we have to parse each release group (only once), it's better to 0261 * do it later, as we don't need it to calculate the score anyway. 0262 * The release date has to be fetched in the second round 0263 * (actually, it's the real reason behind the second round), as we 0264 * want it to be the first release date of the release group: 0265 * http://tickets.musicbrainz.org/browse/SEARCH-218 0266 */ 0267 QVariantMap release = parser->releases.value( releaseID ); 0268 float releaseScore = score; 0269 int maxReleaseScore = maxScore; 0270 0271 track.insert( MusicBrainz::RELEASEID, releaseID ); 0272 track.insert( MusicBrainz::RELEASEGROUPID, 0273 release.value( MusicBrainz::RELEASEGROUPID ) ); 0274 0275 track.insert( Meta::Field::ALBUM, 0276 release.value( Meta::Field::TITLE ) ); 0277 if( metadata.contains( Meta::Field::ALBUM ) ) 0278 { 0279 releaseScore += 12.0 * SIMILARITY( Meta::Field::ALBUM ); 0280 maxReleaseScore += 12; 0281 } 0282 0283 int trackCount = release.value( MusicBrainz::TRACKCOUNT ).toInt(); 0284 if( trackCount > 0 ) 0285 track.insert( MusicBrainz::TRACKCOUNT, trackCount ); 0286 else 0287 track.remove( MusicBrainz::TRACKCOUNT ); 0288 0289 /* 0290 * A track can appear more than once in a release (on different 0291 * discs, or in different versions), but we're going to send it 0292 * multiple times only if it has different properties per 0293 * iteration (yes, if the properties below are defined, at least 0294 * some of them must be different by design). Otherwise, it would 0295 * result in duplicated entries (which is bad for several 0296 * reasons). 0297 */ 0298 foreach( const QVariant &info, 0299 track.value( MusicBrainz::TRACKINFO ).toMap().value( releaseID ).toList() ) 0300 { 0301 QVariantMap trackInfo = info.toMap(); 0302 float currentReleaseScore = releaseScore; 0303 int maxCurrentReleaseScore = maxReleaseScore; 0304 0305 /* 0306 * Track title and length can be different on different 0307 * releases. 0308 */ 0309 QString currentTitle = trackInfo.value( Meta::Field::TITLE ).toString(); 0310 if( currentTitle.isEmpty() ) 0311 currentTitle = title; 0312 track.insert( Meta::Field::TITLE, currentTitle ); 0313 // Same logic as for the artist tag above. 0314 if( metadata.contains( Meta::Field::TITLE ) ) 0315 { 0316 currentReleaseScore += 22.0 * SIMILARITY( Meta::Field::TITLE ); 0317 maxCurrentReleaseScore += 22; 0318 } 0319 0320 qlonglong currentLength = trackInfo.value( Meta::Field::LENGTH ).toLongLong(); 0321 if( currentLength <= 0 ) 0322 currentLength = length; 0323 if( currentLength > 0 ) 0324 track.insert( Meta::Field::LENGTH, currentLength ); 0325 else 0326 track.remove( Meta::Field::LENGTH ); 0327 if( track.contains( Meta::Field::LENGTH ) ) 0328 { 0329 currentReleaseScore += 8.0 * ( 1.0 - float( qMin( qAbs( trackPtr->length() - 0330 track.value( Meta::Field::LENGTH ).toLongLong() ), 0331 lengthTolerance ) ) / lengthTolerance ); 0332 maxCurrentReleaseScore += 8; 0333 } 0334 0335 int currentDiscNumber = trackInfo.value( Meta::Field::DISCNUMBER ).toInt(); 0336 if( currentDiscNumber > 0 ) 0337 track.insert( Meta::Field::DISCNUMBER, currentDiscNumber ); 0338 else 0339 track.remove( Meta::Field::DISCNUMBER ); 0340 if( metadata.contains( Meta::Field::DISCNUMBER ) && 0341 track.contains( Meta::Field::DISCNUMBER ) ) 0342 { 0343 currentReleaseScore += ( metadata.value( Meta::Field::DISCNUMBER ).toInt() == 0344 track.value( Meta::Field::DISCNUMBER ).toInt() )? 6 : 0; 0345 maxCurrentReleaseScore += 6; 0346 } 0347 else if( metadata.value( Meta::Field::DISCNUMBER ).toInt() != 0348 track.value( Meta::Field::DISCNUMBER ).toInt() ) 0349 /* 0350 * Always prefer results with matching disc number, 0351 * even when empty. 0352 */ 0353 currentReleaseScore -= 0.1; 0354 0355 int currentTrackNumber = trackInfo.value( Meta::Field::TRACKNUMBER ).toInt(); 0356 if( currentTrackNumber > 0 ) 0357 track.insert( Meta::Field::TRACKNUMBER, currentTrackNumber ); 0358 else 0359 track.remove( Meta::Field::TRACKNUMBER ); 0360 if( metadata.contains( Meta::Field::TRACKNUMBER ) && 0361 track.contains( Meta::Field::TRACKNUMBER ) ) 0362 { 0363 currentReleaseScore += ( metadata.value( Meta::Field::TRACKNUMBER ).toInt() == 0364 track.value( Meta::Field::TRACKNUMBER ).toInt() )? 6 : 0; 0365 maxCurrentReleaseScore += 6; 0366 } 0367 else if( metadata.value( Meta::Field::TRACKNUMBER ).toInt() != 0368 track.value( Meta::Field::TRACKNUMBER ).toInt() ) 0369 /* 0370 * Always prefer results with matching track number, 0371 * even when empty. 0372 */ 0373 currentReleaseScore -= 0.1; 0374 0375 if( maxCurrentReleaseScore <= 0 ) 0376 continue; 0377 0378 float sim = currentReleaseScore / maxCurrentReleaseScore; 0379 if( sim > MusicBrainz::MINSIMILARITY ) 0380 { 0381 found = true; 0382 track.insert( scoreType, sim ); 0383 sendTrack( trackPtr, track ); 0384 } 0385 } 0386 } 0387 } 0388 else 0389 { 0390 // A track without releases has been found (not too rare). 0391 if( metadata.contains( Meta::Field::TITLE ) ) 0392 { 0393 score += 22.0 * SIMILARITY( Meta::Field::TITLE ); 0394 maxScore += 22; 0395 } 0396 0397 if( track.contains( Meta::Field::LENGTH ) ) 0398 { 0399 score += 8.0 * ( 1.0 - float( qMin( qAbs( trackPtr->length() - 0400 track.value( Meta::Field::LENGTH ).toLongLong() ), 0401 lengthTolerance ) ) / lengthTolerance ); 0402 maxScore += 8; 0403 } 0404 0405 if( maxScore <= 0 ) 0406 continue; 0407 0408 float sim = score / maxScore; 0409 if( sim > MusicBrainz::MINSIMILARITY ) 0410 { 0411 found = true; 0412 track.insert( scoreType, sim ); 0413 sendTrack( trackPtr, track ); 0414 } 0415 } 0416 #undef SIMILARITY 0417 } 0418 m_parsedMetadata.remove( trackPtr ); 0419 } 0420 else if( parser->type() != MusicBrainzXmlParser::TrackList ) 0421 debug() << "Invalid parsing result."; 0422 0423 /* 0424 * Sending an empty result is important: it creates a disabled entry in the tagger 0425 * to show that the track was not found (otherwise, it would pass unnoticed). 0426 */ 0427 if( !found ) 0428 sendTrack( trackPtr, QVariantMap() ); 0429 } 0430 else if( parser->type() == MusicBrainzXmlParser::ReleaseGroup && 0431 !parser->releaseGroups.isEmpty() ) 0432 { 0433 // Cache the release group and flush the queue of tracks. 0434 QString releaseGroupID = parser->releaseGroups.keys().first(); 0435 mb_releaseGroups.insert( releaseGroupID, 0436 parser->releaseGroups.value( releaseGroupID ) ); 0437 foreach( const TrackInfo &trackInfo, mb_queuedTracks.value( releaseGroupID ) ) 0438 sendTrack( trackInfo.first, trackInfo.second ); 0439 mb_queuedTracks.remove( releaseGroupID ); 0440 } 0441 0442 m_parsers.remove( parser ); 0443 parser->deleteLater(); 0444 checkDone(); 0445 } 0446 0447 0448 void 0449 MusicBrainzFinder::sendTrack( const Meta::TrackPtr &track, QVariantMap tags ) 0450 { 0451 if( !tags.isEmpty() ) 0452 { 0453 if( tags.contains( MusicBrainz::RELEASEGROUPID ) ) 0454 { 0455 QString releaseGroupID = tags.value( MusicBrainz::RELEASEGROUPID ).toString(); 0456 if( mb_releaseGroups.contains( releaseGroupID ) ) 0457 { 0458 QVariantMap releaseGroup = mb_releaseGroups.value( releaseGroupID ); 0459 if( releaseGroup.contains( Meta::Field::ARTIST ) ) 0460 tags.insert( Meta::Field::ALBUMARTIST, 0461 releaseGroup.value( Meta::Field::ARTIST ) ); 0462 else if( tags.contains( Meta::Field::ARTIST ) ) 0463 tags.insert( Meta::Field::ALBUMARTIST, 0464 tags.value( Meta::Field::ARTIST ) ); 0465 if( releaseGroup.contains( Meta::Field::YEAR ) ) 0466 tags.insert( Meta::Field::YEAR, 0467 releaseGroup.value( Meta::Field::YEAR ) ); 0468 } 0469 else 0470 { 0471 /* 0472 * The tags reference a release group we don't know yet. Queue the track 0473 * and fetch information about the release group. 0474 */ 0475 if( !mb_queuedTracks.contains( releaseGroupID ) ) 0476 { 0477 QList<TrackInfo> trackList; 0478 trackList.append( qMakePair( track, tags ) ); 0479 mb_queuedTracks.insert( releaseGroupID, trackList ); 0480 m_requests.prepend( qMakePair( Meta::TrackPtr(), 0481 compileReleaseGroupRequest( releaseGroupID ) ) ); 0482 } 0483 else 0484 mb_queuedTracks[releaseGroupID].append( qMakePair( track, tags ) ); 0485 0486 return; 0487 } 0488 } 0489 0490 // Clean metadata from unused fields. 0491 tags.remove( Meta::Field::LENGTH ); 0492 tags.remove( Meta::Field::SCORE ); 0493 tags.remove( MusicBrainz::RELEASELIST ); 0494 tags.remove( MusicBrainz::TRACKINFO ); 0495 } 0496 0497 Q_EMIT trackFound( track, tags ); 0498 } 0499 0500 void 0501 MusicBrainzFinder::checkDone() 0502 { 0503 if( m_requests.isEmpty() && m_replies.isEmpty() && m_parsers.isEmpty() ) 0504 { 0505 /* 0506 * Empty the queue of tracks waiting for release group requests. If the requests 0507 * fail (hint: network failure), remaining queued tracks will silently disappear. 0508 * Sending an empty result makes the user aware of the fact that the track will 0509 * not be tagged. 0510 */ 0511 foreach( const QList<TrackInfo> &trackInfoList, 0512 mb_queuedTracks.values() ) 0513 foreach( const TrackInfo &trackInfo, trackInfoList ) 0514 sendTrack( trackInfo.first, QVariantMap() ); 0515 0516 debug() << "There is no queued request. Stopping timer."; 0517 m_timer->stop(); 0518 Q_EMIT done(); 0519 } 0520 } 0521 0522 QVariantMap 0523 MusicBrainzFinder::guessMetadata( const Meta::TrackPtr &track ) const 0524 { 0525 DEBUG_BLOCK 0526 debug() << "Trying to guess metadata from filename:" << track->playableUrl().fileName(); 0527 QVariantMap metadata; 0528 0529 if( ( track->artist().isNull() || track->artist()->name().isEmpty() ) && 0530 ( track->album().isNull() || track->album()->name().isEmpty() ) ) 0531 { 0532 Meta::FieldHash tags = Meta::Tag::TagGuesser::guessTags( track->playableUrl().fileName() ); 0533 foreach( const quint64 &key, tags.keys() ) 0534 { 0535 switch( key ) 0536 { 0537 case Meta::valAlbum: 0538 metadata.insert( Meta::Field::ALBUM, tags[key] ); 0539 break; 0540 case Meta::valAlbumArtist: 0541 metadata.insert( Meta::Field::ALBUMARTIST, tags[key] ); 0542 break; 0543 case Meta::valArtist: 0544 metadata.insert( Meta::Field::ARTIST, tags[key] ); 0545 break; 0546 case Meta::valDiscNr: 0547 metadata.insert( Meta::Field::DISCNUMBER, tags[key] ); 0548 break; 0549 case Meta::valTitle: 0550 metadata.insert( Meta::Field::TITLE, tags[key] ); 0551 break; 0552 case Meta::valTrackNr: 0553 metadata.insert( Meta::Field::TRACKNUMBER, tags[key] ); 0554 break; 0555 } 0556 } 0557 } 0558 else 0559 metadata.insert( Meta::Field::TITLE, track->name() ); 0560 0561 if( !track->album().isNull() && !track->album()->name().isEmpty() ) 0562 metadata.insert( Meta::Field::ALBUM, track->album()->name() ); 0563 if( !track->artist().isNull() && !track->artist()->name().isEmpty() ) 0564 metadata.insert( Meta::Field::ARTIST, track->artist()->name() ); 0565 if( track->discNumber() > 0 ) 0566 metadata.insert( Meta::Field::DISCNUMBER, track->discNumber() ); 0567 if( track->trackNumber() > 0 ) 0568 metadata.insert( Meta::Field::TRACKNUMBER, track->trackNumber() ); 0569 0570 debug() << "Guessed track info:"; 0571 foreach( const QString &tag, metadata.keys() ) 0572 debug() << '\t' << tag << ":\t" << metadata.value( tag ).toString(); 0573 0574 return metadata; 0575 } 0576 0577 QNetworkRequest 0578 MusicBrainzFinder::compileTrackRequest( const Meta::TrackPtr &track ) 0579 { 0580 QString queryString; 0581 QVariantMap metadata = guessMetadata( track ); 0582 0583 // These characters are not considered in the query, and some of them can break it. 0584 QRegExp unsafe( "[.,:;!?()\\[\\]{}\"]" ); 0585 // http://lucene.apache.org/core/old_versioned_docs/versions/3_4_0/queryparsersyntax.html#Escaping Special Characters 0586 QRegExp special( "([+\\-!(){}\\[\\]\\^\"~*?:\\\\]|&&|\\|\\|)" ); 0587 QString escape( "\\\\1" ); 0588 // We use fuzzy search to bypass typos and small mistakes. 0589 QRegExp endOfWord( "([a-zA-Z0-9])(\\s|$)" ); 0590 QString fuzzy( "\\1~\\2" ); 0591 /* 0592 * The query results in: 0593 * ("track~ title~"^20 track~ title~) AND artist:("artist~ name~"^2 artist~ name~) AND release:("album~ name~"^7 album~ name~) 0594 * Phrases inside quotes are searched as is (and they're given precedence with the ^N 0595 * - where N was found during tests), with the ~ having absolutely no effect (so we 0596 * don't bother removing it). Words outside quotes have a OR logic: this can throw in 0597 * some bad results, but helps a lot with very bad tagged tracks. 0598 * We might be tempted to search also by qdur (quantized duration), but this has 0599 * proved to exclude lots of good results. 0600 */ 0601 #define VALUE( k ) metadata.value( k ).toString().remove( unsafe ).replace( special, escape ).replace( endOfWord, fuzzy ) 0602 if( metadata.contains( Meta::Field::TITLE ) ) 0603 queryString += QString( "(\"%1\"^20 %1)" ).arg( VALUE( Meta::Field::TITLE ) ); 0604 if( metadata.contains( Meta::Field::ARTIST ) ) 0605 queryString += QString( " AND artist:(\"%1\"^2 %1)" ).arg( VALUE( Meta::Field::ARTIST ) ); 0606 if( metadata.contains( Meta::Field::ALBUM ) ) 0607 queryString += QString( " AND release:(\"%1\"^7 %1)" ).arg( VALUE( Meta::Field::ALBUM ) ); 0608 #undef VALUE 0609 0610 m_parsedMetadata.insert( track, metadata ); 0611 0612 QUrl url; 0613 QUrlQuery query; 0614 url.setPath( mb_pathPrefix + "/recording" ); 0615 query.addQueryItem( "limit", "10" ); 0616 query.addQueryItem( "query", queryString ); 0617 url.setQuery( query ); 0618 0619 return compileRequest( url ); 0620 } 0621 0622 QNetworkRequest 0623 MusicBrainzFinder::compilePUIDRequest( const QString &puid ) 0624 { 0625 QUrl url; 0626 QUrlQuery query; 0627 url.setPath( mb_pathPrefix + "/recording" ); 0628 query.addQueryItem( "query", "puid:" + puid ); 0629 url.setQuery( query ); 0630 0631 return compileRequest( url ); 0632 } 0633 0634 QNetworkRequest 0635 MusicBrainzFinder::compileReleaseGroupRequest( const QString &releaseGroupID ) 0636 { 0637 QUrl url; 0638 QUrlQuery query; 0639 url.setPath( mb_pathPrefix + "/release-group/" + releaseGroupID ); 0640 query.addQueryItem( "inc", "artists" ); 0641 url.setQuery( query ); 0642 0643 return compileRequest( url ); 0644 } 0645 0646 QNetworkRequest 0647 MusicBrainzFinder::compileRequest( QUrl &url ) 0648 { 0649 url.setScheme( "http" ); 0650 url.setHost( mb_host ); 0651 url.setPort( mb_port ); 0652 0653 QNetworkRequest req( url ); 0654 req.setRawHeader( "Accept", "application/xml"); 0655 req.setRawHeader( "Connection", "Keep-Alive" ); 0656 req.setRawHeader( "User-Agent" , "Amarok" ); 0657 0658 if( !m_timer->isActive() ) 0659 m_timer->start(); 0660 0661 return req; 0662 } 0663