// (c) 2004 Christian Muehlhaeuser // (c) 2004 Sami Nieminen // (c) 2006 Shane King // (c) 2006 Iain Benson // (c) 2006 Alexandre Oliveira // (c) 2006 Andy Kelk // 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 #include #include #include #include #include #include #include #include #include //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 TQString & artist ) { TQString safeArtist = TQDeepCopy( artist ); if ( AmarokConfig::retrieveSimilarArtists() ) { // Request looks like this: // http://ws.audioscrobbler.com/1.0/artist/Metallica/similar.xml m_similarArtistsBuffer = TQByteArray(); m_artist = artist; m_similarArtistsJob = KIO::get( "http://ws.audioscrobbler.com/1.0/artist/" + safeArtist + "/similar.xml", false, false ); connect( m_similarArtistsJob, TQT_SIGNAL( result( KIO::Job* ) ), this, TQT_SLOT( audioScrobblerSimilarArtistsResult( KIO::Job* ) ) ); connect( m_similarArtistsJob, TQT_SIGNAL( data( KIO::Job*, const TQByteArray& ) ), this, TQT_SLOT( audioScrobblerSimilarArtistsData( KIO::Job*, const TQByteArray& ) ) ); } } /** * 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: // // // // Iron Maiden // // 100 // http://www.last.fm/music/Iron+Maiden // http://static.last.fm/proposedimages/thumbnail/6/1000107/264195.jpg // http://static.last.fm/proposedimages/sidebar/6/1000107/264195.jpg // 1 // // TQDomDocument document; if ( !document.setContent( m_similarArtistsBuffer ) ) { debug() << "Couldn't read similar artists response" << endl; return; } TQDomNodeList values = document.elementsByTagName( "similarartists" ) .item( 0 ).childNodes(); TQStringList 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 TQByteArray& 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 TQString& artist, const TQString& album, const TQString& title, int length, bool now) { m_artist = artist; m_album = album; m_title = title; m_length = length; m_playStartTime = now ? TQDateTime::currentDateTime( Qt::UTC ).toTime_t() : 0; } SubmitItem::SubmitItem( const TQDomElement& 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; } TQDomElement SubmitItem::toDomElement( TQDomDocument& document ) const { TQDomElement item = document.createElement( "item" ); // TODO: In the future, it might be good to store url too //item.setAttribute("url", item->url().url()); TQDomElement artist = document.createElement( "artist" ); TQDomText artistText = document.createTextNode( m_artist ); artist.appendChild( artistText ); item.appendChild( artist ); TQDomElement album = document.createElement( "album" ); TQDomText albumText = document.createTextNode( m_album ); album.appendChild( albumText ); item.appendChild( album ); TQDomElement title = document.createElement( "title" ); TQDomText titleText = document.createTextNode( m_title ); title.appendChild( titleText ); item.appendChild( title ); TQDomElement length = document.createElement( "length" ); TQDomText lengthText = document.createTextNode( TQString::number( m_length ) ); length.appendChild( lengthText ); item.appendChild( length ); TQDomElement playtime = document.createElement( "playtime" ); TQDomText playtimeText = document.createTextNode( TQString::number( m_playStartTime ) ); playtime.appendChild( playtimeText ); item.appendChild( playtime ); return item; } //////////////////////////////////////////////////////////////////////////////// // CLASS SubmitQueue //////////////////////////////////////////////////////////////////////////////// int SubmitQueue::compareItems( TQPtrCollection::Item item1, TQPtrCollection::Item item2 ) { SubmitItem *sItem1 = static_cast( item1 ); SubmitItem *sItem2 = static_cast( item2 ); int result; if ( sItem1 == sItem2 ) { result = 0; } else if ( sItem1->playStartTime() > sItem2->playStartTime() ) { result = 1; } else { result = -1; } return result; } //////////////////////////////////////////////////////////////////////////////// // CLASS ScrobblerSubmitter //////////////////////////////////////////////////////////////////////////////// TQString ScrobblerSubmitter::PROTOCOL_VERSION = "1.1"; TQString ScrobblerSubmitter::CLIENT_ID = "ark"; TQString ScrobblerSubmitter::CLIENT_VERSION = "1.4"; TQString 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, TQT_SIGNAL(timeout()), this, TQT_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 ( TQPtrDictIterator 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() { TQString handshakeUrl = TQString(); uint currentTime = TQDateTime::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= // &v= // &u= handshakeUrl = HANDSHAKE_URL + TQString( "&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= // &v= // &u= // &t= // &a= handshakeUrl = HANDSHAKE_URL + TQString( "&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().data() ); } 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, TQT_SIGNAL( result( KIO::Job* ) ), TQT_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() { TQString 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= // &s=& // a[0]=&t[0]=&b[0]=& // m[0]=&l[0]=&i[0]=