kde-extraapps/amarok/src/statsyncing/jobs/MatchTracksJob.cpp

290 lines
9.9 KiB
C++
Raw Normal View History

2015-01-31 00:30:50 +00:00
/****************************************************************************************
* Copyright (c) 2012 Matěj Laitl <matej@laitl.cz> *
* *
* 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/>. *
****************************************************************************************/
#define DEBUG_PREFIX "StatSyncing"
#include "MatchTracksJob.h"
#include "MetaValues.h"
#include "core/meta/Meta.h"
using namespace StatSyncing;
#undef VERBOSE_DEBUG
#ifdef VERBOSE_DEBUG
#include "core/support/Debug.h"
static void printPerProviderTrackList( const PerProviderTrackList &providerTracks,
const QString *fromArtist = 0L )
{
foreach( ProviderPtr provider, providerTracks.keys() )
{
if( fromArtist )
debug() << provider->prettyName() << "tracks from" << *fromArtist;
else
debug() << provider->prettyName() << "tracks";
foreach( TrackPtr track, providerTracks.value( provider ) )
{
debug() << " " << track->artist() << "-" << track->album() << "-" << track->name();
}
}
}
#include "core/meta/support/MetaConstants.h"
static QString comparisonFieldNames( qint64 fields )
{
QStringList names;
for( qint64 value = 1; value < Meta::valCustom; value *= 2 )
{
if( value & fields )
{
names << Meta::i18nForField( value );
}
}
return names.join( ", " );
}
QDebug operator<<( QDebug dbg, const ProviderPtr &provider )
{
dbg.nospace() << "ProviderPtr(" << provider->prettyName() << ")";
return dbg.space();
}
QDebug operator<<( QDebug dbg, const TrackPtr &track )
{
dbg.nospace() << "TrackPtr(" << track->artist() << " - " << track->name() << ")";
return dbg.space();
}
#endif
qint64 MatchTracksJob::s_comparisonFields( 0 );
qint64
MatchTracksJob::comparisonFields()
{
return s_comparisonFields;
}
MatchTracksJob::MatchTracksJob( const ProviderPtrList &providers, QObject *parent )
: Job( parent )
, m_abort( false )
, m_providers( providers )
{
}
ProviderPtrList
MatchTracksJob::providers() const
{
return m_providers;
}
bool
MatchTracksJob::success() const
{
return !m_abort;
}
void
MatchTracksJob::abort()
{
m_abort = true;
}
// work-around macro vs. template argument clash in foreach
typedef QMultiMap<ProviderPtr, QString> ArtistProviders;
void MatchTracksJob::run()
{
const qint64 possibleFields = Meta::valTitle | Meta::valArtist | Meta::valAlbum |
Meta::valComposer | Meta::valYear | Meta::valTrackNr | Meta::valDiscNr;
const qint64 requiredFields = Meta::valTitle | Meta::valArtist | Meta::valAlbum;
s_comparisonFields = possibleFields;
// map of lowercase artist names to a list of providers that contain it plus their
// preferred representation of the artist name
QMap<QString, QMultiMap<ProviderPtr, QString> > providerArtists;
foreach( ProviderPtr provider, m_providers )
{
QSet<QString> artists = provider->artists();
foreach( const QString &artist, artists )
providerArtists[ artist.toLower() ].insert( provider, artist );
s_comparisonFields &= provider->reliableTrackMetaData();
}
Q_UNUSED( requiredFields ) // silence gcc warning about unused var in non-debug build
Q_ASSERT( ( s_comparisonFields & requiredFields ) == requiredFields );
emit totalSteps( providerArtists.size() );
#ifdef VERBOSE_DEBUG
debug() << "Matching using:" << comparisonFieldNames( s_comparisonFields ).toLocal8Bit().constData();
#endif
foreach( const ArtistProviders &artistProviders, providerArtists )
{
if( m_abort )
break;
matchTracksFromArtist( artistProviders );
emit incrementProgress();
}
emit endProgressOperation( this );
#ifdef VERBOSE_DEBUG
debug();
int tupleCount = m_matchedTuples.count();
debug() << "Found" << tupleCount << "tuples of matched tracks from multiple collections";
foreach( ProviderPtr provider, m_providers )
{
const TrackList uniqueList = m_uniqueTracks.value( provider );
const TrackList excludedList = m_excludedTracks.value( provider );
debug() << provider->prettyName() << "has" << uniqueList.count() << "unique tracks +"
<< excludedList.count() << "duplicate tracks +" << m_matchedTrackCounts[ provider ]
<< " matched =" << uniqueList.count() + excludedList.count() + m_matchedTrackCounts[ provider ];
}
#endif
}
void
MatchTracksJob::matchTracksFromArtist( const QMultiMap<ProviderPtr, QString> &providerArtists )
{
#ifdef VERBOSE_DEBUG
DEBUG_BLOCK
debug() << "providerArtists:" << providerArtists;
#endif
PerProviderTrackList providerTracks;
foreach( ProviderPtr provider, providerArtists.uniqueKeys() )
{
TrackList trackList;
foreach( const QString &artist, providerArtists.values( provider ) )
trackList << provider->artistTracks( artist );
if( trackList.isEmpty() )
continue; // don't add empty lists to providerTracks
// the sorting is important and makes our matching algorithm work
qSort( trackList.begin(), trackList.end(), trackDelegatePtrLessThan<MatchTracksJob> );
scanForScrobblableTracks( trackList );
providerTracks[ provider ] = trackList;
}
#ifdef VERBOSE_DEBUG
debug() << "providerTracks:" << providerTracks;
QScopedPointer<Debug::Block> debugBlockPointer;
if( providerTracks.keys().count() > 1 )
{
debugBlockPointer.reset( new Debug::Block( __PRETTY_FUNCTION__ ) );
printPerProviderTrackList( providerTracks );
}
#endif
// if only one (or less) non-empty provider is left, we're done
while( providerTracks.keys().count() > 1 )
{
TrackPtr firstTrack = findSmallestTrack( providerTracks );
PerProviderTrackList equalTracks = takeTracksEqualTo( firstTrack, providerTracks );
Q_ASSERT( !equalTracks.isEmpty() );
// optimization: continue early if there's only one provider left
if( equalTracks.keys().count() <= 1 )
{
ProviderPtr provider = equalTracks.keys().first();
m_uniqueTracks[ provider ].append( equalTracks[ provider ] );
continue;
}
#ifdef VERBOSE_DEBUG
debug();
debug() << "First track:" << firstTrack->artist() << "-" << firstTrack->album() << "-" << firstTrack->name();
debug() << "Tracks no greater than first track:";
printPerProviderTrackList( equalTracks );
#endif
TrackTuple matchedTuple;
foreach( ProviderPtr provider, equalTracks.keys() )
{
int listSize = equalTracks[ provider ].size();
Q_ASSERT( listSize >= 1 );
if( listSize == 1 )
matchedTuple.insert( provider, equalTracks[ provider ].at( 0 ) );
else
m_excludedTracks[ provider ].append( equalTracks[ provider ] );
}
if( matchedTuple.count() > 1 )
// good, we've found track that matches!
addMatchedTuple( matchedTuple );
else if( matchedTuple.count() == 1 )
{
// only one provider
ProviderPtr provider = matchedTuple.provider( 0 );
m_uniqueTracks[ provider ].append( matchedTuple.track( provider ) );
}
}
if( !providerTracks.isEmpty() ) // some tracks from one provider left
{
ProviderPtr provider = providerTracks.keys().first();
m_uniqueTracks[ provider ].append( providerTracks[ provider ] );
}
}
TrackPtr
MatchTracksJob::findSmallestTrack( const PerProviderTrackList &providerTracks )
{
TrackPtr smallest;
foreach( const TrackList &list, providerTracks )
{
if( !smallest || list.first()->lessThan( *smallest, s_comparisonFields ) )
smallest = list.first();
}
Q_ASSERT( smallest );
return smallest;
}
PerProviderTrackList
MatchTracksJob::takeTracksEqualTo( const TrackPtr &track,
PerProviderTrackList &providerTracks )
{
PerProviderTrackList ret;
foreach( ProviderPtr provider, providerTracks.keys() )
{
while( !providerTracks[ provider ].isEmpty() &&
track->equals( *providerTracks[ provider ].first(), s_comparisonFields ) )
{
ret[ provider ].append( providerTracks[ provider ].takeFirst() );
}
if( providerTracks[ provider ].isEmpty() )
providerTracks.remove( provider );
}
return ret;
}
void
MatchTracksJob::addMatchedTuple( const TrackTuple &tuple )
{
m_matchedTuples.append( tuple );
foreach( ProviderPtr provider, tuple.providers() )
{
m_matchedTrackCounts[ provider ]++;
}
}
void
MatchTracksJob::scanForScrobblableTracks( const TrackList &trackList )
{
foreach( const TrackPtr &track, trackList )
{
// ScrobblingServices take Meta::Track, ensure there is an underlying one
if( track->recentPlayCount() > 0 && track->metaTrack() )
m_tracksToScrobble << track;
}
}