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 }