[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