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