Revert previous commit, was incorrect
[amarok.git] / src / scrobbler.cpp
blob85d8bcfb78e4eac88b03512b726c263d849bf4f8
1 /*
2 Copyright (c) 2004 Christian Muehlhaeuser <chris@chris.de>
3 Copyright (c) 2004 Sami Nieminen <sami.nieminen@iki.fi>
4 Copyright (c) 2006 Shane King <kde@dontletsstart.com>
5 Copyright (c) 2006 Iain Benson <iain@arctos.me.uk>
6 Copyright (c) 2006 Alexandre Oliveira <aleprj@gmail.com>
7 Copyright (c) 2006 Andy Kelk <andy@mopoke.co.uk>
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 #define DEBUG_PREFIX "Scrobbler"
26 #include "scrobbler.h"
28 #include "amarok.h"
29 #include "amarokconfig.h"
30 #include "config-amarok.h"
31 #include "debug.h"
32 #include "enginecontroller.h"
33 #include "metabundle.h"
34 #include "ContextStatusBar.h"
36 #include <KApplication>
37 #include <KCodecs>
38 #include <KIO/Job>
39 #include <kio/jobclasses.h>
40 #include <KLocale>
41 #include <KStandardDirs>
42 #include <KUrl>
44 #include <QtAlgorithms>
45 #include <QDateTime>
46 #include <QHashIterator>
47 #include <QTextStream>
49 #include <unistd.h>
51 //some setups require this
52 #undef PROTOCOL_VERSION
55 ////////////////////////////////////////////////////////////////////////////////
56 // CLASS Scrobbler
57 ////////////////////////////////////////////////////////////////////////////////
59 Scrobbler* Scrobbler::instance()
61 static Scrobbler scrobbler;
62 return &scrobbler;
66 Scrobbler::Scrobbler()
67 : EngineObserver( EngineController::instance() )
68 , m_similarArtistsJob( 0 )
69 , m_validForSending( false )
70 , m_startPos( 0 )
71 , m_submitter( new ScrobblerSubmitter() )
72 , m_item( new SubmitItem() )
76 Scrobbler::~Scrobbler()
78 delete m_item;
79 delete m_submitter;
83 /**
84 * Queries similar artists from Audioscrobbler.
86 void Scrobbler::similarArtists( const QString & artist )
88 QString safeArtist = artist;
89 if ( AmarokConfig::retrieveSimilarArtists() )
91 // Request looks like this:
92 // http://ws.audioscrobbler.com/1.0/artist/Metallica/similar.xml
94 m_similarArtistsBuffer = QByteArray();
95 m_artist = artist;
97 m_similarArtistsJob = KIO::get( "http://ws.audioscrobbler.com/1.0/artist/" + safeArtist + "/similar.xml", KIO::NoReload, KIO::HideProgressInfo );
99 connect( m_similarArtistsJob, SIGNAL( result( KIO::Job* ) ),
100 this, SLOT( audioScrobblerSimilarArtistsResult( KIO::Job* ) ) );
101 connect( m_similarArtistsJob, SIGNAL( data( KIO::Job*, const QByteArray& ) ),
102 this, SLOT( audioScrobblerSimilarArtistsData( KIO::Job*, const QByteArray& ) ) );
108 * Called when the similar artists TransferJob finishes.
110 void Scrobbler::audioScrobblerSimilarArtistsResult( KIO::Job* job ) //SLOT
112 if ( m_similarArtistsJob != job )
113 return; //not the right job, so let's ignore it
115 if ( job->error() )
117 warning() << "KIO error! errno: " << job->error();
118 return;
121 // Result looks like this:
122 // <?xml version="1.0" encoding="UTF-8"?>
123 // <similarartists artist="Metallica" streamable="1" picture="http://static.last.fm/proposedimages/sidebar/6/1000024/288059.jpg" mbid="">
124 // <artist>
125 // <name>Iron Maiden</name>
126 // <mbid></mbid>
127 // <match>100</match>
128 // <url>http://www.last.fm/music/Iron+Maiden</url>
129 // <image_small>http://static.last.fm/proposedimages/thumbnail/6/1000107/264195.jpg</image_small>
130 // <image>http://static.last.fm/proposedimages/sidebar/6/1000107/264195.jpg</image>
131 // <streamable>1</streamable>
132 // </artist>
133 // </similarartists>
135 QDomDocument document;
136 if ( !document.setContent( m_similarArtistsBuffer ) )
138 debug() << "Couldn't read similar artists response";
139 return;
142 QDomNodeList values = document.elementsByTagName( "similarartists" )
143 .item( 0 ).childNodes();
145 QStringList suggestions;
146 for ( int i = 0; i < values.count() && i < 30; i++ ) // limit to top 30 artists
147 suggestions << values.item( i ).namedItem( "name" ).toElement().text();
149 debug() << "Suggestions retrieved (" << suggestions.count() << ")";
150 if ( !suggestions.isEmpty() )
151 emit similarArtistsFetched( m_artist, suggestions );
153 m_similarArtistsJob = 0;
158 * Called when similar artists data is received for the TransferJob.
160 void Scrobbler::audioScrobblerSimilarArtistsData( KIO::Job* job, const QByteArray& data ) //SLOT
162 if ( m_similarArtistsJob != job )
163 return; //not the right job, so let's ignore it
165 uint oldSize = m_similarArtistsBuffer.size();
166 m_similarArtistsBuffer.resize( oldSize + data.size() );
167 memcpy( m_similarArtistsBuffer.data() + oldSize, data.data(), data.size() );
172 * Called when the signal is received.
174 void Scrobbler::engineNewMetaData( const MetaBundle& bundle, bool trackChanged )
176 //debug() << "engineNewMetaData: " << bundle.artist() << ":" << bundle.album() << ":" << bundle.title() << ":" << trackChanged;
177 if ( !trackChanged )
179 debug() << "It's still the same track.";
180 m_item->setArtist( bundle.artist() );
181 m_item->setAlbum( bundle.album() );
182 m_item->setTitle( bundle.title() );
183 return;
186 //to work around xine bug, we have to explictly prevent submission the first few seconds of a track
187 //http://sourceforge.net/tracker/index.php?func=detail&aid=1401026&group_id=9655&atid=109655
188 m_timer.stop();
189 m_timer.setSingleShot( true );
190 m_timer.start( 10000 );
192 m_startPos = 0;
194 // Plugins must not submit tracks played from online radio stations, even
195 // if they appear to be providing correct metadata.
196 if ( !bundle.streamUrl().isEmpty() )
198 debug() << "Won't submit: It's a stream.";
199 m_validForSending = false;
201 else if( bundle.podcastBundle() != NULL )
203 debug() << "Won't submit: It's a podcast.";
204 m_validForSending = false;
206 else
208 *m_item = SubmitItem( bundle.artist(), bundle.album(), bundle.title(), bundle.length() );
209 m_validForSending = true; // check length etc later
215 * Called when cue file detects track change
217 void Scrobbler::subTrack( long currentPos, long startPos, long endPos )
219 //debug() << "subTrack: " << currentPos << ":" << startPos << ":" << endPos;
220 *m_item = SubmitItem( m_item->artist(), m_item->album(), m_item->title(), endPos - startPos );
221 if ( currentPos <= startPos + 2 ) // only submit if starting from the start of the track (need to allow 2 second difference for rounding/delay)
223 m_startPos = startPos * 1000;
224 m_validForSending = true;
226 else
228 debug() << "Won't submit: Detected cuefile jump to " << currentPos - startPos << " seconds into track.";
229 m_validForSending = false;
235 * Called when the signal is received.
237 void Scrobbler::engineTrackPositionChanged( long position, bool userSeek )
239 //debug() << "engineTrackPositionChanged: " << position << ":" << userSeek;
240 if ( !m_validForSending )
241 return;
243 if ( userSeek )
245 m_validForSending = false;
246 debug() << "Won't submit: Seek detected.";
247 return;
250 if ( m_timer.isActive() )
251 return;
253 // Each track must be submitted to the server when it is 50% or 240
254 // seconds complete, whichever comes first.
255 if ( position - m_startPos > 240 * 1000 || position - m_startPos > 0.5 * m_item->length() * 1000 )
257 if ( m_item->valid() )
258 m_submitter->submitItem( new SubmitItem( *m_item ) );
259 else
260 debug() << "Won't submit: No artist, no title, or less than 30 seconds.";
261 m_validForSending = false;
267 * Applies settings from the config dialog.
269 void Scrobbler::applySettings()
271 m_submitter->configure( AmarokConfig::scrobblerUsername(), AmarokConfig::scrobblerPassword(), AmarokConfig::submitPlayedSongs() );
275 ////////////////////////////////////////////////////////////////////////////////
276 // CLASS SubmitItem
277 ////////////////////////////////////////////////////////////////////////////////
280 SubmitItem::SubmitItem(
281 const QString& artist,
282 const QString& album,
283 const QString& title,
284 int length,
285 bool now)
287 m_artist = artist;
288 m_album = album;
289 m_title = title;
290 m_length = length;
291 m_playStartTime = now ? QDateTime::currentDateTime().toUTC().toTime_t() : 0;
295 SubmitItem::SubmitItem( const QDomElement& element )
297 m_artist = element.namedItem( "artist" ).toElement().text();
298 m_album = element.namedItem( "album" ).toElement().text();
299 m_title = element.namedItem( "title" ).toElement().text();
300 m_length = element.namedItem( "length" ).toElement().text().toInt();
301 m_playStartTime = element.namedItem( "playtime" ).toElement().text().toUInt();
305 SubmitItem::SubmitItem()
306 : m_length( 0 )
307 , m_playStartTime( 0 )
312 bool SubmitItem::operator==( const SubmitItem& item )
314 bool result = true;
316 if ( m_artist != item.artist() || m_album != item.album() || m_title != item.title() ||
317 m_length != item.length() || m_playStartTime != item.playStartTime() )
319 result = false;
322 return result;
326 QDomElement SubmitItem::toDomElement( QDomDocument& document ) const
328 QDomElement item = document.createElement( "item" );
329 // TODO: In the future, it might be good to store url too
330 //item.setAttribute("url", item->url().url());
332 QDomElement artist = document.createElement( "artist" );
333 QDomText artistText = document.createTextNode( m_artist );
334 artist.appendChild( artistText );
335 item.appendChild( artist );
337 QDomElement album = document.createElement( "album" );
338 QDomText albumText = document.createTextNode( m_album );
339 album.appendChild( albumText );
340 item.appendChild( album );
342 QDomElement title = document.createElement( "title" );
343 QDomText titleText = document.createTextNode( m_title );
344 title.appendChild( titleText );
345 item.appendChild( title );
347 QDomElement length = document.createElement( "length" );
348 QDomText lengthText = document.createTextNode( QString::number( m_length ) );
349 length.appendChild( lengthText );
350 item.appendChild( length );
352 QDomElement playtime = document.createElement( "playtime" );
353 QDomText playtimeText = document.createTextNode( QString::number( m_playStartTime ) );
354 playtime.appendChild( playtimeText );
355 item.appendChild( playtime );
357 return item;
361 ////////////////////////////////////////////////////////////////////////////////
362 // CLASS SubmitQueue
363 ////////////////////////////////////////////////////////////////////////////////
366 bool
367 SubmitQueue::compareItems( SubmitItem *sItem1, SubmitItem *sItem2 )
369 return !( sItem1 == sItem2 ) && sItem1->playStartTime() < sItem2->playStartTime();
373 ////////////////////////////////////////////////////////////////////////////////
374 // CLASS ScrobblerSubmitter
375 ////////////////////////////////////////////////////////////////////////////////
377 QString ScrobblerSubmitter::PROTOCOL_VERSION = "1.1";
378 QString ScrobblerSubmitter::CLIENT_ID = "ark";
379 QString ScrobblerSubmitter::CLIENT_VERSION = "1.4";
380 QString ScrobblerSubmitter::HANDSHAKE_URL = "http://post.audioscrobbler.com/?hs=true";
383 ScrobblerSubmitter::ScrobblerSubmitter()
384 : m_username( 0 )
385 , m_password( 0 )
386 , m_submitUrl( 0 )
387 , m_challenge( 0 )
388 , m_scrobblerEnabled( false )
389 , m_holdFakeQueue( false )
390 , m_inProgress( false )
391 , m_needHandshake( true )
392 , m_prevSubmitTime( 0 )
393 , m_interval( 0 )
394 , m_backoff( 0 )
395 , m_lastSubmissionFinishTime( 0 )
396 , m_fakeQueueLength( 0 )
398 connect( &m_timer, SIGNAL(timeout()), this, SLOT(scheduledTimeReached()) );
399 readSubmitQueue();
403 ScrobblerSubmitter::~ScrobblerSubmitter()
405 // need to rescue current submit. This may meant it gets submitted twice,
406 // but last.fm handles that, and it's better than losing it when you quit
407 // while a submit is happening
408 //for ( Q3PtrDictIterator<SubmitItem> it( m_ongoingSubmits ); it.current(); ++it )
409 for( QHashIterator<KIO::Job*, SubmitItem*> iter( m_ongoingSubmits ); iter.hasNext(); )
410 m_submitQueue.append( iter.next().value() );
411 m_ongoingSubmits.clear();
412 qStableSort( m_submitQueue.begin(), m_submitQueue.end(), SubmitQueue::compareItems );
414 saveSubmitQueue();
416 qDeleteAll( m_submitQueue );
417 m_submitQueue.clear();
418 qDeleteAll( m_fakeQueue );
419 m_fakeQueue.clear();
424 * Performs handshake with Audioscrobbler.
426 void ScrobblerSubmitter::performHandshake()
428 QString handshakeUrl;
429 uint currentTime = QDateTime::currentDateTime().toUTC().toTime_t();
431 if ( PROTOCOL_VERSION == "1.1" )
433 // Audioscrobbler protocol 1.1 (current)
434 // http://post.audioscrobbler.com/?hs=true
435 // &p=1.1
436 // &c=<clientid>
437 // &v=<clientver>
438 // &u=<user>
439 handshakeUrl =
440 HANDSHAKE_URL +
441 QString(
442 "&p=%1"
443 "&c=%2"
444 "&v=%3"
445 "&u=%4" )
446 .arg( PROTOCOL_VERSION )
447 .arg( CLIENT_ID )
448 .arg( CLIENT_VERSION )
449 .arg( m_username );
452 else if ( PROTOCOL_VERSION == "1.2" )
454 // Audioscrobbler protocol 1.2 (RFC)
455 // http://post.audioscrobbler.com/?hs=true
456 // &p=1.2
457 // &c=<clientid>
458 // &v=<clientversion>
459 // &u=<username>
460 // &t=<unix_timestamp>
461 // &a=<passcode>
462 handshakeUrl =
463 HANDSHAKE_URL +
464 QString(
465 "&p=%1"
466 "&c=%2"
467 "&v=%3"
468 "&u=%4"
469 "&t=%5"
470 "&a=%6" )
471 .arg( PROTOCOL_VERSION )
472 .arg( CLIENT_ID )
473 .arg( CLIENT_VERSION )
474 .arg( m_username )
475 .arg( currentTime )
476 .arg( QString::fromAscii( KMD5( KMD5( m_password.toUtf8() ).hexDigest() + QString::number( currentTime ).toAscii() ).hexDigest() ) );
479 else
481 debug() << "Handshake not implemented for protocol version: " << PROTOCOL_VERSION;
482 return;
485 debug() << "Handshake url: " << handshakeUrl;
487 m_submitResultBuffer = "";
489 m_inProgress = true;
490 KIO::TransferJob* job = KIO::storedGet( handshakeUrl, KIO::NoReload, KIO::HideProgressInfo );
491 connect( job, SIGNAL( result( KIO::Job* ) ), SLOT( audioScrobblerHandshakeResult( KIO::Job* ) ) );
496 * Sets item for submission to Audioscrobbler. Actual submission
497 * depends on things like (is scrobbling enabled, are Audioscrobbler
498 * profile details filled in etc).
500 void ScrobblerSubmitter::submitItem( SubmitItem* item )
502 if ( m_scrobblerEnabled ) {
503 enqueueItem( item );
505 if ( item->playStartTime() == 0 )
506 m_holdFakeQueue = true; // hold on to fake queue until we get it all and can compute when to submit
507 else if ( !schedule( false ) )
508 announceSubmit( item, 1, false ); // couldn't perform submit immediately, let user know
514 * Flushes the submit queues
516 void ScrobblerSubmitter::performSubmit()
518 QString data;
520 // Audioscrobbler accepts max 10 tracks on one submit.
521 SubmitItem* items[10];
522 for ( int submitCounter = 0; submitCounter < 10; submitCounter++ )
523 items[submitCounter] = 0;
525 if ( PROTOCOL_VERSION == "1.1" )
527 // Audioscrobbler protocol 1.1 (current)
528 // http://post.audioscrobbler.com/v1.1-lite.php
529 // u=<user>
530 // &s=<MD5 response>&
531 // a[0]=<artist 0>&t[0]=<track 0>&b[0]=<album 0>&
532 // m[0]=<mbid 0>&l[0]=<length 0>&i[0]=<time 0>&
533 // a[1]=<artist 1>&t[1]=<track 1>&b[1]=<album 1>&
534 // m[1]=<mbid 1>&l[1]=<length 1>&i[1]=<time 1>&
535 // ...
536 // a[n]=<artist n>&t[n]=<track n>&b[n]=<album n>&
537 // m[n]=<mbid n>&l[n]=<length n>&i[n]=<time n>&
540 data =
541 "u=" + KUrl::toPercentEncoding( m_username, "/" ) +
542 "&s=" +
543 KUrl::toPercentEncoding( KMD5( KMD5( m_password.toUtf8() ).hexDigest() +
544 m_challenge.toUtf8() ).hexDigest(), "/" );
546 for ( int submitCounter = 0; submitCounter < 10; submitCounter++ )
548 SubmitItem* itemFromQueue = dequeueItem();
549 if ( itemFromQueue == 0 )
551 if( submitCounter == 0 )
553 // this shouldn't happen, since we shouldn't be scheduled until we have something to do!
554 debug() << "Nothing to submit!";
555 return;
557 else
559 break;
562 else
563 data += '&';
565 items[submitCounter] = itemFromQueue;
566 QDateTime playStartTime = QDateTime();
567 playStartTime.setTime_t( itemFromQueue->playStartTime() );
569 const QString count = QString::number( submitCounter );
571 // FIXME: we have to find something different for doing the encode_string_no_slash to utf-8
572 data +=
573 "a[" + count + "]=" + KUrl::toPercentEncoding( itemFromQueue->artist(), "/" ) +
574 "&t[" + count + "]=" + KUrl::toPercentEncoding( itemFromQueue->title(), "/" ) +
575 "&b[" + count + "]=" + KUrl::toPercentEncoding( itemFromQueue->album(), "/" ) +
576 "&m[" + count + "]=" +
577 "&l[" + count + "]=" + QString::number( itemFromQueue->length() ) +
578 "&i[" + count + "]=" + KUrl::toPercentEncoding( playStartTime.toString( "yyyy-MM-dd hh:mm:ss" ), "/" );
582 else
584 debug() << "Submit not implemented for protocol version: " << PROTOCOL_VERSION;
585 return;
588 debug() << "Submit data: " << data;
590 m_submitResultBuffer = "";
592 m_inProgress = true;
593 KIO::TransferJob* job = KIO::http_post( m_submitUrl, data.toUtf8(), KIO::HideProgressInfo );
594 job->addMetaData( "content-type", "Content-Type: application/x-www-form-urlencoded" );
596 // Loop in reverse order, which helps when items are later fetched from
597 // m_ongoingSubmits and possibly put back to queue, in correct order
598 // (i.e. oldest first).
599 for ( int submitCounter = 9; submitCounter >= 0; submitCounter-- )
600 if ( items[submitCounter] != 0 )
601 m_ongoingSubmits.insert( job, items[submitCounter] );
603 Amarok::ContextStatusBar::instance()->newProgressOperation( job )
604 .setDescription( i18n( "Submitting to last.fm" ) );
606 connect( job, SIGNAL( result( KIO::Job* ) ),
607 this, SLOT( audioScrobblerSubmitResult( KIO::Job* ) ) );
608 connect( job, SIGNAL( data( KIO::Job*, const QByteArray& ) ),
609 this, SLOT( audioScrobblerSubmitData( KIO::Job*, const QByteArray& ) ) );
614 * Configures the username/password and whether to scrobble
616 void ScrobblerSubmitter::configure( const QString& username, const QString& password, bool enabled )
618 if ( username != m_username || password != m_password )
619 m_needHandshake = true;
621 m_username = username;
622 m_password = password;
623 m_scrobblerEnabled = enabled;
624 if ( enabled )
625 schedule( false );
626 else
628 // If submit is disabled, clear submitqueue.
629 qDeleteAll( m_ongoingSubmits );
630 m_ongoingSubmits.clear();
631 qDeleteAll( m_submitQueue );
632 m_submitQueue.clear();
633 qDeleteAll( m_fakeQueue );
634 m_fakeQueue.clear();
635 m_fakeQueueLength = 0;
636 m_timer.stop();
642 * Sync from external device complete, can send them off
644 void ScrobblerSubmitter::syncComplete()
646 m_holdFakeQueue = false;
647 saveSubmitQueue();
648 schedule( false );
653 * Called when timer set up in the schedule function goes off.
655 void ScrobblerSubmitter::scheduledTimeReached()
657 if ( m_needHandshake || m_challenge.isEmpty() )
658 performHandshake();
659 else
660 performSubmit();
664 * Called when handshake TransferJob has finished and data is received.
666 void ScrobblerSubmitter::audioScrobblerHandshakeResult( KIO::Job* job ) //SLOT
668 m_prevSubmitTime = QDateTime::currentDateTime().toUTC().toTime_t();
669 m_inProgress = false;
671 if ( job->error() ) {
672 warning() << "KIO error! errno: " << job->error();
673 schedule( true );
674 return;
677 KIO::StoredTransferJob* const storedJob = static_cast<KIO::StoredTransferJob*>( job );
678 m_submitResultBuffer = QString::fromUtf8( storedJob->data().data(), storedJob->data().size() );
680 // debug()
681 // << "Handshake result received: "
682 // << endl << m_submitResultBuffer;
684 // UPTODATE
685 // <md5 challenge>
686 // <url to submit script>
687 // INTERVAL n (protocol 1.1)
688 if (m_submitResultBuffer.startsWith( "UPTODATE" ) )
690 m_challenge = m_submitResultBuffer.section( "\n", 1, 1 );
691 m_submitUrl = m_submitResultBuffer.section( "\n", 2, 2 );
692 QString interval = m_submitResultBuffer.section( "\n", 3, 3 );
694 if ( interval.startsWith( "INTERVAL" ) )
695 m_interval = interval.mid( 9 ).toUInt();
697 // UPDATE <updateurl (optional)>
698 // <md5 challenge>
699 // <url to submit script>
700 // INTERVAL n (protocol 1.1)
701 else if ( m_submitResultBuffer.startsWith( "UPDATE" ) )
703 warning() << "A new version of Amarok is available";
705 m_challenge = m_submitResultBuffer.section( "\n", 1, 1 );
706 m_submitUrl = m_submitResultBuffer.section( "\n", 2, 2 );
707 QString interval = m_submitResultBuffer.section( "\n", 3, 3 );
708 if ( interval.startsWith( "INTERVAL" ) )
709 m_interval = interval.mid( 9 ).toUInt();
711 // FAILED <reason (optional)>
712 // INTERVAL n (protocol 1.1)
713 else if ( m_submitResultBuffer.startsWith( "FAILED" ) )
715 QString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.indexOf( "\n" ) );
716 if ( reason.length() > 6 )
717 reason = reason.mid( 7 ).trimmed();
719 warning() << "Handshake failed (" << reason << ")";
720 QString interval = m_submitResultBuffer.section( "\n", 1, 1 );
721 if ( interval.startsWith( "INTERVAL" ) )
722 m_interval = interval.mid( 9 ).toUInt();
724 // BADUSER (protocol 1.1) or BADAUTH (protocol 1.2)
725 // INTERVAL n (protocol 1.1)
726 else if ( m_submitResultBuffer.startsWith( "BADUSER" ) ||
727 m_submitResultBuffer.startsWith( "BADAUTH" ) )
729 warning() << "Handshake failed (Authentication failed)";
730 QString interval = m_submitResultBuffer.section( "\n", 1, 1 );
731 if ( interval.startsWith( "INTERVAL" ) )
732 m_interval = interval.mid( 9 ).toUInt();
734 else
735 warning() << "Unknown handshake response: " << m_submitResultBuffer;
737 debug() << "Handshake result parsed: challenge=" << m_challenge << ", submitUrl=" << m_submitUrl;
739 schedule( m_challenge.isEmpty() ); // schedule to submit or re-attempt handshake
744 * Called when submit TransferJob has finished and data is received.
746 void ScrobblerSubmitter::audioScrobblerSubmitResult( KIO::Job* job ) //SLOT
748 m_prevSubmitTime = QDateTime::currentDateTime().toUTC().toTime_t();
749 m_inProgress = false;
751 if ( job->error() ) {
752 warning() << "KIO error! errno: " << job->error();
753 enqueueJob( job );
754 return;
757 // debug()
758 // << "Submit result received: "
759 // << endl << m_submitResultBuffer;
761 // OK
762 // INTERVAL n (protocol 1.1)
763 if (m_submitResultBuffer.startsWith( "OK" ) )
765 debug() << "Submit successful";
766 QString interval = m_submitResultBuffer.section( "\n", 1, 1 );
767 if ( interval.startsWith( "INTERVAL" ) )
768 m_interval = interval.mid( 9 ).toUInt();
770 finishJob( job );
772 // FAILED <reason (optional)>
773 // INTERVAL n (protocol 1.1)
774 else if ( m_submitResultBuffer.startsWith( "FAILED" ) )
776 QString reason = m_submitResultBuffer.mid( 0, m_submitResultBuffer.indexOf( "\n" ) );
777 if ( reason.length() > 6 )
778 reason = reason.mid( 7 ).trimmed();
780 warning() << "Submit failed (" << reason << ")";
782 QString interval = m_submitResultBuffer.section( "\n", 1, 1 );
783 if ( interval.startsWith( "INTERVAL" ) )
784 m_interval = interval.mid( 9 ).toUInt();
786 enqueueJob( job );
788 // BADAUTH
789 // INTERVAL n (protocol 1.1)
790 else if ( m_submitResultBuffer.startsWith( "BADAUTH" ) )
792 warning() << "Submit failed (Authentication failed)";
794 QString interval = m_submitResultBuffer.section( "\n", 1, 1 );
795 if ( interval.startsWith( "INTERVAL" ) )
796 m_interval = interval.mid( 9 ).toUInt();
798 m_challenge = QString();
799 enqueueJob( job );
801 else
803 warning() << "Unknown submit response";
804 enqueueJob( job );
810 * Receives the data from the TransferJob.
812 void ScrobblerSubmitter::audioScrobblerSubmitData(
813 KIO::Job*, const QByteArray& data ) //SLOT
815 // Append new chunk of string
816 m_submitResultBuffer += QString::fromUtf8( data, data.size() );
821 * Checks if it is possible to try to submit the data to Audioscrobbler.
823 bool ScrobblerSubmitter::canSubmit() const
825 if ( !m_scrobblerEnabled || m_username.isEmpty() || m_password.isEmpty() )
827 debug() << "Unable to submit - no uname/pass or disabled";
828 return false;
831 return true;
836 * Enqueues the given item for later submission.
838 void ScrobblerSubmitter::enqueueItem( SubmitItem* item )
840 // Maintain max size of the queue, Audioscrobbler won't accept too old
841 // submissions anyway.
842 for ( uint size = m_fakeQueue.count() + m_submitQueue.count(); size >= 500; size-- )
844 SubmitItem* itemFromQueue = 0;
845 if( !m_fakeQueue.isEmpty() )
847 itemFromQueue = m_fakeQueue.first();
848 m_fakeQueue.removeFirst();
851 if ( itemFromQueue )
853 debug() << "Dropping " << itemFromQueue->artist()
854 << " - " << itemFromQueue->title() << " from fake queue";
855 m_fakeQueueLength -= itemFromQueue->length();
858 delete itemFromQueue;
861 for ( uint size = m_submitQueue.count(); size >= 500; size-- )
863 SubmitItem* itemFromQueue = 0;
864 if( !m_submitQueue.isEmpty() )
866 itemFromQueue = m_submitQueue.first();
867 m_submitQueue.removeFirst();
868 debug() << "Dropping " << itemFromQueue->artist()
869 << " - " << itemFromQueue->title() << " from submit queue";
872 delete itemFromQueue;
875 if( item->playStartTime() == 0 )
877 m_fakeQueue.append( item );
878 qStableSort( m_fakeQueue.begin(), m_fakeQueue.end(), SubmitQueue::compareItems );
879 m_fakeQueueLength += item->length();
881 else
883 m_submitQueue.append( item );
884 qStableSort( m_submitQueue.begin(), m_submitQueue.end(), SubmitQueue::compareItems );
887 if( !m_holdFakeQueue )
889 // Save submit queue to disk so it is more uptodate in case of crash.
890 saveSubmitQueue();
896 * Dequeues one item from the queue.
898 SubmitItem* ScrobblerSubmitter::dequeueItem()
900 SubmitItem* item = 0;
901 if( m_lastSubmissionFinishTime > 0 && !m_holdFakeQueue && !m_fakeQueue.isEmpty() )
903 uint limit = QDateTime::currentDateTime().toUTC().toTime_t();
905 if ( !m_submitQueue.isEmpty() )
906 if ( m_submitQueue.first()->playStartTime() <= limit )
907 limit = m_submitQueue.first()->playStartTime();
909 if( m_lastSubmissionFinishTime + m_fakeQueue.first()->length() <= limit )
911 item = m_fakeQueue.first();
912 m_fakeQueue.removeFirst();
913 // don't backdate earlier than we have to
914 if( m_lastSubmissionFinishTime + m_fakeQueueLength < limit )
915 item->m_playStartTime = limit - m_fakeQueueLength;
916 else
917 item->m_playStartTime = m_lastSubmissionFinishTime;
918 m_fakeQueueLength -= item->length();
922 if( !item && !m_submitQueue.isEmpty() )
924 item = m_submitQueue.first();
925 m_submitQueue.removeFirst();
928 if( item )
930 if( item->playStartTime() < m_lastSubmissionFinishTime )
932 // debug() << "play times screwed up? - " << item->artist() << " - " << item->title() << ": " << item->playStartTime() << " < " << m_lastSubmissionFinishTime;
934 int add = 30;
935 if( item->length() / 2 + 1 > add )
936 add = item->length() / 2 + 1;
937 if( item->playStartTime() + add > m_lastSubmissionFinishTime )
938 m_lastSubmissionFinishTime = item->playStartTime() + add;
940 // Save submit queue to disk so it is more uptodate in case of crash.
941 saveSubmitQueue();
944 return item;
949 * Enqueues items associated with the job. This is used when the job
950 * has failed (e.g. network problems).
952 void ScrobblerSubmitter::enqueueJob( KIO::Job* job )
954 SubmitItem *lastItem = 0;
955 SubmitItem *item = 0;
956 int counter = 0;
957 while ( ( item = m_ongoingSubmits.take( job ) ) != 0 )
959 counter++;
960 lastItem = item;
961 enqueueItem( item );
964 if( lastItem )
965 announceSubmit( lastItem, counter, false );
967 schedule( true ); // arrange to flush queue after failure
972 * Deletes items associated with the job. This is used when the job
973 * has succeeded.
975 void ScrobblerSubmitter::finishJob( KIO::Job* job )
977 SubmitItem *firstItem = 0;
978 SubmitItem *item = 0;
979 int counter = 0;
980 while ( ( item = m_ongoingSubmits.take( job ) ) != 0 )
982 counter++;
983 if ( firstItem == 0 )
984 firstItem = item;
985 else
986 delete item;
989 if( firstItem )
990 announceSubmit( firstItem, counter, true );
991 delete firstItem;
993 schedule( false ); // arrange to flush rest of queue
998 * Announces on StatusBar if the submit was successful or not.
1000 * @param item One of the items
1001 * @param tracks Amount of tracks that were submitted
1002 * @param success Indicates if the submission was successful or not
1004 void ScrobblerSubmitter::announceSubmit( SubmitItem *item, int tracks, bool success ) const
1006 QString _long, _short;
1008 if ( success )
1010 if ( tracks == 1 )
1011 _short = i18n( "'%1' submitted to last.fm" ).arg( item->title() );
1012 else
1014 _short = i18n( "Several tracks submitted to last.fm" );
1016 _long = "<p>";
1017 _long = i18np( "'%1' and one other track submitted",
1018 "'%1' and %1 other tracks submitted", tracks-1 )
1019 .arg( item->title() );
1022 else
1024 if ( tracks == 1 )
1025 _short = i18n( "Failed to submit '%1' to last.fm" ).arg( item->title() );
1026 else
1028 _short = i18n( "Failed to submit several tracks to last.fm" );
1029 _long = "<p>";
1030 _long = i18np( "Failed to submit '%1' and one other track",
1031 "Failed to submit '%1' and %1 other tracks", tracks-1 )
1032 .arg( item->title() );
1036 if ( m_submitQueue.count() + m_fakeQueue.count() > 0 )
1038 _long += "<p>";
1039 _long += i18np( "One track still in queue", "%1 tracks still in queue",
1040 m_submitQueue.count() + m_fakeQueue.count() );
1043 Amarok::ContextStatusBar::instance()->shortLongMessage( _short, _long );
1047 void ScrobblerSubmitter::saveSubmitQueue()
1049 QFile file( m_savePath );
1051 if( !file.open( QIODevice::WriteOnly ) )
1053 debug() << "[SCROBBLER] Couldn't write submit queue to file: " << m_savePath;
1054 return;
1057 if ( m_lastSubmissionFinishTime == 0 )
1058 m_lastSubmissionFinishTime = QDateTime::currentDateTime().toUTC().toTime_t();
1060 QDomDocument newdoc;
1061 QDomElement submitQueue = newdoc.createElement( "submit" );
1062 submitQueue.setAttribute( "product", "Amarok" );
1063 submitQueue.setAttribute( "version", APP_VERSION );
1064 submitQueue.setAttribute( "lastSubmissionFinishTime", m_lastSubmissionFinishTime );
1066 for ( int idx = 0; idx < m_submitQueue.count(); idx++ )
1068 SubmitItem *item = m_submitQueue.at( idx );
1069 QDomElement i = item->toDomElement( newdoc );
1070 submitQueue.appendChild( i );
1073 for ( int idx = 0; idx < m_fakeQueue.count(); idx++ )
1075 SubmitItem *item = m_fakeQueue.at( idx );
1076 QDomElement i = item->toDomElement( newdoc );
1077 submitQueue.appendChild( i );
1080 QDomNode submitNode = newdoc.importNode( submitQueue, true );
1081 newdoc.appendChild( submitNode );
1083 QTextStream stream( &file );
1084 stream.setCodec( "UTF8" );
1085 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1086 stream << newdoc.toString();
1087 file.close();
1091 void ScrobblerSubmitter::readSubmitQueue()
1093 m_savePath = Amarok::saveLocation() + "submit.xml";
1094 QFile file( m_savePath );
1096 if ( !file.open( QIODevice::ReadOnly ) )
1098 debug() << "Couldn't open file: " << m_savePath;
1099 return;
1102 QTextStream stream( &file );
1103 stream.setCodec( "UTF8" );
1105 QDomDocument d;
1106 if( !d.setContent( stream.readAll() ) )
1108 debug() << "Couldn't read file: " << m_savePath;
1109 return;
1112 uint last = 0;
1113 if( d.namedItem( "submit" ).isElement() )
1114 last = d.namedItem( "submit" ).toElement().attribute( "lastSubmissionFinishTime" ).toUInt();
1115 if(last && last > m_lastSubmissionFinishTime)
1116 m_lastSubmissionFinishTime = last;
1118 const QString ITEM( "item" ); //so we don't construct these QStrings all the time
1120 for( QDomNode n = d.namedItem( "submit" ).firstChild(); !n.isNull() && n.nodeName() == ITEM; n = n.nextSibling() )
1121 enqueueItem( new SubmitItem( n.toElement() ) );
1126 * Schedules an Audioscrobbler handshake or submit as required.
1127 * Returns true if an immediate submit was possible
1129 bool ScrobblerSubmitter::schedule( bool failure )
1131 m_timer.stop();
1132 if ( m_inProgress || !canSubmit() )
1133 return false;
1135 uint when, currentTime = QDateTime::currentDateTime().toUTC().toTime_t();
1136 if ( currentTime - m_prevSubmitTime > m_interval )
1137 when = 0;
1138 else
1139 when = m_interval - ( currentTime - m_prevSubmitTime );
1141 if ( failure )
1143 m_backoff = qMin( qMax( m_backoff * 2, unsigned( MIN_BACKOFF ) ), unsigned( MAX_BACKOFF ) );
1144 when = qMax( m_backoff, m_interval );
1146 else
1147 m_backoff = 0;
1149 if ( m_needHandshake || m_challenge.isEmpty() )
1151 m_challenge = QString();
1152 m_needHandshake = false;
1154 if ( when == 0 )
1156 debug() << "Performing immediate handshake";
1157 performHandshake();
1159 else
1161 debug() << "Performing handshake in " << when << " seconds";
1162 m_timer.setSingleShot( true );
1163 m_timer.start( when * 1000 );
1166 else if ( !m_submitQueue.isEmpty() || !m_holdFakeQueue && !m_fakeQueue.isEmpty() )
1168 // if we only have stuff in the fake queue, we need to only schedule for when we can actually do something with it
1169 if ( m_submitQueue.isEmpty() && m_lastSubmissionFinishTime + m_fakeQueue.first()->length() > currentTime + when )
1170 when = m_lastSubmissionFinishTime + m_fakeQueue.first()->length() - currentTime;
1172 if ( when == 0 )
1174 debug() << "Performing immediate submit";
1175 performSubmit();
1176 return true;
1178 else
1180 debug() << "Performing submit in " << when << " seconds";
1181 m_timer.setSingleShot( true );
1182 m_timer.start( when * 1000 );
1184 } else {
1185 debug() << "Nothing to schedule";
1188 return false;
1192 #include "scrobbler.moc"