File indexing completed on 2024-05-05 04:49:21

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 #include "TrackTuple.h"
0018 
0019 #include "MetaValues.h"
0020 #include "core/support/Debug.h"
0021 #include "statsyncing/Options.h"
0022 #include "statsyncing/Provider.h"
0023 
0024 Q_DECLARE_METATYPE(QSet<QString>)
0025 
0026 using namespace StatSyncing;
0027 
0028 const QList<qint64> TrackTuple::s_fields = QList<qint64>() << Meta::valRating
0029     << Meta::valFirstPlayed << Meta::valLastPlayed << Meta::valPlaycount << Meta::valLabel;
0030 
0031 TrackTuple::TrackTuple()
0032 {
0033 }
0034 
0035 void
0036 TrackTuple::insert( ProviderPtr provider, const TrackPtr &track )
0037 {
0038     m_map.insert( provider, track );
0039 }
0040 
0041 ProviderPtrList
0042 TrackTuple::providers() const
0043 {
0044     return m_map.keys();
0045 }
0046 
0047 ProviderPtr
0048 TrackTuple::provider( int i ) const
0049 {
0050     return m_map.keys().value( i );
0051 }
0052 
0053 TrackPtr
0054 TrackTuple::track( const ProviderPtr &provider ) const
0055 {
0056     Q_ASSERT( m_map.contains( provider ) );
0057     return m_map.value( provider );
0058 }
0059 
0060 int
0061 TrackTuple::count() const
0062 {
0063     return m_map.count();
0064 }
0065 
0066 bool
0067 TrackTuple::isEmpty() const
0068 {
0069     return m_map.isEmpty();
0070 }
0071 
0072 bool
0073 TrackTuple::fieldUpdated( qint64 field, const Options &options, ProviderPtr provider ) const
0074 {
0075     if( isEmpty() ||
0076         !(options.syncedFields() & field) ||
0077         ( provider && !m_map.contains( provider ) ) ||
0078         ( provider && !(provider->writableTrackStatsData() & field) ) )
0079     {
0080         return false;
0081     }
0082 
0083     switch( field )
0084     {
0085         case Meta::valRating:
0086         {
0087             int rating = syncedRating( options );
0088             if( rating < 0 )
0089                 return false; // unresolved conflict, not going to write that
0090             if( provider )
0091                 return track( provider )->rating() != rating;
0092 
0093             foreach( const ProviderPtr &prov, m_map.keys() )
0094             {
0095                 if( !(prov->writableTrackStatsData() & field ) )
0096                     continue; // this provider doesn't even know how to write this field
0097                 if( track( prov )->rating() != rating )
0098                     return true;
0099             }
0100             return false;
0101         }
0102 
0103         case Meta::valFirstPlayed:
0104         {
0105             QDateTime firstPlayed = syncedFirstPlayed( options );
0106             if( provider )
0107                 return track( provider )->firstPlayed() != firstPlayed;
0108 
0109             foreach( const ProviderPtr &prov, m_map.keys() )
0110             {
0111                 if( !(prov->writableTrackStatsData() & field ) )
0112                     continue; // this provider doesn't even know how to write this field
0113                 if( track( prov )->firstPlayed() != firstPlayed )
0114                     return true;
0115             }
0116             return false;
0117         }
0118 
0119         case Meta::valLastPlayed:
0120         {
0121             QDateTime lastPlayed = syncedLastPlayed( options );
0122             if( provider )
0123                 return track( provider )->lastPlayed() != lastPlayed;
0124 
0125             foreach( const ProviderPtr &prov, m_map.keys() )
0126             {
0127                 if( !(prov->writableTrackStatsData() & field ) )
0128                     continue; // this provider doesn't even know how to write this field
0129                 if( track( prov )->lastPlayed() != lastPlayed )
0130                     return true;
0131             }
0132             return false;
0133         }
0134 
0135         case Meta::valPlaycount:
0136         {
0137             int playcount = syncedPlaycount( options );
0138             if( provider )
0139                 return track( provider )->playCount() != playcount;
0140 
0141             foreach( const ProviderPtr &prov, m_map.keys() )
0142             {
0143                 if( !(prov->writableTrackStatsData() & field ) )
0144                     continue; // this provider doesn't even know how to write this field
0145                 if( track( prov )->playCount() != playcount )
0146                     return true;
0147             }
0148             return false;
0149         }
0150 
0151         case Meta::valLabel:
0152         {
0153             bool hasConflict = true;
0154             QSet<QString> labels = syncedLabels( options, m_labelProviders, hasConflict );
0155             if( hasConflict )
0156                 return false; // unresolved conflict, not going to write that
0157             if( provider )
0158                 return track( provider )->labels() - options.excludedLabels() != labels;
0159 
0160             foreach( const ProviderPtr &prov, m_map.keys() )
0161             {
0162                 if( !(prov->writableTrackStatsData() & field ) )
0163                     continue; // this provider doesn't even know how to write this field
0164                 if( track( prov )->labels() - options.excludedLabels() != labels )
0165                     return true;
0166             }
0167             return false;
0168         }
0169     }
0170     return false;
0171 }
0172 
0173 bool
0174 TrackTuple::hasUpdate( const Options &options ) const
0175 {
0176     foreach( qint64 field, s_fields )
0177     {
0178         if( fieldUpdated( field, options ) )
0179             return true;
0180     }
0181     return false;
0182 }
0183 
0184 bool
0185 TrackTuple::fieldHasConflict( qint64 field, const Options& options, bool includeResolved ) const
0186 {
0187     switch( field )
0188     {
0189         case Meta::valRating:
0190             // we must disregard currently selected rating provider for includeResolved = true
0191             return syncedRating( options, includeResolved ? ProviderPtr() : m_ratingProvider ) < 0;
0192         case Meta::valLabel:
0193         {
0194             bool hasConflict = false;
0195             // we must disregard currently selected label providers for includeResolved = true
0196             syncedLabels( options, includeResolved ? ProviderPtrSet() : m_labelProviders , hasConflict );
0197             return hasConflict;
0198         }
0199     }
0200     return false;
0201 }
0202 
0203 bool
0204 TrackTuple::hasConflict( const Options &options ) const
0205 {
0206     return fieldHasConflict( Meta::valRating, options )
0207         || fieldHasConflict( Meta::valLabel, options );
0208 }
0209 
0210 ProviderPtr
0211 TrackTuple::ratingProvider() const
0212 {
0213     return m_ratingProvider;
0214 }
0215 
0216 void
0217 TrackTuple::setRatingProvider( const ProviderPtr &provider )
0218 {
0219     if( !provider || m_map.contains( provider ) )
0220         m_ratingProvider = provider;
0221 }
0222 
0223 ProviderPtrSet
0224 TrackTuple::labelProviders() const
0225 {
0226     return m_labelProviders;
0227 }
0228 
0229 void
0230 TrackTuple::setLabelProviders( const ProviderPtrSet &providers )
0231 {
0232     m_labelProviders.clear();
0233     foreach( const ProviderPtr &provider, providers )
0234     {
0235         if( m_map.contains( provider ) )
0236             m_labelProviders.insert( provider );
0237     }
0238 }
0239 
0240 int
0241 TrackTuple::syncedRating( const Options &options ) const
0242 {
0243     return syncedRating( options, m_ratingProvider );
0244 }
0245 
0246 int
0247 TrackTuple::syncedRating( const Options &options, ProviderPtr ratingProvider ) const
0248 {
0249     if( isEmpty() || !(options.syncedFields() & Meta::valRating) )
0250         return 0;
0251     if( ratingProvider ) // a provider has been chosen
0252         return track( ratingProvider )->rating();
0253 
0254     // look for conflict:
0255     int candidate = -1; // rating candidate
0256     QMapIterator<ProviderPtr, TrackPtr> it( m_map );
0257     while( it.hasNext() )
0258     {
0259         it.next();
0260         int rating = it.value()->rating();
0261 
0262         // take rating candidate only from rated tracks or from rating-writable collections
0263         bool canWriteRating = it.key()->writableTrackStatsData() & Meta::valRating;
0264         if( candidate < 0 )
0265         {
0266             if( rating > 0 || canWriteRating )
0267                 candidate = rating;
0268             continue; // nothing to do in this loop iteration in either case
0269         }
0270 
0271         if( rating <= 0 && !canWriteRating )
0272             // skip unrated songs from colls with not-writable rating
0273             continue;
0274 
0275         if( rating != candidate )
0276             return -1;
0277     }
0278     // if candidate == -1, it means there are no colls with writable or non-zero rating
0279     return qMax( 0, candidate );
0280 }
0281 
0282 QDateTime
0283 TrackTuple::syncedFirstPlayed( const Options &options ) const
0284 {
0285     QDateTime first;
0286     if( isEmpty() || !(options.syncedFields() & Meta::valFirstPlayed) )
0287         return first;
0288     foreach( TrackPtr track, m_map )
0289     {
0290         QDateTime trackFirstPlayed = track->firstPlayed();
0291         if( !trackFirstPlayed.isValid() )
0292             continue;
0293         if( !first.isValid() || trackFirstPlayed < first )
0294             first = trackFirstPlayed;
0295     }
0296     return first;
0297 }
0298 
0299 QDateTime
0300 TrackTuple::syncedLastPlayed( const Options &options ) const
0301 {
0302     QDateTime last;
0303     if( isEmpty() || !(options.syncedFields() & Meta::valLastPlayed) )
0304         return last;
0305     foreach( TrackPtr track, m_map )
0306     {
0307         QDateTime trackLastPlayed = track->lastPlayed();
0308         if( !trackLastPlayed.isValid() )
0309             continue;
0310         if( !last.isValid() || trackLastPlayed > last )
0311             last = trackLastPlayed;
0312     }
0313     return last;
0314 }
0315 
0316 int
0317 TrackTuple::syncedPlaycount( const Options &options ) const
0318 {
0319     if( isEmpty() || !(options.syncedFields() & Meta::valPlaycount) )
0320         return 0;
0321     int max = 0;
0322     int sumRecent = 0;
0323     foreach( TrackPtr track, m_map )
0324     {
0325         int recent = track->recentPlayCount();
0326         sumRecent += recent;
0327         max = qMax( max, track->playCount() - recent );
0328     }
0329     return max + sumRecent;
0330 }
0331 
0332 QSet<QString>
0333 TrackTuple::syncedLabels( const Options &options ) const
0334 {
0335     bool dummy = false;
0336     return syncedLabels( options, m_labelProviders, dummy );
0337 }
0338 
0339 QSet<QString>
0340 TrackTuple::syncedLabels( const Options &options, const ProviderPtrSet &labelProviders, bool &hasConflict ) const
0341 {
0342     hasConflict = false;
0343     QSet<QString> labelsCandidate;
0344     if( isEmpty() || !(options.syncedFields() & Meta::valLabel) )
0345         return labelsCandidate;
0346     if( !labelProviders.isEmpty() ) // providers have been chosen
0347     {
0348         foreach( const ProviderPtr &provider, labelProviders )
0349             labelsCandidate |= track( provider )->labels();
0350         return labelsCandidate - options.excludedLabels();
0351     }
0352 
0353     // look for conflict:
0354     bool labelsCandidateAlreadySet = false;
0355     QMapIterator<ProviderPtr, TrackPtr> it( m_map );
0356     while( it.hasNext() )
0357     {
0358         it.next();
0359         QSet<QString> labels = it.value()->labels() - options.excludedLabels();
0360 
0361         // take labels candidate only from labelled tracks or from label-writable collections
0362         bool canWriteLabels = it.key()->writableTrackStatsData() & Meta::valLabel;
0363         if( !labelsCandidateAlreadySet )
0364         {
0365             if( !labels.isEmpty() || canWriteLabels )
0366             {
0367                 labelsCandidate = labels;
0368                 labelsCandidateAlreadySet = true;
0369             }
0370             continue; // nothing to do in this loop iteration in either case
0371         }
0372 
0373         if( labels.isEmpty() && !canWriteLabels )
0374             // skip unlabelled songs from colls with not-writable labels
0375             continue;
0376 
0377         if( labels != labelsCandidate )
0378         {
0379             hasConflict = true;
0380             return QSet<QString>();
0381         }
0382     }
0383     return labelsCandidate;
0384 }
0385 
0386 ProviderPtrSet
0387 TrackTuple::synchronize( const Options &options ) const
0388 {
0389     ProviderPtrSet updatedProviders;
0390     foreach( qint64 field, s_fields )
0391     {
0392         // catches if field should not be at all updated (either no change or not in options )
0393         if( !fieldUpdated( field, options ) )
0394             continue;
0395 
0396         QVariant synced;
0397         switch( field )
0398         {
0399             case Meta::valRating:
0400                 synced = syncedRating( options ); break;
0401             case Meta::valFirstPlayed:
0402                 synced = syncedFirstPlayed( options ); break;
0403             case Meta::valLastPlayed:
0404                 synced = syncedLastPlayed( options ); break;
0405             case Meta::valPlaycount:
0406                 synced = syncedPlaycount( options ); break;
0407             case Meta::valLabel:
0408                 synced.setValue<QSet<QString> >( syncedLabels( options ) ); break;
0409             default:
0410                 warning() << __PRETTY_FUNCTION__ << "unhandled first switch";
0411         }
0412 
0413         QMapIterator<ProviderPtr, TrackPtr> it( m_map );
0414         while( it.hasNext() )
0415         {
0416             it.next();
0417             ProviderPtr provider = it.key();
0418             // we have special case for playcount because it needs to we written even if
0419             // apparently unchanged to reset possible nonzero recentPlayCount
0420             if( field != Meta::valPlaycount && !fieldUpdated( field, options, provider ) )
0421                 continue; // nothing to do for this field and provider
0422 
0423             updatedProviders.insert( provider );
0424             TrackPtr track = it.value();
0425             switch( field )
0426             {
0427                 case Meta::valRating:
0428                     track->setRating( synced.toInt() ); break;
0429                 case Meta::valFirstPlayed:
0430                     track->setFirstPlayed( synced.toDateTime() ); break;
0431                 case Meta::valLastPlayed:
0432                     track->setLastPlayed( synced.toDateTime() ); break;
0433                 case Meta::valPlaycount:
0434                     track->setPlayCount( synced.toInt() ); break;
0435                 case Meta::valLabel:
0436                 {
0437                     QSet<QString> desiredLabels = synced.value<QSet<QString> >();
0438                     /* add back blacklisted labels; we say we don't touch them, so we
0439                      * should neither add them (handled in syncedLabels()) nor remove them
0440                      * (handled here)
0441                      */
0442                     desiredLabels |= track->labels() & options.excludedLabels();
0443                     track->setLabels( desiredLabels );
0444                     break;
0445                 }
0446                 default:
0447                     warning() << __PRETTY_FUNCTION__ << "unhandled second switch";
0448             }
0449         }
0450     }
0451 
0452     foreach( const ProviderPtr &provider, updatedProviders )
0453         track( provider )->commit();
0454     return updatedProviders;
0455 }