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

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 "SynchronizationTrack.h"
0018 
0019 #include "core/support/Debug.h"
0020 #include "core/support/SemaphoreReleaser.h"
0021 
0022 #include <QCoreApplication>
0023 #include <QNetworkReply>
0024 
0025 #include <Track.h>
0026 #include <XmlQuery.h>
0027 
0028 SynchronizationTrack::SynchronizationTrack( QString artist, QString album, QString name,
0029                                             int playCount, bool useFancyRatingTags )
0030     : m_artist( artist )
0031     , m_album( album )
0032     , m_name( name )
0033     , m_rating( 0 )
0034     , m_newRating( 0 )
0035     , m_playCount( playCount )
0036     , m_useFancyRatingTags( useFancyRatingTags )
0037 {
0038     // ensure this object is created in a main thread
0039     Q_ASSERT( thread() == QCoreApplication::instance()->thread() );
0040 
0041     connect( this, &SynchronizationTrack::startTagAddition,
0042              this, &SynchronizationTrack::slotStartTagAddition );
0043     connect( this, &SynchronizationTrack::startTagRemoval,
0044              this, &SynchronizationTrack::slotStartTagRemoval );
0045 }
0046 
0047 QString
0048 SynchronizationTrack::name() const
0049 {
0050     return m_name;
0051 }
0052 
0053 QString
0054 SynchronizationTrack::album() const
0055 {
0056     return m_album;
0057 }
0058 
0059 QString
0060 SynchronizationTrack::artist() const
0061 {
0062     return m_artist;
0063 }
0064 
0065 int
0066 SynchronizationTrack::rating() const
0067 {
0068     return m_rating;
0069 }
0070 
0071 void
0072 SynchronizationTrack::setRating( int rating )
0073 {
0074     m_newRating = rating;
0075 }
0076 
0077 QDateTime
0078 SynchronizationTrack::firstPlayed() const
0079 {
0080     return QDateTime(); // no support on Last.fm side yet
0081 }
0082 
0083 QDateTime
0084 SynchronizationTrack::lastPlayed() const
0085 {
0086     return QDateTime(); // no support on Last.fm side yet
0087 }
0088 
0089 int
0090 SynchronizationTrack::playCount() const
0091 {
0092     return m_playCount;
0093 }
0094 
0095 QSet<QString>
0096 SynchronizationTrack::labels() const
0097 {
0098     return m_labels;
0099 }
0100 
0101 void
0102 SynchronizationTrack::setLabels( const QSet<QString> &labels )
0103 {
0104     m_newLabels = labels;
0105 }
0106 
0107 void
0108 SynchronizationTrack::commit()
0109 {
0110     if( m_newRating == m_rating && m_newLabels == m_labels )
0111         return;
0112 
0113     const QSet<QString> existingLabels = m_labels | m_ratingLabels;
0114     if( m_useFancyRatingTags )
0115     {
0116         // implicitly we remove all ratingLabels here by not including them in m_newLabels
0117         if( m_newRating > 0 )
0118         {
0119             QString ratingLabel = QString( "%1 of 10 stars" ).arg( m_newRating );
0120             m_newLabels.insert( ratingLabel );
0121             m_ratingLabels = QSet<QString>() << ratingLabel;
0122         }
0123     }
0124     else
0125         m_newLabels |= m_ratingLabels; // preserve all rating labels
0126 
0127     QSet<QString> toAdd = m_newLabels - existingLabels;
0128     QSet<QString> toRemove = existingLabels - m_newLabels;
0129 
0130     // first remove, than add Last.fm may limit number of track tags
0131     if( !toRemove.isEmpty() )
0132     {
0133         Q_ASSERT( m_semaphore.available() == 0 );
0134         m_tagsToRemove = toRemove.values();
0135         emit startTagRemoval();
0136         m_semaphore.acquire(); // wait for the job to complete
0137         m_tagsToRemove.clear();
0138     }
0139     if( !toAdd.isEmpty() )
0140     {
0141         Q_ASSERT( m_semaphore.available() == 0 );
0142         emit startTagAddition( toAdd.values() );
0143         m_semaphore.acquire(); // wait for the job to complete
0144     }
0145 
0146     m_rating = m_newRating;
0147     m_labels = m_newLabels - m_ratingLabels;
0148 }
0149 
0150 void
0151 SynchronizationTrack::parseAndSaveLastFmTags( const QSet<QString> &tags )
0152 {
0153     m_labels.clear();
0154     m_ratingLabels.clear();
0155     m_rating = 0;
0156 
0157     // we still match and explicitly ignore rating tags even in m_useFancyRatingTags is false
0158     QRegExp rx( "([0-9]{1,3}) of ([0-9]{1,3}) stars", Qt::CaseInsensitive );
0159     foreach( const QString &tag, tags )
0160     {
0161         if( rx.exactMatch( tag ) )
0162         {
0163             m_ratingLabels.insert( tag );
0164             QStringList texts = rx.capturedTexts();
0165             if( texts.count() != 3 )
0166                 continue;
0167             qreal numerator = texts.at( 1 ).toDouble();
0168             qreal denominator = texts.at( 2 ).toDouble();
0169             if( denominator == 0.0 )
0170                 continue;
0171             m_rating = qBound( 0, qRound( 10.0 * numerator / denominator ), 10 );
0172         }
0173         else
0174             m_labels.insert( tag );
0175     }
0176     if( !m_useFancyRatingTags || m_ratingLabels.count() > 1 )
0177         m_rating = 0; // ambiguous or not requested
0178 
0179     m_newLabels = m_labels;
0180     m_newRating = m_rating;
0181 }
0182 
0183 void
0184 SynchronizationTrack::slotStartTagAddition( QStringList tags )
0185 {
0186     lastfm::MutableTrack track;
0187     track.setArtist( m_artist );
0188     track.setAlbum( m_album );
0189     track.setTitle( m_name );
0190 
0191     if( tags.count() > 10 )
0192         tags = tags.mid( 0, 10 ); // Last.fm says 10 tags is max
0193     QNetworkReply *reply = track.addTags( tags );
0194     connect( reply, &QNetworkReply::finished, this, &SynchronizationTrack::slotTagsAdded );
0195 }
0196 
0197 void
0198 SynchronizationTrack::slotStartTagRemoval()
0199 {
0200     Q_ASSERT( m_tagsToRemove.count() );
0201     lastfm::MutableTrack track;
0202     track.setArtist( m_artist );
0203     track.setAlbum( m_album );
0204     track.setTitle( m_name );
0205 
0206     QNetworkReply *reply = track.removeTag( m_tagsToRemove.takeFirst() );
0207     connect( reply, &QNetworkReply::finished, this, &SynchronizationTrack::slotTagRemoved );
0208 }
0209 
0210 void
0211 SynchronizationTrack::slotTagsAdded()
0212 {
0213     SemaphoreReleaser releaser( &m_semaphore );
0214     QNetworkReply *reply =  qobject_cast<QNetworkReply *>( sender() );
0215     if( !reply )
0216     {
0217         warning() << __PRETTY_FUNCTION__ << "cannot cast sender to QNetworkReply. (?)";
0218         return;
0219     }
0220     reply->deleteLater();
0221 
0222     lastfm::XmlQuery lfm;
0223     if( !lfm.parse( reply->readAll() ) )
0224     {
0225         warning() << __PRETTY_FUNCTION__ << "error adding tags:" << lfm.parseError().message();
0226         return;
0227     }
0228 }
0229 
0230 void
0231 SynchronizationTrack::slotTagRemoved()
0232 {
0233     SemaphoreReleaser releaser( &m_semaphore );
0234     QNetworkReply *reply =  qobject_cast<QNetworkReply *>( sender() );
0235     if( !reply )
0236     {
0237         warning() << __PRETTY_FUNCTION__ << "cannot cast sender to QNetworkReply. (?)";
0238         return;
0239     }
0240     reply->deleteLater();
0241 
0242     lastfm::XmlQuery lfm;
0243     if( !lfm.parse( reply->readAll() ) )
0244     {
0245         warning() << __PRETTY_FUNCTION__ << "error removing a tag:" << lfm.parseError().message();
0246         return;
0247     }
0248 
0249     // remove the next one, sadly only one at a time can be removed
0250     if( !m_tagsToRemove.isEmpty() )
0251     {
0252         releaser.dontRelease();
0253         emit startTagRemoval();
0254     }
0255 }