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