[kde-doc-english] [amarok] /: Make MusicBrainz tagger more accurate and easy

Alberto Villa avilla at FreeBSD.org
Tue Apr 30 06:41:00 UTC 2013


Git commit 6a3bc40e271423eb7b44eae515d9ac75da67ac42 by Alberto Villa.
Committed on 29/04/2013 at 17:59.
Pushed by avilla into branch 'master'.

Make MusicBrainz tagger more accurate and easy

The move to web service 2 makes disc number and artist credit better
defined, and multiple artists are not a problem anymore. Many more
results are now shown, with the new "Choose Best Matches from This
Album" action to avoid mixing tags from different albums.

Some speedups and visual/usability improvements complete the patch.

REVIEW:	105290
GUI:	MusicBrainz tagger: more tags shown, new toolbar, and an improved
	context menu for each search result (with >1 tracks only).
CCMAIL:	amarok-promo at kde.org
DIGEST:	Amarok's MusicBrainz tagger can now help choose the best tags possible.

M  +2    -0    ChangeLog
M  +4    -1    src/CMakeLists.txt
M  +67   -29   src/dialogs/MusicBrainzTagger.cpp
M  +16   -13   src/dialogs/MusicBrainzTagger.h
M  +2    -2    src/dialogs/MusicBrainzTagger.ui
M  +445  -292  src/musicbrainz/MusicBrainzFinder.cpp
M  +27   -26   src/musicbrainz/MusicBrainzFinder.h
M  +13   -11   src/musicbrainz/MusicBrainzMeta.h
D  +0    -783  src/musicbrainz/MusicBrainzTags.cpp
D  +0    -135  src/musicbrainz/MusicBrainzTags.h
A  +497  -0    src/musicbrainz/MusicBrainzTagsItem.cpp     [License: GPL (v2+)]
A  +74   -0    src/musicbrainz/MusicBrainzTagsItem.h     [License: GPL (v2+)]
A  +337  -0    src/musicbrainz/MusicBrainzTagsModel.cpp     [License: GPL (v2+)]
A  +74   -0    src/musicbrainz/MusicBrainzTagsModel.h     [License: GPL (v2+)]
C  +32   -15   src/musicbrainz/MusicBrainzTagsModelDelegate.cpp [from: src/musicbrainz/MusicBrainzMeta.h - 056% similarity]
C  +12   -17   src/musicbrainz/MusicBrainzTagsModelDelegate.h [from: src/musicbrainz/MusicBrainzMeta.h - 069% similarity]
A  +237  -0    src/musicbrainz/MusicBrainzTagsView.cpp     [License: GPL (v2+)]
C  +32   -15   src/musicbrainz/MusicBrainzTagsView.h [from: src/musicbrainz/MusicBrainzMeta.h - 060% similarity]
M  +284  -148  src/musicbrainz/MusicBrainzXmlParser.cpp
M  +23   -20   src/musicbrainz/MusicBrainzXmlParser.h

http://commits.kde.org/amarok/6a3bc40e271423eb7b44eae515d9ac75da67ac42

diff --git a/ChangeLog b/ChangeLog
index 717096a..4cc2fa0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,8 @@ VERSION 2.8-Beta 1
    * Added Ctrl+H shortcut to randomize playlist, patch by Harsh Gupta. (BR 208061)
 
   CHANGES:
+   * Update the MusicBrainz tagger to MusicBrainz web service 2, make it show some more
+     good suggestions for tagging, and add some options to help choosing the best results.
    * Add note about generating .mood files into Moodbar Options; patch by Harsh Gupta with
      tweaks by Matěj Laitl. (BR 289483)
    * Amarok now depends on Qt 4.8.2.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6156a83..4c16c1c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -674,7 +674,10 @@ set(amaroklib_LIB_SRCS
     browsers/filebrowser/FileBrowser.cpp
     browsers/filebrowser/FileView.cpp
     musicbrainz/MusicBrainzFinder.cpp
-    musicbrainz/MusicBrainzTags.cpp
+    musicbrainz/MusicBrainzTagsItem.cpp
+    musicbrainz/MusicBrainzTagsModel.cpp
+    musicbrainz/MusicBrainzTagsModelDelegate.cpp
+    musicbrainz/MusicBrainzTagsView.cpp
     musicbrainz/MusicBrainzXmlParser.cpp
     OpmlOutline.cpp
     OpmlParser.cpp
diff --git a/src/dialogs/MusicBrainzTagger.cpp b/src/dialogs/MusicBrainzTagger.cpp
index 9ac99a6..21753e7 100644
--- a/src/dialogs/MusicBrainzTagger.cpp
+++ b/src/dialogs/MusicBrainzTagger.cpp
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -21,11 +22,19 @@
 #include "core/meta/support/MetaConstants.h"
 #include "core/meta/support/MetaUtility.h"
 #include "core/support/Debug.h"
+#include "musicbrainz/MusicBrainzFinder.h"
+#include "musicbrainz/MusicBrainzTagsModel.h"
+#include "musicbrainz/MusicBrainzTagsModelDelegate.h"
+#ifdef HAVE_LIBOFA
+#include "musicbrainz/MusicDNSFinder.h"
+#endif
 #include "ui_MusicBrainzTagger.h"
 
-#include <KMessageBox>
-#include <QTimer>
+#include <KIcon>
+
+#include <QSortFilterProxyModel>
 #include <QToolBar>
+#include <QToolButton>
 
 MusicBrainzTagger::MusicBrainzTagger( const Meta::TrackList &tracks,
                                       QWidget *parent )
@@ -33,7 +42,7 @@ MusicBrainzTagger::MusicBrainzTagger( const Meta::TrackList &tracks,
     , ui( new Ui::MusicBrainzTagger() )
 {
     DEBUG_BLOCK
-    foreach( Meta::TrackPtr track, tracks)
+    foreach( Meta::TrackPtr track, tracks )
     {
         if( !track->playableUrl().toLocalFile().isEmpty() )
             m_tracks << track;
@@ -46,10 +55,6 @@ MusicBrainzTagger::MusicBrainzTagger( const Meta::TrackList &tracks,
 
 MusicBrainzTagger::~MusicBrainzTagger()
 {
-    delete mb_finder;
-#ifdef HAVE_LIBOFA
-    delete mdns_finder;
-#endif
     delete ui;
 }
 
@@ -59,27 +64,61 @@ MusicBrainzTagger::init()
     DEBUG_BLOCK
     setButtons( KDialog::None );
     setAttribute( Qt::WA_DeleteOnClose );
-    setMinimumSize( 480, 300 );
+    setMinimumSize( 550, 300 );
+
+    m_resultsModel = new MusicBrainzTagsModel( this );
+    m_resultsModelDelegate = new MusicBrainzTagsModelDelegate( this );
+    m_resultsProxyModel = new QSortFilterProxyModel( this );
+
+    m_resultsProxyModel->setSourceModel( m_resultsModel );
+    m_resultsProxyModel->setSortRole( MusicBrainzTagsModel::SortRole );
+    m_resultsProxyModel->setDynamicSortFilter( true );
+
+    ui->resultsView->setModel( m_resultsProxyModel );
+    ui->resultsView->setItemDelegate( m_resultsModelDelegate );
+    // The column is not important, they all have the same data.
+    ui->resultsView->sortByColumn( 0, Qt::AscendingOrder );
 
     if( m_tracks.count() > 1 )
     {
         QToolBar *toolBar = new QToolBar( this );
-        toolBar->addAction( i18n( "Expand All" ), ui->treeView_Result, SLOT(expandAll()) );
-        toolBar->addAction( i18n( "Collapse All" ), ui->treeView_Result, SLOT(collapseAll()) );
-        toolBar->addAction( i18n( "Expand Unchosen" ), ui->treeView_Result, SLOT(expandUnChosen()) );
-        toolBar->addAction( i18n( "Collapse Chosen" ), ui->treeView_Result, SLOT(collapseChosen()) );
+        toolBar->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
+
+        QAction *lastAction = toolBar->addAction( KIcon( "tools-wizard" ), i18n( "Choose Best Matches" ), m_resultsModel, SLOT(chooseBestMatches()) );
+        lastAction->setToolTip( i18n( "Use the top result for each undecided track. Alternatively, you can click on <b>Choose Best Matches from This Album</b> in the context menu of a good suggestion; it may give even better results because it prevents mixing different album releases together." ) );
+        lastAction = toolBar->addAction( KIcon( "edit-clear" ), i18n( "Clear Choices" ), m_resultsModel, SLOT(clearChoices()) );
+        lastAction->setToolTip( i18n( "Clear all choices, even manually made ones." ) );
+
+        toolBar->addSeparator();
+
+        QToolButton *lastButton = new QToolButton( toolBar );
+        lastAction = new QAction( i18n( "Collapse Chosen" ), lastButton );
+        connect( lastAction, SIGNAL(triggered()),
+                 ui->resultsView, SLOT(collapseChosen()) );
+        lastButton->setDefaultAction( lastAction );
+        lastAction = new QAction( i18n( "Collapse All" ), lastButton );
+        connect( lastAction, SIGNAL(triggered()),
+                 ui->resultsView, SLOT(collapseAll()) );
+        lastButton->addAction( lastAction );
+        toolBar->addWidget( lastButton );
+
+        lastButton = new QToolButton( toolBar );
+        lastAction = new QAction( i18n( "Expand Unchosen" ), lastButton );
+        connect( lastAction, SIGNAL(triggered()),
+                 ui->resultsView, SLOT(expandUnchosen()) );
+        lastButton->setDefaultAction( lastAction );
+        lastAction = new QAction( i18n( "Expand All" ), lastButton );
+        connect( lastAction, SIGNAL(triggered()),
+                 ui->resultsView, SLOT(expandAll()) );
+        lastButton->addAction( lastAction );
+        toolBar->addWidget( lastButton );
+
         ui->verticalLayout->insertWidget( 0, toolBar );
     }
 
     ui->progressBar->hide();
 
     mb_finder = new MusicBrainzFinder( this );
-    q_resultsModel = new MusicBrainzTagsModel( this );
-    q_resultsModelDelegate = new MusicBrainzTagsModelDelegate( this );
-    ui->treeView_Result->setModel( q_resultsModel );
-    ui->treeView_Result->setItemDelegateForColumn( 0, q_resultsModelDelegate );
-    ui->treeView_Result->header()->setClickable( true );
-
 #ifdef HAVE_LIBOFA
     mdns_finder = new MusicDNSFinder( this );
     connect( mdns_finder, SIGNAL(trackFound(Meta::TrackPtr,QString)),
@@ -89,10 +128,8 @@ MusicBrainzTagger::init()
 #endif
     connect( mb_finder, SIGNAL(done()), SLOT(searchDone()) );
     connect( mb_finder, SIGNAL(trackFound(Meta::TrackPtr,QVariantMap)),
-             q_resultsModel, SLOT(addTrack(Meta::TrackPtr,QVariantMap)) );
+             m_resultsModel, SLOT(addTrack(Meta::TrackPtr,QVariantMap)) );
     connect( mb_finder, SIGNAL(progressStep()), SLOT(progressStep()) );
-    connect( ui->treeView_Result->header(), SIGNAL(sectionClicked(int)),
-             q_resultsModel, SLOT(selectAll(int)) );
     connect( ui->pushButton_saveAndClose, SIGNAL(clicked(bool)), SLOT(saveAndExit()) );
     connect( ui->pushButton_cancel, SIGNAL(clicked(bool)), SLOT(reject()) );
 }
@@ -100,21 +137,23 @@ MusicBrainzTagger::init()
 void
 MusicBrainzTagger::search()
 {
-    ui->progressBar->setRange( 0, m_tracks.count() * 2 );
-    ui->progressBar->setValue( 0 );
-    ui->horizontalSpacer->changeSize( 0, 0, QSizePolicy::Ignored );
-    ui->progressBar->show();
+    int barSize = m_tracks.count();
     mb_finder->run( m_tracks );
 #ifdef HAVE_LIBOFA
+    barSize *= 2;
     mdns_searchDone = false;
     mdns_finder->run( m_tracks );
 #endif
+    ui->progressBar->setRange( 0, barSize );
+    ui->progressBar->setValue( 0 );
+    ui->horizontalSpacer->changeSize( 0, 0, QSizePolicy::Ignored );
+    ui->progressBar->show();
 }
 
 void
 MusicBrainzTagger::saveAndExit()
 {
-    QMap < Meta::TrackPtr, QVariantMap > result = q_resultsModel->getAllChecked();
+    QMap<Meta::TrackPtr, QVariantMap> result = m_resultsModel->chosenItems();
 
     if( !result.isEmpty() )
         emit sendResult( result );
@@ -132,8 +171,8 @@ MusicBrainzTagger::searchDone()
 #endif
     ui->horizontalSpacer->changeSize( 0, 0, QSizePolicy::Expanding );
     ui->progressBar->hide();
-    ui->treeView_Result->expandAll();
-    ui->treeView_Result->header()->resizeSections( QHeaderView::ResizeToContents );
+    ui->resultsView->expandAll();
+    ui->resultsView->header()->resizeSections( QHeaderView::ResizeToContents );
 }
 
 #ifdef HAVE_LIBOFA
@@ -154,4 +193,3 @@ MusicBrainzTagger::progressStep()
 }
 
 #include "MusicBrainzTagger.moc"
-
diff --git a/src/dialogs/MusicBrainzTagger.h b/src/dialogs/MusicBrainzTagger.h
index 5fa0271..928f005 100644
--- a/src/dialogs/MusicBrainzTagger.h
+++ b/src/dialogs/MusicBrainzTagger.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -18,22 +19,23 @@
 #define MUSICBRAINZTAGGER_H
 
 #include "config.h"
-#include <KDialog>
 #include "core/meta/Meta.h"
-#include "musicbrainz/MusicBrainzFinder.h"
-#include "musicbrainz/MusicBrainzTags.h"
-#include <QItemSelectionModel>
 
-#ifdef HAVE_LIBOFA
-    #include "musicbrainz/MusicDNSFinder.h"
-#endif
+#include <KDialog>
 
 namespace Ui
 {
     class MusicBrainzTagger;
 }
 
-class TrackListModel;
+class MusicBrainzFinder;
+class MusicBrainzTagsModel;
+class MusicBrainzTagsModelDelegate;
+#ifdef HAVE_LIBOFA
+class MusicDNSFinder;
+#endif
+
+class QSortFilterProxyModel;
 
 class MusicBrainzTagger : public KDialog
 {
@@ -48,16 +50,16 @@ class MusicBrainzTagger : public KDialog
         virtual ~MusicBrainzTagger();
 
     signals:
-        void sendResult( const QMap < Meta::TrackPtr, QVariantMap > result );
+        void sendResult( const QMap<Meta::TrackPtr, QVariantMap> result );
 
     private slots:
-        void saveAndExit();
         void search();
+        void progressStep();
         void searchDone();
 #ifdef HAVE_LIBOFA
         void mdnsSearchDone();
 #endif
-        void progressStep();
+        void saveAndExit();
 
     private:
         void init();
@@ -71,8 +73,9 @@ class MusicBrainzTagger : public KDialog
         MusicDNSFinder *mdns_finder;
         bool mdns_searchDone;
 #endif
-        MusicBrainzTagsModel *q_resultsModel;
-        MusicBrainzTagsModelDelegate *q_resultsModelDelegate;
+        MusicBrainzTagsModel *m_resultsModel;
+        MusicBrainzTagsModelDelegate *m_resultsModelDelegate;
+        QSortFilterProxyModel *m_resultsProxyModel;
 };
 
 #endif // MUSICBRAINZTAGGER_H
diff --git a/src/dialogs/MusicBrainzTagger.ui b/src/dialogs/MusicBrainzTagger.ui
index 6762418..156f8d3 100644
--- a/src/dialogs/MusicBrainzTagger.ui
+++ b/src/dialogs/MusicBrainzTagger.ui
@@ -13,7 +13,7 @@
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
-    <widget class="MusicBrainzTagsView" name="treeView_Result">
+    <widget class="MusicBrainzTagsView" name="resultsView">
      <property name="alternatingRowColors">
       <bool>true</bool>
      </property>
@@ -77,7 +77,7 @@
   <customwidget>
    <class>MusicBrainzTagsView</class>
    <extends>QTreeView</extends>
-   <header>musicbrainz/MusicBrainzTags.h</header>
+   <header>musicbrainz/MusicBrainzTagsView.h</header>
   </customwidget>
  </customwidgets>
  <resources/>
diff --git a/src/musicbrainz/MusicBrainzFinder.cpp b/src/musicbrainz/MusicBrainzFinder.cpp
index 6635d04..dd8d393 100644
--- a/src/musicbrainz/MusicBrainzFinder.cpp
+++ b/src/musicbrainz/MusicBrainzFinder.cpp
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -24,100 +25,91 @@
 #include "MusicBrainzMeta.h"
 #include "TagsFromFileNameGuesser.h"
 
+#include <ThreadWeaver/Weaver>
+
 #include <QAuthenticator>
 #include <QNetworkAccessManager>
 #include <QTimer>
-#include <threadweaver/ThreadWeaver.h>
 
-/* Levenshtein distance algorithm implementation carefully pirated from wikibooks
+/*
+ * Levenshtein distance algorithm implementation carefully pirated from Wikibooks
  * (http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance)
  * and modified (a bit) to return similarity measure instead of distance.
  */
-float similarity( const QString &s1, const QString &s2 )
+float
+similarity( const QString &s1, const QString &s2 )
 {
-    const uint len1 = s1.length(), len2 = s2.length();
-    QVector < QVector < unsigned int > > d( len1 + 1, QVector < uint >( len2 + 1 ) );
-    d[0][0] = 0;
-    for( uint i = 1; i <= len1; ++i) d[i][0] = i;
-    for( uint i = 1; i <= len2; ++i) d[0][i] = i;
-
-    for( uint i = 1; i <= len1; ++i )
-        for( uint j = 1; j <= len2; ++j )
-            d[i][j] = qMin( qMin( d[i - 1][j] + 1, d[i][j - 1] + 1 ),
-                            d[i - 1][j - 1] + ( s1[i - 1] == s2[j - 1] ? 0 : 1 ) );
-
-    return 1.0 - ( float )d[len1][len2] / ( len1 + len2 );
+    const size_t len1 = s1.length(), len2 = s2.length();
+    QVector<uint> col( len2 + 1 ), prevCol( len2 + 1 );
+
+    for( uint i = 0; i <= len2; i++ )
+        prevCol[i] = i;
+    for( uint i = 0; i < len1; i++ )
+    {
+        col[0] = i + 1;
+        for( uint j = 0; j < len2; j++ )
+            col[j + 1] = qMin( qMin( 1 + col[j], 1 + prevCol[1 + j] ),
+                               prevCol[j] + ( s1[i] == s2[j] ? 0 : 1 ) );
+        col.swap( prevCol );
+    }
+
+    return 1.0 - ( float )prevCol[len2] / ( len1 + len2 );
 }
 
 MusicBrainzFinder::MusicBrainzFinder( QObject *parent, const QString &host,
-                                     const int port, const QString &pathPrefix,
-                                     const QString &username, const QString &password )
-                 : QObject( parent )
-                 , mb_host( host )
-                 , mb_port( port )
-                 , mb_pathPrefix( pathPrefix )
-                 , mb_username( username )
-                 , mb_password( password )
+                                      const int port, const QString &pathPrefix,
+                                      const QString &username, const QString &password )
+    : QObject( parent )
+    , mb_host( host )
+    , mb_port( port )
+    , mb_pathPrefix( pathPrefix )
+    , mb_username( username )
+    , mb_password( password )
 {
     DEBUG_BLOCK
 
     debug() << "Initiating MusicBrainz search:";
-    debug() << "\tHost:\t\t" << mb_host;
-    debug() << "\tPort:\t\t" << mb_port;
-    debug() << "\tPath Prefix:\t" << mb_pathPrefix;
-    debug() << "\tUsername:\t" << mb_username;
-    debug() << "\tPassword:\t" << mb_password;
+    debug() << "\thost:\t\t" << mb_host;
+    debug() << "\tport:\t\t" << mb_port;
+    debug() << "\tpath prefix:\t" << mb_pathPrefix;
+    debug() << "\tusername:\t" << mb_username;
+    debug() << "\tpassword:\t" << mb_password;
 
     net = The::networkAccessManager();
 
-    _timer = new QTimer( this );
-    _timer->setInterval( 1000 );
+    m_timer = new QTimer( this );
+    m_timer->setInterval( 1000 );
 
-    connect( net, SIGNAL(finished(QNetworkReply*)), SLOT(gotReply(QNetworkReply*)) );
     connect( net, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)),
-            SLOT(authenticationRequest(QNetworkReply*,QAuthenticator*)) );
-    connect( _timer, SIGNAL(timeout()), SLOT(sendNewRequest()) );
+             SLOT(gotAuthenticationRequest(QNetworkReply*,QAuthenticator*)) );
+    connect( net, SIGNAL(finished(QNetworkReply*)),
+             SLOT(gotReply(QNetworkReply*)) );
+    connect( m_timer, SIGNAL(timeout()), SLOT(sendNewRequest()) );
 }
 
-MusicBrainzFinder::~MusicBrainzFinder()
+bool
+MusicBrainzFinder::isRunning() const
 {
-    if( _timer )
-        delete _timer;
-}
-
-void
-MusicBrainzFinder::lookUpByPUID( const Meta::TrackPtr &track, const QString &puid )
-{
-    m_requests.append( qMakePair( track, compilePUIDRequest( puid ) ) );
-
-    if( !_timer->isActive() )
-        _timer->start();
+    return !( m_requests.isEmpty() && m_replies.isEmpty() &&
+              m_parsers.isEmpty() ) || m_timer->isActive();
 }
 
 void
 MusicBrainzFinder::run( const Meta::TrackList &tracks )
 {
-    foreach( Meta::TrackPtr track, tracks )
-        m_requests.append( qMakePair( track, compileRequest( track ) ) );
+    foreach( const Meta::TrackPtr &track, tracks )
+        m_requests.append( qMakePair( track, compileTrackRequest( track ) ) );
 
-    _timer->start();
-}
-
-bool
-MusicBrainzFinder::isRunning()
-{
-    return !( m_parsers.isEmpty() && m_requests.isEmpty() &&
-              m_replyes.isEmpty() ) || _timer->isActive();
+    m_timer->start();
 }
 
 void
-MusicBrainzFinder::authenticationRequest( QNetworkReply *reply, QAuthenticator *authenticator )
+MusicBrainzFinder::lookUpByPUID( const Meta::TrackPtr &track, const QString &puid )
 {
-    if( reply->url().host() == mb_host )
-    {
-        authenticator->setUser( mb_username );
-        authenticator->setPassword( mb_password );
-    }
+    m_requests.append( qMakePair( track, compilePUIDRequest( puid ) ) );
+
+    if( !m_timer->isActive() )
+        m_timer->start();
 }
 
 void
@@ -129,50 +121,74 @@ MusicBrainzFinder::sendNewRequest()
         checkDone();
         return;
     }
-    QPair < Meta::TrackPtr, QNetworkRequest > req = m_requests.takeFirst();
+    QPair<Meta::TrackPtr, QNetworkRequest> req = m_requests.takeFirst();
     QNetworkReply *reply = net->get( req.second );
-    m_replyes.insert( reply, req.first );
+    m_replies.insert( reply, req.first );
     connect( reply, SIGNAL(error(QNetworkReply::NetworkError)),
-             this, SLOT(replyError(QNetworkReply::NetworkError)) );
+             this, SLOT(gotReplyError(QNetworkReply::NetworkError)) );
 
-    debug() << "Request sent: " << req.second.url().toString();
+    debug() << "Request sent:" << req.second.url().toString();
 }
 
 void
-MusicBrainzFinder::gotReply( QNetworkReply *reply )
+MusicBrainzFinder::gotAuthenticationRequest( const QNetworkReply *reply,
+                                             QAuthenticator *authenticator ) const
 {
-    DEBUG_BLOCK
-    if( reply->error() == QNetworkReply::NoError && m_replyes.contains( reply ) )
+    if( reply->url().host() == mb_host )
     {
-        QString document( reply->readAll() );
-        MusicBrainzXmlParser *parser = new MusicBrainzXmlParser( document );
-        if( !m_replyes.value( reply ).isNull() )
-            m_parsers.insert( parser, m_replyes.value( reply ) );
-
-        connect( parser, SIGNAL(done(ThreadWeaver::Job*)), SLOT(parsingDone(ThreadWeaver::Job*)) );
-        ThreadWeaver::Weaver::instance()->enqueue( parser );
+        authenticator->setUser( mb_username );
+        authenticator->setPassword( mb_password );
     }
-
-    m_replyes.remove( reply );
-    reply->deleteLater();
-    checkDone();
 }
 
 void
-MusicBrainzFinder::replyError( QNetworkReply::NetworkError code )
+MusicBrainzFinder::gotReplyError( QNetworkReply::NetworkError code )
 {
     DEBUG_BLOCK
-    QNetworkReply *reply = qobject_cast< QNetworkReply * >( sender() );
+    QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
     if( !reply )
         return;
 
-    if( !m_replyes.contains( reply ) || code == QNetworkReply::NoError )
+    if( !m_replies.contains( reply ) || code == QNetworkReply::NoError )
         return;
 
-    debug() << "Error occurred during network request: " << reply->errorString();
+    debug() << "Error occurred during network request:" << reply->errorString();
     disconnect( reply, SIGNAL(error(QNetworkReply::NetworkError)),
-                this, SLOT(replyError(QNetworkReply::NetworkError)) );
-    m_replyes.remove( reply );
+                this, SLOT(gotReplyError(QNetworkReply::NetworkError)) );
+
+    // Send an empty result to populate the tagger.
+    sendTrack( m_replies.value( reply ), QVariantMap() );
+
+    m_replies.remove( reply );
+    reply->deleteLater();
+    checkDone();
+}
+
+void
+MusicBrainzFinder::gotReply( QNetworkReply *reply )
+{
+    DEBUG_BLOCK
+    if( m_replies.contains( reply ) )
+    {
+        if( reply->error() == QNetworkReply::NoError )
+        {
+            QString document( reply->readAll() );
+            MusicBrainzXmlParser *parser = new MusicBrainzXmlParser( document );
+            m_parsers.insert( parser, m_replies.value( reply ) );
+
+            connect( parser, SIGNAL(done(ThreadWeaver::Job*)),
+                     SLOT(parsingDone(ThreadWeaver::Job*)) );
+            ThreadWeaver::Weaver::instance()->enqueue( parser );
+        }
+        else
+            /*
+             * Send an empty result to populate the tagger. In theory, this part should
+             * never be reachable, but you never know.
+             */
+            sendTrack( m_replies.value( reply ), QVariantMap() );
+    }
+
+    m_replies.remove( reply );
     reply->deleteLater();
     checkDone();
 }
@@ -181,322 +197,459 @@ void
 MusicBrainzFinder::parsingDone( ThreadWeaver::Job *_parser )
 {
     DEBUG_BLOCK
-    MusicBrainzXmlParser *parser = qobject_cast< MusicBrainzXmlParser * >( _parser );
-    disconnect( parser, SIGNAL(done(ThreadWeaver::Job*)), this, SLOT(parsingDone(ThreadWeaver::Job*)) );
-    if( m_parsers.contains( parser ) )
+    MusicBrainzXmlParser *parser = qobject_cast<MusicBrainzXmlParser *>( _parser );
+    disconnect( parser, SIGNAL(done(ThreadWeaver::Job*)),
+                this, SLOT(parsingDone(ThreadWeaver::Job*)) );
+
+    if( m_parsers.contains( parser ) && !m_parsers.value( parser ).isNull() )
     {
-        Meta::TrackPtr trackPtr = m_parsers.take( parser );
+        // When m_parsers.value( parser ) is not empty, we've been parsing tracks.
+        Meta::TrackPtr trackPtr = m_parsers.value( parser );
+        bool found = false;
 
         emit progressStep();
-        if( parser->type() == MusicBrainzXmlParser::TrackList && !parser->tracks.isEmpty() )
+        if( parser->type() == MusicBrainzXmlParser::TrackList &&
+            !parser->tracks.isEmpty() )
         {
-            if( m_parsedMetaData.contains( trackPtr ) )
+            QVariantMap metadata = m_parsedMetadata.value( trackPtr );
+
+            QString scoreType = MusicBrainz::MUSICBRAINZ;
+            // Maximum allowed error in track length (seconds).
+            qlonglong lengthTolerance = 30;
+
+            // If there is no parsed metadata, a fingerprint lookup was done.
+            if( !m_parsedMetadata.contains( trackPtr ) )
             {
-                QVariantMap metadata = m_parsedMetaData.value( trackPtr );
-                foreach( QVariantMap track, parser->tracks.values() )
-                {
-                    if( track.value( Meta::Field::SCORE ).toInt() < 50 )
-                        continue;
+                scoreType = MusicBrainz::MUSICDNS;
+                lengthTolerance = 10;
+            }
 
-                    float s = 0;
-                    int maxPossibleScore = 0;
+            lengthTolerance *= 1000;
+            foreach( QVariantMap track, parser->tracks.values() )
+            {
+#define SIMILARITY( k ) similarity( metadata.value( k ).toString().toLower(), \
+                                    track.value( k ).toString().toLower() )
+                if( track.value( Meta::Field::SCORE ).toInt() < 50 )
+                    continue;
+
+                QString title = track.value( Meta::Field::TITLE ).toString();
+                qlonglong length = track.value( Meta::Field::LENGTH ).toLongLong();
+                float score = 0;
+                int maxScore = 0;
+
+                /*
+                 * We don't check for the entry to exist because a result without an
+                 * artist deserves a bad score.
+                 */
+                if( metadata.contains( Meta::Field::ARTIST ) )
+                {
+                    score += 6.0 * SIMILARITY( Meta::Field::ARTIST );
+                    maxScore += 6;
+                }
 
-                    QString release;
-                    if( metadata.contains( Meta::Field::ALBUM ) && track.contains( MusicBrainz::RELEASELIST ) )
+                if( track.contains( MusicBrainz::RELEASELIST ) )
+                {
+                    // We try to send as many tracks as are the related releases.
+                    foreach( const QString &releaseID,
+                             track.value( MusicBrainz::RELEASELIST ).toStringList() )
                     {
-                        float releaseScore = 0;
-                        float score = 0;
+                        /*
+                         * The album artist could be parsed and inserted here, but since
+                         * we have to parse each release group (only once), it's better to
+                         * do it later, as we don't need it to calculate the score anyway.
+                         * The release date has to be fetched in the second round
+                         * (actually, it's the real reason behind the second round), as we
+                         * want it to be the first release date of the release group:
+                         * http://tickets.musicbrainz.org/browse/SEARCH-218
+                         */
+                        QVariantMap release = parser->releases.value( releaseID );
+                        float releaseScore = score;
+                        int maxReleaseScore = maxScore;
+
+                        track.insert( MusicBrainz::RELEASEID, releaseID );
+                        track.insert( MusicBrainz::RELEASEGROUPID,
+                                      release.value( MusicBrainz::RELEASEGROUPID ) );
+
+                        track.insert( Meta::Field::ALBUM,
+                                      release.value( Meta::Field::TITLE ) );
+                        if( metadata.contains( Meta::Field::ALBUM ) )
+                        {
+                            releaseScore += 12.0 * SIMILARITY( Meta::Field::ALBUM );
+                            maxReleaseScore += 12;
+                        }
 
-                        foreach( QString releaseID, track.value( MusicBrainz::RELEASELIST ).toStringList() )
+                        int trackCount = release.value( MusicBrainz::TRACKCOUNT ).toInt();
+                        if( trackCount > 0 )
+                            track.insert( MusicBrainz::TRACKCOUNT, trackCount );
+                        else
+                            track.remove( MusicBrainz::TRACKCOUNT );
+
+                        /*
+                         * A track can appear more than once in a release (on different
+                         * discs, or in different versions), but we're going to send it
+                         * multiple times only if it has different properties per
+                         * iteration (yes, if the properties below are defined, at least
+                         * some of them must be different by design). Otherwise, it would
+                         * result in duplicated entries (which is bad for several
+                         * reasons).
+                         */
+                        foreach( const QVariant &info,
+                                 track.value( MusicBrainz::TRACKINFO ).toMap().value( releaseID ).toList() )
                         {
-                            if( ( score =  12.0 * similarity( metadata.value( Meta::Field::ALBUM ).toString().toLower(),
-                                  parser->releases.value( releaseID ).value( Meta::Field::TITLE )
-                                  .toString().toLower() ) ) > releaseScore )
+                            QVariantMap trackInfo = info.toMap();
+                            float currentReleaseScore = releaseScore;
+                            int maxCurrentReleaseScore = maxReleaseScore;
+
+                            /*
+                             * Track title and length can be different on different
+                             * releases.
+                             */
+                            QString currentTitle = trackInfo.value( Meta::Field::TITLE ).toString();
+                            if( currentTitle.isEmpty() )
+                                currentTitle = title;
+                            track.insert( Meta::Field::TITLE, currentTitle );
+                            // Same logic as for the artist tag above.
+                            if( metadata.contains( Meta::Field::TITLE ) )
                             {
-                                releaseScore = score;
-                                release = releaseID;
+                                currentReleaseScore += 22.0 * SIMILARITY( Meta::Field::TITLE );
+                                maxCurrentReleaseScore += 22;
                             }
-                        }
-                        s += releaseScore;
-                        maxPossibleScore += 12;
-                    }
-
-                    if( release.isEmpty() && track.contains( MusicBrainz::RELEASELIST ) &&
-                        !track.value( MusicBrainz::RELEASELIST ).toStringList().isEmpty() )
-                        release = track.value( MusicBrainz::RELEASELIST ).toStringList().first();
-
-                    if( !release.isEmpty() )
-                    {
-                        track.insert( MusicBrainz::RELEASEID, release );
-                        track.insert( Meta::Field::TRACKNUMBER,
-                                      track.value( MusicBrainz::TRACKOFFSET ).toMap().value( release ).toInt() );
-                    }
 
-                    if( metadata.contains( Meta::Field::ARTIST ) && track.contains( Meta::Field::ARTIST ) )
-                    {
-                        s += 6.0 * similarity( metadata.value( Meta::Field::ARTIST ).toString().toLower(),
-                                            track.value( Meta::Field::ARTIST ).toString().toLower() );
-                        maxPossibleScore += 6;
-                    }
+                            qlonglong currentLength = trackInfo.value( Meta::Field::LENGTH ).toLongLong();
+                            if( currentLength <= 0 )
+                                currentLength = length;
+                            if( currentLength > 0 )
+                                track.insert( Meta::Field::LENGTH, currentLength );
+                            else
+                                track.remove( Meta::Field::LENGTH );
+                            if( track.contains( Meta::Field::LENGTH ) )
+                            {
+                                currentReleaseScore += 8.0 * ( 1.0 - float( qMin( qAbs( trackPtr->length() -
+                                                       track.value( Meta::Field::LENGTH ).toLongLong() ),
+                                                       lengthTolerance ) ) / lengthTolerance );
+                                maxCurrentReleaseScore += 8;
+                            }
 
-                    if( metadata.contains( Meta::Field::TITLE ) && track.contains( Meta::Field::TITLE ) )
-                    {
-                        s += 22.0 * similarity( metadata.value( Meta::Field::TITLE).toString().toLower(),
-                                            track.value( Meta::Field::TITLE ).toString().toLower() );
-                        maxPossibleScore += 22;
+                            int currentDiscNumber = trackInfo.value( Meta::Field::DISCNUMBER ).toInt();
+                            if( currentDiscNumber > 0 )
+                                track.insert( Meta::Field::DISCNUMBER, currentDiscNumber );
+                            else
+                                track.remove( Meta::Field::DISCNUMBER );
+                            if( metadata.contains( Meta::Field::DISCNUMBER ) &&
+                                track.contains( Meta::Field::DISCNUMBER ) )
+                            {
+                                currentReleaseScore += ( metadata.value( Meta::Field::DISCNUMBER ).toInt() ==
+                                                       track.value( Meta::Field::DISCNUMBER ).toInt() )? 6 : 0;
+                                maxCurrentReleaseScore += 6;
+                            }
+                            else if( metadata.value( Meta::Field::DISCNUMBER ).toInt() !=
+                                     track.value( Meta::Field::DISCNUMBER ).toInt() )
+                                /*
+                                 * Always prefer results with matching disc number,
+                                 * even when empty.
+                                 */
+                                currentReleaseScore -= 0.1;
+
+                            int currentTrackNumber = trackInfo.value( Meta::Field::TRACKNUMBER ).toInt();
+                            if( currentTrackNumber > 0 )
+                                track.insert( Meta::Field::TRACKNUMBER, currentTrackNumber );
+                            else
+                                track.remove( Meta::Field::TRACKNUMBER );
+                            if( metadata.contains( Meta::Field::TRACKNUMBER ) &&
+                                track.contains( Meta::Field::TRACKNUMBER ) )
+                            {
+                                currentReleaseScore += ( metadata.value( Meta::Field::TRACKNUMBER ).toInt() ==
+                                                       track.value( Meta::Field::TRACKNUMBER ).toInt() )? 6 : 0;
+                                maxCurrentReleaseScore += 6;
+                            }
+                            else if( metadata.value( Meta::Field::TRACKNUMBER ).toInt() !=
+                                     track.value( Meta::Field::TRACKNUMBER ).toInt() )
+                                /*
+                                 * Always prefer results with matching track number,
+                                 * even when empty.
+                                 */
+                                currentReleaseScore -= 0.1;
+
+                            if( maxCurrentReleaseScore <= 0 )
+                                continue;
+
+                            float sim = currentReleaseScore / maxCurrentReleaseScore;
+                            if( sim > MusicBrainz::MINSIMILARITY )
+                            {
+                                found = true;
+                                track.insert( scoreType, sim );
+                                sendTrack( trackPtr, track );
+                            }
+                        }
                     }
-
-                    if( metadata.contains( Meta::Field::TRACKNUMBER ) && track.contains( Meta::Field::TRACKNUMBER ) )
+                }
+                else
+                {
+                    // A track without releases has been found (not too rare).
+                    if( metadata.contains( Meta::Field::TITLE ) )
                     {
-                        s += ( metadata.value( Meta::Field::TRACKNUMBER ).toInt()
-                               == track.value( Meta::Field::TRACKNUMBER ).toInt() )? 6 : 0;
-                        maxPossibleScore += 6;
+                        score += 22.0 * SIMILARITY( Meta::Field::TITLE );
+                        maxScore += 22;
                     }
 
                     if( track.contains( Meta::Field::LENGTH ) )
                     {
-                        s += 8.0 * ( 1.0 - float( qMin( qAbs( trackPtr->length() -
-                                     track.value( Meta::Field::LENGTH ).toLongLong() ),
-                                     Q_INT64_C( 30000 ) ) ) / 30000 );
-                        maxPossibleScore += 8;
+                        score += 8.0 * ( 1.0 - float( qMin( qAbs( trackPtr->length() -
+                                 track.value( Meta::Field::LENGTH ).toLongLong() ),
+                                 lengthTolerance ) ) / lengthTolerance );
+                        maxScore += 8;
                     }
 
-                    float sim = s / maxPossibleScore;
+                    if( maxScore <= 0 )
+                        continue;
+
+                    float sim = score / maxScore;
                     if( sim > MusicBrainz::MINSIMILARITY )
                     {
-                        track.insert( MusicBrainz::SIMILARITY, sim );
-                        track.insert( MusicBrainz::MUSICBRAINZ, true );
+                        found = true;
+                        track.insert( scoreType, sim );
                         sendTrack( trackPtr, track );
                     }
                 }
-                m_parsedMetaData.remove( trackPtr );
+#undef SIMILARITY
+            }
+            m_parsedMetadata.remove( trackPtr );
+        }
+        else if( parser->type() != MusicBrainzXmlParser::TrackList )
+            debug() << "Invalid parsing result.";
+
+        /*
+         * Sending an empty result is important: it creates a disabled entry in the tagger
+         * to show that the track was not found (otherwise, it would pass unnoticed).
+         */
+        if( !found )
+            sendTrack( trackPtr, QVariantMap() );
+    }
+    else if( parser->type() == MusicBrainzXmlParser::ReleaseGroup &&
+             !parser->releaseGroups.isEmpty() )
+    {
+        // Cache the release group and flush the queue of tracks.
+        QString releaseGroupID = parser->releaseGroups.keys().first();
+        mb_releaseGroups.insert( releaseGroupID,
+                                 parser->releaseGroups.value( releaseGroupID ) );
+        foreach( const TrackInfo &trackInfo, mb_queuedTracks.value( releaseGroupID ) )
+            sendTrack( trackInfo.first, trackInfo.second );
+        mb_queuedTracks.remove( releaseGroupID );
+    }
+
+    m_parsers.remove( parser );
+    parser->deleteLater();
+    checkDone();
+}
+
+
+void
+MusicBrainzFinder::sendTrack( const Meta::TrackPtr &track, QVariantMap tags )
+{
+    if( !tags.isEmpty() )
+    {
+        if( tags.contains( MusicBrainz::RELEASEGROUPID ) )
+        {
+            QString releaseGroupID = tags.value( MusicBrainz::RELEASEGROUPID ).toString();
+            if( mb_releaseGroups.contains( releaseGroupID ) )
+            {
+                QVariantMap releaseGroup = mb_releaseGroups.value( releaseGroupID );
+                if( releaseGroup.contains( Meta::Field::ARTIST ) )
+                    tags.insert( Meta::Field::ALBUMARTIST,
+                                 releaseGroup.value( Meta::Field::ARTIST ) );
+                else if( tags.contains( Meta::Field::ARTIST ) )
+                    tags.insert( Meta::Field::ALBUMARTIST,
+                                 tags.value( Meta::Field::ARTIST ) );
+                if( releaseGroup.contains( Meta::Field::YEAR ) )
+                    tags.insert( Meta::Field::YEAR,
+                                 releaseGroup.value( Meta::Field::YEAR ) );
             }
             else
             {
-                foreach( QString trackID, parser->tracks.keys() )
+                /*
+                 * The tags reference a release group we don't know yet. Queue the track
+                 * and fetch information about the release group.
+                 */
+                if( !mb_queuedTracks.contains( releaseGroupID ) )
                 {
-                    QVariantMap track = parser->grabTrackByID( trackID );
-                    float sim = 1.0 - float( qMin( qAbs( trackPtr->length() -
-                                track.value( Meta::Field::LENGTH ).toLongLong() ),
-                                Q_INT64_C( 10000 ) ) ) / 10000;
-                    if( sim > MusicBrainz::MINSIMILARITY )
-                    {
-                        track.insert( MusicBrainz::SIMILARITY, sim );
-                        track.insert( MusicBrainz::MUSICDNS, true );
-                        sendTrack( trackPtr, track );
-                    }
+                    QList<TrackInfo> trackList;
+                    trackList.append( qMakePair( track, tags ) );
+                    mb_queuedTracks.insert( releaseGroupID, trackList );
+                    m_requests.prepend( qMakePair( Meta::TrackPtr(),
+                                                   compileReleaseGroupRequest( releaseGroupID ) ) );
                 }
+                else
+                    mb_queuedTracks[releaseGroupID].append( qMakePair( track, tags ) );
+
+                return;
             }
         }
-        else if( parser->type() == MusicBrainzXmlParser::Track && !parser->tracks.isEmpty() )
-        {
-            emit progressStep();
-            QVariantMap curTrack = parser->grabTrackByLength( trackPtr->length() );
-            curTrack.insert( MusicBrainz::SIMILARITY, 1.0 );
-            curTrack.insert( MusicBrainz::MUSICBRAINZ, true );
-            sendTrack( trackPtr, curTrack );
-        }
-        else
-            debug() << "Unexpected parsing result";
-    }
-    else if( parser->type() == MusicBrainzXmlParser::Release && !parser->releases.isEmpty() )
-    {
-        QString release = parser->releases.keys().first();
-        mb_releasesCache.insert( release, parser->releases.value( release ) );
-        foreach( QString trackID, mb_waitingForReleaseQueue.value( release ) )
-            sendTrack( mb_tracks.take( trackID ), mb_trackInfo.take( trackID ) );
-        mb_waitingForReleaseQueue.remove( release );
+
+        // Clean metadata from unused fields.
+        tags.remove( Meta::Field::LENGTH );
+        tags.remove( Meta::Field::SCORE );
+        tags.remove( MusicBrainz::RELEASELIST );
+        tags.remove( MusicBrainz::TRACKINFO );
     }
 
-    parser->deleteLater();
-    checkDone();
+    emit trackFound( track, tags );
 }
 
 void
 MusicBrainzFinder::checkDone()
 {
-    if( m_parsers.isEmpty() && m_requests.isEmpty() && m_replyes.isEmpty() && mb_waitingForReleaseQueue.isEmpty() )
+    if( m_requests.isEmpty() && m_replies.isEmpty() && m_parsers.isEmpty() )
     {
-        debug() << "There is no any queued requests. Stopping timer.";
-        _timer->stop();
+        /*
+         * Empty the queue of tracks waiting for release group requests. If the requests
+         * fail (hint: network failure), remeaining queued tracks will silently disappear.
+         * Sending an empty result makes the user aware of the fact that the track will
+         * not be tagged.
+         */
+        foreach( const QList<TrackInfo> &trackInfoList,
+                 mb_queuedTracks.values() )
+            foreach( const TrackInfo &trackInfo, trackInfoList )
+                sendTrack( trackInfo.first, QVariantMap() );
+
+        debug() << "There is no queued request. Stopping timer.";
+        m_timer->stop();
         emit done();
     }
 }
 
 QVariantMap
-MusicBrainzFinder::guessMetadata( const Meta::TrackPtr &track )
+MusicBrainzFinder::guessMetadata( const Meta::TrackPtr &track ) const
 {
     DEBUG_BLOCK
-    debug() << "Trying to guess metadata from filename: " << track->playableUrl().fileName().toLower();
+    debug() << "Trying to guess metadata from filename:" << track->playableUrl().fileName();
     QVariantMap metadata;
 
     if( ( track->artist().isNull() || track->artist()->name().isEmpty() ) &&
         ( track->album().isNull() || track->album()->name().isEmpty() ) )
     {
         Meta::FieldHash tags = Meta::Tag::TagGuesser::guessTags( track->playableUrl().fileName() );
-        foreach( quint64 key, tags.keys() )
+        foreach( const quint64 &key, tags.keys() )
+        {
             switch( key )
             {
-                case Meta::valAlbum:
-                    metadata.insert( Meta::Field::ALBUM, tags[key] );
-                    break;
-                case Meta::valAlbumArtist:
-                    metadata.insert( Meta::Field::ALBUMARTIST, tags[key] );
-                    break;
-                case Meta::valArtist:
-                    metadata.insert( Meta::Field::ARTIST, tags[key] );
-                    break;
-                case Meta::valTitle:
-                    metadata.insert( Meta::Field::TITLE, tags[key] );
-                    break;
-                case Meta::valTrackNr:
-                    metadata.insert( Meta::Field::TRACKNUMBER, tags[key] );
+            case Meta::valAlbum:
+                metadata.insert( Meta::Field::ALBUM, tags[key] );
+                break;
+            case Meta::valAlbumArtist:
+                metadata.insert( Meta::Field::ALBUMARTIST, tags[key] );
+                break;
+            case Meta::valArtist:
+                metadata.insert( Meta::Field::ARTIST, tags[key] );
+                break;
+            case Meta::valDiscNr:
+                metadata.insert( Meta::Field::DISCNUMBER, tags[key] );
+                break;
+            case Meta::valTitle:
+                metadata.insert( Meta::Field::TITLE, tags[key] );
+                break;
+            case Meta::valTrackNr:
+                metadata.insert( Meta::Field::TRACKNUMBER, tags[key] );
+                break;
             }
+        }
     }
     else
         metadata.insert( Meta::Field::TITLE, track->name() );
 
-    if( !track->artist().isNull() && !track->artist()->name().isEmpty() )
-        metadata.insert( Meta::Field::ARTIST, track->artist()->name() );
     if( !track->album().isNull() && !track->album()->name().isEmpty() )
         metadata.insert( Meta::Field::ALBUM, track->album()->name() );
+    if( !track->artist().isNull() && !track->artist()->name().isEmpty() )
+        metadata.insert( Meta::Field::ARTIST, track->artist()->name() );
+    if( track->discNumber() > 0 )
+        metadata.insert( Meta::Field::DISCNUMBER, track->discNumber() );
     if( track->trackNumber() > 0 )
         metadata.insert( Meta::Field::TRACKNUMBER, track->trackNumber() );
 
     debug() << "Guessed track info:";
-    foreach( QString tag, metadata.keys() )
+    foreach( const QString &tag, metadata.keys() )
         debug() << '\t' << tag << ":\t" << metadata.value( tag ).toString();
 
     return metadata;
 }
 
-void
-MusicBrainzFinder::sendTrack( const Meta::TrackPtr track, const QVariantMap &info )
-{
-    if( info.isEmpty() )
-        return;
-
-    QVariantMap tags = info;
-    if( tags.contains( MusicBrainz::RELEASEID ) )
-    {
-        QString releaseID = tags.value( MusicBrainz::RELEASEID ).toString();
-        if( mb_releasesCache.contains( releaseID ) )
-        {
-            QVariantMap release = mb_releasesCache.value( releaseID );
-            tags.insert( Meta::Field::ALBUM, release.value( Meta::Field::TITLE) );
-
-            if( release.contains( Meta::Field::YEAR ) )
-                tags.insert( Meta::Field::YEAR, release.value( Meta::Field::YEAR ) );
-
-            if( release.contains( Meta::Field::ARTIST ) )
-                tags.insert( Meta::Field::ALBUMARTIST, release.value( Meta::Field::ARTIST ) );
-        }
-        else
-        {
-            if( !mb_waitingForReleaseQueue.contains( releaseID ) )
-            {
-                mb_waitingForReleaseQueue.insert( releaseID,
-                QStringList( tags.value( MusicBrainz::TRACKID ).toString() ) );
-                m_requests.prepend( qMakePair( Meta::TrackPtr(), compileReleaseRequest( releaseID ) ) );
-            }
-            else
-                mb_waitingForReleaseQueue[ releaseID ].append( tags.value( MusicBrainz::TRACKID ).toString() );
-
-            mb_tracks.insert( tags.value( MusicBrainz::TRACKID ).toString(), track );
-            mb_trackInfo.insert( tags.value( MusicBrainz::TRACKID ).toString(), tags );
-
-            return;
-        }
-    }
-
-    //Clean metadata from unused fields
-    tags.remove( Meta::Field::LENGTH );
-    tags.remove( Meta::Field::SCORE );
-    tags.remove( MusicBrainz::RELEASELIST );
-    tags.remove( MusicBrainz::TRACKOFFSET );
-
-    emit trackFound( track, tags );
-}
-
 QNetworkRequest
-MusicBrainzFinder::compileRequest( const Meta::TrackPtr &track )
+MusicBrainzFinder::compileTrackRequest( const Meta::TrackPtr &track )
 {
-    QString query = QString( "dur:(%1)" ).arg( track->length() );
+    QString query;
     QVariantMap metadata = guessMetadata( track );
 
-    QRegExp escape( "([~!\\^&*()\\-+\\[\\]{}\\\\:\"?])" );
-    QString replacement( "\\\\1" );
-#define VALUE(s) metadata.value( s ).toString().replace( escape, replacement )
+    // These characters are not considered in the query, and some of them can break it.
+    QRegExp unsafe( "[.,:;!?()\\[\\]{}\"]" );
+    // http://lucene.apache.org/core/old_versioned_docs/versions/3_4_0/queryparsersyntax.html#Escaping Special Characters
+    QRegExp special( "([+\\-!(){}\\[\\]\\^\"~*?:\\\\]|&&|\\|\\|)" );
+    QString escape( "\\\\1" );
+    // We use fuzzy search to bypass typos and small mistakes.
+    QRegExp endOfWord( "([a-zA-Z0-9])(\\s|$)" );
+    QString fuzzy( "\\1~\\2" );
+    /*
+     * The query results in:
+     * ("track~ title~"^20 track~ title~) AND artist:("artist~ name~"^2 artist~ name~) AND release:("album~ name~"^7 album~ name~)
+     * Phrases inside quotes are searched as is (and they're given precedence with the ^N
+     * - where N was found during tests), with the ~ having absolutely no effect (so we
+     * don't bother removing it). Words outside quotes have a OR logic: this can throw in
+     * some bad results, but helps a lot with very bad tagged tracks.
+     * We might be tempted to search also by qdur (quantized duration), but this has
+     * proved to exclude lots of good results.
+     */
+#define VALUE( k ) metadata.value( k ).toString().replace( unsafe, "" ).replace( special, escape ).replace( endOfWord, fuzzy )
     if( metadata.contains( Meta::Field::TITLE ) )
-        query += QString( " track:(%1)" ).arg( VALUE( Meta::Field::TITLE ) );
+        query += QString( "(\"%1\"^20 %1)" ).arg( VALUE( Meta::Field::TITLE ) );
     if( metadata.contains( Meta::Field::ARTIST ) )
-        query += QString( " artist:(%1)" ).arg( VALUE( Meta::Field::ARTIST ) );
+        query += QString( " AND artist:(\"%1\"^2 %1)" ).arg( VALUE( Meta::Field::ARTIST ) );
     if( metadata.contains( Meta::Field::ALBUM ) )
-        query += QString ( " release:(%1)" ).arg( VALUE( Meta::Field::ALBUM ) );
+        query += QString( " AND release:(\"%1\"^7 %1)" ).arg( VALUE( Meta::Field::ALBUM ) );
 #undef VALUE
 
-    m_parsedMetaData.insert( track, metadata );
+    m_parsedMetadata.insert( track, metadata );
 
     QUrl url;
-    url.setScheme( "http" );
-    url.setHost( mb_host );
-    url.setPort( mb_port );
-    url.setPath( mb_pathPrefix+"/track/" );
-    url.addQueryItem( "type", "xml" );
+    url.setPath( mb_pathPrefix + "/recording" );
     url.addQueryItem( "limit", "10" );
     url.addQueryItem( "query", query );
 
-    QNetworkRequest req( url);
-    req.setRawHeader( "User-Agent" , "Amarok" );
-    req.setRawHeader( "Connection", "Keep-Alive" );
-
-    if( !_timer->isActive() )
-        _timer->start();
-
-    return req;
+    return compileRequest( url );
 }
 
 QNetworkRequest
-MusicBrainzFinder::compileReleaseRequest( const QString &releasId )
+MusicBrainzFinder::compilePUIDRequest( const QString &puid )
 {
     QUrl url;
-    url.setScheme( "http" );
-    url.setHost( mb_host );
-    url.setPort( mb_port );
-    url.setPath( mb_pathPrefix+"/release/"+ releasId );
-    url.addQueryItem( "type", "xml" );
-    url.addQueryItem( "inc", "artist+release-events" );
+    url.setPath( mb_pathPrefix + "/recording" );
+    url.addQueryItem( "query", "puid:" + puid );
 
-    QNetworkRequest req( url );
-    req.setRawHeader( "User-Agent" , "Amarok" );
-    req.setRawHeader( "Connection", "Keep-Alive" );
+    return compileRequest( url );
+}
 
-    if( !_timer->isActive() )
-        _timer->start();
+QNetworkRequest
+MusicBrainzFinder::compileReleaseGroupRequest( const QString &releaseGroupID )
+{
+    QUrl url;
+    url.setPath( mb_pathPrefix + "/release-group/" + releaseGroupID );
+    url.addQueryItem( "inc", "artists" );
 
-    return req;
+    return compileRequest( url );
 }
 
 QNetworkRequest
-MusicBrainzFinder::compilePUIDRequest( const QString &puid )
+MusicBrainzFinder::compileRequest( QUrl &url )
 {
-    QUrl url;
     url.setScheme( "http" );
     url.setHost( mb_host );
     url.setPort( mb_port );
-    url.setPath( mb_pathPrefix+"/track/" );
-    url.addQueryItem( "type", "xml" );
-    url.addQueryItem( "puid", puid );
 
     QNetworkRequest req( url );
     req.setRawHeader( "User-Agent" , "Amarok" );
     req.setRawHeader( "Connection", "Keep-Alive" );
 
-    if( !_timer->isActive() )
-        _timer->start();
+    if( !m_timer->isActive() )
+        m_timer->start();
 
     return req;
 }
 
 #include "MusicBrainzFinder.moc"
-
diff --git a/src/musicbrainz/MusicBrainzFinder.h b/src/musicbrainz/MusicBrainzFinder.h
index db8cb05..d9faf61 100644
--- a/src/musicbrainz/MusicBrainzFinder.h
+++ b/src/musicbrainz/MusicBrainzFinder.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -22,49 +23,53 @@
 #include "musicbrainz/MusicBrainzXmlParser.h"
 #include "network/NetworkAccessManagerProxy.h"
 
+typedef QPair<Meta::TrackPtr, QVariantMap> TrackInfo;
+
 class MusicBrainzFinder : public QObject
 {
     Q_OBJECT
 
     public:
+        explicit MusicBrainzFinder( QObject *parent = 0,
+                                    const QString &host = "musicbrainz.org",
+                                    const int port = 80,
+                                    const QString &pathPrefix = "/ws/2",
+                                    const QString &username = QString(),
+                                    const QString &password = QString() );
 
-        MusicBrainzFinder( QObject *parent = 0,  const QString &host = "musicbrainz.org",
-                           const int port = 80, const QString &pathPrefix = "/ws/1",
-                           const QString &username = QString(), const QString &password = QString() );
-
-        ~MusicBrainzFinder();
-
-        bool isRunning();
+        bool isRunning() const;
 
     signals:
         void progressStep();
         void trackFound( const Meta::TrackPtr track, const QVariantMap tags );
-
         void done();
 
     public slots:
-        void run(  const Meta::TrackList &tracks );
+        void run( const Meta::TrackList &tracks );
 
         void lookUpByPUID( const Meta::TrackPtr &track, const QString &puid );
 
     private slots:
         void sendNewRequest();
+        void gotAuthenticationRequest( const QNetworkReply *reply,
+                                       QAuthenticator *authenticator ) const;
+        void gotReplyError( QNetworkReply::NetworkError code );
         void gotReply( QNetworkReply *reply );
-        void authenticationRequest( QNetworkReply *reply, QAuthenticator *authenticator );
-        void replyError( QNetworkReply::NetworkError code );
 
         void parsingDone( ThreadWeaver::Job *_parser );
 
     private:
-        QNetworkRequest compileRequest( const Meta::TrackPtr &track );
-        QNetworkRequest compileReleaseRequest( const QString &releasId );
+        QVariantMap guessMetadata( const Meta::TrackPtr &track ) const;
+
+        QNetworkRequest compileRequest( QUrl &url );
+        QNetworkRequest compileTrackRequest( const Meta::TrackPtr &track );
         QNetworkRequest compilePUIDRequest( const QString &puid );
+        QNetworkRequest compileReleaseGroupRequest( const QString &releaseGroupID );
 
+        void sendTrack( const Meta::TrackPtr &track, QVariantMap tags );
         void checkDone();
 
-        QVariantMap guessMetadata( const Meta::TrackPtr &track );
-
-        void sendTrack( const Meta::TrackPtr track, const QVariantMap &info );
+        QTimer *m_timer;
 
         QString mb_host;
         int mb_port;
@@ -72,19 +77,15 @@ class MusicBrainzFinder : public QObject
         QString mb_username;
         QString mb_password;
 
-        QMap < Meta::TrackPtr, QVariantMap > m_parsedMetaData;
-
         QNetworkAccessManager *net;
-        QList < QPair < Meta::TrackPtr, QNetworkRequest > > m_requests;
-        QMap < QNetworkReply *, Meta::TrackPtr > m_replyes;
-        QMap < MusicBrainzXmlParser *, Meta::TrackPtr > m_parsers;
+        QList<QPair<Meta::TrackPtr, QNetworkRequest> > m_requests;
+        QMap<QNetworkReply *, Meta::TrackPtr> m_replies;
+        QMap<MusicBrainzXmlParser *, Meta::TrackPtr> m_parsers;
 
-        QMap < QString, Meta::TrackPtr > mb_tracks;     // MB Track ID -> TrackPtr
-        QMap < QString, QVariantMap > mb_trackInfo;     // MB Track ID -> Track info
-        QMap < QString, QVariantMap > mb_releasesCache; // MB Release ID -> Release info
-        QMap < QString, QStringList > mb_waitingForReleaseQueue;
+        QMap<Meta::TrackPtr, QVariantMap> m_parsedMetadata;
 
-        QTimer *_timer;
+        QMap<QString, QVariantMap> mb_releaseGroups;
+        QMap<QString, QList<TrackInfo> > mb_queuedTracks;
 };
 
 #endif // MUSICBRAINZFINDER_H
diff --git a/src/musicbrainz/MusicBrainzMeta.h b/src/musicbrainz/MusicBrainzMeta.h
index 5cbd190..6e21232 100644
--- a/src/musicbrainz/MusicBrainzMeta.h
+++ b/src/musicbrainz/MusicBrainzMeta.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -21,18 +22,19 @@
 
 namespace MusicBrainz
 {
-    static const QString ARTISTID   = "mb:ArtistID";
-    static const QString RELEASEID  = "mb:ReleaseID";
-    static const QString RELEASELIST= "mb:ReleaseList";
-    static const QString TRACKID    = "mb:TrackID";
-    static const QString TRACKOFFSET= "mb:TrackOffset";
+    static const QString ARTISTID       = "mb:ArtistID";
+    static const QString RELEASEGROUPID = "mb:ReleaseGroupID";
+    static const QString RELEASEID      = "mb:ReleaseID";
+    static const QString RELEASELIST    = "mb:ReleaseList";
+    static const QString TRACKCOUNT     = "mb:TrackCount";
+    static const QString TRACKID        = "mb:TrackID";
+    static const QString TRACKINFO      = "mb:TrackInfo";
 
-    static const QString SIMILARITY = "mb:similarity";
+    static const QString MUSICBRAINZ    = "mb:musicbrainz";
+    static const QString MUSICDNS       = "mb:musicdns";
 
-    static const QString MUSICBRAINZ= "mb:musicbrainz";
-    static const QString MUSICDNS   = "mb:musicdns";
-
-    static const qreal MINSIMILARITY= 0.6;
+    static const QString SIMILARITY     = "mb:similarity";
+    static const qreal   MINSIMILARITY  = 0.6;
 }
 
-#endif //MUSICBRAINZMETA_H
+#endif // MUSICBRAINZMETA_H
diff --git a/src/musicbrainz/MusicBrainzTags.cpp b/src/musicbrainz/MusicBrainzTags.cpp
deleted file mode 100644
index c90c2f8..0000000
--- a/src/musicbrainz/MusicBrainzTags.cpp
+++ /dev/null
@@ -1,783 +0,0 @@
-/****************************************************************************************
- * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
- *                                                                                      *
- * This program is free software; you can redistribute it and/or modify it under        *
- * the terms of the GNU General Public License as published by the Free Software        *
- * Foundation; either version 2 of the License, or (at your option) any later           *
- * version.                                                                             *
- *                                                                                      *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
- * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
- *                                                                                      *
- * You should have received a copy of the GNU General Public License along with         *
- * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
- ****************************************************************************************/
-
-#include "MusicBrainzTags.h"
-
-#include "AmarokMimeData.h"
-#include "core/support/Debug.h"
-#include "core/meta/support/MetaConstants.h"
-#include "core/meta/support/MetaUtility.h"
-#include "MusicBrainzMeta.h"
-
-#include <KStandardDirs>
-
-#include <QApplication>
-#include <QContextMenuEvent>
-#include <QDesktopServices>
-#include <QMenu>
-
-//---------------------------------MusciBrainzTagsItem-----------------------------------
-
-#define DEBUG_PREFIX "MusicBrainzTagsItem"
-
-MusciBrainzTagsItem::MusciBrainzTagsItem( MusciBrainzTagsItem *parent,
-                                          const Meta::TrackPtr track,
-                                          const QVariantMap tags )
-                   : m_parent( parent )
-                   , m_track( track )
-                   , m_data( tags )
-                   , m_checked( false )
-                   , m_dataLock( QReadWriteLock::Recursive )
-                   , m_childrenLock( QReadWriteLock::Recursive )
-                   , m_parentLock( QReadWriteLock::Recursive )
-{
-}
-
-MusciBrainzTagsItem::~MusciBrainzTagsItem()
-{
-    qDeleteAll( m_childItems );
-}
-
-MusciBrainzTagsItem *
-MusciBrainzTagsItem::parent() const
-{
-    QReadLocker lock( &m_parentLock );
-    return m_parent;
-}
-
-void
-MusciBrainzTagsItem::setParent( MusciBrainzTagsItem *parent )
-{
-    m_parentLock.lockForWrite();
-    m_parent = parent;
-    m_parentLock.unlock();
-}
-
-MusciBrainzTagsItem *
-MusciBrainzTagsItem::child( const int row ) const
-{
-    QReadLocker lock( &m_childrenLock );
-    return m_childItems.value( row );
-}
-
-void
-MusciBrainzTagsItem::appendChild( MusciBrainzTagsItem *child )
-{
-    if( child->track().isNull() || child->data().isEmpty() )
-    {
-        delete child;
-        return;
-    }
-
-    if( m_track.isNull() )
-    {
-        bool found = false;
-
-        m_childrenLock.lockForRead();
-        foreach( MusciBrainzTagsItem *item, m_childItems )
-            if( item->track() == child->track() )
-            {
-                item->appendChild( child );
-                found = true;
-                break;
-            }
-        m_childrenLock.unlock();
-
-        if( !found )
-        {
-            MusciBrainzTagsItem *newChild = new MusciBrainzTagsItem( this, child->track() );
-            newChild->appendChild( child );
-            m_childrenLock.lockForWrite();
-            m_childItems.append( newChild );
-            m_childrenLock.unlock();
-        }
-    }
-    else
-    {
-        if( m_track != child->track() )
-        {
-            debug() << "Try to insert track data to the wrong tree branch.";
-            delete child;
-            return;
-        }
-
-        bool notFound = true;
-        child->setParent( this );
-
-        for( int i = 0; i < childCount(); i++ )
-            if( m_childItems.value( i )->dataValue( MusicBrainz::TRACKID ).toString()
-                == child->dataValue( MusicBrainz::TRACKID ).toString() )
-            {
-                debug() << "This track ID already in the Tree: "
-                        << child->dataValue( MusicBrainz::TRACKID ).toString();
-
-                if( child->dataContains( MusicBrainz::MUSICDNS ) )
-                {
-                    m_childItems.value( i )->setDataValue( MusicBrainz::MUSICBRAINZ,
-                                      m_childItems.value( i )->dataValue( MusicBrainz::SIMILARITY ) );
-                    m_childItems.value( i )->setDataValue( MusicBrainz::MUSICDNS,
-                                      child->dataValue( MusicBrainz::SIMILARITY ) );
-                }
-                else
-                {
-                    m_childItems.value( i )->setDataValue( MusicBrainz::MUSICBRAINZ,
-                                       child->dataValue( MusicBrainz::SIMILARITY ) );
-                    m_childItems.value( i )->setDataValue( MusicBrainz::MUSICDNS,
-                                       m_childItems.value( i )->dataValue( MusicBrainz::SIMILARITY ) );
-                }
-
-                m_childItems.value( i )->setDataValue( MusicBrainz::SIMILARITY,
-                                      child->dataValue( MusicBrainz::SIMILARITY ).toFloat() +
-                                      m_childItems.value( i )->dataValue( MusicBrainz::SIMILARITY ).toFloat() );
-
-                delete child;
-                notFound = false;
-                break;
-            }
-
-        if( notFound )
-        {
-            m_childrenLock.lockForWrite();
-            m_childItems.append( child );
-            m_childrenLock.unlock();
-        }
-
-        float isim, jsim;
-
-        for( int i = 0; i < childCount() - 1; i++ )
-            for( int j = i + 1; j < childCount(); j++ )
-            {
-                isim = m_childItems.value( i )->dataValue( MusicBrainz::SIMILARITY ).toFloat();
-                if( !m_childItems.value( i )->dataContains( MusicBrainz::MUSICBRAINZ ) )
-                    isim -= 1.0;
-
-                jsim = m_childItems.value( j )->dataValue( MusicBrainz::SIMILARITY ).toFloat();
-                if( !m_childItems.value( j )->dataContains( MusicBrainz::MUSICBRAINZ ) )
-                    jsim -= 1.0;
-
-                if( isim < jsim )
-                {
-                    m_childrenLock.lockForWrite();
-                    m_childItems.swap( i, j );
-                    m_childrenLock.unlock();
-                }
-            }
-    }
-}
-
-int
-MusciBrainzTagsItem::childCount() const
-{
-    QReadLocker lock( &m_childrenLock );
-    return m_childItems.count();
-}
-
-MusciBrainzTagsItem *
-MusciBrainzTagsItem::checkedItem() const
-{
-    if( m_data.isEmpty() )
-    {
-        QReadLocker lock( &m_childrenLock );
-        foreach( MusciBrainzTagsItem *item, m_childItems )
-            if( item->checked() )
-                return item;
-    }
-    return 0;
-}
-
-void
-MusciBrainzTagsItem::checkFirst()
-{
-    if( !m_data.isEmpty() )
-        return;
-
-    if( childCount() && !checkedItem() )
-        m_childItems.first()->setChecked( true );
-}
-
-void MusciBrainzTagsItem::uncheckAll()
-{
-    if( !parent() )
-        foreach( MusciBrainzTagsItem *item, m_childItems )
-            item->uncheckAll();
-
-    if( m_data.isEmpty() )
-        foreach( MusciBrainzTagsItem *item, m_childItems )
-            item->setChecked( false );
-
-    m_dataLock.lockForWrite();
-    m_checked = false;
-    m_dataLock.unlock();
-}
-
-int
-MusciBrainzTagsItem::row() const
-{
-    if( parent() )
-    {
-        QReadLocker lock( &m_childrenLock );
-        return m_parent->m_childItems.indexOf( const_cast< MusciBrainzTagsItem * >( this ) );
-    }
-
-    return 0;
-}
-
-Qt::ItemFlags
-MusciBrainzTagsItem::flags() const
-{
-    if( m_data.isEmpty() )
-        return Qt::NoItemFlags;
-
-    return Qt::ItemIsUserCheckable;
-}
-
-QVariant
-MusciBrainzTagsItem::data( int column ) const
-{
-    if( m_data.isEmpty() )
-    {
-        switch( column )
-        {
-            case 0: return ( !m_track->name().isEmpty() )? m_track->name() : m_track->playableUrl().fileName();
-            case 1: return ( !m_track->artist().isNull() )? m_track->artist()->name() : QVariant();
-            case 2: return ( !m_track->album().isNull() )? m_track->album()->name() : QVariant();
-            case 3: return ( !m_track->album().isNull() &&
-                             m_track->album()->hasAlbumArtist() )
-                             ? m_track->album()->albumArtist()->name() : QVariant();
-        }
-
-        return QVariant();
-    }
-
-    switch( column )
-    {
-        case 0: return dataValue( Meta::Field::TITLE );
-        case 1: return dataValue( Meta::Field::ARTIST );;
-        case 2: return dataValue( Meta::Field::ALBUM );
-        case 3: return dataValue( Meta::Field::ALBUMARTIST );
-    }
-
-    return QVariant();
-}
-
-QVariantMap
-MusciBrainzTagsItem::data() const
-{
-    QReadLocker lock( &m_dataLock );
-    return m_data;
-}
-
-void
-MusciBrainzTagsItem::setData( QVariantMap tags )
-{
-    m_dataLock.lockForWrite();
-    m_data = tags;
-    m_dataLock.unlock();
-}
-
-QVariant
-MusciBrainzTagsItem::dataValue( const QString &key ) const
-{
-    QReadLocker lock( &m_dataLock );
-    if( m_data.contains( key ) )
-        return m_data.value( key );
-
-    return QVariant();
-}
-
-void
-MusciBrainzTagsItem::setDataValue( const QString &key, const QVariant &value )
-{
-    m_dataLock.lockForWrite();
-    m_data.insert( key, value );
-    m_dataLock.unlock();
-}
-
-bool
-MusciBrainzTagsItem::dataContains( const QString &key )
-{
-    QReadLocker lock( &m_dataLock );
-    return m_data.contains( key );
-}
-
-bool
-MusciBrainzTagsItem::checked() const
-{
-    QReadLocker lock( &m_dataLock );
-    if( m_data.isEmpty() )
-    {
-        foreach( MusciBrainzTagsItem *child, m_childItems )
-            if( child->checked() )
-                return true;
-        return false;
-    }
-    return m_checked;
-}
-
-void
-MusciBrainzTagsItem::setChecked( bool checked )
-{
-    if( m_data.isEmpty() )
-        return;
-
-    m_dataLock.lockForWrite();
-    m_checked = checked;
-    m_dataLock.unlock();
-}
-
-Meta::TrackPtr
-MusciBrainzTagsItem::track() const
-{
-    QReadLocker lock( &m_dataLock );
-    return m_track;
-}
-
-//-------------------------------MusicBrainzTagsModel------------------------------------
-
-#undef DEBUG_PREFIX
-#define DEBUG_PREFIX "MusicBrainzTagsModel"
-
-MusicBrainzTagsModel::MusicBrainzTagsModel( QObject* parent)
-                    : QAbstractItemModel( parent )
-{
-    QVariantMap headerData;
-    headerData.insert( MusicBrainz::SIMILARITY, "%" );
-    headerData.insert( Meta::Field::TITLE, i18n( "Title" ) );
-    headerData.insert( Meta::Field::ARTIST, i18n( "Artist" ) );
-    headerData.insert( Meta::Field::ALBUM, i18n( "Album" ) );
-    headerData.insert( Meta::Field::ALBUMARTIST, i18n( "Album Artist" ) );
-    m_rootItem = new MusciBrainzTagsItem( 0, Meta::TrackPtr(), headerData );
-}
-
-MusicBrainzTagsModel::~MusicBrainzTagsModel()
-{
-    delete m_rootItem;
-}
-
-QModelIndex
-MusicBrainzTagsModel::parent( const QModelIndex &index ) const
-{
-    if( !index.isValid() )
-        return QModelIndex();
-
-    MusciBrainzTagsItem *childItem = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-    MusciBrainzTagsItem *parentItem = childItem->parent();
-
-    if( parentItem == m_rootItem )
-        return QModelIndex();
-
-    return createIndex( parentItem->row(), 0, parentItem );
-}
-
-QModelIndex
-MusicBrainzTagsModel::index( int row, int column, const QModelIndex &parent ) const
-{
-    if( !hasIndex( row, column, parent ) )
-        return QModelIndex();
-
-    MusciBrainzTagsItem *parentItem;
-
-    if( !parent.isValid() )
-        parentItem = m_rootItem;
-    else
-        parentItem = static_cast< MusciBrainzTagsItem * >( parent.internalPointer() );
-
-    MusciBrainzTagsItem *childItem = parentItem->child( row );
-
-    if( childItem )
-        return createIndex( row, column, childItem );
-    else
-        return QModelIndex();
-}
-
-QVariant
-MusicBrainzTagsModel::data( const QModelIndex &index, int role ) const
-{
-    if( !index.isValid() )
-        return QVariant();
-
-    MusciBrainzTagsItem *item = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-
-    if( role == Qt::DisplayRole )
-        return item->data( index.column() );
-    else if( role == Qt::CheckStateRole &&
-             index.column() == 0 &&
-             item->flags() == Qt::ItemIsUserCheckable )
-        return item->checked() ? Qt::Checked : Qt::Unchecked;
-    else if( role == Qt::BackgroundRole &&
-             item->dataContains( MusicBrainz::SIMILARITY ) )
-    {
-        if( item->dataContains( MusicBrainz::MUSICBRAINZ ) &&
-            item->dataContains( MusicBrainz::MUSICDNS ) )
-            return QColor( Qt::green );
-
-        float sim = ( item->dataValue( MusicBrainz::SIMILARITY ).toFloat() - MusicBrainz::MINSIMILARITY ) /
-                    ( 1.0 - MusicBrainz::MINSIMILARITY );
-
-        quint8 c1 = 255, c2 = 255;
-        if( sim < 0.5 )
-            c2 = ( 170 + 170 * sim );
-        else
-            c1 = ( 255 - 170 * ( sim - 0.5 ) );
-
-        if( item->dataContains( MusicBrainz::MUSICDNS ) )
-            return QColor( 0, c2, c1 );
-        else
-            return QColor( c1, c2, 0 );
-    }
-    else if( role == Qt::ToolTipRole )
-    {
-        QString toolTip;
-        if( item->dataContains( MusicBrainz::MUSICBRAINZ ) &&
-            item->dataContains( MusicBrainz::MUSICDNS ) )
-        {
-            toolTip += i18n( "MusicBrainz match ratio: %1%",
-                             100 * item->dataValue( MusicBrainz::MUSICBRAINZ ).toFloat() );
-            toolTip += "\n" + i18n( "MusicDNS match ratio: %1%",
-                                    100 * item->dataValue( MusicBrainz::MUSICDNS ).toFloat() );
-        }
-        else if( item->dataContains( MusicBrainz::MUSICBRAINZ ) )
-            toolTip += i18n( "MusicBrainz match ratio: %1%",
-                             100 * item->dataValue( MusicBrainz::SIMILARITY ).toFloat() );
-        else if( item->dataContains( MusicBrainz::MUSICDNS ) )
-            toolTip += i18n( "MusicDNS match ratio: %1%",
-                             100 * item->dataValue( MusicBrainz::SIMILARITY ).toFloat() );
-        else
-            return QVariant();
-
-        return toolTip;
-    }
-    else if( role == Qt::FontRole && item->parent() == m_rootItem )
-    {
-        QFont font;
-        font.setItalic( true );
-        return font;
-    }
-    else if( role == Qt::ForegroundRole && item->parent() != m_rootItem )
-        return QColor(  Qt::black );
-
-    return QVariant();
-}
-
-bool
-MusicBrainzTagsModel::setData( const QModelIndex &index, const QVariant &value, int role )
-{
-    if( !index.isValid() || role != Qt::CheckStateRole  || index.column() != 0 )
-        return false;
-
-    MusciBrainzTagsItem *item = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-    if( item == m_rootItem || item->parent() == m_rootItem )
-        return false;
-
-    MusciBrainzTagsItem *itemParent = item->parent();
-    itemParent->uncheckAll();
-    item->setChecked( value.toBool() );
-    emit dataChanged( createIndex( 0, 0, itemParent ),
-                      createIndex( itemParent->childCount(), 0, itemParent ) );
-    return true;
-}
-
-QVariant
-MusicBrainzTagsModel::headerData( int section, Qt::Orientation orientation, int role ) const
-{
-    if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
-        return m_rootItem->data( section );
-    else if( orientation == Qt::Horizontal && role == Qt::ToolTipRole && section == 0 )
-        return i18n( "Click here to choose best matches" );
-
-    return QVariant();
-}
-
-Qt::ItemFlags
-MusicBrainzTagsModel::flags( const QModelIndex &index ) const
-{
-    if( !index.isValid() )
-        return QAbstractItemModel::flags( index );
-
-    return QAbstractItemModel::flags( index ) |
-           static_cast< MusciBrainzTagsItem * >( index.internalPointer() )->flags();
-}
-
-int
-MusicBrainzTagsModel::rowCount( const QModelIndex &parent ) const
-{
-    MusciBrainzTagsItem *parentItem;
-
-    if( parent.column() > 0 )
-        return 0;
-
-    if( !parent.isValid() )
-        parentItem = m_rootItem;
-    else
-        parentItem = static_cast< MusciBrainzTagsItem * >( parent.internalPointer() );
-
-    return parentItem->childCount();
-}
-
-int
-MusicBrainzTagsModel::columnCount( const QModelIndex &parent ) const
-{
-    Q_UNUSED( parent );
-    return 4;
-}
-
-void
-MusicBrainzTagsModel::addTrack( const Meta::TrackPtr track, const QVariantMap tags )
-{
-    m_rootItem->appendChild( new MusciBrainzTagsItem( m_rootItem, track, tags ) );
-    emit layoutChanged();
-}
-
-void
-MusicBrainzTagsModel::selectAll( int section )
-{
-    if( section != 0 )
-        return;
-
-    for( int i = 0; i < m_rootItem->childCount(); i++ )
-    {
-        m_rootItem->child( i )->checkFirst();
-        emit dataChanged( createIndex( 0, 0, m_rootItem->child( i ) ),
-                          createIndex( m_rootItem->child( i )->childCount(),
-                                       0, m_rootItem->child( i ) ) );
-    }
-}
-
-QMap < Meta::TrackPtr, QVariantMap >
-MusicBrainzTagsModel::getAllChecked()
-{
-    QMap < Meta::TrackPtr, QVariantMap > result;
-
-    for( int i = 0; i < m_rootItem->childCount(); i++ )
-    {
-        MusciBrainzTagsItem *item = m_rootItem->child( i )->checkedItem();
-        if( item )
-        {
-            QVariantMap data = item->data();
-            data.remove( MusicBrainz::ARTISTID );
-            data.remove( MusicBrainz::MUSICBRAINZ );
-            data.remove( MusicBrainz::MUSICDNS );
-            data.remove( MusicBrainz::RELEASEID );
-            data.remove( MusicBrainz::SIMILARITY );
-            data.remove( MusicBrainz::TRACKID );
-            result.insert( item->track(), data );
-        }
-    }
-
-    return result;
-}
-
-//-----------------------------MusicBrainzTagsModelDelegate------------------------------
-
-#undef DEBUG_PREFIX
-#define DEBUG_PREFIX "MusicBrainzTagsModelDelegate"
-
-MusicBrainzTagsModelDelegate::MusicBrainzTagsModelDelegate( QObject *parent )
-                            : QItemDelegate( parent )
-{
-}
-
-void
-MusicBrainzTagsModelDelegate::drawCheck( QPainter *painter, const QStyleOptionViewItem &option,
-                                         const QRect &rect, Qt::CheckState state ) const
-{
-    if( !rect.isValid() )
-        return;
-
-    QStyleOptionViewItem opt( option );
-    opt.rect = rect;
-    opt.state = opt.state & ~QStyle::State_HasFocus;
-
-    switch( state )
-    {
-        case Qt::Unchecked:
-            opt.state |= QStyle::State_Off;
-            break;
-        case Qt::PartiallyChecked:
-            opt.state |= QStyle::State_NoChange;
-            break;
-        case Qt::Checked:
-            opt.state |= QStyle::State_On;
-            break;
-    }
-
-    QApplication::style()->drawPrimitive( QStyle::PE_IndicatorRadioButton, &opt, painter );
-}
-
-//---------------------------------MusicBrainzTagsView-----------------------------------
-
-#undef DEBUG_PREFIX
-#define DEBUG_PREFIX "MusicBrainzTagsView"
-
-MusicBrainzTagsView::MusicBrainzTagsView( QWidget *parent )
-                   : QTreeView( parent )
-{
-    artistIcon = new QIcon( KStandardDirs::locate( "data", "amarok/images/mb_aicon.png" ) );
-    releaseIcon = new QIcon( KStandardDirs::locate( "data", "amarok/images/mb_licon.png" ) );
-    trackIcon = new QIcon( KStandardDirs::locate( "data", "amarok/images/mb_ticon.png" ) );
-}
-
-MusicBrainzTagsView::~MusicBrainzTagsView()
-{
-    delete artistIcon;
-    delete releaseIcon;
-    delete trackIcon;
-}
-
-void
-MusicBrainzTagsView::contextMenuEvent( QContextMenuEvent *event )
-{
-    DEBUG_BLOCK
-    QModelIndex index = indexAt( event->pos() );
-
-    if ( !index.isValid() || !index.internalPointer() )
-    {
-        event->ignore();
-        return;
-    }
-
-    MusciBrainzTagsItem *item = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-    if( item->flags() != Qt::ItemIsUserCheckable )
-    {
-        event->ignore();
-        return;
-    }
-
-    QVariantMap data = item->data();
-
-    QMenu *menu = new QMenu( this );
-
-    QList < QAction * > actions;
-    if( data.contains( MusicBrainz::ARTISTID ) )
-    {
-        QAction *action = new QAction( *artistIcon, i18n( "Artist page" ), menu );
-        connect( action, SIGNAL(triggered()), SLOT(openArtistPage()) );
-        actions << action;
-    }
-    if( data.contains( MusicBrainz::RELEASEID ) )
-    {
-        QAction *action = new QAction( *releaseIcon, i18n( "Album page" ), menu );
-        connect( action, SIGNAL(triggered()), SLOT(openReleasePage()) );
-        actions << action;
-    }
-    if( data.contains( MusicBrainz::TRACKID ) )
-    {
-        QAction *action = new QAction( *trackIcon, i18n( "Track page" ), menu );
-        connect( action, SIGNAL(triggered()), SLOT(openTrackPage()) );
-        actions << action;
-    }
-
-    if( actions.isEmpty() )
-    {
-        delete menu;
-        event->ignore();
-        return;
-    }
-
-    menu->addActions( actions );
-    menu->exec( event->globalPos() );
-    event->accept();
-}
-
-void
-MusicBrainzTagsView::openArtistPage()
-{
-    if( !selectedIndexes().first().isValid() || !selectedIndexes().first().internalPointer() )
-        return;
-
-    QVariantMap data = static_cast< MusciBrainzTagsItem * > ( selectedIndexes().first().internalPointer() )->data();
-    if( !data.contains( MusicBrainz::ARTISTID ) )
-        return;
-
-    QString url = QString( "http://musicbrainz.org/artist/%1.html" )
-                  .arg( data.value( MusicBrainz::ARTISTID ).toString() );
-
-    QDesktopServices::openUrl( url );
-}
-
-void
-MusicBrainzTagsView::openReleasePage()
-{
-    if( !selectedIndexes().first().isValid() || !selectedIndexes().first().internalPointer() )
-        return;
-
-    QVariantMap data = static_cast< MusciBrainzTagsItem * > ( selectedIndexes().first().internalPointer() )->data();
-    if( !data.contains( MusicBrainz::RELEASEID ) )
-        return;
-
-    QString url = QString( "http://musicbrainz.org/release/%1.html" )
-                  .arg( data.value( MusicBrainz::RELEASEID ).toString() );
-
-    QDesktopServices::openUrl( url );
-}
-
-void
-MusicBrainzTagsView::openTrackPage()
-{
-    if( !selectedIndexes().first().isValid() || !selectedIndexes().first().internalPointer() )
-        return;
-
-    QVariantMap data = static_cast< MusciBrainzTagsItem * > ( selectedIndexes().first().internalPointer() )->data();
-    if( !data.contains( MusicBrainz::TRACKID ) )
-        return;
-
-    QString url = QString( "http://musicbrainz.org/track/%1.html" )
-                  .arg( data.value( MusicBrainz::TRACKID ).toString() );
-
-    QDesktopServices::openUrl( url );
-}
-
-void
-MusicBrainzTagsView::collapseChosen()
-{
-    DEBUG_BLOCK
-
-    MusicBrainzTagsModel *model = static_cast< MusicBrainzTagsModel * >( this->model() );
-
-    if( !model )
-        return;
-
-    for( int i = 0; i < model->rowCount(); i++ )
-    {
-        QModelIndex index = model->index( i, 0 );
-        MusciBrainzTagsItem *item = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-        if( item && item->checked() )
-            collapse( index );
-    }
-}
-
-
-void
-MusicBrainzTagsView::expandUnChosen()
-{
-    DEBUG_BLOCK
-
-    MusicBrainzTagsModel *model = static_cast< MusicBrainzTagsModel * >( this->model() );
-
-    if( !model )
-        return;
-
-    for( int i = 0; i < model->rowCount(); i++ )
-    {
-        QModelIndex index = model->index( i, 0 );
-        MusciBrainzTagsItem *item = static_cast< MusciBrainzTagsItem * >( index.internalPointer() );
-        if( item && !item->checked() )
-            expand( index );
-    }
-}
-
-#include "MusicBrainzTags.moc"
-
diff --git a/src/musicbrainz/MusicBrainzTags.h b/src/musicbrainz/MusicBrainzTags.h
deleted file mode 100644
index f70d494..0000000
--- a/src/musicbrainz/MusicBrainzTags.h
+++ /dev/null
@@ -1,135 +0,0 @@
-/****************************************************************************************
- * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
- *                                                                                      *
- * This program is free software; you can redistribute it and/or modify it under        *
- * the terms of the GNU General Public License as published by the Free Software        *
- * Foundation; either version 2 of the License, or (at your option) any later           *
- * version.                                                                             *
- *                                                                                      *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
- * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
- *                                                                                      *
- * You should have received a copy of the GNU General Public License along with         *
- * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
- ****************************************************************************************/
-
-#ifndef MUSICBRAINZTAGSMODEL_H
-#define MUSICBRAINZTAGSMODEL_H
-
-#include <KLocalizedString>
-#include <core/meta/Meta.h>
-#include <QAbstractItemModel>
-#include <QReadWriteLock>
-#include <QItemDelegate>
-#include <QTreeView>
-
-class MusciBrainzTagsItem
-{
-    public:
-        MusciBrainzTagsItem( MusciBrainzTagsItem *parent = 0,
-                             const Meta::TrackPtr track = Meta::TrackPtr(),
-                             const QVariantMap tags = QVariantMap() );
-
-        ~MusciBrainzTagsItem();
-
-        MusciBrainzTagsItem *parent() const;
-        MusciBrainzTagsItem *child( const int row ) const;
-        void appendChild( MusciBrainzTagsItem *child );
-        int childCount() const;
-        int row() const;
-
-        Qt::ItemFlags flags() const;
-
-        Meta::TrackPtr track() const;
-        QVariant data( int column ) const;
-        QVariantMap data() const;
-        QVariant dataValue( const QString &key ) const;
-        bool dataContains( const QString &key );
-        void setData( QVariantMap tags );
-
-        MusciBrainzTagsItem *checkedItem() const;
-        void checkFirst();
-        void uncheckAll();
-        bool checked() const;
-        void setChecked( bool checked );
-
-    private:
-        void setDataValue( const QString &key, const QVariant &value );
-        void setParent( MusciBrainzTagsItem *parent );
-        MusciBrainzTagsItem *m_parent;
-        QList< MusciBrainzTagsItem * > m_childItems;
-
-        Meta::TrackPtr m_track;
-        QVariantMap m_data;
-        bool m_checked;
-
-        mutable QReadWriteLock m_dataLock;
-        mutable QReadWriteLock m_childrenLock;
-        mutable QReadWriteLock m_parentLock;
-};
-
-class MusicBrainzTagsModel : public QAbstractItemModel
-{
-    Q_OBJECT
-
-    public:
-        MusicBrainzTagsModel( QObject *parent = 0 );
-        ~MusicBrainzTagsModel();
-
-        QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const;
-        QModelIndex parent( const QModelIndex &index ) const;
-
-        Qt::ItemFlags flags( const QModelIndex &index ) const;
-        QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
-        bool setData( const QModelIndex &index, const QVariant &value, int role );
-        QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
-
-        int rowCount( const QModelIndex &parent = QModelIndex() ) const;
-        int columnCount( const QModelIndex &parent = QModelIndex() ) const;
-
-        QMap < Meta::TrackPtr, QVariantMap > getAllChecked(); 
-
-    public slots:
-        void addTrack( const Meta::TrackPtr track, const QVariantMap tags );
-        void selectAll( int section );
-
-    private:
-        MusciBrainzTagsItem *m_rootItem;
-};
-
-class MusicBrainzTagsModelDelegate : public QItemDelegate
-{
-    public:
-        explicit MusicBrainzTagsModelDelegate( QObject *parent = 0 );
-    protected:
-        virtual void drawCheck( QPainter *painter, const QStyleOptionViewItem &option,
-                                const QRect &rect, Qt::CheckState state ) const;
-};
-
-class MusicBrainzTagsView : public QTreeView
-{
-    Q_OBJECT
-    public:
-        explicit MusicBrainzTagsView( QWidget *parent = 0 );
-
-        ~MusicBrainzTagsView();
-
-    public slots:
-        void collapseChosen();
-        void expandUnChosen();
-
-    protected:
-        virtual void contextMenuEvent( QContextMenuEvent *event );
-
-    private slots:
-        void openArtistPage();
-        void openReleasePage();
-        void openTrackPage();
-
-    private:
-        QIcon *artistIcon;
-        QIcon *releaseIcon;
-        QIcon *trackIcon;
-};
-#endif // MUSICBRAINZTAGSMODEL_H
diff --git a/src/musicbrainz/MusicBrainzTagsItem.cpp b/src/musicbrainz/MusicBrainzTagsItem.cpp
new file mode 100644
index 0000000..c000c75
--- /dev/null
+++ b/src/musicbrainz/MusicBrainzTagsItem.cpp
@@ -0,0 +1,497 @@
+/****************************************************************************************
+ * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
+ *                                                                                      *
+ * This program is free software; you can redistribute it and/or modify it under        *
+ * the terms of the GNU General Public License as published by the Free Software        *
+ * Foundation; either version 2 of the License, or (at your option) any later           *
+ * version.                                                                             *
+ *                                                                                      *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
+ *                                                                                      *
+ * You should have received a copy of the GNU General Public License along with         *
+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
+ ****************************************************************************************/
+
+#include "MusicBrainzTagsItem.h"
+
+#include "AmarokMimeData.h"
+#include "core/support/Debug.h"
+#include "MusicBrainzMeta.h"
+
+#define DEBUG_PREFIX "MusicBrainzTagsItem"
+
+MusicBrainzTagsItem::MusicBrainzTagsItem( MusicBrainzTagsItem *parent,
+                                          const Meta::TrackPtr track,
+                                          const QVariantMap tags )
+    : m_parent( parent )
+    , m_track( track )
+    , m_data( tags )
+    , m_chosen( false )
+    , m_dataLock( QReadWriteLock::Recursive )
+    , m_parentLock( QReadWriteLock::Recursive )
+    , m_childrenLock( QReadWriteLock::Recursive )
+{
+}
+
+MusicBrainzTagsItem::~MusicBrainzTagsItem()
+{
+    qDeleteAll( m_childItems );
+}
+
+MusicBrainzTagsItem *
+MusicBrainzTagsItem::parent() const
+{
+    QReadLocker lock( &m_parentLock );
+    return m_parent;
+}
+
+void
+MusicBrainzTagsItem::setParent( MusicBrainzTagsItem *parent )
+{
+    QWriteLocker lock( &m_parentLock );
+    m_parent = parent;
+}
+
+MusicBrainzTagsItem *
+MusicBrainzTagsItem::child( const int row ) const
+{
+    QReadLocker lock( &m_childrenLock );
+    return m_childItems.value( row );
+}
+
+void
+MusicBrainzTagsItem::appendChild( MusicBrainzTagsItem *newItem )
+{
+    DEBUG_BLOCK
+
+    if( newItem->track().isNull() )
+    {
+        delete newItem;
+        return;
+    }
+
+    if( m_track.isNull() )
+    {
+        // Root item.
+        bool found = false;
+
+        /*
+         * A write lock is required, because with a read lock two or more threads
+         * referencing the same track could search for a matching item at the same time,
+         * fail, and queue up to create a new one, thus resulting in duplicates.
+         */
+        QWriteLocker lock( &m_childrenLock );
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+        {
+            if( item->track() == newItem->track() )
+            {
+                found = true;
+                if( !newItem->data().isEmpty() )
+                    item->appendChild( newItem );
+                break;
+            }
+        }
+
+        if( !found )
+        {
+            MusicBrainzTagsItem *newChild = new MusicBrainzTagsItem( this, newItem->track() );
+            if( !newItem->data().isEmpty() )
+                newChild->appendChild( newItem );
+            m_childItems.append( newChild );
+        }
+    }
+    else
+    {
+        if( m_track != newItem->track() )
+        {
+            debug() << "Trying to insert track data to the wrong tree branch.";
+            delete newItem;
+            return;
+        }
+
+        bool found = false;
+        newItem->setParent( this );
+
+        // Already locked in parent call (the same logic applies).
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+        {
+            if( newItem == item )
+            {
+                found = true;
+                // Merge the two matching results.
+                debug() << "Track" << newItem->dataValue( MusicBrainz::TRACKID ).toString() << "already in the tree.";
+                item->mergeWith( newItem );
+                delete newItem;
+                break;
+            }
+        }
+
+        if( !found )
+        {
+            newItem->dataInsert( MusicBrainz::SIMILARITY,
+                                 newItem->dataValue( MusicBrainz::MUSICBRAINZ ).toFloat() +
+                                 newItem->dataValue( MusicBrainz::MUSICDNS ).toFloat() );
+
+            QVariantList trackList;
+            QVariantList artistList;
+            QVariantList releaseList;
+            if( newItem->dataContains( MusicBrainz::TRACKID ) )
+                trackList.append( newItem->dataValue( MusicBrainz::TRACKID ) );
+            if( newItem->dataContains( MusicBrainz::ARTISTID ) )
+                artistList.append( newItem->dataValue( MusicBrainz::ARTISTID ) );
+            if( newItem->dataContains( MusicBrainz::RELEASEID ) )
+                releaseList.append( newItem->dataValue( MusicBrainz::RELEASEID ) );
+            newItem->dataInsert( MusicBrainz::TRACKID, trackList );
+            newItem->dataInsert( MusicBrainz::ARTISTID, artistList );
+            newItem->dataInsert( MusicBrainz::RELEASEID, releaseList );
+
+            m_childItems.append( newItem );
+        }
+    }
+}
+
+void
+MusicBrainzTagsItem::mergeWith( MusicBrainzTagsItem *item )
+{
+    /*
+     * The lock is inherited from appendChild(). This method is not supposed to be called
+     * elsewhere.
+     */
+
+    // Calculate the future score of the result when merged.
+    if( !item->dataContains( MusicBrainz::MUSICBRAINZ ) &&
+        dataContains( MusicBrainz::MUSICBRAINZ ) )
+        item->dataInsert( MusicBrainz::MUSICBRAINZ,
+                          dataValue( MusicBrainz::MUSICBRAINZ ) );
+    if( !item->dataContains( MusicBrainz::MUSICDNS ) &&
+        dataContains( MusicBrainz::MUSICDNS ) )
+        item->dataInsert( MusicBrainz::MUSICDNS,
+                          dataValue( MusicBrainz::MUSICDNS ) );
+    item->dataInsert( MusicBrainz::SIMILARITY,
+                      item->dataValue( MusicBrainz::MUSICBRAINZ ).toFloat() +
+                      item->dataValue( MusicBrainz::MUSICDNS ).toFloat() );
+
+    QVariantList trackList = dataValue( MusicBrainz::TRACKID ).toList();
+    QVariantList artistList = dataValue( MusicBrainz::ARTISTID ).toList();
+    QVariantList releaseList = dataValue( MusicBrainz::RELEASEID ).toList();
+    if( item->score() > score() )
+    {
+        // Update the score.
+        if( item->dataContains( MusicBrainz::MUSICBRAINZ ) )
+            dataInsert( MusicBrainz::MUSICBRAINZ,
+                        item->dataValue( MusicBrainz::MUSICBRAINZ ) );
+        if( item->dataContains( MusicBrainz::MUSICDNS ) )
+            dataInsert( MusicBrainz::MUSICDNS,
+                        item->dataValue( MusicBrainz::MUSICDNS ) );
+        dataInsert( MusicBrainz::SIMILARITY,
+                    item->dataValue( MusicBrainz::SIMILARITY ) );
+
+        if( item->dataContains( MusicBrainz::TRACKID ) )
+            trackList.prepend( item->dataValue( MusicBrainz::TRACKID ) );
+        if( item->dataContains( MusicBrainz::ARTISTID ) )
+            artistList.prepend( item->dataValue( MusicBrainz::ARTISTID ) );
+        if( item->dataContains( MusicBrainz::RELEASEID ) )
+            releaseList.prepend( item->dataValue( MusicBrainz::RELEASEID ) );
+    }
+    else
+    {
+        if( item->dataContains( MusicBrainz::TRACKID ) )
+            trackList.append( item->dataValue( MusicBrainz::TRACKID ) );
+        if( item->dataContains( MusicBrainz::ARTISTID ) )
+            artistList.append( item->dataValue( MusicBrainz::ARTISTID ) );
+        if( item->dataContains( MusicBrainz::RELEASEID ) )
+            releaseList.append( item->dataValue( MusicBrainz::RELEASEID ) );
+    }
+    dataInsert( MusicBrainz::TRACKID, trackList );
+    dataInsert( MusicBrainz::ARTISTID, artistList );
+    dataInsert( MusicBrainz::RELEASEID, releaseList );
+}
+
+int
+MusicBrainzTagsItem::childCount() const
+{
+    QReadLocker lock( &m_childrenLock );
+    return m_childItems.count();
+}
+
+int
+MusicBrainzTagsItem::row() const
+{
+    if( parent() )
+    {
+        QReadLocker lock( &m_childrenLock );
+        return m_parent->m_childItems.indexOf( const_cast<MusicBrainzTagsItem *>( this ) );
+    }
+
+    return 0;
+}
+
+Meta::TrackPtr
+MusicBrainzTagsItem::track() const
+{
+    QReadLocker lock( &m_dataLock );
+    return m_track;
+}
+
+float
+MusicBrainzTagsItem::score() const
+{
+    QReadLocker lock( &m_dataLock );
+    float score = dataValue( MusicBrainz::SIMILARITY ).toFloat();
+
+    /*
+     * Results of fingerprint-only lookup go on bottom as they are weak matches (only
+     * their length is compared).
+     */
+    if( !dataContains( MusicBrainz::MUSICBRAINZ ) )
+        score -= 1.0;
+
+    return score;
+}
+
+QVariantMap
+MusicBrainzTagsItem::data() const
+{
+    QReadLocker lock( &m_dataLock );
+    return m_data;
+}
+
+QVariant
+MusicBrainzTagsItem::data( const int column ) const
+{
+    if( m_data.isEmpty() )
+    {
+        switch( column )
+        {
+        case 0:
+            {
+                QString title;
+                int trackNumber = m_track->trackNumber();
+                if( trackNumber > 0 )
+                    title += QString( "%1 - " ).arg( trackNumber );
+                title += m_track->prettyName();
+                return title;
+            }
+        case 1:
+            return ( !m_track->artist().isNull() )? m_track->artist()->name() : QVariant();
+        case 2:
+            {
+                if( m_track->album().isNull() )
+                    return QVariant();
+                QString album = m_track->album()->name();
+                int discNumber = m_track->discNumber();
+                if( discNumber > 0 )
+                    album += QString( " (disc %1)" ).arg( discNumber );
+                return album;
+            }
+        case 3:
+            return ( !m_track->album().isNull() && m_track->album()->hasAlbumArtist() )?
+                   m_track->album()->albumArtist()->name() : QVariant();
+        case 4:
+            return ( m_track->year()->year() > 0 )? m_track->year()->year() : QVariant();
+        }
+
+        return QVariant();
+    }
+
+    switch( column )
+    {
+    case 0:
+        {
+            QString title;
+            QVariant trackNumber = dataValue( Meta::Field::TRACKNUMBER );
+            if( trackNumber.toInt() > 0 )
+            {
+                title += trackNumber.toString();
+                int trackCount = dataValue( MusicBrainz::TRACKCOUNT ).toInt();
+                if ( trackCount > 0 )
+                    title += QString( "/%1" ).arg( trackCount );
+                title += " - ";
+            }
+            title += dataValue( Meta::Field::TITLE ).toString();
+            return title;
+        }
+    case 1:
+        return dataValue( Meta::Field::ARTIST );;
+    case 2:
+        {
+            QString album = dataValue( Meta::Field::ALBUM ).toString();
+            int discNumber = dataValue( Meta::Field::DISCNUMBER ).toInt();
+            if( discNumber > 0 )
+                album += QString( " (disc %1)" ).arg( discNumber );
+            return album;
+        }
+    case 3:
+        return dataValue( Meta::Field::ALBUMARTIST );
+    case 4:
+        return dataValue( Meta::Field::YEAR );
+    }
+
+    return QVariant();
+}
+
+void
+MusicBrainzTagsItem::setData( const QVariantMap &tags )
+{
+    QWriteLocker lock( &m_dataLock );
+    m_data = tags;
+}
+
+bool
+MusicBrainzTagsItem::dataContains( const QString &key ) const
+{
+    QReadLocker lock( &m_dataLock );
+    return m_data.contains( key );
+}
+
+QVariant
+MusicBrainzTagsItem::dataValue( const QString &key ) const
+{
+    QReadLocker lock( &m_dataLock );
+    if( m_data.contains( key ) )
+        return m_data.value( key );
+
+    return QVariant();
+}
+
+void
+MusicBrainzTagsItem::dataInsert( const QString &key, const QVariant &value )
+{
+    QWriteLocker lock( &m_dataLock );
+    m_data.insert( key, value );
+}
+
+bool
+MusicBrainzTagsItem::isChosen() const
+{
+    QReadLocker lock( &m_dataLock );
+    if( m_data.isEmpty() )
+    {
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+            if( item->isChosen() )
+                return true;
+        return false;
+    }
+
+    return m_chosen;
+}
+
+void
+MusicBrainzTagsItem::setChosen( bool chosen )
+{
+    if( m_data.isEmpty() )
+        return;
+
+    QWriteLocker lock( &m_dataLock );
+    m_chosen = chosen;
+}
+
+MusicBrainzTagsItem *
+MusicBrainzTagsItem::chosenItem() const
+{
+    if( m_data.isEmpty() )
+    {
+        QReadLocker lock( &m_childrenLock );
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+            if( item->isChosen() )
+                return item;
+    }
+
+    return 0;
+}
+
+bool
+MusicBrainzTagsItem::chooseBestMatch()
+{
+    if( !m_data.isEmpty() )
+        return false;
+
+    QReadLocker lock( &m_childrenLock );
+    if( !childCount() || isChosen() )
+        return false;
+
+    MusicBrainzTagsItem *bestMatch;
+    float maxScore = 0;
+    foreach( MusicBrainzTagsItem *item, m_childItems )
+    {
+        if( item->score() > maxScore )
+        {
+            bestMatch = item;
+            maxScore = item->score();
+        }
+    }
+
+    bestMatch->setChosen( true );
+    return true;
+}
+
+bool
+MusicBrainzTagsItem::chooseBestMatchFromRelease( const QStringList &releases )
+{
+    if( !m_data.isEmpty() )
+        return false;
+
+    QReadLocker lock( &m_childrenLock );
+    if( !childCount() || isChosen() )
+        return false;
+
+    MusicBrainzTagsItem *bestMatch = 0;
+    float maxScore = 0;
+    QSet<QString> idList = releases.toSet();
+    foreach( MusicBrainzTagsItem *item, m_childItems )
+    {
+        /*
+         * Match any of the releases referenced by selected entry. This should guarantee
+         * that best results are always chosen when available.
+         */
+        if( item->score() > maxScore &&
+            !item->dataValue( MusicBrainz::RELEASEID ).toStringList().toSet().intersect( idList ).isEmpty() )
+        {
+            bestMatch = item;
+            maxScore = item->score();
+        }
+    }
+
+    if( bestMatch )
+    {
+        bestMatch->setChosen( true );
+        return true;
+    }
+
+    return false;
+}
+
+void
+MusicBrainzTagsItem::clearChoices()
+{
+    QReadLocker lock( &m_childrenLock );
+    if( !parent() )
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+            item->clearChoices();
+    else if( m_data.isEmpty() )
+        foreach( MusicBrainzTagsItem *item, m_childItems )
+            item->setChosen( false );
+}
+
+bool
+MusicBrainzTagsItem::operator==( const MusicBrainzTagsItem* item ) const
+{
+    QReadLocker lock( &m_dataLock );
+#define MATCH( k, t ) dataValue( k ).t() == item->dataValue( k ).t()
+    /*
+     * This is the information shown to the user: he will never be able to
+     * distinguish between two tracks with the same information.
+     */
+    return MATCH( Meta::Field::TITLE, toString ) &&
+           MATCH( Meta::Field::ARTIST, toString ) &&
+           MATCH( Meta::Field::ALBUM, toString ) &&
+           MATCH( Meta::Field::ALBUMARTIST, toString ) &&
+           MATCH( Meta::Field::YEAR, toInt ) &&
+           MATCH( MusicBrainz::TRACKCOUNT, toInt ) &&
+           MATCH( Meta::Field::DISCNUMBER, toInt ) &&
+           MATCH( Meta::Field::TRACKNUMBER, toInt );
+#undef MATCH
+}
diff --git a/src/musicbrainz/MusicBrainzTagsItem.h b/src/musicbrainz/MusicBrainzTagsItem.h
new file mode 100644
index 0000000..f0f259c
--- /dev/null
+++ b/src/musicbrainz/MusicBrainzTagsItem.h
@@ -0,0 +1,74 @@
+/****************************************************************************************
+ * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
+ *                                                                                      *
+ * This program is free software; you can redistribute it and/or modify it under        *
+ * the terms of the GNU General Public License as published by the Free Software        *
+ * Foundation; either version 2 of the License, or (at your option) any later           *
+ * version.                                                                             *
+ *                                                                                      *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
+ *                                                                                      *
+ * You should have received a copy of the GNU General Public License along with         *
+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
+ ****************************************************************************************/
+
+#ifndef MUSICBRAINZTAGSITEM_H
+#define MUSICBRAINZTAGSITEM_H
+
+#include "core/meta/Meta.h"
+
+#include <QReadWriteLock>
+
+class MusicBrainzTagsItem
+{
+    public:
+        explicit MusicBrainzTagsItem( MusicBrainzTagsItem *parent = 0,
+                                      const Meta::TrackPtr track = Meta::TrackPtr(),
+                                      const QVariantMap tags = QVariantMap() );
+        ~MusicBrainzTagsItem();
+
+        MusicBrainzTagsItem *parent() const;
+        MusicBrainzTagsItem *child( const int row ) const;
+        void appendChild( MusicBrainzTagsItem *child );
+        int childCount() const;
+        int row() const;
+
+        Meta::TrackPtr track() const;
+        float score() const;
+        QVariantMap data() const;
+        QVariant data( const int column ) const;
+        void setData( const QVariantMap &tags );
+        bool dataContains( const QString &key ) const;
+        QVariant dataValue( const QString &key ) const;
+
+        bool isChosen() const;
+        void setChosen( bool chosen );
+        MusicBrainzTagsItem *chosenItem() const;
+        bool chooseBestMatch();
+        bool chooseBestMatchFromRelease( const QStringList &releases );
+        void clearChoices();
+
+        bool operator==( const MusicBrainzTagsItem *item ) const;
+
+    private:
+        void setParent( MusicBrainzTagsItem *parent );
+        void mergeWith( MusicBrainzTagsItem *item );
+        void dataInsert( const QString &key, const QVariant &value );
+
+        MusicBrainzTagsItem *m_parent;
+        QList<MusicBrainzTagsItem *> m_childItems;
+
+        Meta::TrackPtr m_track;
+        QVariantMap m_data;
+
+        bool m_chosen;
+
+        mutable QReadWriteLock m_dataLock;
+        mutable QReadWriteLock m_parentLock;
+        mutable QReadWriteLock m_childrenLock;
+};
+
+#endif // MUSICBRAINZTAGSITEM_H
diff --git a/src/musicbrainz/MusicBrainzTagsModel.cpp b/src/musicbrainz/MusicBrainzTagsModel.cpp
new file mode 100644
index 0000000..4eea133
--- /dev/null
+++ b/src/musicbrainz/MusicBrainzTagsModel.cpp
@@ -0,0 +1,337 @@
+/****************************************************************************************
+ * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
+ *                                                                                      *
+ * This program is free software; you can redistribute it and/or modify it under        *
+ * the terms of the GNU General Public License as published by the Free Software        *
+ * Foundation; either version 2 of the License, or (at your option) any later           *
+ * version.                                                                             *
+ *                                                                                      *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
+ *                                                                                      *
+ * You should have received a copy of the GNU General Public License along with         *
+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
+ ****************************************************************************************/
+
+#include "MusicBrainzTagsModel.h"
+
+#include "AmarokMimeData.h"
+#include "core/support/Debug.h"
+#include "MusicBrainzMeta.h"
+#include "MusicBrainzTagsItem.h"
+
+#include <KLocalizedString>
+
+#include <QFont>
+
+#define DEBUG_PREFIX "MusicBrainzTagsModel"
+
+MusicBrainzTagsModel::MusicBrainzTagsModel( QObject *parent )
+    : QAbstractItemModel( parent )
+{
+    QVariantMap headerData;
+    headerData.insert( MusicBrainz::SIMILARITY, "%" );
+    headerData.insert( Meta::Field::TITLE, i18n( "Title" ) );
+    headerData.insert( Meta::Field::ARTIST, i18n( "Artist" ) );
+    headerData.insert( Meta::Field::ALBUM, i18n( "Album" ) );
+    headerData.insert( Meta::Field::ALBUMARTIST, i18n( "Album Artist" ) );
+    headerData.insert( Meta::Field::YEAR, i18n( "Year" ) );
+    m_rootItem = new MusicBrainzTagsItem( 0, Meta::TrackPtr(), headerData );
+}
+
+MusicBrainzTagsModel::~MusicBrainzTagsModel()
+{
+    delete m_rootItem;
+}
+
+QModelIndex
+MusicBrainzTagsModel::index( int row, int column, const QModelIndex &parent ) const
+{
+    if( !hasIndex( row, column, parent ) )
+        return QModelIndex();
+
+    MusicBrainzTagsItem *parentItem;
+
+    if( !parent.isValid() )
+        parentItem = m_rootItem;
+    else
+        parentItem = static_cast<MusicBrainzTagsItem *>( parent.internalPointer() );
+
+    MusicBrainzTagsItem *childItem = parentItem->child( row );
+
+    if( childItem )
+        return createIndex( row, column, childItem );
+    else
+        return QModelIndex();
+}
+
+QModelIndex
+MusicBrainzTagsModel::parent( const QModelIndex &index ) const
+{
+    if( !index.isValid() )
+        return QModelIndex();
+
+    MusicBrainzTagsItem *childItem = static_cast<MusicBrainzTagsItem *>( index.internalPointer() );
+    MusicBrainzTagsItem *parentItem = childItem->parent();
+
+    if( parentItem == m_rootItem )
+        return QModelIndex();
+
+    return this->index( parentItem->row(), 0 );
+}
+
+QVariant
+MusicBrainzTagsModel::data( const QModelIndex &index, int role ) const
+{
+    if( !index.isValid() )
+        return QVariant();
+
+    MusicBrainzTagsItem *item = static_cast<MusicBrainzTagsItem *>( index.internalPointer() );
+
+    if( role == Qt::DisplayRole )
+        return item->data( index.column() );
+    else if( role == MusicBrainzTagsModel::SortRole )
+    {
+        if( item->parent() == m_rootItem )
+            return item->track()->prettyUrl();
+        else
+        {
+            /*
+             * Sort order is ascending, but we want the results to be sorted from the
+             * highest score to the lower.
+             */
+            return item->score() * -1;
+        }
+    }
+    else if( role == MusicBrainzTagsModel::TracksRole )
+    {
+        QStringList trackList = item->dataValue( MusicBrainz::TRACKID ).toStringList();
+        trackList.removeDuplicates();
+        return trackList;
+    }
+    else if( role == MusicBrainzTagsModel::ArtistsRole )
+    {
+        QVariantList artistList = item->dataValue( MusicBrainz::ARTISTID ).toList();
+        return artistList;
+    }
+    else if( role == MusicBrainzTagsModel::ReleasesRole )
+    {
+        QStringList releaseList = item->dataValue( MusicBrainz::RELEASEID ).toStringList();
+        releaseList.removeDuplicates();
+        return releaseList;
+    }
+    else if( role == Qt::CheckStateRole &&
+             index.column() == 0 &&
+             index.flags() & Qt::ItemIsUserCheckable )
+        return item->isChosen()? Qt::Checked : Qt::Unchecked;
+    else if( role == MusicBrainzTagsModel::ChosenStateRole &&
+             item->parent() == m_rootItem )
+        return item->isChosen()? MusicBrainzTagsModel::Chosen : MusicBrainzTagsModel::Unchosen;
+    else if( role == Qt::BackgroundRole &&
+             item->dataContains( MusicBrainz::SIMILARITY ) )
+    {
+        if( item->dataContains( MusicBrainz::MUSICBRAINZ ) &&
+            item->dataContains( MusicBrainz::MUSICDNS ) )
+            return QColor( Qt::green );
+
+        float sim = ( item->dataValue( MusicBrainz::SIMILARITY ).toFloat() - MusicBrainz::MINSIMILARITY ) /
+                    ( 1.0 - MusicBrainz::MINSIMILARITY );
+
+        quint8 c1 = 255, c2 = 255;
+        if( sim < 0.5 )
+            c2 = ( 170 + 170 * sim );
+        else
+            c1 = ( 255 - 170 * ( sim - 0.5 ) );
+
+        if( item->dataContains( MusicBrainz::MUSICDNS ) )
+            return QColor( 0, c2, c1 );
+        else
+            return QColor( c1, c2, 0 );
+    }
+    else if( role == Qt::ToolTipRole )
+    {
+        QStringList toolTip;
+        if( item->parent() == m_rootItem )
+            toolTip.append( item->track()->prettyUrl() );
+        else
+        {
+            if( item->dataContains( MusicBrainz::MUSICBRAINZ ) )
+                toolTip.append( i18n( "MusicBrainz match ratio: %1%",
+                                      100 * item->dataValue( MusicBrainz::MUSICBRAINZ ).toFloat() ) );
+            if( item->dataContains( MusicBrainz::MUSICDNS ) )
+                toolTip.append( i18n( "MusicDNS match ratio: %1%",
+                                      100 * item->dataValue( MusicBrainz::MUSICDNS ).toFloat() ) );
+        }
+
+        return toolTip.join( "\n" );
+    }
+    else if( role == Qt::FontRole )
+    {
+        QFont font;
+        if( item->parent() == m_rootItem )
+            font.setItalic( true );
+        else if( item->isChosen() )
+            font.setBold( true );
+        return font;
+    }
+    else if( role == Qt::ForegroundRole && item->parent() != m_rootItem )
+        return QColor( Qt::black );
+
+    return QVariant();
+}
+
+bool
+MusicBrainzTagsModel::setData( const QModelIndex &index, const QVariant &value, int role )
+{
+    if( !index.isValid() || role != Qt::CheckStateRole || index.column() != 0 )
+        return false;
+
+    MusicBrainzTagsItem *item = static_cast<MusicBrainzTagsItem *>( index.internalPointer() );
+    MusicBrainzTagsItem *parentItem = item->parent();
+    if( item == m_rootItem || parentItem == m_rootItem )
+        return false;
+
+    parentItem->clearChoices();
+    item->setChosen( value.toBool() );
+    QModelIndex parent = index.parent();
+    emit dataChanged( this->index( 0, 0, parent ),
+                      this->index( rowCount( parent ) - 1, 0, parent ) );
+    return true;
+}
+
+Qt::ItemFlags
+MusicBrainzTagsModel::flags( const QModelIndex &index ) const
+{
+    if( !index.isValid() || !parent( index ).isValid() )
+        // Disable items with no children.
+        return QAbstractItemModel::flags( index ) ^
+               ( ( !static_cast<MusicBrainzTagsItem *>( index.internalPointer() )->childCount() )?
+                 Qt::ItemIsEnabled : Qt::NoItemFlags );
+
+    return QAbstractItemModel::flags( index ) | Qt::ItemIsUserCheckable;
+}
+
+QVariant
+MusicBrainzTagsModel::headerData( int section, Qt::Orientation orientation, int role ) const
+{
+    if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
+        return m_rootItem->data( section );
+
+    return QVariant();
+}
+
+int
+MusicBrainzTagsModel::rowCount( const QModelIndex &parent ) const
+{
+    MusicBrainzTagsItem *parentItem;
+
+    if( parent.column() > 0 )
+        return 0;
+
+    if( !parent.isValid() )
+        parentItem = m_rootItem;
+    else
+        parentItem = static_cast<MusicBrainzTagsItem *>( parent.internalPointer() );
+
+    return parentItem->childCount();
+}
+
+int
+MusicBrainzTagsModel::columnCount( const QModelIndex &parent ) const
+{
+    Q_UNUSED( parent );
+    return 5;
+}
+
+void
+MusicBrainzTagsModel::addTrack( const Meta::TrackPtr track, const QVariantMap tags )
+{
+    QModelIndex parent;
+    int row = rowCount();
+    for( int i = 0; i < m_rootItem->childCount(); i++ )
+    {
+        MusicBrainzTagsItem *item = m_rootItem->child( i );
+        if( track == item->track() )
+        {
+            parent = index( i, 0 );
+            row = rowCount( parent );
+            break;
+        }
+    }
+
+    beginInsertRows( parent, row, row );
+    m_rootItem->appendChild( new MusicBrainzTagsItem( m_rootItem, track, tags ) );
+    endInsertRows();
+}
+
+QMap<Meta::TrackPtr, QVariantMap>
+MusicBrainzTagsModel::chosenItems() const
+{
+    QMap<Meta::TrackPtr, QVariantMap> result;
+
+    for( int i = 0; i < m_rootItem->childCount(); i++ )
+    {
+        MusicBrainzTagsItem *item = m_rootItem->child( i )->chosenItem();
+        if( item )
+        {
+            QVariantMap data = item->data();
+            data.remove( MusicBrainz::ARTISTID );
+            data.remove( MusicBrainz::MUSICBRAINZ );
+            data.remove( MusicBrainz::MUSICDNS );
+            data.remove( MusicBrainz::RELEASEID );
+            data.remove( MusicBrainz::SIMILARITY );
+            data.remove( MusicBrainz::TRACKCOUNT );
+            data.remove( MusicBrainz::TRACKID );
+            result.insert( item->track(), data );
+        }
+    }
+
+    return result;
+}
+
+void
+MusicBrainzTagsModel::chooseBestMatches()
+{
+    for( int i = 0; i < m_rootItem->childCount(); i++ )
+    {
+        MusicBrainzTagsItem *item = m_rootItem->child( i );
+        if( item->chooseBestMatch() )
+        {
+            QModelIndex parent = index( i, 0 );
+            emit dataChanged( index( 0, 0, parent ),
+                              index( rowCount( parent ) - 1, 0, parent ) );
+        }
+    }
+}
+
+void
+MusicBrainzTagsModel::chooseBestMatchesFromRelease( const QStringList &releases )
+{
+    for( int i = 0; i < m_rootItem->childCount(); i++ )
+    {
+        MusicBrainzTagsItem *item = m_rootItem->child( i );
+        if( item->chooseBestMatchFromRelease( releases ) )
+        {
+            QModelIndex parent = index( i, 0 );
+            emit dataChanged( index( 0, 0, parent ),
+                              index( rowCount( parent ) - 1, 0, parent ) );
+        }
+    }
+}
+
+void
+MusicBrainzTagsModel::clearChoices()
+{
+    for( int i = 0; i < m_rootItem->childCount(); i++ )
+    {
+        MusicBrainzTagsItem *item = m_rootItem->child( i );
+        item->clearChoices();
+        QModelIndex parent = index( i, 0 );
+        emit dataChanged( index( 0, 0, parent ),
+                          index( rowCount( parent ) - 1, 0, parent ) );
+    }
+}
+
+#include "MusicBrainzTagsModel.moc"
diff --git a/src/musicbrainz/MusicBrainzTagsModel.h b/src/musicbrainz/MusicBrainzTagsModel.h
new file mode 100644
index 0000000..7028507
--- /dev/null
+++ b/src/musicbrainz/MusicBrainzTagsModel.h
@@ -0,0 +1,74 @@
+/****************************************************************************************
+ * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
+ *                                                                                      *
+ * This program is free software; you can redistribute it and/or modify it under        *
+ * the terms of the GNU General Public License as published by the Free Software        *
+ * Foundation; either version 2 of the License, or (at your option) any later           *
+ * version.                                                                             *
+ *                                                                                      *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
+ *                                                                                      *
+ * You should have received a copy of the GNU General Public License along with         *
+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
+ ****************************************************************************************/
+
+#ifndef MUSICBRAINZTAGSMODEL_H
+#define MUSICBRAINZTAGSMODEL_H
+
+#include "core/meta/Meta.h"
+
+#include <QAbstractItemModel>
+
+class MusicBrainzTagsItem;
+
+class MusicBrainzTagsModel : public QAbstractItemModel
+{
+    Q_OBJECT
+
+    public:
+        enum {
+            SortRole = Qt::UserRole,
+            TracksRole,
+            ArtistsRole,
+            ReleasesRole,
+            ChosenStateRole
+        };
+
+        enum ChosenState {
+            Unchosen,
+            Chosen
+        };
+
+        explicit MusicBrainzTagsModel( QObject *parent = 0 );
+        ~MusicBrainzTagsModel();
+
+        QModelIndex index( int row, int column,
+                           const QModelIndex &parent = QModelIndex() ) const;
+        QModelIndex parent( const QModelIndex &index ) const;
+
+        QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+        bool setData( const QModelIndex &index, const QVariant &value, int role );
+        Qt::ItemFlags flags( const QModelIndex &index ) const;
+        QVariant headerData( int section, Qt::Orientation orientation,
+                             int role = Qt::DisplayRole ) const;
+
+        int rowCount( const QModelIndex &parent = QModelIndex() ) const;
+        int columnCount( const QModelIndex &parent = QModelIndex() ) const;
+
+        QMap<Meta::TrackPtr, QVariantMap> chosenItems() const;
+        void chooseBestMatchesFromRelease( const QStringList &releases );
+
+    public slots:
+        void addTrack( const Meta::TrackPtr track, const QVariantMap tags );
+
+        void chooseBestMatches();
+        void clearChoices();
+
+    private:
+        MusicBrainzTagsItem *m_rootItem;
+};
+
+#endif // MUSICBRAINZTAGSMDOEL_H
diff --git a/src/musicbrainz/MusicBrainzMeta.h b/src/musicbrainz/MusicBrainzTagsModelDelegate.cpp
similarity index 56%
copy from src/musicbrainz/MusicBrainzMeta.h
copy to src/musicbrainz/MusicBrainzTagsModelDelegate.cpp
index 5cbd190..6bce6c6 100644
--- a/src/musicbrainz/MusicBrainzMeta.h
+++ b/src/musicbrainz/MusicBrainzTagsModelDelegate.cpp
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -14,25 +15,41 @@
  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
  ****************************************************************************************/
 
-#ifndef MUSICBRAINZMETA_H
-#define MUSICBRAINZMETA_H
+#include "MusicBrainzTagsModelDelegate.h"
 
-#include <QString>
+#include <QApplication>
 
-namespace MusicBrainz
+#define DEBUG_PREFIX "MusicBrainzTagsModelDelegate"
+
+MusicBrainzTagsModelDelegate::MusicBrainzTagsModelDelegate( QObject *parent )
+    : QItemDelegate( parent )
 {
-    static const QString ARTISTID   = "mb:ArtistID";
-    static const QString RELEASEID  = "mb:ReleaseID";
-    static const QString RELEASELIST= "mb:ReleaseList";
-    static const QString TRACKID    = "mb:TrackID";
-    static const QString TRACKOFFSET= "mb:TrackOffset";
+}
 
-    static const QString SIMILARITY = "mb:similarity";
+void
+MusicBrainzTagsModelDelegate::drawCheck( QPainter *painter,
+                                         const QStyleOptionViewItem &option,
+                                         const QRect &rect, Qt::CheckState state ) const
+{
+    if( !rect.isValid() )
+        return;
 
-    static const QString MUSICBRAINZ= "mb:musicbrainz";
-    static const QString MUSICDNS   = "mb:musicdns";
+    QStyleOptionViewItem opt( option );
+    opt.rect = rect;
+    opt.state &= ~QStyle::State_HasFocus;
 
-    static const qreal MINSIMILARITY= 0.6;
-}
+    switch( state )
+    {
+    case Qt::Unchecked:
+        opt.state |= QStyle::State_Off;
+        break;
+    case Qt::PartiallyChecked:
+        opt.state |= QStyle::State_NoChange;
+        break;
+    case Qt::Checked:
+        opt.state |= QStyle::State_On;
+        break;
+    }
 
-#endif //MUSICBRAINZMETA_H
+    QApplication::style()->drawPrimitive( QStyle::PE_IndicatorRadioButton, &opt, painter );
+}
diff --git a/src/musicbrainz/MusicBrainzMeta.h b/src/musicbrainz/MusicBrainzTagsModelDelegate.h
similarity index 69%
copy from src/musicbrainz/MusicBrainzMeta.h
copy to src/musicbrainz/MusicBrainzTagsModelDelegate.h
index 5cbd190..d276af7 100644
--- a/src/musicbrainz/MusicBrainzMeta.h
+++ b/src/musicbrainz/MusicBrainzTagsModelDelegate.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -14,25 +15,19 @@
  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
  ****************************************************************************************/
 
-#ifndef MUSICBRAINZMETA_H
-#define MUSICBRAINZMETA_H
+#ifndef MUSICBRAINZTAGSMODELDELEGATE_H
+#define MUSICBRAINZTAGSMODELDELEGATE_H
 
-#include <QString>
+#include <QItemDelegate>
 
-namespace MusicBrainz
+class MusicBrainzTagsModelDelegate : public QItemDelegate
 {
-    static const QString ARTISTID   = "mb:ArtistID";
-    static const QString RELEASEID  = "mb:ReleaseID";
-    static const QString RELEASELIST= "mb:ReleaseList";
-    static const QString TRACKID    = "mb:TrackID";
-    static const QString TRACKOFFSET= "mb:TrackOffset";
+    public:
+        explicit MusicBrainzTagsModelDelegate( QObject *parent = 0 );
 
-    static const QString SIMILARITY = "mb:similarity";
+    protected:
+        virtual void drawCheck( QPainter *painter, const QStyleOptionViewItem &option,
+                                const QRect &rect, Qt::CheckState state ) const;
+};
 
-    static const QString MUSICBRAINZ= "mb:musicbrainz";
-    static const QString MUSICDNS   = "mb:musicdns";
-
-    static const qreal MINSIMILARITY= 0.6;
-}
-
-#endif //MUSICBRAINZMETA_H
+#endif // MUSICBRAINZTAGSMODELDELEGATE_H
diff --git a/src/musicbrainz/MusicBrainzTagsView.cpp b/src/musicbrainz/MusicBrainzTagsView.cpp
new file mode 100644
index 0000000..259876a
--- /dev/null
+++ b/src/musicbrainz/MusicBrainzTagsView.cpp
@@ -0,0 +1,237 @@
+/****************************************************************************************
+ * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
+ *                                                                                      *
+ * This program is free software; you can redistribute it and/or modify it under        *
+ * the terms of the GNU General Public License as published by the Free Software        *
+ * Foundation; either version 2 of the License, or (at your option) any later           *
+ * version.                                                                             *
+ *                                                                                      *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
+ *                                                                                      *
+ * You should have received a copy of the GNU General Public License along with         *
+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
+ ****************************************************************************************/
+
+#include "MusicBrainzTagsView.h"
+
+#include "core/support/Debug.h"
+#include "MusicBrainzTagsModel.h"
+
+#include <KActionMenu>
+#include <KLocalizedString>
+#include <KStandardDirs>
+
+#include <QContextMenuEvent>
+#include <QDesktopServices>
+#include <QMenu>
+#include <QSortFilterProxyModel>
+
+#define DEBUG_PREFIX "MusicBrainzTagsView"
+
+MusicBrainzTagsView::MusicBrainzTagsView( QWidget *parent )
+    : QTreeView( parent )
+{
+    m_artistIcon = KIcon( KStandardDirs::locate( "data", "amarok/images/mb_aicon.png" ) );
+    m_releaseIcon = KIcon( KStandardDirs::locate( "data", "amarok/images/mb_licon.png" ) );
+    m_trackIcon = KIcon( KStandardDirs::locate( "data", "amarok/images/mb_ticon.png" ) );
+}
+
+MusicBrainzTagsModel *
+MusicBrainzTagsView::sourceModel() const
+{
+    QSortFilterProxyModel *model = qobject_cast<QSortFilterProxyModel *>( this->model() );
+    if( !model )
+        return 0;
+
+    MusicBrainzTagsModel *sourceModel = qobject_cast<MusicBrainzTagsModel *>( model->sourceModel() );
+    return sourceModel;
+}
+
+void
+MusicBrainzTagsView::contextMenuEvent( QContextMenuEvent *event )
+{
+    QModelIndex index = indexAt( event->pos() );
+    if( !index.isValid() || !index.internalPointer() )
+    {
+        event->ignore();
+        return;
+    }
+
+    QAbstractItemModel *model = this->model();
+    if( !model )
+        return;
+
+    if( ~index.flags() & Qt::ItemIsUserCheckable )
+    {
+        event->ignore();
+        return;
+    }
+
+    QMenu *menu = new QMenu( this );
+    QList<QAction *> actions;
+
+    if( model->rowCount() > 1 && !index.data( MusicBrainzTagsModel::ReleasesRole ).isNull() )
+    {
+        QAction *action = new QAction( KIcon( "filename-album-amarok" ),
+                                       i18n( "Choose Best Matches from This Album" ), menu );
+        connect( action, SIGNAL(triggered()), SLOT(chooseBestMatchesFromRelease()) );
+        menu->addAction( action );
+        menu->addSeparator();
+    }
+
+    QVariantMap artists;
+    if( !index.data( MusicBrainzTagsModel::ArtistsRole ).toList().isEmpty() )
+        artists = index.data( MusicBrainzTagsModel::ArtistsRole ).toList().first().toMap();
+    if( !artists.isEmpty() )
+    {
+        KActionMenu *action = new KActionMenu( m_artistIcon, i18n( "Go to Artist Page" ), menu );
+        if( artists.size() > 1 )
+        {
+            foreach( const QVariant &id, artists.keys() )
+            {
+                QAction *subAction = new QAction( artists.value( id.toString() ).toString(), action );
+                subAction->setData( id );
+                connect( subAction, SIGNAL(triggered()), SLOT(openArtistPage()) );
+                action->addAction( subAction );
+            }
+        }
+        else
+        {
+            action->setData( artists.keys().first() );
+            connect( action, SIGNAL(triggered()), SLOT(openArtistPage()) );
+        }
+        actions << action;
+    }
+
+    if( !index.data( MusicBrainzTagsModel::ReleasesRole ).toList().isEmpty() )
+    {
+        QAction *action = new QAction( m_releaseIcon, i18n( "Go to Album Page" ), menu );
+        connect( action, SIGNAL(triggered()), SLOT(openReleasePage()) );
+        actions << action;
+    }
+
+    if( !index.data( MusicBrainzTagsModel::TracksRole ).toList().isEmpty() )
+    {
+        QAction *action = new QAction( m_trackIcon, i18n( "Go to Track Page" ), menu );
+        connect( action, SIGNAL(triggered()), SLOT(openTrackPage()) );
+        actions << action;
+    }
+
+    if( actions.isEmpty() )
+    {
+        delete menu;
+        event->ignore();
+        return;
+    }
+
+    menu->addActions( actions );
+    menu->exec( event->globalPos() );
+    event->accept();
+}
+
+void
+MusicBrainzTagsView::collapseChosen()
+{
+    QAbstractItemModel *model = this->model();
+    if( !model )
+        return;
+
+    for( int i = 0; i < model->rowCount(); i++ )
+    {
+        QModelIndex index = model->index( i, 0 );
+        if( index.isValid() &&
+            index.data( MusicBrainzTagsModel::ChosenStateRole ) == MusicBrainzTagsModel::Chosen )
+            collapse( index );
+    }
+}
+
+void
+MusicBrainzTagsView::expandUnchosen()
+{
+    QAbstractItemModel *model = this->model();
+    if( !model )
+        return;
+
+    for( int i = 0; i < model->rowCount(); i++ )
+    {
+        QModelIndex index = model->index( i, 0 );
+        if( index.isValid() &&
+            index.data( MusicBrainzTagsModel::ChosenStateRole ) == MusicBrainzTagsModel::Unchosen )
+            expand( index );
+    }
+}
+
+void
+MusicBrainzTagsView::chooseBestMatchesFromRelease() const
+{
+    QModelIndex index = selectedIndexes().first();
+    if( !index.isValid() || !index.internalPointer() )
+        return;
+
+    MusicBrainzTagsModel *sourceModel = this->sourceModel();
+    if( !sourceModel )
+        return;
+
+    QStringList releases = index.data( MusicBrainzTagsModel::ReleasesRole ).toStringList();
+    if( releases.isEmpty() )
+        return;
+
+    sourceModel->chooseBestMatchesFromRelease( releases );
+}
+
+void
+MusicBrainzTagsView::openArtistPage() const
+{
+    QModelIndex index = selectedIndexes().first();
+    if( !index.isValid() || !index.internalPointer() )
+        return;
+
+    QAction *action = qobject_cast<QAction *>( sender() );
+    if( !action )
+        return;
+
+    QString artistID = action->data().toString();
+    if( artistID.isEmpty() )
+        return;
+
+    QString url = QString( "http://musicbrainz.org/artist/%1.html" ).arg( artistID );
+
+    QDesktopServices::openUrl( url );
+}
+
+void
+MusicBrainzTagsView::openReleasePage() const
+{
+    QModelIndex index = selectedIndexes().first();
+    if( !index.isValid() || !index.internalPointer() )
+        return;
+
+    QString releaseID = index.data( MusicBrainzTagsModel::ReleasesRole ).toStringList().first();
+    if( releaseID.isEmpty() )
+        return;
+
+    QString url = QString( "http://musicbrainz.org/release/%1.html" ).arg( releaseID );
+
+    QDesktopServices::openUrl( url );
+}
+
+void
+MusicBrainzTagsView::openTrackPage() const
+{
+    QModelIndex index = selectedIndexes().first();
+    if( !index.isValid() || !index.internalPointer() )
+        return;
+
+    QString trackID = index.data( MusicBrainzTagsModel::TracksRole ).toStringList().first();
+    if( trackID.isEmpty() )
+        return;
+
+    QString url = QString( "http://musicbrainz.org/recording/%1.html" ).arg( trackID );
+
+    QDesktopServices::openUrl( url );
+}
+
+#include "MusicBrainzTagsView.moc"
diff --git a/src/musicbrainz/MusicBrainzMeta.h b/src/musicbrainz/MusicBrainzTagsView.h
similarity index 60%
copy from src/musicbrainz/MusicBrainzMeta.h
copy to src/musicbrainz/MusicBrainzTagsView.h
index 5cbd190..6997e34 100644
--- a/src/musicbrainz/MusicBrainzMeta.h
+++ b/src/musicbrainz/MusicBrainzTagsView.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -14,25 +15,41 @@
  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
  ****************************************************************************************/
 
-#ifndef MUSICBRAINZMETA_H
-#define MUSICBRAINZMETA_H
+#ifndef MUSICBRAINZTAGSVIEW_H
+#define MUSICBRAINZTAGSVIEW_H
 
-#include <QString>
+#include <KIcon>
 
-namespace MusicBrainz
+#include <QTreeView>
+
+class MusicBrainzTagsModel;
+
+class MusicBrainzTagsView : public QTreeView
 {
-    static const QString ARTISTID   = "mb:ArtistID";
-    static const QString RELEASEID  = "mb:ReleaseID";
-    static const QString RELEASELIST= "mb:ReleaseList";
-    static const QString TRACKID    = "mb:TrackID";
-    static const QString TRACKOFFSET= "mb:TrackOffset";
+    Q_OBJECT
+
+    public:
+        explicit MusicBrainzTagsView( QWidget *parent = 0 );
+
+        MusicBrainzTagsModel *sourceModel() const;
+
+    public slots:
+        void collapseChosen();
+        void expandUnchosen();
 
-    static const QString SIMILARITY = "mb:similarity";
+    protected:
+        virtual void contextMenuEvent( QContextMenuEvent *event );
 
-    static const QString MUSICBRAINZ= "mb:musicbrainz";
-    static const QString MUSICDNS   = "mb:musicdns";
+    private slots:
+        void chooseBestMatchesFromRelease() const;
+        void openArtistPage() const;
+        void openReleasePage() const;
+        void openTrackPage() const;
 
-    static const qreal MINSIMILARITY= 0.6;
-}
+    private:
+        KIcon m_artistIcon;
+        KIcon m_releaseIcon;
+        KIcon m_trackIcon;
+};
 
-#endif //MUSICBRAINZMETA_H
+#endif // MUSICBRAINZTAGSVIEW_H
diff --git a/src/musicbrainz/MusicBrainzXmlParser.cpp b/src/musicbrainz/MusicBrainzXmlParser.cpp
index 473d27a..30cdbea 100644
--- a/src/musicbrainz/MusicBrainzXmlParser.cpp
+++ b/src/musicbrainz/MusicBrainzXmlParser.cpp
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -22,10 +23,13 @@
 #include "core/support/Debug.h"
 #include "MusicBrainzMeta.h"
 
+#include <QStringList>
+#include <QVariantList>
+
 MusicBrainzXmlParser::MusicBrainzXmlParser( QString &doc )
-                    : ThreadWeaver::Job()
-                    , m_doc( "musicbrainz" )
-                    , m_type( 0 )
+    : ThreadWeaver::Job()
+    , m_doc( "musicbrainz" )
+    , m_type( 0 )
 {
     m_doc.setContent( doc );
 }
@@ -48,20 +52,15 @@ void
 MusicBrainzXmlParser::parseElement( const QDomElement &e )
 {
     QString elementName = e.tagName();
-    if( elementName == "track-list" )
+    if( elementName == "recording-list" )
     {
         m_type = TrackList;
-        parseTrackList( e );
-    }
-    else if( elementName == "release" )
-    {
-        m_type = Release;
-        parseRelease( e );
+        parseRecordingList( e );
     }
-    else if( elementName == "track" )
+    else if( elementName == "release-group" )
     {
-        m_type = Track;
-        parseTrack( e );
+        m_type = ReleaseGroup;
+        parseReleaseGroup( e );
     }
     else
         parseChildren( e );
@@ -80,7 +79,7 @@ MusicBrainzXmlParser::parseChildren( const QDomElement &e )
 }
 
 QStringList
-MusicBrainzXmlParser::parseTrackList( const QDomElement &e )
+MusicBrainzXmlParser::parseRecordingList( const QDomElement &e )
 {
     QDomNode dNode = e.firstChild();
     QDomElement dElement;
@@ -92,8 +91,8 @@ MusicBrainzXmlParser::parseTrackList( const QDomElement &e )
         {
             dElement = dNode.toElement();
 
-            if( dElement.tagName() == "track" )
-                list << parseTrack( dElement );
+            if( dElement.tagName() == "recording" )
+                list << parseRecording( dElement );
         }
         dNode = dNode.nextSibling();
     }
@@ -101,24 +100,21 @@ MusicBrainzXmlParser::parseTrackList( const QDomElement &e )
 }
 
 QString
-MusicBrainzXmlParser::parseTrack( const QDomElement &e )
+MusicBrainzXmlParser::parseRecording( const QDomElement &e )
 {
     QString id;
     QVariantMap track;
 
     if( e.hasAttribute( "id" ) )
-    {
         id = e.attribute( "id" );
-        if( id.isEmpty() )
-            return QString();
-        if( tracks.contains( id ) )
-            track = tracks.value( id );
-        else
-            track.insert( MusicBrainz::TRACKID, id );
-        if( track.isEmpty() )
-            return id;
-    }
+    if( id.isEmpty() )
+        return id;
+
+    if( tracks.contains( id ) )
+        track = tracks.value( id );
     else
+        track.insert( MusicBrainz::TRACKID, id );
+    if( track.isEmpty() )
         return id;
 
     if( e.hasAttribute( "ext:score" ) )
@@ -137,23 +133,42 @@ MusicBrainzXmlParser::parseTrack( const QDomElement &e )
 
             if( elementName == "title" )
                 track.insert( Meta::Field::TITLE, dElement.text() );
-            else if( elementName == "duration" )
+            else if( elementName == "length" )
             {
                 int length = dElement.text().toInt();
                 if( length > 0 )
                     track.insert( Meta::Field::LENGTH, length );
             }
-            else if( elementName == "artist" )
+            else if( elementName == "artist-credit" )
             {
-                track.insert( MusicBrainz::ARTISTID, parseArtist( dElement ) );
-                track.insert( Meta::Field::ARTIST,
-                              artists.value( track.value( MusicBrainz::ARTISTID ).toString() ) );
+                QStringList idList = parseArtist( dElement );
+                if( !idList.isEmpty() )
+                {
+                    QString artist;
+                    QVariantMap artistInfo;
+                    foreach( const QString &id, idList )
+                    {
+                        if( artists.contains( id ) )
+                        {
+                            artistInfo.insert( id, artists.value( id ) );
+                            artist += artists.value( id );
+                        }
+                        else
+                            // If it's not among IDs, it's a joinphrase attribute.
+                            artist += id;
+                    }
+                    if( !artistInfo.isEmpty() )
+                    {
+                        track.insert( MusicBrainz::ARTISTID, artistInfo );
+                        track.insert( Meta::Field::ARTIST, artist );
+                    }
+                }
             }
             else if( elementName == "release-list" )
             {
-                currentTrackOffsets.clear();
+                m_currentTrackInfo.clear();
                 track.insert( MusicBrainz::RELEASELIST, parseReleaseList( dElement ) );
-                track.insert( MusicBrainz::TRACKOFFSET, currentTrackOffsets );
+                track.insert( MusicBrainz::TRACKINFO, m_currentTrackInfo );
             }
         }
         dNode = dNode.nextSibling();
@@ -181,7 +196,7 @@ MusicBrainzXmlParser::parseReleaseList( const QDomElement &e )
         }
         dNode = dNode.nextSibling();
     }
-
+    list.removeDuplicates();
     return list;
 }
 
@@ -192,18 +207,15 @@ MusicBrainzXmlParser::parseRelease( const QDomElement &e )
     QVariantMap release;
 
     if( e.hasAttribute( "id" ) )
-    {
         id = e.attribute( "id" );
-        if( id.isEmpty() )
-            return QString();
-        if( releases.contains( id ) )
-            release = releases.value( id );
-        else
-            release.insert( MusicBrainz::RELEASEID, id );
-        if( release.isEmpty() )
-            return id;
-    }
+    if( id.isEmpty() )
+        return id;
+
+    if( releases.contains( id ) )
+        release = releases.value( id );
     else
+        release.insert( MusicBrainz::RELEASEID, id );
+    if( release.isEmpty() )
         return id;
 
     QDomNode dNode = e.firstChild();
@@ -218,39 +230,35 @@ MusicBrainzXmlParser::parseRelease( const QDomElement &e )
             elementName = dElement.tagName();
 
             if( elementName == "title" )
+                /*
+                 * Avoid checking for "(disc N)" string as it's not a safe way to detect
+                 * disc number.
+                 */
+                release.insert( Meta::Field::TITLE, dElement.text() );
+            else if( elementName == "medium-list" )
             {
-                QString releaseName = dElement.text();
-                QRegExp diskNrMatcher( "^.*\\(disc (\\d+).*\\)$" );
-                if( diskNrMatcher.exactMatch( releaseName ) )
+                QVariantMap info = parseMediumList( dElement );
+                QVariantList trackCountList = info.values( MusicBrainz::TRACKCOUNT );
+                int trackCount = 0;
+                foreach( const QVariant &count, trackCountList )
                 {
-                    release.insert( Meta::Field::DISCNUMBER, diskNrMatcher.cap( 1 ) );
-                    //releaseName.truncate( releaseName.indexOf( "(disc " ) );
+                    trackCount += count.toInt();
+                    if( count.toInt() > 0 )
+                        release.insert( MusicBrainz::TRACKCOUNT, count.toInt() );
                 }
-
-                release.insert( Meta::Field::TITLE, releaseName.trimmed() );
-            }
-            else if( elementName == "artist" )
-            {
-                QString artistID = parseArtist( dElement );
-                if( !artistID.isEmpty() && artists.contains( artistID ) )
+                if( info.contains( Meta::Field::DISCNUMBER ) )
                 {
-                    release.insert( MusicBrainz::ARTISTID, artistID );
-                    release.insert( Meta::Field::ARTIST,
-                                    artists.value( artistID ) );
+                    int discNumber = info.value( Meta::Field::DISCNUMBER ).toInt();
+                    if( discNumber < 1 || discNumber == 1 &&
+                        ( trackCount <= 0 || trackCountList.size() != 2 ) )
+                        info.remove( Meta::Field::DISCNUMBER );
                 }
+                QVariantList trackInfoList = m_currentTrackInfo.value( id ).toList();
+                trackInfoList.append( info );
+                m_currentTrackInfo.insert( id, trackInfoList );
             }
-            else if( elementName == "track-list" && dElement.hasAttribute( "offset" ) )
-            {
-                int offset = dElement.attribute( "offset" ).toInt() + 1;
-                if( offset > 0 )
-                    currentTrackOffsets.insert( id, offset );
-            }
-            else if( elementName == "release-event-list" )
-            {
-                int year = parseReleaseEventList( dElement );
-                if( year > 0 )
-                    release.insert( Meta::Field::YEAR, year );
-            }
+            else if( elementName == "release-group" )
+                release.insert( MusicBrainz::RELEASEGROUPID, parseReleaseGroup( dElement ) );
         }
         dNode = dNode.nextSibling();
     }
@@ -259,135 +267,263 @@ MusicBrainzXmlParser::parseRelease( const QDomElement &e )
     return id;
 }
 
-int
-MusicBrainzXmlParser::parseReleaseEventList( const QDomElement &e )
+QVariantMap
+MusicBrainzXmlParser::parseMediumList( const QDomElement &e )
 {
     QDomNode dNode = e.firstChild();
     QDomElement dElement;
-    QList < int > years;
+    QString elementName;
+    QVariantMap info;
 
     while( !dNode.isNull() )
     {
         if( dNode.isElement() )
         {
             dElement = dNode.toElement();
+            elementName = dElement.tagName();
 
-            if( dElement.tagName() == "event" )
+            if( elementName == "track-count" )
+                info.insert( MusicBrainz::TRACKCOUNT, dElement.text().toInt() );
+            else if( elementName == "medium" )
+                info.unite( parseMedium( dElement ) );
+        }
+        dNode = dNode.nextSibling();
+    }
+    return info;
+}
+
+QVariantMap
+MusicBrainzXmlParser::parseMedium( const QDomElement &e )
+{
+    QDomNode dNode = e.firstChild();
+    QDomElement dElement;
+    QString elementName;
+    QVariantMap info;
+
+    while( !dNode.isNull() )
+    {
+        if( dNode.isElement() )
+        {
+            dElement = dNode.toElement();
+            elementName = dElement.tagName();
+
+            if( elementName == "position" )
+            {
+                int discNumber = dElement.text().toInt();
+                if( discNumber > 0 )
+                    info.insert( Meta::Field::DISCNUMBER, discNumber );
+            }
+            else if( elementName == "track-list" )
             {
-                unsigned int year = parseReleaseEvent( dElement );
-                if( year )
-                    years.append( year );
+                if( dElement.hasAttribute( "count" ) )
+                    info.insert( MusicBrainz::TRACKCOUNT,
+                                 -1 * dElement.attribute( "count" ).toInt() );
+                info.unite( parseTrackList( dElement ) );
             }
         }
         dNode = dNode.nextSibling();
     }
+    return info;
+}
 
-    if( years.empty() )
-        return 0;
+QVariantMap
+MusicBrainzXmlParser::parseTrackList( const QDomElement &e )
+{
+    QDomNode dNode = e.firstChild();
+    QDomElement dElement;
+    QVariantMap info;
+
+    while( !dNode.isNull() )
+    {
+        if( dNode.isElement() )
+        {
+            dElement = dNode.toElement();
 
-    qSort( years );
-    return years.first();
+            if( dElement.tagName() == "track" )
+                info = parseTrack( dElement );
+        }
+        dNode = dNode.nextSibling();
+    }
+    return info;
 }
 
-int
-MusicBrainzXmlParser::parseReleaseEvent( const QDomElement &e )
+QVariantMap
+MusicBrainzXmlParser::parseTrack( const QDomElement &e )
 {
-    int year = 0 ;
-    if( e.hasAttribute( "date" ) )
+    QDomNode dNode = e.firstChild();
+    QDomElement dElement;
+    QString elementName;
+    QVariantMap info;
+
+    while( !dNode.isNull() )
     {
-        QRegExp yearCutter( "^(\\d{4}).*$" );
-        if( yearCutter.exactMatch( e.attribute( "date" ) ) )
-            year = yearCutter.cap( 1 ).toInt();
+        if( dNode.isElement() )
+        {
+            dElement = dNode.toElement();
+            elementName = dElement.tagName();
 
+            /*
+             * Ignore any <artist-credit /> tag because per track-artists are used
+             * inconsistently (especially with classical). Composer tag should be used to
+             * get more information. Should the tag differ from the main (<recording />'s)
+             * one only by language, "joinphrase" attribute, etc., we better use the main
+             * one (as confirmed by MusicBrainz developers).
+             */
+            if( elementName == "title" )
+                info.insert( Meta::Field::TITLE, dElement.text() );
+            else if( elementName == "length" )
+            {
+                int length = dElement.text().toInt();
+                if( length > 0 )
+                    info.insert( Meta::Field::LENGTH, length );
+            }
+            else if( elementName == "number" )
+            {
+                int number = dElement.text().toInt();
+                if( number > 0 )
+                    info.insert( Meta::Field::TRACKNUMBER, number );
+            }
+        }
+        dNode = dNode.nextSibling();
     }
-    return year;
+    return info;
 }
 
 QString
-MusicBrainzXmlParser::parseArtist( const QDomElement &e )
+MusicBrainzXmlParser::parseReleaseGroup( const QDomElement &e )
 {
     QString id;
-    QString artist;
+    QVariantMap releaseGroup;
 
     if( e.hasAttribute( "id" ) )
-    {
         id = e.attribute( "id" );
-        if( id.isEmpty() )
-            return QString();
-    }
+    if( id.isEmpty() )
+        return id;
+
+    if( releaseGroups.contains( id ) )
+        releaseGroup = releaseGroups.value( id );
     else
+        releaseGroup.insert( MusicBrainz::RELEASEGROUPID, id );
+    if( releaseGroup.isEmpty() )
+        return id;
+
+    if( m_type != ReleaseGroup )
         return id;
 
     QDomNode dNode = e.firstChild();
     QDomElement dElement;
+    QString elementName;
+
     while( !dNode.isNull() )
     {
         if( dNode.isElement() )
         {
             dElement = dNode.toElement();
+            elementName = dElement.tagName();
 
-            if( dElement.tagName() == "name" )
-                artist = dElement.text();
+            if( elementName == "artist-credit" )
+            {
+                QStringList idList = parseArtist( dElement );
+                if( !idList.isEmpty() )
+                {
+                    QString artist;
+                    QVariantMap artistInfo;
+                    foreach( const QString &id, idList )
+                    {
+                        if( artists.contains( id ) )
+                        {
+                            artistInfo.insert( id, artists.value( id ) );
+                            artist += artists.value( id );
+                        }
+                        else
+                            // If it's not among IDs, it's a joinphrase attribute.
+                            artist += id;
+                    }
+                    if( !artistInfo.isEmpty() )
+                    {
+                        releaseGroup.insert( MusicBrainz::ARTISTID, artistInfo );
+                        releaseGroup.insert( Meta::Field::ARTIST, artist );
+                    }
+                }
+            }
+            else if( elementName == "first-release-date" )
+            {
+                int year = 0;
+                QRegExp yearMatcher( "^(\\d{4}).*$" );
+                if( yearMatcher.exactMatch( dElement.text() ) )
+                    year = yearMatcher.cap( 1 ).toInt();
+                if( year > 0 )
+                    releaseGroup.insert( Meta::Field::YEAR, year );
+            }
         }
         dNode = dNode.nextSibling();
     }
 
-    artists.insert( id, artist );
+    releaseGroups.insert( id, releaseGroup );
     return id;
 }
 
-QVariantMap
-MusicBrainzXmlParser::grabTrackByLength( const quint64 length )
+QStringList
+MusicBrainzXmlParser::parseArtist( const QDomElement &e )
 {
-    QString chosenTrack;
-    quint64 min = length;
-    quint64 difference = 0;
-    foreach( QString trackID, tracks.keys() )
+    QDomNode dNode = e.firstChild(), dNode2, dNode3;
+    QDomElement dElement, dElement2, dElement3;
+    QStringList idList;
+    QString id;
+
+    while( !dNode.isNull() )
     {
-        difference = qAbs< qint64 >( length - tracks.value( trackID ).value( Meta::Field::LENGTH ).toULongLong() );
-        if( difference < min )
+        if( dNode.isElement() )
         {
-            chosenTrack = trackID;
-            min = difference;
+            dElement = dNode.toElement();
+
+            if( dElement.tagName() == "name-credit" )
+            {
+                /*
+                 * <name-credit /> can have a <name /> tag which overwrites the
+                 * <artist />'s one. It's set per track or per release, so it's better to
+                 * ignore it to avoid having the same artist twice, maybe spelled
+                 * differently, which is bad for library organization. The <name /> tag
+                 * under <artist /> is global, instead, so let's use it.
+                 */
+                dNode2 = dNode.firstChild();
+                while( !dNode2.isNull() )
+                {
+                    if( dNode2.isElement() )
+                    {
+                        dElement2 = dNode2.toElement();
+
+                        if( dElement2.tagName() == "artist" )
+                        {
+                            dNode3 = dNode2.firstChild();
+                            while( !dNode3.isNull() )
+                            {
+                                if( dNode3.isElement() )
+                                {
+                                    dElement3 = dNode3.toElement();
+
+                                    if( dElement3.tagName() == "name" )
+                                    {
+                                        if( dElement2.hasAttribute( "id" ) )
+                                            id = dElement2.attribute( "id" );
+                                        if( id.isEmpty() )
+                                            return QStringList();
+                                        artists.insert( id, dElement3.text() );
+                                        idList.append( id );
+                                        if( dElement.hasAttribute( "joinphrase" ) )
+                                            idList.append( dElement.attribute( "joinphrase" ) );
+                                    }
+                                }
+                                dNode3 = dNode3.nextSibling();
+                            }
+                        }
+                    }
+                    dNode2 = dNode2.nextSibling();
+                }
+            }
         }
+        dNode = dNode.nextSibling();
     }
 
-    QVariantMap track = chosenTrack.isEmpty() ? tracks.values().first() : tracks.value( chosenTrack );
-    QString releaseId;
-
-    if( !track.value( MusicBrainz::RELEASELIST ).toStringList().isEmpty() )
-        releaseId = track.value( MusicBrainz::RELEASELIST ).toStringList().first();
-
-    QVariantMap release = releases.value( releaseId );
-    track.insert( MusicBrainz::RELEASEID, releaseId );
-    track.insert( Meta::Field::ALBUM, release.value( Meta::Field::TITLE ).toString() );
-    track.insert( Meta::Field::DISCNUMBER, release.value( Meta::Field::DISCNUMBER ).toInt() );
-    track.insert( Meta::Field::TRACKNUMBER,
-                  track.value( MusicBrainz::TRACKOFFSET ).toMap().value( releaseId ).toInt() );
-    track.remove( MusicBrainz::RELEASELIST );
-    track.remove( MusicBrainz::TRACKOFFSET );
-    return track;
-}
-
-QVariantMap
-MusicBrainzXmlParser::grabTrackByID( const QString &ID )
-{
-    if( !tracks.contains( ID ) )
-        return QVariantMap();
-
-    QVariantMap track = tracks.value( ID );
-    QString releaseId;
-
-    if( !track.value( MusicBrainz::RELEASELIST ).toStringList().isEmpty() )
-        releaseId = track.value( MusicBrainz::RELEASELIST ).toStringList().first();
-
-    QVariantMap release = releases.value( releaseId );
-    track.insert( MusicBrainz::RELEASEID, releaseId );
-    track.insert( Meta::Field::ALBUM, release.value( Meta::Field::TITLE ).toString() );
-    track.insert( Meta::Field::DISCNUMBER, release.value( Meta::Field::DISCNUMBER ).toInt() );
-    track.insert( Meta::Field::TRACKNUMBER,
-                  track.value( MusicBrainz::TRACKOFFSET ).toMap().value( releaseId ).toInt() );
-    track.remove( MusicBrainz::RELEASELIST );
-    track.remove( MusicBrainz::TRACKOFFSET );
-    return track;
+    return idList;
 }
diff --git a/src/musicbrainz/MusicBrainzXmlParser.h b/src/musicbrainz/MusicBrainzXmlParser.h
index 171a340..c7f9e72 100644
--- a/src/musicbrainz/MusicBrainzXmlParser.h
+++ b/src/musicbrainz/MusicBrainzXmlParser.h
@@ -1,5 +1,6 @@
 /****************************************************************************************
  * Copyright (c) 2010 Sergey Ivanov <123kash at gmail.com>                                 *
+ * Copyright (c) 2013 Alberto Villa <avilla at FreeBSD.org>                                *
  *                                                                                      *
  * This program is free software; you can redistribute it and/or modify it under        *
  * the terms of the GNU General Public License as published by the Free Software        *
@@ -17,55 +18,57 @@
 #ifndef MUSICBRAINZXMLPARSER_H
 #define MUSICBRAINZXMLPARSER_H
 
+#include <ThreadWeaver/Job>
+
 #include <QDomDocument>
 #include <QStringList>
 #include <QVariantMap>
 
-#include <threadweaver/Job.h>
-
 class MusicBrainzXmlParser : public ThreadWeaver::Job
 {
     Q_OBJECT
+
     public:
-        enum
-        {
-            Release     = 1,
-            TrackList   = 2,
-            Track       = 3
+        enum {
+            TrackList,
+            ReleaseGroup
         };
 
-        MusicBrainzXmlParser( QString &doc );
+        explicit MusicBrainzXmlParser( QString &doc );
+
         void run();
 
         int type();
 
-        QVariantMap grabTrackByLength( const quint64 length );
-        QVariantMap grabTrackByID( const QString &ID );
-
-        QMap< QString, QVariantMap > tracks;
-        QMap< QString, QString > artists;
-        QMap< QString, QVariantMap > releases;
+        QMap<QString, QVariantMap> tracks;
+        QMap<QString, QString> artists;
+        QMap<QString, QVariantMap> releases;
+        QMap<QString, QVariantMap> releaseGroups;
 
     private:
         void parseElement( const QDomElement &e );
         void parseChildren( const QDomElement &e );
 
+        QStringList parseRecordingList( const QDomElement &e );
+        QString parseRecording( const QDomElement &e );
+
         QStringList parseReleaseList( const QDomElement &e );
         QString parseRelease( const QDomElement &e );
 
-        int parseReleaseEventList( const QDomElement &e );
-        int parseReleaseEvent( const QDomElement &e );
+        QVariantMap parseMediumList( const QDomElement &e );
+        QVariantMap parseMedium( const QDomElement &e );
+        QVariantMap parseTrackList( const QDomElement &e );
+        QVariantMap parseTrack( const QDomElement &e );
 
-        QStringList parseTrackList( const QDomElement &e );
-        QString parseTrack( const QDomElement &e );
+        QString parseReleaseGroup( const QDomElement &e );
 
-        QString parseArtist( const QDomElement &e );
+        QStringList parseArtist( const QDomElement &e );
 
         QDomDocument m_doc;
 
         int m_type;
 
-        QVariantMap currentTrackOffsets;
+        QVariantMap m_currentTrackInfo;
 };
 
 #endif // MUSICBRAINZXMLPARSER_H


More information about the kde-doc-english mailing list