diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
commit | 3ce9174229de91411a9abf5381a1f335fe0c6a98 (patch) | |
tree | 84b2736fa1b0d3fbf9c60fc04f510d2a13916b09 /amarok/src/scrobbler.cpp | |
download | amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.tar.gz amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.zip |
Added abandoned KDE3 version of Amarok
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/amarok@1072335 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'amarok/src/scrobbler.cpp')
-rw-r--r-- | amarok/src/scrobbler.cpp | 1185 |
1 files changed, 1185 insertions, 0 deletions
diff --git a/amarok/src/scrobbler.cpp b/amarok/src/scrobbler.cpp new file mode 100644 index 00000000..61837e55 --- /dev/null +++ b/amarok/src/scrobbler.cpp @@ -0,0 +1,1185 @@ +// (c) 2004 Christian Muehlhaeuser <chris@chris.de> +// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi> +// (c) 2006 Shane King <kde@dontletsstart.com> +// (c) 2006 Iain Benson <iain@arctos.me.uk> +// (c) 2006 Alexandre Oliveira <aleprj@gmail.com> +// (c) 2006 Andy Kelk <andy@mopoke.co.uk> +// See COPYING file for licensing information. + +#define DEBUG_PREFIX "Scrobbler" + +#include "amarok.h" +#include "amarokconfig.h" +#include "collectiondb.h" +#include "config.h" +#include "debug.h" +#include "enginecontroller.h" +#include "playlist.h" +#include "scrobbler.h" +#include "statusbar.h" + +#include <unistd.h> + +#include <qdatetime.h> +#include <qdeepcopy.h> + +#include <kapplication.h> +#include <kio/job.h> +#include <kio/jobclasses.h> +#include <klocale.h> +#include <kmdcodec.h> +#include <kstandarddirs.h> +#include <kurl.h> + +//some setups require this +#undef PROTOCOL_VERSION + + +//////////////////////////////////////////////////////////////////////////////// +// CLASS Scrobbler +//////////////////////////////////////////////////////////////////////////////// + +Scrobbler* Scrobbler::instance() +{ + static Scrobbler scrobbler; + return &scrobbler; +} + + +Scrobbler::Scrobbler() + : EngineObserver( EngineController::instance() ) + , m_similarArtistsJob( 0 ) + , m_validForSending( false ) + , m_startPos( 0 ) + , m_submitter( new ScrobblerSubmitter() ) + , m_item( new SubmitItem() ) +{} + + +Scrobbler::~Scrobbler() +{ + delete m_item; + delete m_submitter; +} + + +/** + * Queries similar artists from Audioscrobbler. + */ +void Scrobbler::similarArtists( const QString & artist ) +{ + QString safeArtist = QDeepCopy<QString>( artist ); + if ( AmarokConfig::retrieveSimilarArtists() ) + { +// Request looks like this: +// http://ws.audioscrobbler.com/1.0/artist/Metallica/similar.xml + + m_similarArtistsBuffer = QByteArray(); + m_artist = artist; + + m_similarArtistsJob = KIO::get( "http://ws.audioscrobbler.com/1.0/artist/" + safeArtist + "/similar.xml", false, false ); + + connect( m_similarArtistsJob, SIGNAL( result( KIO::Job* ) ), + this, SLOT( audioScrobblerSimilarArtistsResult( KIO::Job* ) ) ); + connect( m_similarArtistsJob, SIGNAL( data( KIO::Job*, const QByteArray& ) ), + this, SLOT( audioScrobblerSimilarArtistsData( KIO::Job*, const QByteArray& ) ) ); + } +} + + +/** + * Called when the similar artists TransferJob finishes. + */ +void Scrobbler::audioScrobblerSimilarArtistsResult( KIO::Job* job ) //SLOT +{ + if ( m_similarArtistsJob != job ) + return; //not the right job, so let's ignore it + + if ( job->error() ) + { + warning() << "KIO error! errno: " << job->error() << endl; + return; + } + +// Result looks like this: +// <?xml version="1.0" encoding="UTF-8"?> +// <similarartists artist="Metallica" streamable="1" picture="http://static.last.fm/proposedimages/sidebar/6/1000024/288059.jpg" mbid=""> +// <artist> +// <name>Iron Maiden</name> +// <mbid></mbid> +// <match>100</match> +// <url>http://www.last.fm/music/Iron+Maiden</url> +// <image_small>http://static.last.fm/proposedimages/thumbnail/6/1000107/264195.jpg</image_small> +// <image>http://static.last.fm/proposedimages/sidebar/6/1000107/264195.jpg</image> +// <streamable>1</streamable> +// </artist> +// </similarartists> + + QDomDocument document; + if ( !document.setContent( m_similarArtistsBuffer ) ) + { + debug() << "Couldn't read similar artists response" << endl; + return; + } + + QDomNodeList values = document.elementsByTagName( "similarartists" ) + .item( 0 ).childNodes(); + + QStringList suggestions; + for ( uint i = 0; i < values.count() && i < 30; i++ ) // limit to top 30 artists + suggestions << values.item( i ).namedItem( "name" ).toElement().text(); + + debug() << "Suggestions retrieved (" << suggestions.count() << ")" << endl; + if ( !suggestions.isEmpty() ) + emit similarArtistsFetched( m_artist, suggestions ); + + m_similarArtistsJob = 0; +} + + +/** + * Called when similar artists data is received for the TransferJob. + */ +void Scrobbler::audioScrobblerSimilarArtistsData( KIO::Job* job, const QByteArray& data ) //SLOT +{ + if ( m_similarArtistsJob != job ) + return; //not the right job, so let's ignore it + + uint oldSize = m_similarArtistsBuffer.size(); + m_similarArtistsBuffer.resize( oldSize + data.size() ); + memcpy( m_similarArtistsBuffer.data() + oldSize, data.data(), data.size() ); +} + + +/** + * Called when the signal is received. + */ +void Scrobbler::engineNewMetaData( const MetaBundle& bundle, bool trackChanged ) +{ + //debug() << "engineNewMetaData: " << bundle.artist() << ":" << bundle.album() << ":" << bundle.title() << ":" << trackChanged << endl; + if ( !trackChanged ) + { + debug() << "It's still the same track." << endl; + m_item->setArtist( bundle.artist() ); + m_item->setAlbum( bundle.album() ); + m_item->setTitle( bundle.title() ); + return; + } + + //to work around xine bug, we have to explictly prevent submission the first few seconds of a track + //http://sourceforge.net/tracker/index.php?func=detail&aid=1401026&group_id=9655&atid=109655 + m_timer.stop(); + m_timer.start( 10000, true ); + + m_startPos = 0; + + // Plugins must not submit tracks played from online radio stations, even + // if they appear to be providing correct metadata. + if ( !bundle.streamUrl().isEmpty() ) + { + debug() << "Won't submit: It's a stream." << endl; + m_validForSending = false; + } + else if( bundle.podcastBundle() != NULL ) + { + debug() << "Won't submit: It's a podcast." << endl; + m_validForSending = false; + } + else + { + *m_item = SubmitItem( bundle.artist(), bundle.album(), bundle.title(), bundle.length() ); + m_validForSending = true; // check length etc later + } +} + + +/** + * Called when cue file detects track change + */ +void Scrobbler::subTrack( long currentPos, long startPos, long endPos ) +{ + //debug() << "subTrack: " << currentPos << ":" << startPos << ":" << endPos << endl; + *m_item = SubmitItem( m_item->artist(), m_item->album(), m_item->title(), endPos - startPos ); + if ( currentPos <= startPos + 2 ) // only submit if starting from the start of the track (need to allow 2 second difference for rounding/delay) + { + m_startPos = startPos * 1000; + m_validForSending = true; + } + else + { + debug() << "Won't submit: Detected cuefile jump to " << currentPos - startPos << " seconds into track." << endl; + m_validForSending = false; + } +} + + +/** + * Called when the signal is received. + */ +void Scrobbler::engineTrackPositionChanged( long position, bool userSeek ) +{ + //debug() << "engineTrackPositionChanged: " << position << ":" << userSeek << endl; + if ( !m_validForSending ) + return; + + if ( userSeek ) + { + m_validForSending = false; + debug() << "Won't submit: Seek detected." << endl; + return; + } + + if ( m_timer.isActive() ) + return; + + // Each track must be submitted to the server when it is 50% or 240 + // seconds complete, whichever comes first. + if ( position - m_startPos > 240 * 1000 || position - m_startPos > 0.5 * m_item->length() * 1000 ) + { + if ( m_item->valid() ) + m_submitter->submitItem( new SubmitItem( *m_item ) ); + else + debug() << "Won't submit: No artist, no title, or less than 30 seconds." << endl; + m_validForSending = false; + } +} + + +/** + * Applies settings from the config dialog. + */ +void Scrobbler::applySettings() +{ + m_submitter->configure( AmarokConfig::scrobblerUsername(), AmarokConfig::scrobblerPassword(), AmarokConfig::submitPlayedSongs() ); +} + + +//////////////////////////////////////////////////////////////////////////////// +// CLASS SubmitItem +//////////////////////////////////////////////////////////////////////////////// + + +SubmitItem::SubmitItem( + const QString& artist, + const QString& album, + const QString& title, + int length, + bool now) +{ + m_artist = artist; + m_album = album; + m_title = title; + m_length = length; + m_playStartTime = now ? QDateTime::currentDateTime( Qt::UTC ).toTime_t() : 0; +} + + +SubmitItem::SubmitItem( const QDomElement& element ) +{ + m_artist = element.namedItem( "artist" ).toElement().text(); + m_album = element.namedItem( "album" ).toElement().text(); + m_title = element.namedItem( "title" ).toElement().text(); + m_length = element.namedItem( "length" ).toElement().text().toInt(); + m_playStartTime = element.namedItem( "playtime" ).toElement().text().toUInt(); +} + + +SubmitItem::SubmitItem() + : m_length( 0 ) + , m_playStartTime( 0 ) +{ +} + + +bool SubmitItem::operator==( const SubmitItem& item ) +{ + bool result = true; + + if ( m_artist != item.artist() || m_album != item.album() || m_title != item.title() || + m_length != item.length() || m_playStartTime != item.playStartTime() ) + { + result = false; + } + + return result; +} + + +QDomElement SubmitItem::toDomElement( QDomDocument& document ) const +{ + QDomElement item = document.createElement( "item" ); + // TODO: In the future, it might be good to store url too + //item.setAttribute("url", item->url().url()); + + QDomElement artist = document.createElement( "artist" ); + QDomText artistText = document.createTextNode( m_artist ); + artist.appendChild( artistText ); + item.appendChild( artist ); + + QDomElement album = document.createElement( "album" ); + QDomText albumText = document.createTextNode( m_album ); + album.appendChild( albumText ); + item.appendChild( album ); + + QDomElement title = document.createElement( "title" ); + QDomText titleText = document.createTextNode( m_title ); + title.appendChild( titleText ); + item.appendChild( title ); + + QDomElement length = document.createElement( "length" ); + QDomText lengthText = document.createTextNode( QString::number( m_length ) ); + length.appendChild( lengthText ); + item.appendChild( length ); + + QDomElement playtime = document.createElement( "playtime" ); + QDomText playtimeText = document.createTextNode( QString::number( m_playStartTime ) ); + playtime.appendChild( playtimeText ); + item.appendChild( playtime ); + + return item; +} + + +//////////////////////////////////////////////////////////////////////////////// +// CLASS SubmitQueue +//////////////////////////////////////////////////////////////////////////////// + + +int SubmitQueue::compareItems( QPtrCollection::Item item1, QPtrCollection::Item item2 ) +{ + SubmitItem *sItem1 = static_cast<SubmitItem*>( item1 ); + SubmitItem *sItem2 = static_cast<SubmitItem*>( item2 ); + int result; + + if ( sItem1 == sItem2 ) + { + result = 0; + } + else if ( sItem1->playStartTime() > sItem2->playStartTime() ) + { + result = 1; + } + else + { + result = -1; + } + + return result; +} + + +//////////////////////////////////////////////////////////////////////////////// +// CLASS ScrobblerSubmitter +//////////////////////////////////////////////////////////////////////////////// + +QString ScrobblerSubmitter::PROTOCOL_VERSION = "1.1"; +QString ScrobblerSubmitter::CLIENT_ID = "ark"; +QString ScrobblerSubmitter::CLIENT_VERSION = "1.4"; +QString ScrobblerSubmitter::HANDSHAKE_URL = "http://post.audioscrobbler.com/?hs=true"; + + +ScrobblerSubmitter::ScrobblerSubmitter() + : m_username( 0 ) + , m_password( 0 ) + , m_submitUrl( 0 ) + , m_challenge( 0 ) + , m_scrobblerEnabled( false ) + , m_holdFakeQueue( false ) + , m_inProgress( false ) + , m_needHandshake( true ) + , m_prevSubmitTime( 0 ) + , m_interval( 0 ) + , m_backoff( 0 ) + , m_lastSubmissionFinishTime( 0 ) + , m_fakeQueueLength( 0 ) +{ + connect( &m_timer, SIGNAL(timeout()), this, SLOT(scheduledTimeReached()) ); + readSubmitQueue(); +} + + +ScrobblerSubmitter::~ScrobblerSubmitter() +{ + // need to rescue current submit. This may meant it gets submitted twice, + // but last.fm handles that, and it's better than losing it when you quit + // while a submit is happening + for ( QPtrDictIterator<SubmitItem> it( m_ongoingSubmits ); it.current(); ++it ) + m_submitQueue.inSort( it.current() ); + m_ongoingSubmits.clear(); + + saveSubmitQueue(); + + m_submitQueue.setAutoDelete( true ); + m_submitQueue.clear(); + m_fakeQueue.setAutoDelete( true ); + m_fakeQueue.clear(); +} + + +/** + * Performs handshake with Audioscrobbler. + */ +void ScrobblerSubmitter::performHandshake() +{ + QString handshakeUrl = QString::null; + uint currentTime = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + + if ( PROTOCOL_VERSION == "1.1" ) + { + // Audioscrobbler protocol 1.1 (current) + // http://post.audioscrobbler.com/?hs=true + // &p=1.1 + // &c=<clientid> + // &v=<clientver> + // &u=<user> + handshakeUrl = + HANDSHAKE_URL + + QString( + "&p=%1" + "&c=%2" + "&v=%3" + "&u=%4" ) + .arg( PROTOCOL_VERSION ) + .arg( CLIENT_ID ) + .arg( CLIENT_VERSION ) + .arg( m_username ); + } + + else if ( PROTOCOL_VERSION == "1.2" ) + { + // Audioscrobbler protocol 1.2 (RFC) + // http://post.audioscrobbler.com/?hs=true + // &p=1.2 + // &c=<clientid> + // &v=<clientversion> + // &u=<username> + // &t=<unix_timestamp> + // &a=<passcode> + handshakeUrl = + HANDSHAKE_URL + + QString( + "&p=%1" + "&c=%2" + "&v=%3" + "&u=%4" + "&t=%5" + "&a=%6" ) + .arg( PROTOCOL_VERSION ) + .arg( CLIENT_ID ) + .arg( CLIENT_VERSION ) + .arg( m_username ) + .arg( currentTime ) + .arg( KMD5( KMD5( m_password.utf8() ).hexDigest() + + currentTime ).hexDigest() ); + } + + else + { + debug() << "Handshake not implemented for protocol version: " << PROTOCOL_VERSION << endl; + return; + } + + debug() << "Handshake url: " << handshakeUrl << endl; + + m_submitResultBuffer = ""; + + m_inProgress = true; + KIO::TransferJob* job = KIO::storedGet( handshakeUrl, false, false ); + connect( job, SIGNAL( result( KIO::Job* ) ), SLOT( audioScrobblerHandshakeResult( KIO::Job* ) ) ); +} + + +/** + * Sets item for submission to Audioscrobbler. Actual submission + * depends on things like (is scrobbling enabled, are Audioscrobbler + * profile details filled in etc). + */ +void ScrobblerSubmitter::submitItem( SubmitItem* item ) +{ + if ( m_scrobblerEnabled ) { + enqueueItem( item ); + + if ( item->playStartTime() == 0 ) + m_holdFakeQueue = true; // hold on to fake queue until we get it all and can compute when to submit + else if ( !schedule( false ) ) + announceSubmit( item, 1, false ); // couldn't perform submit immediately, let user know + } +} + + +/** + * Flushes the submit queues + */ +void ScrobblerSubmitter::performSubmit() +{ + QString data; + + // Audioscrobbler accepts max 10 tracks on one submit. + SubmitItem* items[10]; + for ( int submitCounter = 0; submitCounter < 10; submitCounter++ ) + items[submitCounter] = 0; + + if ( PROTOCOL_VERSION == "1.1" ) + { + // Audioscrobbler protocol 1.1 (current) + // http://post.audioscrobbler.com/v1.1-lite.php + // u=<user> + // &s=<MD5 response>& + // a[0]=<artist 0>&t[0]=<track 0>&b[0]=<album 0>& + // m[0]=<mbid 0>&l[0]=<length 0>&i[0]=<time 0>& + // a[1]=<artist 1>&t[1]=<track 1>&b[1]=<album 1>& + // m[1]=<mbid 1>&l[1]=<length 1>&i[1]=<time 1>& + // ... + // a[n]=<artist n>&t[n]=<track n>&b[n]=<album n>& + // m[n]=<mbid n>&l[n]=<length n>&i[n]=<time n>& + + + data = + "u=" + KURL::encode_string_no_slash( m_username ) + + "&s=" + + KURL::encode_string_no_slash( KMD5( KMD5( m_password.utf8() ).hexDigest() + + m_challenge.utf8() ).hexDigest() ); + + m_submitQueue.first(); + for ( int submitCounter = 0; submitCounter < 10; submitCounter++ ) + { + SubmitItem* itemFromQueue = dequeueItem(); + if ( itemFromQueue == 0 ) + { + if( submitCounter == 0 ) + { + // this shouldn't happen, since we shouldn't be scheduled until we have something to do! + debug() << "Nothing to submit!" << endl; + return; + } + else + { + break; + } + } + else + data += '&'; + + items[submitCounter] = itemFromQueue; + QDateTime playStartTime = QDateTime(); + playStartTime.setTime_t( itemFromQueue->playStartTime() ); + + const QString count = QString::number( submitCounter ); + + data += + "a[" + count + "]=" + KURL::encode_string_no_slash( itemFromQueue->artist(), 106 /*utf-8*/ ) + + "&t[" + count + "]=" + KURL::encode_string_no_slash( itemFromQueue->title(), 106 /*utf-8*/ ) + + "&b[" + count + "]=" + KURL::encode_string_no_slash( itemFromQueue->album(), 106 /*utf-8*/ ) + + "&m[" + count + "]=" + + "&l[" + count + "]=" + QString::number( itemFromQueue->length() ) + + "&i[" + count + "]=" + KURL::encode_string_no_slash( playStartTime.toString( "yyyy-MM-dd hh:mm:ss" ) ); + } + } + + else + { + debug() << "Submit not implemented for protocol version: " << PROTOCOL_VERSION << endl; + return; + } + + debug() << "Submit data: " << data << endl; + + m_submitResultBuffer = ""; + + m_inProgress = true; + KIO::TransferJob* job = KIO::http_post( m_submitUrl, data.utf8(), false ); + job->addMetaData( "content-type", "Content-Type: application/x-www-form-urlencoded" ); + + // Loop in reverse order, which helps when items are later fetched from + // m_ongoingSubmits and possibly put back to queue, in correct order + // (i.e. oldest first). + for ( int submitCounter = 9; submitCounter >= 0; submitCounter-- ) + if ( items[submitCounter] != 0 ) + m_ongoingSubmits.insert( job, items[submitCounter] ); + + Amarok::StatusBar::instance()->newProgressOperation( job ) + .setDescription( i18n( "Submitting to last.fm" ) ); + + connect( job, SIGNAL( result( KIO::Job* ) ), + this, SLOT( audioScrobblerSubmitResult( KIO::Job* ) ) ); + connect( job, SIGNAL( data( KIO::Job*, const QByteArray& ) ), + this, SLOT( audioScrobblerSubmitData( KIO::Job*, const QByteArray& ) ) ); +} + + +/** + * Configures the username/password and whether to scrobble + */ +void ScrobblerSubmitter::configure( const QString& username, const QString& password, bool enabled ) +{ + if ( username != m_username || password != m_password ) + m_needHandshake = true; + + m_username = username; + m_password = password; + m_scrobblerEnabled = enabled; + if ( enabled ) + schedule( false ); + else + { + // If submit is disabled, clear submitqueue. + m_ongoingSubmits.setAutoDelete( true ); + m_ongoingSubmits.clear(); + m_ongoingSubmits.setAutoDelete( false ); + m_submitQueue.setAutoDelete( true ); + m_submitQueue.clear(); + m_submitQueue.setAutoDelete( false ); + m_fakeQueue.setAutoDelete( true ); + m_fakeQueue.clear(); + m_fakeQueue.setAutoDelete( false ); + m_fakeQueueLength = 0; + m_timer.stop(); + } +} + + +/** + * Sync from external device complete, can send them off + */ +void ScrobblerSubmitter::syncComplete() +{ + m_holdFakeQueue = false; + saveSubmitQueue(); + schedule( false ); +} + + +/** + * Called when timer set up in the schedule function goes off. + */ +void ScrobblerSubmitter::scheduledTimeReached() +{ + if ( m_needHandshake || m_challenge.isEmpty() ) + performHandshake(); + else + performSubmit(); +} + +/** + * Called when handshake TransferJob has finished and data is received. + */ +void ScrobblerSubmitter::audioScrobblerHandshakeResult( KIO::Job* job ) //SLOT +{ + m_prevSubmitTime = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + m_inProgress = false; + + if ( job->error() ) { + warning() << "KIO error! errno: " << job->error() << endl; + schedule( true ); + return; + } + + KIO::StoredTransferJob* const storedJob = static_cast<KIO::StoredTransferJob*>( job ); + m_submitResultBuffer = QString::fromUtf8( storedJob->data().data(), storedJob->data().size() ); + +// debug() +// << "Handshake result received: " +// << endl << m_submitResultBuffer << endl; + + // UPTODATE + // <md5 challenge> + // <url to submit script> + // INTERVAL n (protocol 1.1) + if (m_submitResultBuffer.startsWith( "UPTODATE" ) ) + { + m_challenge = m_submitResultBuffer.section( "\n", 1, 1 ); + m_submitUrl = m_submitResultBuffer.section( "\n", 2, 2 ); + QString interval = m_submitResultBuffer.section( "\n", 3, 3 ); + + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + } + // UPDATE <updateurl (optional)> + // <md5 challenge> + // <url to submit script> + // INTERVAL n (protocol 1.1) + else if ( m_submitResultBuffer.startsWith( "UPDATE" ) ) + { + warning() << "A new version of Amarok is available" << endl; + + m_challenge = m_submitResultBuffer.section( "\n", 1, 1 ); + m_submitUrl = m_submitResultBuffer.section( "\n", 2, 2 ); + QString interval = m_submitResultBuffer.section( "\n", 3, 3 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + } + // FAILED <reason (optional)> + // INTERVAL n (protocol 1.1) + else if ( m_submitResultBuffer.startsWith( "FAILED" ) ) + { + QString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.find( "\n" ) ); + if ( reason.length() > 6 ) + reason = reason.mid( 7 ).stripWhiteSpace(); + + warning() << "Handshake failed (" << reason << ")" << endl; + QString interval = m_submitResultBuffer.section( "\n", 1, 1 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + } + // BADUSER (protocol 1.1) or BADAUTH (protocol 1.2) + // INTERVAL n (protocol 1.1) + else if ( m_submitResultBuffer.startsWith( "BADUSER" ) || + m_submitResultBuffer.startsWith( "BADAUTH" ) ) + { + warning() << "Handshake failed (Authentication failed)" << endl; + QString interval = m_submitResultBuffer.section( "\n", 1, 1 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + } + else + warning() << "Unknown handshake response: " << m_submitResultBuffer << endl; + + debug() << "Handshake result parsed: challenge=" << m_challenge << ", submitUrl=" << m_submitUrl << endl; + + schedule( m_challenge.isEmpty() ); // schedule to submit or re-attempt handshake +} + + +/** + * Called when submit TransferJob has finished and data is received. + */ +void ScrobblerSubmitter::audioScrobblerSubmitResult( KIO::Job* job ) //SLOT +{ + m_prevSubmitTime = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + m_inProgress = false; + + if ( job->error() ) { + warning() << "KIO error! errno: " << job->error() << endl; + enqueueJob( job ); + return; + } + +// debug() +// << "Submit result received: " +// << endl << m_submitResultBuffer << endl; + + // OK + // INTERVAL n (protocol 1.1) + if (m_submitResultBuffer.startsWith( "OK" ) ) + { + debug() << "Submit successful" << endl; + QString interval = m_submitResultBuffer.section( "\n", 1, 1 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + + finishJob( job ); + } + // FAILED <reason (optional)> + // INTERVAL n (protocol 1.1) + else if ( m_submitResultBuffer.startsWith( "FAILED" ) ) + { + QString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.find( "\n" ) ); + if ( reason.length() > 6 ) + reason = reason.mid( 7 ).stripWhiteSpace(); + + warning() << "Submit failed (" << reason << ")" << endl; + + QString interval = m_submitResultBuffer.section( "\n", 1, 1 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + + enqueueJob( job ); + } + // BADAUTH + // INTERVAL n (protocol 1.1) + else if ( m_submitResultBuffer.startsWith( "BADAUTH" ) ) + { + warning() << "Submit failed (Authentication failed)" << endl; + + QString interval = m_submitResultBuffer.section( "\n", 1, 1 ); + if ( interval.startsWith( "INTERVAL" ) ) + m_interval = interval.mid( 9 ).toUInt(); + + m_challenge = QString::null; + enqueueJob( job ); + } + else + { + warning() << "Unknown submit response" << endl; + enqueueJob( job ); + } +} + + +/** + * Receives the data from the TransferJob. + */ +void ScrobblerSubmitter::audioScrobblerSubmitData( + KIO::Job*, const QByteArray& data ) //SLOT +{ + // Append new chunk of string + m_submitResultBuffer += QString::fromUtf8( data, data.size() ); +} + + +/** + * Checks if it is possible to try to submit the data to Audioscrobbler. + */ +bool ScrobblerSubmitter::canSubmit() const +{ + if ( !m_scrobblerEnabled || m_username.isEmpty() || m_password.isEmpty() ) + { + debug() << "Unable to submit - no uname/pass or disabled" << endl; + return false; + } + + return true; +} + + +/** + * Enqueues the given item for later submission. + */ +void ScrobblerSubmitter::enqueueItem( SubmitItem* item ) +{ + // Maintain max size of the queue, Audioscrobbler won't accept too old + // submissions anyway. + m_fakeQueue.first(); + for ( uint size = m_fakeQueue.count() + m_submitQueue.count(); size >= 500; size-- ) + { + SubmitItem* itemFromQueue = m_fakeQueue.getFirst(); + m_fakeQueue.removeFirst(); + + if ( itemFromQueue ) + { + debug() << "Dropping " << itemFromQueue->artist() + << " - " << itemFromQueue->title() << " from fake queue" << endl; + m_fakeQueueLength -= itemFromQueue->length(); + } + + delete itemFromQueue; + } + m_submitQueue.first(); + for ( uint size = m_submitQueue.count(); size >= 500; size-- ) + { + SubmitItem* itemFromQueue = m_submitQueue.getFirst(); + m_submitQueue.removeFirst(); + debug() << "Dropping " << itemFromQueue->artist() + << " - " << itemFromQueue->title() << " from submit queue" << endl; + + delete itemFromQueue; + } + + if( item->playStartTime() == 0 ) + { + m_fakeQueue.inSort( item ); + m_fakeQueueLength += item->length(); + } + else + { + m_submitQueue.inSort( item ); + } + + if( !m_holdFakeQueue ) + { + // Save submit queue to disk so it is more uptodate in case of crash. + saveSubmitQueue(); + } +} + + +/** + * Dequeues one item from the queue. + */ +SubmitItem* ScrobblerSubmitter::dequeueItem() +{ + SubmitItem* item = 0; + if( m_lastSubmissionFinishTime > 0 && !m_holdFakeQueue && m_fakeQueue.getFirst() ) + { + uint limit = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + + if ( m_submitQueue.getFirst() ) + if ( m_submitQueue.getFirst()->playStartTime() <= limit ) + limit = m_submitQueue.getFirst()->playStartTime(); + + if( m_lastSubmissionFinishTime + m_fakeQueue.getFirst()->length() <= limit ) + { + m_fakeQueue.first(); + item = m_fakeQueue.take(); + // don't backdate earlier than we have to + if( m_lastSubmissionFinishTime + m_fakeQueueLength < limit ) + item->m_playStartTime = limit - m_fakeQueueLength; + else + item->m_playStartTime = m_lastSubmissionFinishTime; + m_fakeQueueLength -= item->length(); + } + } + + if( !item ) + { + m_submitQueue.first(); + item = m_submitQueue.take(); + } + + if( item ) + { + if( item->playStartTime() < m_lastSubmissionFinishTime ) + { +// debug() << "play times screwed up? - " << item->artist() << " - " << item->title() << ": " << item->playStartTime() << " < " << m_lastSubmissionFinishTime << endl; + } + int add = 30; + if( item->length() / 2 + 1 > add ) + add = item->length() / 2 + 1; + if( item->playStartTime() + add > m_lastSubmissionFinishTime ) + m_lastSubmissionFinishTime = item->playStartTime() + add; + + // Save submit queue to disk so it is more uptodate in case of crash. + saveSubmitQueue(); + } + + return item; +} + + +/** + * Enqueues items associated with the job. This is used when the job + * has failed (e.g. network problems). + */ +void ScrobblerSubmitter::enqueueJob( KIO::Job* job ) +{ + SubmitItem *lastItem = 0; + SubmitItem *item = 0; + int counter = 0; + while ( ( item = m_ongoingSubmits.take( job ) ) != 0 ) + { + counter++; + lastItem = item; + enqueueItem( item ); + } + m_submitQueue.first(); + + if( lastItem ) + announceSubmit( lastItem, counter, false ); + + schedule( true ); // arrange to flush queue after failure +} + + +/** + * Deletes items associated with the job. This is used when the job + * has succeeded. + */ +void ScrobblerSubmitter::finishJob( KIO::Job* job ) +{ + SubmitItem *firstItem = 0; + SubmitItem *item = 0; + int counter = 0; + while ( ( item = m_ongoingSubmits.take( job ) ) != 0 ) + { + counter++; + if ( firstItem == 0 ) + firstItem = item; + else + delete item; + } + + if( firstItem ) + announceSubmit( firstItem, counter, true ); + delete firstItem; + + schedule( false ); // arrange to flush rest of queue +} + + +/** + * Announces on StatusBar if the submit was successful or not. + * + * @param item One of the items + * @param tracks Amount of tracks that were submitted + * @param success Indicates if the submission was successful or not + */ +void ScrobblerSubmitter::announceSubmit( SubmitItem *item, int tracks, bool success ) const +{ + QString _long, _short; + + if ( success ) + { + if ( tracks == 1 ) + _short = i18n( "'%1' submitted to last.fm" ).arg( item->title() ); + else + { + _short = i18n( "Several tracks submitted to last.fm" ); + + _long = "<p>"; + _long = i18n( "'%1' and one other track submitted", + "'%1' and %n other tracks submitted", tracks-1 ) + .arg( item->title() ); + } + } + else + { + if ( tracks == 1 ) + _short = i18n( "Failed to submit '%1' to last.fm" ).arg( item->title() ); + else + { + _short = i18n( "Failed to submit several tracks to last.fm" ); + _long = "<p>"; + _long = i18n( "Failed to submit '%1' and one other track", + "Failed to submit '%1' and %n other tracks", tracks-1 ) + .arg( item->title() ); + } + } + + if ( m_submitQueue.count() + m_fakeQueue.count() > 0 ) + { + _long += "<p>"; + _long += i18n( "One track still in queue", "%n tracks still in queue", + m_submitQueue.count() + m_fakeQueue.count() ); + } + + Amarok::StatusBar::instance()->shortLongMessage( _short, _long ); +} + + +void ScrobblerSubmitter::saveSubmitQueue() +{ + QFile file( m_savePath ); + + if( !file.open( IO_WriteOnly ) ) + { + debug() << "[SCROBBLER] Couldn't write submit queue to file: " << m_savePath << endl; + return; + } + + if ( m_lastSubmissionFinishTime == 0 ) + m_lastSubmissionFinishTime = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + + QDomDocument newdoc; + QDomElement submitQueue = newdoc.createElement( "submit" ); + submitQueue.setAttribute( "product", "Amarok" ); + submitQueue.setAttribute( "version", APP_VERSION ); + submitQueue.setAttribute( "lastSubmissionFinishTime", m_lastSubmissionFinishTime ); + + m_submitQueue.first(); + for ( uint idx = 0; idx < m_submitQueue.count(); idx++ ) + { + SubmitItem *item = m_submitQueue.at( idx ); + QDomElement i = item->toDomElement( newdoc ); + submitQueue.appendChild( i ); + } + m_fakeQueue.first(); + for ( uint idx = 0; idx < m_fakeQueue.count(); idx++ ) + { + SubmitItem *item = m_fakeQueue.at( idx ); + QDomElement i = item->toDomElement( newdoc ); + submitQueue.appendChild( i ); + } + + QDomNode submitNode = newdoc.importNode( submitQueue, true ); + newdoc.appendChild( submitNode ); + + QTextStream stream( &file ); + stream.setEncoding( QTextStream::UnicodeUTF8 ); + stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + stream << newdoc.toString(); + file.close(); +} + + +void ScrobblerSubmitter::readSubmitQueue() +{ + m_savePath = Amarok::saveLocation() + "submit.xml"; + QFile file( m_savePath ); + + if ( !file.open( IO_ReadOnly ) ) + { + debug() << "Couldn't open file: " << m_savePath << endl; + return; + } + + QTextStream stream( &file ); + stream.setEncoding( QTextStream::UnicodeUTF8 ); + + QDomDocument d; + if( !d.setContent( stream.read() ) ) + { + debug() << "Couldn't read file: " << m_savePath << endl; + return; + } + + uint last = 0; + if( d.namedItem( "submit" ).isElement() ) + last = d.namedItem( "submit" ).toElement().attribute( "lastSubmissionFinishTime" ).toUInt(); + if(last && last > m_lastSubmissionFinishTime) + m_lastSubmissionFinishTime = last; + + const QString ITEM( "item" ); //so we don't construct these QStrings all the time + + for( QDomNode n = d.namedItem( "submit" ).firstChild(); !n.isNull() && n.nodeName() == ITEM; n = n.nextSibling() ) + enqueueItem( new SubmitItem( n.toElement() ) ); + + m_submitQueue.first(); +} + + +/** + * Schedules an Audioscrobbler handshake or submit as required. + * Returns true if an immediate submit was possible + */ +bool ScrobblerSubmitter::schedule( bool failure ) +{ + m_timer.stop(); + if ( m_inProgress || !canSubmit() ) + return false; + + uint when, currentTime = QDateTime::currentDateTime( Qt::UTC ).toTime_t(); + if ( currentTime - m_prevSubmitTime > m_interval ) + when = 0; + else + when = m_interval - ( currentTime - m_prevSubmitTime ); + + if ( failure ) + { + m_backoff = kMin( kMax( m_backoff * 2, unsigned( MIN_BACKOFF ) ), unsigned( MAX_BACKOFF ) ); + when = kMax( m_backoff, m_interval ); + } + else + m_backoff = 0; + + if ( m_needHandshake || m_challenge.isEmpty() ) + { + m_challenge = QString::null; + m_needHandshake = false; + + if ( when == 0 ) + { + debug() << "Performing immediate handshake" << endl; + performHandshake(); + } + else + { + debug() << "Performing handshake in " << when << " seconds" << endl; + m_timer.start( when * 1000, true ); + } + } + else if ( !m_submitQueue.isEmpty() || !m_holdFakeQueue && !m_fakeQueue.isEmpty() ) + { + // if we only have stuff in the fake queue, we need to only schedule for when we can actually do something with it + if ( m_submitQueue.isEmpty() && m_lastSubmissionFinishTime + m_fakeQueue.getFirst()->length() > currentTime + when ) + when = m_lastSubmissionFinishTime + m_fakeQueue.getFirst()->length() - currentTime; + + if ( when == 0 ) + { + debug() << "Performing immediate submit" << endl; + performSubmit(); + return true; + } + else + { + debug() << "Performing submit in " << when << " seconds" << endl; + m_timer.start( when * 1000, true ); + } + } else { + debug() << "Nothing to schedule" << endl; + } + + return false; +} + + +#include "scrobbler.moc" |