File indexing completed on 2024-05-19 04:50:28

0001 /****************************************************************************************
0002  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #define DEBUG_PREFIX "StatSyncing"
0018 
0019 #include "MatchTracksJob.h"
0020 
0021 #include "MetaValues.h"
0022 #include "core/meta/Meta.h"
0023 
0024 #include <algorithm>
0025 
0026 using namespace StatSyncing;
0027 
0028 #undef VERBOSE_DEBUG
0029 #ifdef VERBOSE_DEBUG
0030 #include "core/support/Debug.h"
0031 static void printPerProviderTrackList( const PerProviderTrackList &providerTracks,
0032                                        const QString *fromArtist = 0L )
0033 {
0034     foreach( ProviderPtr provider, providerTracks.keys() )
0035     {
0036         if( fromArtist )
0037             debug() << provider->prettyName() << "tracks from" << *fromArtist;
0038         else
0039             debug() << provider->prettyName() << "tracks";
0040         foreach( TrackPtr track, providerTracks.value( provider ) )
0041         {
0042             debug() << "  " << track->artist() << "-" << track->album() << "-" << track->name();
0043         }
0044     }
0045 }
0046 
0047 #include "core/meta/support/MetaConstants.h"
0048 static QString comparisonFieldNames( qint64 fields )
0049 {
0050     QStringList names;
0051     for( qint64 value = 1; value < Meta::valCustom; value *= 2 )
0052     {
0053         if( value & fields )
0054         {
0055             names << Meta::i18nForField( value );
0056         }
0057     }
0058     return names.join( ", " );
0059 }
0060 
0061 QDebug operator<<( QDebug dbg, const ProviderPtr &provider )
0062 {
0063     dbg.nospace() << "ProviderPtr(" << provider->prettyName() << ")";
0064     return dbg.space();
0065 }
0066 
0067 QDebug operator<<( QDebug dbg, const TrackPtr &track )
0068 {
0069     dbg.nospace() << "TrackPtr(" << track->artist() << " - " << track->name() << ")";
0070     return dbg.space();
0071 }
0072 #endif
0073 
0074 qint64 MatchTracksJob::s_comparisonFields( 0 );
0075 
0076 qint64
0077 MatchTracksJob::comparisonFields()
0078 {
0079     return s_comparisonFields;
0080 }
0081 
0082 MatchTracksJob::MatchTracksJob( const ProviderPtrList &providers, QObject *parent )
0083     : QObject(parent)
0084     , ThreadWeaver::Job()
0085     , m_abort( false )
0086     , m_providers( providers )
0087 {
0088 }
0089 
0090 ProviderPtrList
0091 MatchTracksJob::providers() const
0092 {
0093     return m_providers;
0094 }
0095 
0096 bool
0097 MatchTracksJob::success() const
0098 {
0099     return !m_abort;
0100 }
0101 
0102 void
0103 MatchTracksJob::abort()
0104 {
0105     m_abort = true;
0106 }
0107 
0108 // work-around macro vs. template argument clash in foreach
0109 typedef QMultiMap<ProviderPtr, QString> ArtistProviders;
0110 
0111 void MatchTracksJob::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread)
0112 {
0113     Q_UNUSED(self);
0114     Q_UNUSED(thread);
0115     const qint64 possibleFields = Meta::valTitle | Meta::valArtist | Meta::valAlbum |
0116         Meta::valComposer | Meta::valYear | Meta::valTrackNr | Meta::valDiscNr;
0117     const qint64 requiredFields = Meta::valTitle | Meta::valArtist | Meta::valAlbum;
0118     s_comparisonFields = possibleFields;
0119 
0120     // map of lowercase artist names to a list of providers that contain it plus their
0121     // preferred representation of the artist name
0122     QMap<QString, QMultiMap<ProviderPtr, QString> > providerArtists;
0123     foreach( ProviderPtr provider, m_providers )
0124     {
0125         QSet<QString> artists = provider->artists();
0126         foreach( const QString &artist, artists )
0127             providerArtists[ artist.toLower() ].insert( provider, artist );
0128         s_comparisonFields &= provider->reliableTrackMetaData();
0129     }
0130     Q_UNUSED( requiredFields ) // silence gcc warning about unused var in non-debug build
0131     Q_ASSERT( ( s_comparisonFields & requiredFields ) == requiredFields );
0132     Q_EMIT totalSteps( providerArtists.size() );
0133 #ifdef VERBOSE_DEBUG
0134     debug() << "Matching using:" << comparisonFieldNames( s_comparisonFields ).toLocal8Bit().constData();
0135 #endif
0136 
0137     foreach( const ArtistProviders &artistProviders, providerArtists )
0138     {
0139         if( m_abort )
0140             break;
0141         matchTracksFromArtist( artistProviders );
0142         Q_EMIT incrementProgress();
0143     }
0144     Q_EMIT endProgressOperation( this );
0145 
0146 #ifdef VERBOSE_DEBUG
0147     debug();
0148     int tupleCount = m_matchedTuples.count();
0149     debug() << "Found" << tupleCount << "tuples of matched tracks from multiple collections";
0150     foreach( ProviderPtr provider, m_providers )
0151     {
0152         const TrackList uniqueList = m_uniqueTracks.value( provider );
0153         const TrackList excludedList = m_excludedTracks.value( provider );
0154         debug() << provider->prettyName() << "has" << uniqueList.count() << "unique tracks +"
0155                 << excludedList.count() << "duplicate tracks +" << m_matchedTrackCounts[ provider ]
0156                 << " matched =" << uniqueList.count() + excludedList.count() + m_matchedTrackCounts[ provider ];
0157     }
0158 #endif
0159 }
0160 
0161 void MatchTracksJob::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread)
0162 {
0163     Q_EMIT started(self);
0164     ThreadWeaver::Job::defaultBegin(self, thread);
0165 }
0166 
0167 void MatchTracksJob::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread)
0168 {
0169     ThreadWeaver::Job::defaultEnd(self, thread);
0170     if (!self->success()) {
0171         Q_EMIT failed(self);
0172     }
0173     Q_EMIT done(self);
0174 }
0175 
0176 void
0177 MatchTracksJob::matchTracksFromArtist( const QMultiMap<ProviderPtr, QString> &providerArtists )
0178 {
0179 #ifdef VERBOSE_DEBUG
0180     DEBUG_BLOCK
0181     debug() << "providerArtists:" << providerArtists;
0182 #endif
0183     PerProviderTrackList providerTracks;
0184     foreach( ProviderPtr provider, providerArtists.uniqueKeys() )
0185     {
0186         TrackList trackList;
0187         foreach( const QString &artist, providerArtists.values( provider ) )
0188             trackList << provider->artistTracks( artist );
0189         if( trackList.isEmpty() )
0190             continue;  // don't add empty lists to providerTracks
0191         // the sorting is important and makes our matching algorithm work
0192         std::sort( trackList.begin(), trackList.end(), trackDelegatePtrLessThan<MatchTracksJob> );
0193 
0194         scanForScrobblableTracks( trackList );
0195         providerTracks[ provider ] = trackList;
0196     }
0197 
0198 #ifdef VERBOSE_DEBUG
0199     debug() << "providerTracks:" << providerTracks;
0200     QScopedPointer<Debug::Block> debugBlockPointer;
0201     if( providerTracks.keys().count() > 1 )
0202     {
0203         debugBlockPointer.reset( new Debug::Block( __PRETTY_FUNCTION__ ) );
0204         printPerProviderTrackList( providerTracks );
0205     }
0206 #endif
0207 
0208     // if only one (or less) non-empty provider is left, we're done
0209     while( providerTracks.keys().count() > 1 )
0210     {
0211         TrackPtr firstTrack = findSmallestTrack( providerTracks );
0212         PerProviderTrackList equalTracks = takeTracksEqualTo( firstTrack, providerTracks );
0213         Q_ASSERT( !equalTracks.isEmpty() );
0214 
0215         // optimization: continue early if there's only one provider left
0216         if( equalTracks.keys().count() <= 1 )
0217         {
0218             ProviderPtr provider = equalTracks.keys().first();
0219             m_uniqueTracks[ provider ].append( equalTracks[ provider ] );
0220             continue;
0221         }
0222 
0223 #ifdef VERBOSE_DEBUG
0224         debug();
0225         debug() << "First track:" << firstTrack->artist() << "-" << firstTrack->album() << "-" << firstTrack->name();
0226         debug() << "Tracks no greater than first track:";
0227         printPerProviderTrackList( equalTracks );
0228 #endif
0229 
0230         TrackTuple matchedTuple;
0231         foreach( ProviderPtr provider, equalTracks.keys() )
0232         {
0233             int listSize = equalTracks[ provider ].size();
0234             Q_ASSERT( listSize >= 1 );
0235             if( listSize == 1 )
0236                 matchedTuple.insert( provider, equalTracks[ provider ].at( 0 ) );
0237             else
0238                 m_excludedTracks[ provider ].append( equalTracks[ provider ] );
0239         }
0240 
0241         if( matchedTuple.count() > 1 )
0242             // good, we've found track that matches!
0243             addMatchedTuple( matchedTuple );
0244         else if( matchedTuple.count() == 1 )
0245         {
0246             // only one provider
0247             ProviderPtr provider = matchedTuple.provider( 0 );
0248             m_uniqueTracks[ provider ].append( matchedTuple.track( provider ) );
0249         }
0250     }
0251 
0252     if( !providerTracks.isEmpty() ) // some tracks from one provider left
0253     {
0254         ProviderPtr provider = providerTracks.keys().first();
0255         m_uniqueTracks[ provider ].append( providerTracks[ provider ] );
0256     }
0257 }
0258 
0259 TrackPtr
0260 MatchTracksJob::findSmallestTrack( const PerProviderTrackList &providerTracks )
0261 {
0262     TrackPtr smallest;
0263     foreach( const TrackList &list, providerTracks )
0264     {
0265         if( !smallest || list.first()->lessThan( *smallest, s_comparisonFields ) )
0266             smallest = list.first();
0267     }
0268     Q_ASSERT( smallest );
0269     return smallest;
0270 }
0271 
0272 PerProviderTrackList
0273 MatchTracksJob::takeTracksEqualTo( const TrackPtr &track,
0274                                    PerProviderTrackList &providerTracks )
0275 {
0276     PerProviderTrackList ret;
0277     foreach( ProviderPtr provider, providerTracks.keys() )
0278     {
0279         while( !providerTracks[ provider ].isEmpty() &&
0280                track->equals( *providerTracks[ provider ].first(), s_comparisonFields ) )
0281         {
0282             ret[ provider ].append( providerTracks[ provider ].takeFirst() );
0283         }
0284         if( providerTracks[ provider ].isEmpty() )
0285             providerTracks.remove( provider );
0286     }
0287     return ret;
0288 }
0289 
0290 void
0291 MatchTracksJob::addMatchedTuple( const TrackTuple &tuple )
0292 {
0293     m_matchedTuples.append( tuple );
0294     foreach( ProviderPtr provider, tuple.providers() )
0295     {
0296         m_matchedTrackCounts[ provider ]++;
0297     }
0298 }
0299 
0300 void
0301 MatchTracksJob::scanForScrobblableTracks( const TrackList &trackList )
0302 {
0303     foreach( const TrackPtr &track, trackList )
0304     {
0305         // ScrobblingServices take Meta::Track, ensure there is an underlying one
0306         if( track->recentPlayCount() > 0 && track->metaTrack() )
0307             m_tracksToScrobble << track;
0308     }
0309 }