You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
amarok/amarok/src/scrobbler.cpp

1186 lines
36 KiB

// (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 <tqdatetime.h>
#include <tqdeepcopy.h>
#include <tdeapplication.h>
#include <tdeio/job.h>
#include <tdeio/jobclasses.h>
#include <tdelocale.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 TQString & artist )
{
TQString safeArtist = TQDeepCopy<TQString>( 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 = TDEIO::get( "http://ws.audioscrobbler.com/1.0/artist/" + safeArtist + "/similar.xml", false, false );
connect( m_similarArtistsJob, TQT_SIGNAL( result( TDEIO::Job* ) ),
this, TQT_SLOT( audioScrobblerSimilarArtistsResult( TDEIO::Job* ) ) );
connect( m_similarArtistsJob, TQT_SIGNAL( data( TDEIO::Job*, const TQByteArray& ) ),
this, TQT_SLOT( audioScrobblerSimilarArtistsData( TDEIO::Job*, const TQByteArray& ) ) );
}
}
/**
* Called when the similar artists TransferJob finishes.
*/
void Scrobbler::audioScrobblerSimilarArtistsResult( TDEIO::Job* job ) //SLOT
{
if ( m_similarArtistsJob != job )
return; //not the right job, so let's ignore it
if ( job->error() )
{
warning() << "TDEIO 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>
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( TDEIO::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<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
////////////////////////////////////////////////////////////////////////////////
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<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()
{
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=<clientid>
// &v=<clientver>
// &u=<user>
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=<clientid>
// &v=<clientversion>
// &u=<username>
// &t=<unix_timestamp>
// &a=<passcode>
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;
TDEIO::TransferJob* job = TDEIO::storedGet( handshakeUrl, false, false );
connect( job, TQT_SIGNAL( result( TDEIO::Job* ) ), TQT_SLOT( audioScrobblerHandshakeResult( TDEIO::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=<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;
TQDateTime playStartTime = TQDateTime();
playStartTime.setTime_t( itemFromQueue->playStartTime() );
const TQString count = TQString::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 + "]=" + TQString::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;
TDEIO::TransferJob* job = TDEIO::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, TQT_SIGNAL( result( TDEIO::Job* ) ),
this, TQT_SLOT( audioScrobblerSubmitResult( TDEIO::Job* ) ) );
connect( job, TQT_SIGNAL( data( TDEIO::Job*, const TQByteArray& ) ),
this, TQT_SLOT( audioScrobblerSubmitData( TDEIO::Job*, const TQByteArray& ) ) );
}
/**
* Configures the username/password and whether to scrobble
*/
void ScrobblerSubmitter::configure( const TQString& username, const TQString& 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( TDEIO::Job* job ) //SLOT
{
m_prevSubmitTime = TQDateTime::currentDateTime( Qt::UTC ).toTime_t();
m_inProgress = false;
if ( job->error() ) {
warning() << "TDEIO error! errno: " << job->error() << endl;
schedule( true );
return;
}
TDEIO::StoredTransferJob* const storedJob = static_cast<TDEIO::StoredTransferJob*>( job );
m_submitResultBuffer = TQString::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 );
TQString 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 );
TQString 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" ) )
{
TQString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.find( "\n" ) );
if ( reason.length() > 6 )
reason = reason.mid( 7 ).stripWhiteSpace();
warning() << "Handshake failed (" << reason << ")" << endl;
TQString 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;
TQString 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( TDEIO::Job* job ) //SLOT
{
m_prevSubmitTime = TQDateTime::currentDateTime( Qt::UTC ).toTime_t();
m_inProgress = false;
if ( job->error() ) {
warning() << "TDEIO 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;
TQString 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" ) )
{
TQString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.find( "\n" ) );
if ( reason.length() > 6 )
reason = reason.mid( 7 ).stripWhiteSpace();
warning() << "Submit failed (" << reason << ")" << endl;
TQString 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;
TQString interval = m_submitResultBuffer.section( "\n", 1, 1 );
if ( interval.startsWith( "INTERVAL" ) )
m_interval = interval.mid( 9 ).toUInt();
m_challenge = TQString();
enqueueJob( job );
}
else
{
warning() << "Unknown submit response" << endl;
enqueueJob( job );
}
}
/**
* Receives the data from the TransferJob.
*/
void ScrobblerSubmitter::audioScrobblerSubmitData(
TDEIO::Job*, const TQByteArray& data ) //SLOT
{
// Append new chunk of string
m_submitResultBuffer += TQString::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 = TQDateTime::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( TDEIO::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( TDEIO::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
{
TQString _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()
{
TQFile 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 = TQDateTime::currentDateTime( Qt::UTC ).toTime_t();
TQDomDocument newdoc;
TQDomElement 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 );
TQDomElement 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 );
TQDomElement i = item->toDomElement( newdoc );
submitQueue.appendChild( i );
}
TQDomNode submitNode = newdoc.importNode( submitQueue, true );
newdoc.appendChild( submitNode );
TQTextStream stream( &file );
stream.setEncoding( TQTextStream::UnicodeUTF8 );
stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
stream << newdoc.toString();
file.close();
}
void ScrobblerSubmitter::readSubmitQueue()
{
m_savePath = Amarok::saveLocation() + "submit.xml";
TQFile file( m_savePath );
if ( !file.open( IO_ReadOnly ) )
{
debug() << "Couldn't open file: " << m_savePath << endl;
return;
}
TQTextStream stream( &file );
stream.setEncoding( TQTextStream::UnicodeUTF8 );
TQDomDocument 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 TQString ITEM( "item" ); //so we don't construct these TQStrings all the time
for( TQDomNode 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 = TQDateTime::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 = TQString();
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"