File indexing completed on 2024-04-21 04:47:50

0001 /****************************************************************************************
0002  * Copyright (c) 2004 Frederik Holljen <fh@ez.no>                                       *
0003  * Copyright (c) 2004,2005 Max Howell <max.howell@methylblue.com>                       *
0004  * Copyright (c) 2004-2013 Mark Kretschmann <kretschmann@kde.org>                       *
0005  * Copyright (c) 2006,2008 Ian Monroe <ian@monroe.nu>                                   *
0006  * Copyright (c) 2008 Jason A. Donenfeld <Jason@zx2c4.com>                              *
0007  * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0008  * Copyright (c) 2009 Artur Szymiec <artur.szymiec@gmail.com>                           *
0009  *                                                                                      *
0010  * This program is free software; you can redistribute it and/or modify it under        *
0011  * the terms of the GNU General Public License as published by the Free Software        *
0012  * Foundation; either version 2 of the License, or (at your option) any later           *
0013  * version.                                                                             *
0014  *                                                                                      *
0015  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0016  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0017  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0018  *                                                                                      *
0019  * You should have received a copy of the GNU General Public License along with         *
0020  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0021  ****************************************************************************************/
0022 
0023 #define DEBUG_PREFIX "EngineController"
0024 
0025 #include "EngineController.h"
0026 
0027 #include "MainWindow.h"
0028 #include "amarokconfig.h"
0029 #include "core-impl/collections/support/CollectionManager.h"
0030 #include "core/capabilities/MultiPlayableCapability.h"
0031 #include "core/capabilities/MultiSourceCapability.h"
0032 #include "core/capabilities/SourceInfoCapability.h"
0033 #include "core/logger/Logger.h"
0034 #include "core/meta/Meta.h"
0035 #include "core/meta/support/MetaConstants.h"
0036 #include "core/meta/support/MetaUtility.h"
0037 #include "core/support/Amarok.h"
0038 #include "core/support/Components.h"
0039 #include "core/support/Debug.h"
0040 #include "playback/DelayedDoers.h"
0041 #include "playback/Fadeouter.h"
0042 #include "playback/PowerManager.h"
0043 #include "playlist/PlaylistActions.h"
0044 
0045 #include <phonon/AudioOutput>
0046 #include <phonon/BackendCapabilities>
0047 #include <phonon/MediaObject>
0048 #include <phonon/VolumeFaderEffect>
0049 
0050 #include <QCoreApplication>
0051 #include <QUrlQuery>
0052 #include <QTimer>
0053 #include <QtMath>
0054 
0055 #include <KLocalizedString>
0056 
0057 #include <thread>
0058 
0059 
0060 // for slotMetaDataChanged()
0061 typedef QPair<Phonon::MetaData, QString> FieldPair;
0062 
0063 namespace The {
0064     EngineController* engineController() { return EngineController::instance(); }
0065 }
0066 
0067 EngineController *
0068 EngineController::instance()
0069 {
0070     return Amarok::Components::engineController();
0071 }
0072 
0073 EngineController::EngineController()
0074     : m_boundedPlayback( nullptr )
0075     , m_multiPlayback( nullptr )
0076     , m_multiSource( nullptr )
0077     , m_playWhenFetched( true )
0078     , m_volume( 0 )
0079     , m_currentAudioCdTrack( 0 )
0080     , m_pauseTimer( new QTimer( this ) )
0081     , m_lastStreamStampPosition( -1 )
0082     , m_ignoreVolumeChangeAction ( false )
0083     , m_ignoreVolumeChangeObserve ( false )
0084     , m_tickInterval( 0 )
0085     , m_lastTickPosition( -1 )
0086     , m_lastTickCount( 0 )
0087     , m_mutex( QMutex::Recursive )
0088 {
0089     DEBUG_BLOCK
0090     // ensure this object is created in a main thread
0091     Q_ASSERT( thread() == QCoreApplication::instance()->thread() );
0092 
0093     connect( this, &EngineController::fillInSupportedMimeTypes, this, &EngineController::slotFillInSupportedMimeTypes );
0094     connect( this, &EngineController::trackFinishedPlaying, this, &EngineController::slotTrackFinishedPlaying );
0095 
0096     new PowerManager( this ); // deals with inhibiting suspend etc.
0097 
0098     m_pauseTimer->setSingleShot( true );
0099     connect( m_pauseTimer, &QTimer::timeout, this, &EngineController::slotPause );
0100     m_equalizerController = new EqualizerController( this );
0101 }
0102 
0103 EngineController::~EngineController()
0104 {
0105     DEBUG_BLOCK //we like to know when singletons are destroyed
0106 
0107     // don't do any of the after-processing that normally happens when
0108     // the media is stopped - that's what endSession() is for
0109     if( m_media )
0110     {
0111         m_media->blockSignals(true);
0112         m_media->stop();
0113     }
0114 
0115     delete m_boundedPlayback;
0116     m_boundedPlayback = nullptr;
0117     delete m_multiPlayback; // need to get a new instance of multi if played again
0118     m_multiPlayback = nullptr;
0119 
0120     delete m_media.data();
0121     delete m_audio.data();
0122     delete m_audioDataOutput.data();
0123 }
0124 
0125 void
0126 EngineController::initializePhonon()
0127 {
0128     DEBUG_BLOCK
0129 
0130     m_path.disconnect();
0131     m_dataPath.disconnect();
0132 
0133     // QWeakPointers reset themselves to null if the object is deleted
0134     delete m_media.data();
0135     delete m_controller.data();
0136     delete m_audio.data();
0137     delete m_audioDataOutput.data();
0138     delete m_preamp.data();
0139     delete m_fader.data();
0140 
0141     using namespace Phonon;
0142     PERF_LOG( "EngineController: loading phonon objects" )
0143     m_media = new MediaObject( this );
0144 
0145     // Enable zeitgeist support on linux
0146     //TODO: make this configurable by the user.
0147     m_media->setProperty( "PlaybackTracking", true );
0148 
0149     m_audio = new AudioOutput( MusicCategory, this );
0150     m_audioDataOutput = new AudioDataOutput( this );
0151     m_audioDataOutput->setDataSize( DATAOUTPUT_DATA_SIZE ); // The number of samples that Phonon sends per signal
0152 
0153     m_path = createPath( m_media.data(), m_audio.data() );
0154 
0155     m_controller = new MediaController( m_media.data() );
0156 
0157     m_equalizerController->initialize( m_path );
0158 
0159     // HACK we turn off replaygain manually on OSX, until the phonon coreaudio backend is fixed.
0160     // as the default is specified in the .cfg file, we can't just tell it to be a different default on OSX
0161 #ifdef Q_WS_MAC
0162     AmarokConfig::setReplayGainMode( AmarokConfig::EnumReplayGainMode::Off );
0163     AmarokConfig::setFadeoutOnStop( false );
0164 #endif
0165 
0166     // we now try to create pre-amp unconditionally, however we check that it is valid.
0167     // So now m_preamp is null   equals   not available at all
0168     QScopedPointer<VolumeFaderEffect> preamp( new VolumeFaderEffect( this ) );
0169     if( preamp->isValid() )
0170     {
0171         m_preamp = preamp.take();
0172         m_path.insertEffect( m_preamp.data() );
0173     }
0174 
0175     QScopedPointer<VolumeFaderEffect> fader( new VolumeFaderEffect( this ) );
0176     if( fader->isValid() )
0177     {
0178         fader->setFadeCurve( VolumeFaderEffect::Fade9Decibel );
0179         m_fader = fader.take();
0180         m_path.insertEffect( m_fader.data() );
0181         m_dataPath = createPath( m_fader.data(), m_audioDataOutput.data() );
0182     }
0183     else
0184         m_dataPath = createPath( m_media.data(), m_audioDataOutput.data() );
0185 
0186     m_media.data()->setTickInterval( 100 );
0187     m_tickInterval = m_media.data()->tickInterval();
0188     debug() << "Tick Interval (actual): " << m_tickInterval;
0189     PERF_LOG( "EngineController: loaded phonon objects" )
0190 
0191     // Get the next track when there is 2 seconds left on the current one.
0192     m_media.data()->setPrefinishMark( 2000 );
0193 
0194     connect( m_media.data(), &MediaObject::finished, this, &EngineController::slotFinished );
0195     connect( m_media.data(), &MediaObject::aboutToFinish, this, &EngineController::slotAboutToFinish );
0196     connect( m_media.data(), &MediaObject::metaDataChanged, this, &EngineController::slotMetaDataChanged );
0197     connect( m_media.data(), &MediaObject::stateChanged, this, &EngineController::slotStateChanged );
0198     connect( m_media.data(), &MediaObject::tick, this, &EngineController::slotTick );
0199     connect( m_media.data(), &MediaObject::totalTimeChanged, this, &EngineController::slotTrackLengthChanged );
0200     connect( m_media.data(), &MediaObject::currentSourceChanged, this, &EngineController::slotNewTrackPlaying );
0201     connect( m_media.data(), &MediaObject::seekableChanged, this, &EngineController::slotSeekableChanged );
0202     connect( m_audio.data(), &AudioOutput::volumeChanged, this, &EngineController::slotVolumeChanged );
0203     connect( m_audio.data(), &AudioOutput::mutedChanged, this, &EngineController::slotMutedChanged );
0204     connect( m_audioDataOutput.data(), &AudioDataOutput::dataReady, this, &EngineController::audioDataReady );
0205 
0206     connect( m_controller.data(), &MediaController::titleChanged, this, &EngineController::slotTitleChanged );
0207 
0208     // Read the volume from phonon
0209     m_volume = qBound<qreal>( 0, qRound(m_audio.data()->volume()*100), 100 );
0210 
0211     if( m_currentTrack )
0212     {
0213         unsubscribeFrom( m_currentTrack );
0214         m_currentTrack.clear();
0215     }
0216     if( m_currentAlbum )
0217     {
0218         unsubscribeFrom( m_currentAlbum );
0219         m_currentAlbum.clear();
0220     }
0221 }
0222 
0223 
0224 //////////////////////////////////////////////////////////////////////////////////////////
0225 // PUBLIC
0226 //////////////////////////////////////////////////////////////////////////////////////////
0227 
0228 
0229 QStringList
0230 EngineController::supportedMimeTypes()
0231 {
0232     // this ensures that slotFillInSupportedMimeTypes() is called in the main thread. It
0233     // will be called directly if we are called in the main thread (so that no deadlock
0234     // can occur) and indirectly if we are called in non-main thread.
0235     Q_EMIT fillInSupportedMimeTypes();
0236 
0237     // ensure slotFillInSupportedMimeTypes() called above has already finished:
0238     m_supportedMimeTypesSemaphore.acquire();
0239     return m_supportedMimeTypes;
0240 }
0241 
0242 void
0243 EngineController::slotFillInSupportedMimeTypes()
0244 {
0245     // we assume non-empty == already filled in
0246     if( !m_supportedMimeTypes.isEmpty() )
0247     {
0248         // unblock waiting for the semaphore in supportedMimeTypes():
0249         m_supportedMimeTypesSemaphore.release();
0250         return;
0251     }
0252 
0253     QRegExp avFilter( "^(audio|video)/", Qt::CaseInsensitive );
0254     m_supportedMimeTypes = Phonon::BackendCapabilities::availableMimeTypes().filter( avFilter );
0255 
0256     // Add whitelist hacks
0257     // MP4 Audio Books have a different extension that KFileItem/Phonon don't grok
0258     if( !m_supportedMimeTypes.contains( "audio/x-m4b" ) )
0259         m_supportedMimeTypes << "audio/x-m4b";
0260 
0261     // technically, "audio/flac" is not a valid mimetype (not on IANA list), but some things expect it
0262     if( m_supportedMimeTypes.contains( "audio/x-flac" ) && !m_supportedMimeTypes.contains( "audio/flac" ) )
0263         m_supportedMimeTypes << "audio/flac";
0264 
0265     // technically, "audio/mp4" is the official mime type, but sometimes Phonon returns audio/x-m4a
0266     if( m_supportedMimeTypes.contains( "audio/x-m4a" ) && !m_supportedMimeTypes.contains( "audio/mp4" ) )
0267         m_supportedMimeTypes << "audio/mp4";
0268 
0269     // unblock waiting for the semaphore in supportedMimeTypes(). We can over-shoot
0270     // resource number so that next call to supportedMimeTypes won't have to
0271     // wait for main loop; this is however just an optimization and we could have safely
0272     // released just one resource. Note that this code-path is reached only once, so
0273     // overflow cannot happen.
0274     m_supportedMimeTypesSemaphore.release( 100000 );
0275 }
0276 
0277 void
0278 EngineController::restoreSession()
0279 {
0280     //here we restore the session
0281     //however, do note, this is always done, KDE session management is not involved
0282 
0283     if( AmarokConfig::resumePlayback() )
0284     {
0285         const QUrl url = QUrl::fromUserInput(AmarokConfig::resumeTrack());
0286         Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
0287 
0288         // Only give a resume time for local files, because resuming remote protocols can have weird side effects.
0289         // See: https://bugs.kde.org/show_bug.cgi?id=172897
0290         if( url.isLocalFile() )
0291             play( track, AmarokConfig::resumeTime(), AmarokConfig::resumePaused() );
0292         else
0293             play( track, 0, AmarokConfig::resumePaused() );
0294     }
0295 }
0296 
0297 void
0298 EngineController::endSession()
0299 {
0300     //only update song stats, when we're not going to resume it
0301     if ( !AmarokConfig::resumePlayback() && m_currentTrack )
0302     {
0303         Q_EMIT stopped( trackPositionMs(), m_currentTrack->length() );
0304         unsubscribeFrom( m_currentTrack );
0305         if( m_currentAlbum )
0306             unsubscribeFrom( m_currentAlbum );
0307         Q_EMIT trackChanged( Meta::TrackPtr( nullptr ) );
0308     }
0309     Q_EMIT sessionEnded( AmarokConfig::resumePlayback() && m_currentTrack );
0310 }
0311 
0312 EqualizerController*
0313 EngineController::equalizerController() const
0314 {
0315     return m_equalizerController;
0316 }
0317 
0318 //////////////////////////////////////////////////////////////////////////////////////////
0319 // PUBLIC SLOTS
0320 //////////////////////////////////////////////////////////////////////////////////////////
0321 
0322 void
0323 EngineController::play() //SLOT
0324 {
0325     DEBUG_BLOCK
0326 
0327     if( isPlaying() )
0328         return;
0329 
0330     if( isPaused() )
0331     {
0332         if( m_currentTrack && m_currentTrack->type() == "stream" )
0333         {
0334             debug() << "This is a stream that cannot be resumed after pausing. Restarting instead.";
0335             play( m_currentTrack );
0336             return;
0337         }
0338         else
0339         {
0340             m_pauseTimer->stop();
0341             if( supportsFadeout() )
0342                 m_fader->setVolume( 1.0 );
0343             m_media->play();
0344             Q_EMIT trackPlaying( m_currentTrack );
0345             return;
0346         }
0347     }
0348 
0349     The::playlistActions()->play();
0350 }
0351 
0352 void
0353 EngineController::play( Meta::TrackPtr track, uint offset, bool startPaused )
0354 {
0355     DEBUG_BLOCK
0356 
0357     if( !track ) // Guard
0358         return;
0359 
0360     // clear the current track without sending playbackEnded or trackChangeNotify yet
0361     stop( /* forceInstant */ true, /* playingWillContinue */ true );
0362 
0363     // we grant exclusive access to setting new m_currentTrack to newTrackPlaying()
0364     m_nextTrack = track;
0365     debug() << "play: bounded is "<<m_boundedPlayback<<"current"<<track->name();
0366     m_boundedPlayback = track->create<Capabilities::BoundedPlaybackCapability>();
0367     m_multiPlayback = track->create<Capabilities::MultiPlayableCapability>();
0368 
0369     track->prepareToPlay();
0370     m_nextUrl = track->playableUrl();
0371 
0372     if( m_multiPlayback )
0373     {
0374         connect( m_multiPlayback, &Capabilities::MultiPlayableCapability::playableUrlFetched,
0375                  this, &EngineController::slotPlayableUrlFetched );
0376         m_multiPlayback->fetchFirst();
0377     }
0378     else if( m_boundedPlayback )
0379     {
0380         debug() << "Starting bounded playback of url " << track->playableUrl() << " at position " << m_boundedPlayback->startPosition();
0381         playUrl( track->playableUrl(), m_boundedPlayback->startPosition(), startPaused );
0382     }
0383     else
0384     {
0385         debug() << "Just a normal, boring track... :-P";
0386         playUrl( track->playableUrl(), offset, startPaused );
0387     }
0388 }
0389 
0390 void
0391 EngineController::replay() // slot
0392 {
0393     DEBUG_BLOCK
0394 
0395     seekTo( 0 );
0396     Q_EMIT trackPositionChanged( 0, true );
0397 }
0398 
0399 void
0400 EngineController::playUrl( const QUrl &url, uint offset, bool startPaused )
0401 {
0402     DEBUG_BLOCK
0403 
0404     m_media->stop();
0405 
0406     debug() << "URL: " << url << url.url();
0407     debug() << "Offset: " << offset;
0408 
0409     m_currentAudioCdTrack = 0;
0410     if( url.scheme() == "audiocd" )
0411     {
0412         QStringList pathItems = url.path().split( QLatin1Char('/'), Qt::KeepEmptyParts );
0413         if( pathItems.count() != 3 )
0414         {
0415             error() << __PRETTY_FUNCTION__ << url.url() << "is not in expected format";
0416             return;
0417         }
0418         bool ok = false;
0419         int trackNumber = pathItems.at( 2 ).toInt( &ok );
0420         if( !ok || trackNumber <= 0 )
0421         {
0422             error() << __PRETTY_FUNCTION__ << "failed to get positive track number from" << url.url();
0423             return;
0424         }
0425         QString device = QUrlQuery(url).queryItemValue( "device" );
0426 
0427         m_media->setCurrentSource( Phonon::MediaSource( Phonon::Cd, device ) );
0428         m_currentAudioCdTrack = trackNumber;
0429     }
0430     else
0431     {
0432         // keep in sync with setNextTrack(), slotPlayableUrlFetched()
0433         m_media->setCurrentSource( url );
0434     }
0435 
0436     m_media->clearQueue();
0437 
0438     if( m_currentAudioCdTrack )
0439     {
0440         // call to play() is asynchronous and ->setCurrentTitle() can be only called on
0441         // playing, buffering or paused media.
0442         m_media->pause();
0443         DelayedTrackChanger *trackChanger = new DelayedTrackChanger( m_media.data(),
0444                 m_controller.data(), m_currentAudioCdTrack, offset, startPaused );
0445         connect( trackChanger, &DelayedTrackChanger::trackPositionChanged,
0446                  this, &EngineController::trackPositionChanged );
0447     }
0448     else if( offset )
0449     {
0450         // call to play() is asynchronous and ->seek() can be only called on playing,
0451         // buffering or paused media. Calling play() would lead to audible glitches,
0452         // so call pause() that doesn't suffer from such problem.
0453         m_media->pause();
0454         DelayedSeeker *seeker = new DelayedSeeker( m_media.data(), offset, startPaused );
0455         connect( seeker, &DelayedSeeker::trackPositionChanged,
0456                  this, &EngineController::trackPositionChanged );
0457     }
0458     else
0459     {
0460         if( startPaused )
0461         {
0462             m_media->pause();
0463         }
0464         else
0465         {
0466             m_pauseTimer->stop();
0467             if( supportsFadeout() )
0468                 m_fader->setVolume( 1.0 );
0469             m_media->play();
0470         }
0471     }
0472 }
0473 
0474 void
0475 EngineController::pause() //SLOT
0476 {
0477     if( supportsFadeout() && AmarokConfig::fadeoutOnPause() )
0478     {
0479         m_fader->fadeOut( AmarokConfig::fadeoutLength() );
0480         m_pauseTimer->start( AmarokConfig::fadeoutLength() + 500 );
0481         return;
0482     }
0483 
0484     slotPause();
0485 }
0486 
0487 void
0488 EngineController::slotPause()
0489 {
0490     if( supportsFadeout() && AmarokConfig::fadeoutOnPause() )
0491     {
0492         // Reset VolumeFaderEffect to full volume
0493         m_fader->setVolume( 1.0 );
0494 
0495         // Wait a bit before pausing the pipeline. Necessary for the new fader setting to take effect.
0496         QTimer::singleShot( 1000, m_media.data(), &Phonon::MediaObject::pause );
0497     }
0498     else
0499     {
0500         m_media->pause();
0501     }
0502 
0503     Q_EMIT paused();
0504 }
0505 
0506 void
0507 EngineController::stop( bool forceInstant, bool playingWillContinue ) //SLOT
0508 {
0509     DEBUG_BLOCK
0510 
0511     /* Only do fade-out when all conditions are met:
0512      * a) instant stop is not requested
0513      * b) we aren't already in a fadeout
0514      * c) we are currently playing (not paused etc.)
0515      * d) Amarok is configured to fadeout at all
0516      * e) configured fadeout length is positive
0517      * f) Phonon fader to do it is actually available
0518      */
0519     bool doFadeOut = !forceInstant
0520                   && !m_fadeouter
0521                   && m_media->state() == Phonon::PlayingState
0522                   && AmarokConfig::fadeoutOnStop()
0523                   && AmarokConfig::fadeoutLength() > 0
0524                   && m_fader;
0525 
0526     // let Amarok know that the previous track is no longer playing; if we will fade-out
0527     // ::stop() is called after the fade by Fadeouter.
0528     if( m_currentTrack && !doFadeOut )
0529     {
0530         unsubscribeFrom( m_currentTrack );
0531         if( m_currentAlbum )
0532             unsubscribeFrom( m_currentAlbum );
0533         const qint64 pos = trackPositionMs();
0534         // updateStreamLength() intentionally not here, we're probably in the middle of a track
0535         const qint64 length = trackLength();
0536         Q_EMIT trackFinishedPlaying( m_currentTrack, pos / qMax<double>( length, pos ) );
0537 
0538         m_currentTrack = nullptr;
0539         m_currentAlbum = nullptr;
0540         if( !playingWillContinue )
0541         {
0542             Q_EMIT stopped( pos, length );
0543             Q_EMIT trackChanged( m_currentTrack );
0544         }
0545     }
0546 
0547     {
0548         QMutexLocker locker( &m_mutex );
0549         delete m_boundedPlayback;
0550         m_boundedPlayback = nullptr;
0551         delete m_multiPlayback; // need to get a new instance of multi if played again
0552         m_multiPlayback = nullptr;
0553         m_multiSource.reset();
0554 
0555         m_nextTrack.clear();
0556         m_nextUrl.clear();
0557         m_media->clearQueue();
0558     }
0559 
0560     if( doFadeOut )
0561     {
0562         m_fadeouter = new Fadeouter( m_media, m_fader, AmarokConfig::fadeoutLength() );
0563         // even though we don't pass forceInstant, doFadeOut will be false because
0564         // m_fadeouter will be still valid
0565         connect( m_fadeouter.data(), &Fadeouter::fadeoutFinished,
0566                  this, &EngineController::regularStop );
0567     }
0568     else
0569     {
0570         m_media->stop();
0571         m_media->setCurrentSource( Phonon::MediaSource() );
0572     }
0573 }
0574 
0575 void
0576 EngineController::regularStop()
0577 {
0578     stop( false, false );
0579 }
0580 
0581 bool
0582 EngineController::isPaused() const
0583 {
0584     return m_media->state() == Phonon::PausedState;
0585 }
0586 
0587 bool
0588 EngineController::isPlaying() const
0589 {
0590     return !isPaused() && !isStopped();
0591 }
0592 
0593 bool
0594 EngineController::isStopped() const
0595 {
0596     return
0597         m_media->state() == Phonon::StoppedState ||
0598         m_media->state() == Phonon::LoadingState ||
0599         m_media->state() == Phonon::ErrorState;
0600 }
0601 
0602 void
0603 EngineController::playPause() //SLOT
0604 {
0605     DEBUG_BLOCK
0606     debug() << "PlayPause: EngineController state" << m_media->state();
0607 
0608     if( isPlaying() )
0609         pause();
0610     else
0611         play();
0612 }
0613 
0614 void
0615 EngineController::seekTo( int ms ) //SLOT
0616 {
0617     DEBUG_BLOCK
0618 
0619     if( m_media->isSeekable() )
0620     {
0621 
0622         debug() << "seek to: " << ms;
0623         int seekTo;
0624 
0625         if( m_boundedPlayback )
0626         {
0627             seekTo = m_boundedPlayback->startPosition() + ms;
0628             if( seekTo < m_boundedPlayback->startPosition() )
0629                 seekTo = m_boundedPlayback->startPosition();
0630             else if( seekTo > m_boundedPlayback->startPosition() + trackLength() )
0631                 seekTo = m_boundedPlayback->startPosition() + trackLength();
0632         }
0633         else
0634             seekTo = ms;
0635 
0636         m_media->seek( static_cast<qint64>( seekTo ) );
0637         Q_EMIT trackPositionChanged( seekTo, true ); /* User seek */
0638     }
0639     else
0640         debug() << "Stream is not seekable.";
0641 }
0642 
0643 
0644 void
0645 EngineController::seekBy( int ms ) //SLOT
0646 {
0647     qint64 newPos = m_media->currentTime() + ms;
0648     seekTo( newPos <= 0 ? 0 : newPos );
0649 }
0650 
0651 int
0652 EngineController::increaseVolume( int ticks ) //SLOT
0653 {
0654     return setVolume( volume() + ticks );
0655 }
0656 
0657 int
0658 EngineController::decreaseVolume( int ticks ) //SLOT
0659 {
0660     return setVolume( volume() - ticks );
0661 }
0662 
0663 int
0664 EngineController::regularIncreaseVolume() //SLOT
0665 {
0666     return increaseVolume();
0667 }
0668 
0669 int
0670 EngineController::regularDecreaseVolume() //SLOT
0671 {
0672     return decreaseVolume();
0673 }
0674 
0675 int
0676 EngineController::setVolume( int percent ) //SLOT
0677 {
0678     percent = qBound<qreal>( 0, percent, 100 );
0679     m_volume = percent;
0680 
0681     const qreal volume =  percent / 100.0;
0682     if ( !m_ignoreVolumeChangeAction && m_audio->volume() != volume )
0683     {
0684         m_ignoreVolumeChangeObserve = true;
0685         m_audio->setVolume( volume );
0686 
0687         AmarokConfig::setMasterVolume( percent );
0688         Q_EMIT volumeChanged( percent );
0689     }
0690     m_ignoreVolumeChangeAction = false;
0691 
0692     return percent;
0693 }
0694 
0695 int
0696 EngineController::volume() const
0697 {
0698     return m_volume;
0699 }
0700 
0701 bool
0702 EngineController::isMuted() const
0703 {
0704     return m_audio->isMuted();
0705 }
0706 
0707 void
0708 EngineController::setMuted( bool mute ) //SLOT
0709 {
0710     m_audio->setMuted( mute ); // toggle mute
0711     if( !isMuted() )
0712         setVolume( m_volume );
0713 
0714     AmarokConfig::setMuteState( mute );
0715     Q_EMIT muteStateChanged( mute );
0716 }
0717 
0718 void
0719 EngineController::toggleMute() //SLOT
0720 {
0721     setMuted( !isMuted() );
0722 }
0723 
0724 Meta::TrackPtr
0725 EngineController::currentTrack() const
0726 {
0727     return m_currentTrack;
0728 }
0729 
0730 qint64
0731 EngineController::trackLength() const
0732 {
0733     //When starting a last.fm stream, Phonon still shows the old track's length--trust
0734     //Meta::Track over Phonon
0735     if( m_currentTrack && m_currentTrack->length() > 0 )
0736         return m_currentTrack->length();
0737     else
0738         return m_media->totalTime(); //may return -1
0739 }
0740 
0741 void
0742 EngineController::setNextTrack( Meta::TrackPtr track )
0743 {
0744     DEBUG_BLOCK
0745     if( !track )
0746         return;
0747 
0748     track->prepareToPlay();
0749     QUrl url = track->playableUrl();
0750     if( url.isEmpty() )
0751         return;
0752 
0753     QMutexLocker locker( &m_mutex );
0754     if( isPlaying() )
0755     {
0756         m_media->clearQueue();
0757         // keep in sync with playUrl(), slotPlayableUrlFetched()
0758         if( url.scheme() != "audiocd" ) // we don't support gapless for CD, bug 305708
0759             m_media->enqueue( url );
0760         m_nextTrack = track;
0761         m_nextUrl = url;
0762     }
0763     else
0764         play( track );
0765 }
0766 
0767 bool
0768 EngineController::isStream()
0769 {
0770     Phonon::MediaSource::Type type = Phonon::MediaSource::Invalid;
0771     if( m_media )
0772         // type is determined purely from the MediaSource constructor used in
0773         // setCurrentSource(). For streams we use the QUrl one, see playUrl()
0774         type = m_media->currentSource().type();
0775     return type == Phonon::MediaSource::Url || type == Phonon::MediaSource::Stream;
0776 }
0777 
0778 bool
0779 EngineController::isSeekable() const
0780 {
0781     if( m_media )
0782         return m_media->isSeekable();
0783     return false;
0784 }
0785 
0786 int
0787 EngineController::trackPosition() const
0788 {
0789     return trackPositionMs() / 1000;
0790 }
0791 
0792 qint64
0793 EngineController::trackPositionMs() const
0794 {
0795     return m_media->currentTime();
0796 }
0797 
0798 bool
0799 EngineController::supportsFadeout() const
0800 {
0801     return m_fader;
0802 }
0803 
0804 bool EngineController::supportsGainAdjustments() const
0805 {
0806     return m_preamp;
0807 }
0808 
0809 bool EngineController::supportsAudioDataOutput() const
0810 {
0811     const Phonon::AudioDataOutput out;
0812     return out.isValid();
0813 }
0814 
0815 
0816 //////////////////////////////////////////////////////////////////////////////////////////
0817 // PRIVATE SLOTS
0818 //////////////////////////////////////////////////////////////////////////////////////////
0819 
0820 void
0821 EngineController::slotTick( qint64 position )
0822 {
0823     if( m_boundedPlayback )
0824     {
0825         qint64 newPosition = position;
0826         Q_EMIT trackPositionChanged(
0827                     static_cast<long>( position - m_boundedPlayback->startPosition() ),
0828                     false
0829                 );
0830 
0831         // Calculate a better position.  Sometimes the position doesn't update
0832         // with a good resolution (for example, 1 sec for TrueAudio files in the
0833         // Xine-1.1.18 backend).  This tick function, in those cases, just gets
0834         // called multiple times with the same position.  We count how many
0835         // times this has been called prior, and adjust for it.
0836         if( position == m_lastTickPosition )
0837             newPosition += ++m_lastTickCount * m_tickInterval;
0838         else
0839             m_lastTickCount = 0;
0840 
0841         m_lastTickPosition = position;
0842 
0843         //don't go beyond the stop point
0844         if( newPosition >= m_boundedPlayback->endPosition() )
0845         {
0846             slotAboutToFinish();
0847         }
0848     }
0849     else
0850     {
0851         m_lastTickPosition = position;
0852         Q_EMIT trackPositionChanged( static_cast<long>( position ), false );
0853     }
0854 }
0855 
0856 void
0857 EngineController::slotAboutToFinish()
0858 {
0859     DEBUG_BLOCK
0860 
0861     if( m_fadeouter )
0862     {
0863         debug() << "slotAboutToFinish(): a fadeout is in progress, don't queue new track";
0864         return;
0865     }
0866 
0867     if( m_multiPlayback )
0868     {
0869         DEBUG_LINE_INFO
0870         m_mutex.lock();
0871         m_playWhenFetched = false;
0872         m_mutex.unlock();
0873         m_multiPlayback->fetchNext();
0874         debug() << "The queue has: " << m_media->queue().size() << " tracks in it";
0875     }
0876     else if( m_multiSource )
0877     {
0878         debug() << "source finished, lets get the next one";
0879         QUrl nextSource = m_multiSource->nextUrl();
0880 
0881         if( !nextSource.isEmpty() )
0882         { //more sources
0883             m_mutex.lock();
0884             m_playWhenFetched = false;
0885             m_mutex.unlock();
0886             debug() << "playing next source: " << nextSource;
0887             slotPlayableUrlFetched( nextSource );
0888         }
0889         else if( m_media->queue().isEmpty() )
0890         {
0891             debug() << "no more sources, skip to next track";
0892             m_multiSource.reset(); // don't confuse slotFinished
0893             The::playlistActions()->requestNextTrack();
0894         }
0895     }
0896     else if( m_boundedPlayback )
0897     {
0898         debug() << "finished a track that consists of part of another track, go to next track even if this url is technically not done yet";
0899 
0900         //stop this track, now, as the source track might go on and on, and
0901         //there might not be any more tracks in the playlist...
0902         stop( true );
0903         The::playlistActions()->requestNextTrack();
0904     }
0905     else if( m_media->queue().isEmpty() )
0906         The::playlistActions()->requestNextTrack();
0907 }
0908 
0909 void
0910 EngineController::slotFinished()
0911 {
0912     DEBUG_BLOCK
0913 
0914     // paranoia checking, m_currentTrack shouldn't really be null
0915     if( m_currentTrack )
0916     {
0917         debug() << "Track finished completely, updating statistics";
0918         unsubscribeFrom( m_currentTrack ); // don't bother with trackMetadataChanged()
0919         stampStreamTrackLength(); // update track length in stream for accurate scrobbling
0920         Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 );
0921         subscribeTo( m_currentTrack );
0922     }
0923 
0924     if( !m_multiPlayback && !m_multiSource )
0925     {
0926         // again. at this point the track is finished so it's trackPositionMs is 0
0927         if( !m_nextTrack && m_nextUrl.isEmpty() )
0928             Q_EMIT stopped( m_currentTrack ? m_currentTrack->length() : 0,
0929                           m_currentTrack ? m_currentTrack->length() : 0 );
0930         unsubscribeFrom( m_currentTrack );
0931         if( m_currentAlbum )
0932             unsubscribeFrom( m_currentAlbum );
0933         m_currentTrack = nullptr;
0934         m_currentAlbum = nullptr;
0935         if( !m_nextTrack && m_nextUrl.isEmpty() ) // we will the trackChanged signal later
0936             Q_EMIT trackChanged( Meta::TrackPtr() );
0937         m_media->setCurrentSource( Phonon::MediaSource() );
0938     }
0939 
0940     m_mutex.lock(); // in case setNextTrack is being handled right now.
0941 
0942     // Non-local urls are not enqueued so we must play them explicitly.
0943     if( m_nextTrack )
0944     {
0945         DEBUG_LINE_INFO
0946         play( m_nextTrack );
0947     }
0948     else if( !m_nextUrl.isEmpty() )
0949     {
0950         DEBUG_LINE_INFO
0951         playUrl( m_nextUrl, 0 );
0952     }
0953     else
0954     {
0955         DEBUG_LINE_INFO
0956         // possibly we are waiting for a fetch
0957         m_playWhenFetched = true;
0958     }
0959 
0960     m_mutex.unlock();
0961 }
0962 
0963 static const qreal log10over20 = 0.1151292546497022842; // ln(10) / 20
0964 
0965 void
0966 EngineController::slotNewTrackPlaying( const Phonon::MediaSource &source )
0967 {
0968     DEBUG_BLOCK
0969 
0970     if( source.type() == Phonon::MediaSource::Empty )
0971     {
0972         debug() << "Empty MediaSource (engine stop)";
0973         return;
0974     }
0975 
0976     if( m_currentTrack )
0977     {
0978         unsubscribeFrom( m_currentTrack );
0979         if( m_currentAlbum )
0980             unsubscribeFrom( m_currentAlbum );
0981     }
0982     // only update stats if we are called for something new, some phonon back-ends (at
0983     // least phonon-gstreamer-4.6.1) call slotNewTrackPlaying twice with the same source
0984     if( m_currentTrack && ( m_nextTrack || !m_nextUrl.isEmpty() ) )
0985     {
0986         debug() << "Previous track finished completely, updating statistics";
0987         stampStreamTrackLength(); // update track length in stream for accurate scrobbling
0988         Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 );
0989 
0990         if( m_multiSource )
0991             // advance source of a multi-source track
0992             m_multiSource->setSource( m_multiSource->current() + 1 );
0993     }
0994     m_nextUrl.clear();
0995 
0996     if( m_nextTrack )
0997     {
0998         // already unsubscribed
0999         m_currentTrack = m_nextTrack;
1000         m_nextTrack.clear();
1001 
1002         m_multiSource.reset( m_currentTrack->create<Capabilities::MultiSourceCapability>() );
1003         if( m_multiSource )
1004         {
1005             debug() << "Got a MultiSource Track with" <<  m_multiSource->sources().count() << "sources";
1006             connect( m_multiSource.data(), &Capabilities::MultiSourceCapability::urlChanged,
1007                      this, &EngineController::slotPlayableUrlFetched );
1008         }
1009     }
1010 
1011     if( m_currentTrack
1012         && AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off )
1013     {
1014         Meta::ReplayGainTag mode;
1015         // gain is usually negative (but may be positive)
1016         mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track)
1017             ? Meta::ReplayGain_Track_Gain
1018             : Meta::ReplayGain_Album_Gain;
1019         qreal gain = m_currentTrack->replayGain( mode );
1020 
1021         // peak is usually positive and smaller than gain (but may be negative)
1022         mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track)
1023             ? Meta::ReplayGain_Track_Peak
1024             : Meta::ReplayGain_Album_Peak;
1025         qreal peak = m_currentTrack->replayGain( mode );
1026         if( gain + peak > 0.0 )
1027         {
1028             debug() << "Gain of" << gain << "would clip at absolute peak of" << gain + peak;
1029             gain -= gain + peak;
1030         }
1031 
1032         if( m_preamp )
1033         {
1034             debug() << "Using gain of" << gain << "with relative peak of" << peak;
1035             // we calculate the volume change ourselves, because m_preamp->setVolumeDecibel is
1036             // a little confused about minus signs
1037             m_preamp->setVolume( qExp( gain * log10over20 ) );
1038         }
1039         else
1040             warning() << "Would use gain of" << gain << ", but current Phonon backend"
1041                       << "doesn't seem to support pre-amplifier (VolumeFaderEffect)";
1042     }
1043     else if( m_preamp )
1044     {
1045         m_preamp->setVolume( 1.0 );
1046     }
1047 
1048     bool useTrackWithinStreamDetection = false;
1049     if( m_currentTrack )
1050     {
1051         subscribeTo( m_currentTrack );
1052         Meta::AlbumPtr m_currentAlbum = m_currentTrack->album();
1053         if( m_currentAlbum )
1054             subscribeTo( m_currentAlbum );
1055         /** We only use detect-tracks-in-stream for tracks that have stream type
1056          * (exactly, we purposely exclude stream/lastfm) *and* that don't have length
1057          * already filled in. Bug 311852 */
1058         if( m_currentTrack->type() == "stream" && m_currentTrack->length() == 0 )
1059             useTrackWithinStreamDetection = true;
1060     }
1061 
1062     m_lastStreamStampPosition = useTrackWithinStreamDetection ? 0 : -1;
1063     Q_EMIT trackChanged( m_currentTrack );
1064     Q_EMIT trackPlaying( m_currentTrack );
1065 }
1066 
1067 void
1068 EngineController::slotStateChanged( Phonon::State newState, Phonon::State oldState ) //SLOT
1069 {
1070     debug() << "slotStateChanged from" << oldState << "to" << newState;
1071 
1072     static const int maxErrors = 5;
1073     static int errorCount = 0;
1074 
1075     // Sanity checks:
1076     if( newState == oldState )
1077         return;
1078 
1079     if( newState == Phonon::ErrorState )  // If media is borked, skip to next track
1080     {
1081         Q_EMIT trackError( m_currentTrack );
1082 
1083         warning() << "Phonon failed to play this URL. Error: " << m_media->errorString();
1084         warning() << "Forcing phonon engine reinitialization.";
1085 
1086         /* In case of error Phonon MediaObject automatically switches to KioMediaSource,
1087            which cause problems: runs StopAfterCurrentTrack mode, force PlayPause button to
1088            reply the track (can't be paused). So we should reinitiate Phonon after each Error.
1089         */
1090         initializePhonon();
1091 
1092         errorCount++;
1093         if ( errorCount >= maxErrors )
1094         {
1095             // reset error count
1096             errorCount = 0;
1097 
1098             Amarok::Logger::longMessage(
1099                             i18n( "Too many errors encountered in playlist. Playback stopped." ),
1100                             Amarok::Logger::Warning
1101                         );
1102             error() << "Stopping playlist.";
1103         }
1104         else
1105             // and start the next song, even if the current failed to start playing
1106             The::playlistActions()->requestUserNextTrack();
1107 
1108     }
1109     else if( newState == Phonon::PlayingState )
1110     {
1111         errorCount = 0;
1112         Q_EMIT playbackStateChanged();
1113     }
1114     else if( newState == Phonon::StoppedState ||
1115              newState == Phonon::PausedState )
1116     {
1117         Q_EMIT playbackStateChanged();
1118     }
1119 }
1120 
1121 void
1122 EngineController::slotPlayableUrlFetched( const QUrl &url )
1123 {
1124     DEBUG_BLOCK
1125     debug() << "Fetched url: " << url;
1126     if( url.isEmpty() )
1127     {
1128         DEBUG_LINE_INFO
1129         The::playlistActions()->requestNextTrack();
1130         return;
1131     }
1132 
1133     if( !m_playWhenFetched )
1134     {
1135         DEBUG_LINE_INFO
1136         m_mutex.lock();
1137         m_media->clearQueue();
1138         // keep synced with setNextTrack(), playUrl()
1139         m_media->enqueue( url );
1140         m_nextTrack.clear();
1141         m_nextUrl = url;
1142         debug() << "The next url we're playing is: " << m_nextUrl;
1143         // reset this flag each time
1144         m_playWhenFetched = true;
1145         m_mutex.unlock();
1146     }
1147     else
1148     {
1149         DEBUG_LINE_INFO
1150         m_mutex.lock();
1151         playUrl( url, 0 );
1152         m_mutex.unlock();
1153     }
1154 }
1155 
1156 void
1157 EngineController::slotTrackLengthChanged( qint64 milliseconds )
1158 {
1159     debug() << "slotTrackLengthChanged(" << milliseconds << ")";
1160     Q_EMIT trackLengthChanged( ( !m_multiPlayback || !m_boundedPlayback )
1161                              ? trackLength() : milliseconds );
1162 }
1163 
1164 void
1165 EngineController::slotMetaDataChanged()
1166 {
1167     QVariantMap meta;
1168     meta.insert( Meta::Field::URL, m_media->currentSource().url() );
1169     static const QList<FieldPair> fieldPairs = QList<FieldPair>()
1170             << FieldPair( Phonon::ArtistMetaData, Meta::Field::ARTIST )
1171             << FieldPair( Phonon::AlbumMetaData, Meta::Field::ALBUM )
1172             << FieldPair( Phonon::TitleMetaData, Meta::Field::TITLE )
1173             << FieldPair( Phonon::GenreMetaData, Meta::Field::GENRE )
1174             << FieldPair( Phonon::TracknumberMetaData, Meta::Field::TRACKNUMBER )
1175             << FieldPair( Phonon::DescriptionMetaData, Meta::Field::COMMENT );
1176     foreach( const FieldPair &pair, fieldPairs )
1177     {
1178         QStringList values = m_media->metaData( pair.first );
1179         if( !values.isEmpty() )
1180             meta.insert( pair.second, values.first() );
1181     }
1182 
1183     // note: don't rely on m_currentTrack here. At least some Phonon backends first Q_EMIT
1184     // totalTimeChanged(), then metaDataChanged() and only then currentSourceChanged()
1185     // which currently sets correct m_currentTrack.
1186     if( isInRecentMetaDataHistory( meta ) )
1187     {
1188         // slotMetaDataChanged() triggered by phonon, but we've already seen
1189         // exactly the same metadata recently. Ignoring for now.
1190         return;
1191     }
1192 
1193     // following is an implementation of song end (and length) within a stream detection.
1194     // This normally fires minutes after the track has started playing so m_currentTrack
1195     // should be accurate
1196     if( m_currentTrack && m_lastStreamStampPosition >= 0 )
1197     {
1198         stampStreamTrackLength();
1199         Q_EMIT trackFinishedPlaying( m_currentTrack, 1.0 );
1200 
1201         // update track length to 0 because length emitted by stampStreamTrackLength()
1202         // is for the previous song
1203         meta.insert( Meta::Field::LENGTH, 0 );
1204     }
1205 
1206     debug() << "slotMetaDataChanged(): new meta-data:" << meta;
1207     Q_EMIT currentMetadataChanged( meta );
1208 }
1209 
1210 void
1211 EngineController::slotSeekableChanged( bool seekable )
1212 {
1213     Q_EMIT seekableChanged( seekable );
1214 }
1215 
1216 void
1217 EngineController::slotTitleChanged( int titleNumber )
1218 {
1219     DEBUG_BLOCK
1220     if ( titleNumber != m_currentAudioCdTrack )
1221     {
1222         The::playlistActions()->requestNextTrack();
1223         slotFinished();
1224     }
1225 }
1226 
1227 void EngineController::slotVolumeChanged( qreal newVolume )
1228 {
1229     int percent = qBound<qreal>( 0, qRound(newVolume * 100), 100 );
1230 
1231     if ( !m_ignoreVolumeChangeObserve && m_volume != percent )
1232     {
1233         m_ignoreVolumeChangeAction = true;
1234 
1235         m_volume = percent;
1236         AmarokConfig::setMasterVolume( percent );
1237         Q_EMIT volumeChanged( percent );
1238     }
1239     else
1240         m_volume = percent;
1241 
1242     m_ignoreVolumeChangeObserve = false;
1243 }
1244 
1245 void EngineController::slotMutedChanged( bool mute )
1246 {
1247     AmarokConfig::setMuteState( mute );
1248     Q_EMIT muteStateChanged( mute );
1249 }
1250 
1251 void
1252 EngineController::slotTrackFinishedPlaying( Meta::TrackPtr track, double playedFraction )
1253 {
1254     Q_ASSERT( track );
1255     debug() << "slotTrackFinishedPlaying("
1256             << ( track->artist() ? track->artist()->name() : QStringLiteral( "[no artist]" ) )
1257             << "-" << ( track->album() ? track->album()->name() : QStringLiteral( "[no album]" ) )
1258             << "-" << track->name()
1259             << "," << playedFraction << ")";
1260 
1261     // Track::finishedPlaying is thread-safe and can take a long time to finish.
1262     std::thread thread( &Meta::Track::finishedPlaying, track, playedFraction );
1263     thread.detach();
1264 }
1265 
1266 void
1267 EngineController::metadataChanged( const Meta::TrackPtr &track )
1268 {
1269     Meta::AlbumPtr album = m_currentTrack->album();
1270     if( m_currentAlbum != album ) {
1271         if( m_currentAlbum )
1272             unsubscribeFrom( m_currentAlbum );
1273         m_currentAlbum = album;
1274         if( m_currentAlbum )
1275             subscribeTo( m_currentAlbum );
1276     }
1277     Q_EMIT trackMetadataChanged( track );
1278 }
1279 
1280 void
1281 EngineController::metadataChanged( const Meta::AlbumPtr &album )
1282 {
1283     Q_EMIT albumMetadataChanged( album );
1284 }
1285 
1286 QString EngineController::prettyNowPlaying( bool progress ) const
1287 {
1288     Meta::TrackPtr track = currentTrack();
1289 
1290     if( track )
1291     {
1292         QString title       = track->name().toHtmlEscaped();
1293         QString prettyTitle = track->prettyName().toHtmlEscaped();
1294         QString artist      = track->artist() ? track->artist()->name().toHtmlEscaped() : QString();
1295         QString album       = track->album() ? track->album()->name().toHtmlEscaped() : QString();
1296 
1297         // ugly because of translation requirements
1298         if( !title.isEmpty() && !artist.isEmpty() && !album.isEmpty() )
1299             title = i18nc( "track by artist on album", "<b>%1</b> by <b>%2</b> on <b>%3</b>", title, artist, album );
1300         else if( !title.isEmpty() && !artist.isEmpty() )
1301             title = i18nc( "track by artist", "<b>%1</b> by <b>%2</b>", title, artist );
1302         else if( !album.isEmpty() )
1303             // we try for pretty title as it may come out better
1304             title = i18nc( "track on album", "<b>%1</b> on <b>%2</b>", prettyTitle, album );
1305         else
1306             title = "<b>" + prettyTitle + "</b>";
1307 
1308         if( title.isEmpty() )
1309             title = i18n( "Unknown track" );
1310 
1311         QScopedPointer<Capabilities::SourceInfoCapability> sic( track->create<Capabilities::SourceInfoCapability>() );
1312         if( sic )
1313         {
1314             QString source = sic->sourceName();
1315             if( !source.isEmpty() )
1316                 title += ' ' + i18nc( "track from source", "from <b>%1</b>", source );
1317         }
1318 
1319         if( track->length() > 0 )
1320         {
1321             QString length = Meta::msToPrettyTime( track->length() ).toHtmlEscaped();
1322             title += " (";
1323             if( progress )
1324                     title += Meta::msToPrettyTime( m_lastTickPosition ).toHtmlEscaped() + '/';
1325             title += length + ')';
1326         }
1327 
1328         return title;
1329     }
1330     else
1331         return i18n( "No track playing" );
1332 }
1333 
1334 bool
1335 EngineController::isInRecentMetaDataHistory( const QVariantMap &meta )
1336 {
1337     // search for Metadata in history
1338     for( int i = 0; i < m_metaDataHistory.size(); i++)
1339     {
1340         if( m_metaDataHistory.at( i ) == meta ) // we already had that one -> spam!
1341         {
1342             m_metaDataHistory.move( i, 0 ); // move spam to the beginning of the list
1343             return true;
1344         }
1345     }
1346 
1347     if( m_metaDataHistory.size() == 12 )
1348         m_metaDataHistory.removeLast();
1349 
1350     m_metaDataHistory.insert( 0, meta );
1351     return false;
1352 }
1353 
1354 void
1355 EngineController::stampStreamTrackLength()
1356 {
1357     if( m_lastStreamStampPosition < 0 )
1358         return;
1359 
1360     qint64 currentPosition = trackPositionMs();
1361     debug() << "stampStreamTrackLength(): m_lastStreamStampPosition:" << m_lastStreamStampPosition
1362             << "currentPosition:" << currentPosition;
1363     if( currentPosition == m_lastStreamStampPosition )
1364         return;
1365     qint64 length = qMax( currentPosition - m_lastStreamStampPosition, qint64( 0 ) );
1366     updateStreamLength( length );
1367 
1368     m_lastStreamStampPosition = currentPosition;
1369 }
1370 
1371 void
1372 EngineController::updateStreamLength( qint64 length )
1373 {
1374     if( !m_currentTrack )
1375     {
1376         warning() << __PRETTY_FUNCTION__ << "called with cull m_currentTrack";
1377         return;
1378     }
1379 
1380     // Last.fm scrobbling needs to know track length before it can scrobble:
1381     QVariantMap lengthMetaData;
1382     // we cannot use m_media->currentSource()->url() here because it is already empty, bug 309976
1383     lengthMetaData.insert( Meta::Field::URL, QUrl( m_currentTrack->playableUrl() ) );
1384     lengthMetaData.insert( Meta::Field::LENGTH, length );
1385     debug() << "updateStreamLength(): emitting currentMetadataChanged(" << lengthMetaData << ")";
1386     Q_EMIT currentMetadataChanged( lengthMetaData );
1387 }
1388