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"
29 #include "amarokconfig.h"
30 #include "config-amarok.h"
32 #include "enginecontroller.h"
33 #include "metabundle.h"
34 #include "ContextStatusBar.h"
36 #include <KApplication>
39 #include <kio/jobclasses.h>
41 #include <KStandardDirs>
44 #include <QtAlgorithms>
46 #include <QHashIterator>
47 #include <QTextStream>
51 //some setups require this
52 #undef PROTOCOL_VERSION
55 ////////////////////////////////////////////////////////////////////////////////
57 ////////////////////////////////////////////////////////////////////////////////
59 Scrobbler
* Scrobbler::instance()
61 static Scrobbler scrobbler
;
66 Scrobbler::Scrobbler()
67 : EngineObserver( EngineController::instance() )
68 , m_similarArtistsJob( 0 )
69 , m_validForSending( false )
71 , m_submitter( new ScrobblerSubmitter() )
72 , m_item( new SubmitItem() )
76 Scrobbler::~Scrobbler()
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();
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
117 warning() << "KIO error! errno: " << job
->error();
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="">
125 // <name>Iron Maiden</name>
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>
135 QDomDocument document
;
136 if ( !document
.setContent( m_similarArtistsBuffer
) )
138 debug() << "Couldn't read similar artists response";
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;
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() );
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
189 m_timer
.setSingleShot( true );
190 m_timer
.start( 10000 );
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;
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;
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
)
245 m_validForSending
= false;
246 debug() << "Won't submit: Seek detected.";
250 if ( m_timer
.isActive() )
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
) );
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 ////////////////////////////////////////////////////////////////////////////////
277 ////////////////////////////////////////////////////////////////////////////////
280 SubmitItem::SubmitItem(
281 const QString
& artist
,
282 const QString
& album
,
283 const QString
& title
,
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()
307 , m_playStartTime( 0 )
312 bool SubmitItem::operator==( const SubmitItem
& item
)
316 if ( m_artist
!= item
.artist() || m_album
!= item
.album() || m_title
!= item
.title() ||
317 m_length
!= item
.length() || m_playStartTime
!= item
.playStartTime() )
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
);
361 ////////////////////////////////////////////////////////////////////////////////
363 ////////////////////////////////////////////////////////////////////////////////
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()
388 , m_scrobblerEnabled( false )
389 , m_holdFakeQueue( false )
390 , m_inProgress( false )
391 , m_needHandshake( true )
392 , m_prevSubmitTime( 0 )
395 , m_lastSubmissionFinishTime( 0 )
396 , m_fakeQueueLength( 0 )
398 connect( &m_timer
, SIGNAL(timeout()), this, SLOT(scheduledTimeReached()) );
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
);
416 qDeleteAll( m_submitQueue
);
417 m_submitQueue
.clear();
418 qDeleteAll( m_fakeQueue
);
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
446 .arg( PROTOCOL_VERSION
)
448 .arg( CLIENT_VERSION
)
452 else if ( PROTOCOL_VERSION
== "1.2" )
454 // Audioscrobbler protocol 1.2 (RFC)
455 // http://post.audioscrobbler.com/?hs=true
458 // &v=<clientversion>
460 // &t=<unix_timestamp>
471 .arg( PROTOCOL_VERSION
)
473 .arg( CLIENT_VERSION
)
476 .arg( QString::fromAscii( KMD5( KMD5( m_password
.toUtf8() ).hexDigest() + QString::number( currentTime
).toAscii() ).hexDigest() ) );
481 debug() << "Handshake not implemented for protocol version: " << PROTOCOL_VERSION
;
485 debug() << "Handshake url: " << handshakeUrl
;
487 m_submitResultBuffer
= "";
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
) {
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()
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
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>&
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>&
541 "u=" + KUrl::toPercentEncoding( m_username
, "/" ) +
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!";
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
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" ), "/" );
584 debug() << "Submit not implemented for protocol version: " << PROTOCOL_VERSION
;
588 debug() << "Submit data: " << data
;
590 m_submitResultBuffer
= "";
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
;
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
);
635 m_fakeQueueLength
= 0;
642 * Sync from external device complete, can send them off
644 void ScrobblerSubmitter::syncComplete()
646 m_holdFakeQueue
= false;
653 * Called when timer set up in the schedule function goes off.
655 void ScrobblerSubmitter::scheduledTimeReached()
657 if ( m_needHandshake
|| m_challenge
.isEmpty() )
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();
677 KIO::StoredTransferJob
* const storedJob
= static_cast<KIO::StoredTransferJob
*>( job
);
678 m_submitResultBuffer
= QString::fromUtf8( storedJob
->data().data(), storedJob
->data().size() );
681 // << "Handshake result received: "
682 // << endl << m_submitResultBuffer;
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)>
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();
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();
758 // << "Submit result received: "
759 // << endl << m_submitResultBuffer;
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();
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();
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();
803 warning() << "Unknown submit response";
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";
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();
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();
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.
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
;
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();
930 if( item
->playStartTime() < m_lastSubmissionFinishTime
)
932 // debug() << "play times screwed up? - " << item->artist() << " - " << item->title() << ": " << item->playStartTime() << " < " << m_lastSubmissionFinishTime;
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.
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;
957 while ( ( item
= m_ongoingSubmits
.take( job
) ) != 0 )
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
975 void ScrobblerSubmitter::finishJob( KIO::Job
* job
)
977 SubmitItem
*firstItem
= 0;
978 SubmitItem
*item
= 0;
980 while ( ( item
= m_ongoingSubmits
.take( job
) ) != 0 )
983 if ( firstItem
== 0 )
990 announceSubmit( firstItem
, counter
, true );
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
;
1011 _short
= i18n( "'%1' submitted to last.fm" ).arg( item
->title() );
1014 _short
= i18n( "Several tracks submitted to last.fm" );
1017 _long
= i18np( "'%1' and one other track submitted",
1018 "'%1' and %1 other tracks submitted", tracks
-1 )
1019 .arg( item
->title() );
1025 _short
= i18n( "Failed to submit '%1' to last.fm" ).arg( item
->title() );
1028 _short
= i18n( "Failed to submit several tracks to last.fm" );
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 )
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
;
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();
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
;
1102 QTextStream
stream( &file
);
1103 stream
.setCodec( "UTF8" );
1106 if( !d
.setContent( stream
.readAll() ) )
1108 debug() << "Couldn't read file: " << m_savePath
;
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
)
1132 if ( m_inProgress
|| !canSubmit() )
1135 uint when
, currentTime
= QDateTime::currentDateTime().toUTC().toTime_t();
1136 if ( currentTime
- m_prevSubmitTime
> m_interval
)
1139 when
= m_interval
- ( currentTime
- m_prevSubmitTime
);
1143 m_backoff
= qMin( qMax( m_backoff
* 2, unsigned( MIN_BACKOFF
) ), unsigned( MAX_BACKOFF
) );
1144 when
= qMax( m_backoff
, m_interval
);
1149 if ( m_needHandshake
|| m_challenge
.isEmpty() )
1151 m_challenge
= QString();
1152 m_needHandshake
= false;
1156 debug() << "Performing immediate handshake";
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
;
1174 debug() << "Performing immediate submit";
1180 debug() << "Performing submit in " << when
<< " seconds";
1181 m_timer
.setSingleShot( true );
1182 m_timer
.start( when
* 1000 );
1185 debug() << "Nothing to schedule";
1192 #include "scrobbler.moc"