1 /***************************************************************************
2 * Copyright (C) 2004 Frederik Holljen <fh@ez.no> *
3 * (C) 2004,5 Max Howell <max.howell@methylblue.com> *
4 * (C) 2004,5 Mark Kretschmann *
5 * (C) 2006 Ian Monroe *
7 * This program is free software; you can redistribute it and/or modify *
8 * it under the terms of the GNU General Public License as published by *
9 * the Free Software Foundation; either version 2 of the License, or *
10 * (at your option) any later version. *
12 ***************************************************************************/
14 #define DEBUG_PREFIX "controller"
16 #include "enginecontroller.h"
19 #include "amarokconfig.h"
20 #include "collection/CollectionManager.h"
22 #include "enginebase.h"
24 #include "MainWindow.h"
25 #include "mediabrowser.h"
26 #include "meta/meta.h"
27 #include "pluginmanager.h"
28 #include "statusbar.h"
29 #include "TheInstances.h"
30 #include "playlist/PlaylistModel.h"
32 #include <KApplication>
33 #include <kio/global.h>
35 #include <KMessageBox>
45 EngineController::ExtensionCache
EngineController::s_extensionCache
;
49 EngineController::instance()
51 //will only be instantiated the first time this function is called
52 //will work with the inline directive
53 static EngineController Instance
;
59 EngineController::EngineController()
64 , m_xFadeThisTrack( false )
65 , m_timer( new QTimer( this ) )
66 , m_playFailureCount( 0 )
68 , m_positionOffset( 0 )
69 , m_lastPositionOffset( 0 )
71 m_voidEngine
= m_engine
= loadEngine( "void-engine" );
73 connect( m_timer
, SIGNAL( timeout() ), SLOT( slotMainTimer() ) );
76 EngineController::~EngineController()
78 DEBUG_FUNC_INFO
//we like to know when singletons are destroyed
82 //////////////////////////////////////////////////////////////////////////////////////////
84 //////////////////////////////////////////////////////////////////////////////////////////
87 EngineController::loadEngine() //static
89 /// always returns a valid pointer to EngineBase
92 //TODO remember song position, and resume playback
94 // new engine, new ext cache required
95 extensionCache().clear();
97 if( m_engine
!= m_voidEngine
) {
98 EngineBase
*oldEngine
= m_engine
;
100 // we assign this first for thread-safety,
101 // EngineController::engine() must always return an engine!
102 m_engine
= m_voidEngine
;
104 // we unload the old engine first because there are a number of
105 // bugs associated with keeping one engine loaded while loading
106 // another, eg xine-engine can't init(), and aRts-engine crashes
107 PluginManager::unload( oldEngine
);
109 // the engine is not required to do this when we unload it but
110 // we need to do it to ensure Amarok looks correct.
111 // We don't do this for the void-engine because that
112 // means Amarok sets all components to empty on startup, which is
113 // their responsibility.
114 slotStateChanged( Engine::Empty
);
117 m_engine
= loadEngine( AmarokConfig::soundSystem() );
119 const QString engineName
= PluginManager::getService( m_engine
)->property( "X-KDE-Amarok-name" ).toString();
121 if( !AmarokConfig::soundSystem().isEmpty() && engineName
!= AmarokConfig::soundSystem() ) {
122 //AmarokConfig::soundSystem() is empty on the first-ever-run
124 Amarok::StatusBar::instance()->longMessageThreadSafe( i18n(
125 "Sorry, the '%1' could not be loaded, instead we have loaded the '%2'.", AmarokConfig::soundSystem(), engineName
),
126 KDE::StatusBar::Sorry
);
128 AmarokConfig::setSoundSystem( engineName
);
131 // Important: Make sure soundSystem is not empty
132 if( AmarokConfig::soundSystem().isEmpty() )
133 AmarokConfig::setSoundSystem( engineName
);
138 #include <q3valuevector.h>
140 EngineController::loadEngine( const QString
&engineName
)
142 /// always returns a valid plugin (exits if it can't get one)
146 QString query
= "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] != '%1'";
147 KService::List offers
= PluginManager::query( query
.arg( engineName
) );
149 // sort by rank, QValueList::operator[] is O(n), so this is quite inefficient
150 #define rank( x ) (x)->property( "X-KDE-Amarok-rank" ).toInt()
151 for( int n
= offers
.count()-1, i
= 0; i
< n
; i
++ )
152 for( int j
= n
; j
> i
; j
-- )
153 if( rank( offers
[j
] ) > rank( offers
[j
-1] ) )
154 qSwap( offers
[j
], offers
[j
-1] );
157 // this is the actual engine we want
158 query
= "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] == '%1'";
159 offers
= PluginManager::query( query
.arg( engineName
) ) + offers
;
161 foreach( KService::Ptr service
, offers
) {
162 Amarok::Plugin
*plugin
= PluginManager::createFromService( service
);
165 QObject
*bar
= Amarok::StatusBar::instance();
166 EngineBase
*engine
= static_cast<EngineBase
*>( plugin
);
168 connect( engine
, SIGNAL(stateChanged( Engine::State
)),
169 this, SLOT(slotStateChanged( Engine::State
)) );
170 connect( engine
, SIGNAL(trackEnded()),
171 this, SLOT(slotTrackEnded()) );
174 connect( engine
, SIGNAL(statusText( const QString
& )),
175 bar
, SLOT(shortMessage( const QString
& )) );
176 connect( engine
, SIGNAL(infoMessage( const QString
& )),
177 bar
, SLOT(longMessage( const QString
& )) );
179 connect( engine
, SIGNAL(showConfigDialog( const QByteArray
& )),
180 kapp
, SLOT(slotConfigAmarok( const QByteArray
& )) );
181 connect( engine
, SIGNAL( metaData( QHash
<qint64
, QString
> ) ), SLOT( slotEngineMetaData( QHash
<qint64
, QString
> ) ) );
186 warning() << "Could not init() an engine\n";
190 KRun::runCommand( "kbuildsycoca4", 0 );
192 KMessageBox::error( 0, i18n(
193 "<p>Amarok could not find any sound-engine plugins. "
194 "Amarok is now updating the KDE configuration database. Please wait a couple of minutes, then restart Amarok.</p>"
195 "<p>If this does not help, "
196 "it is likely that Amarok is installed under the wrong prefix, please fix your installation using:<pre>"
197 "$ cd /path/to/amarok/source-code/<br>"
198 "$ su -c \"make uninstall\"<br>"
199 "$ ./configure --prefix=`kde-config --prefix` && su -c \"make install\"<br>"
202 "More information can be found in the README file. For further assistance join us at #amarok on irc.freenode.net.</p>" ) );
204 // don't use QApplication::exit, as the eventloop may not have started yet
205 std::exit( EXIT_SUCCESS
);
207 // Not executed, just here to prevent compiler warning
212 bool EngineController::canDecode( const KUrl
&url
) //static
214 //NOTE this function must be thread-safe
216 const QString fileName
= url
.fileName();
217 const QString ext
= Amarok::extension( fileName
);
220 // if ( PlaylistFile::isPlaylistFile( fileName ) ) return false;
222 // Ignore protocols "fetchcover" and "musicbrainz", they're not local but we don't really want them in the playlist :)
223 if ( url
.protocol() == "fetchcover" || url
.protocol() == "musicbrainz" ) return false;
225 // Accept non-local files, since we can't test them for validity at this point
226 // TODO actually, only accept unconditionally http stuff
227 // TODO this actually makes things like "Blarrghgjhjh:!!!" automatically get inserted
229 // TODO remove for Amarok 1.3 and above silly checks, instead check for http type servers
230 if ( !url
.isLocalFile() ) return true;
232 // If extension is already in the cache, return cache result
233 if ( extensionCache().contains( ext
) )
234 return s_extensionCache
[ext
];
236 // If file has 0 bytes, ignore it and return false, not to infect the cache with corrupt files.
237 // TODO also ignore files that are too small?
238 if ( !QFileInfo(url
.path()).size() )
241 const bool valid
= engine()->canDecode( url
);
243 if( engine() != EngineController::instance()->m_voidEngine
)
245 //we special case this as otherwise users hate us
246 if ( !valid
&& ext
.toLower() == "mp3" && !installDistroCodec(AmarokConfig::soundSystem()) )
247 Amarok::StatusBar::instance()->longMessageThreadSafe(
248 i18n( "<p>The %1 claims it <b>cannot</b> play MP3 files."
249 "<p>You may want to choose a different engine from the <i>Configure Dialog</i>, or examine "
250 "the installation of the multimedia-framework that the current engine uses. "
251 "<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok HandBook</i>.", AmarokConfig::soundSystem() ), KDE::StatusBar::Error
);
253 // Cache this result for the next lookup
254 if ( !ext
.isEmpty() )
255 extensionCache().insert( ext
, valid
);
261 bool EngineController::installDistroCodec( const QString
& engine
/*Filetype type*/)
263 KService::List services
= KServiceTypeTrader::self()->query( "Amarok/CodecInstall"
264 , QString("[X-KDE-Amarok-codec] == 'mp3' and [X-KDE-Amarok-engine] == '%1'").arg(engine
) );
265 if( !services
.isEmpty() )
267 KService::Ptr service
= services
.first(); //list is not empty
268 QString installScript
= service
->exec();
269 if( !installScript
.isNull() ) //just a sanity check
271 KGuiItem
installButton("Install MP3 Support");
272 if(KMessageBox::questionYesNo(MainWindow::self()
273 , i18n("Amarok currently cannot play MP3 files.")
274 , i18n( "No MP3 Support" )
276 , KStandardGuiItem::no()
277 , "codecInstallWarning" ) == KMessageBox::Yes
)
279 KRun::runCommand(installScript
, 0);
287 void EngineController::restoreSession()
289 //here we restore the session
290 //however, do note, this is always done, KDE session management is not involved
292 if( !AmarokConfig::resumeTrack().isEmpty() )
294 const KUrl url
= AmarokConfig::resumeTrack();
295 Meta::TrackPtr track
= CollectionManager::instance()->trackForUrl( url
);
297 play( track
, AmarokConfig::resumeTime() );
302 void EngineController::endSession()
304 //only update song stats, when we're not going to resume it
305 if ( !AmarokConfig::resumePlayback() && m_currentTrack
)
307 trackEnded( trackPosition(), m_currentTrack
->length() * 1000, "quit" );
310 PluginManager::unload( m_voidEngine
);
314 //////////////////////////////////////////////////////////////////////////////////////////
316 //////////////////////////////////////////////////////////////////////////////////////////
318 void EngineController::previous() //SLOT
320 emit
orderPrevious();
324 void EngineController::next( bool forceNext
) //SLOT
326 m_previousUrl
= m_currentTrack
->url();
328 emit
orderNext(forceNext
);
332 void EngineController::play() //SLOT
334 if ( m_engine
->state() == Engine::Paused
)
338 else emit
orderCurrent();
341 void EngineController::play( const Meta::TrackPtr
& track
, uint offset
)
344 m_currentTrack
= track
;
345 KUrl url
= track
->playableUrl();
346 if( m_engine
->load( url
, url
.protocol() == "http" || url
.protocol() == "rtsp" ) )
348 if( m_engine
->play( offset
) )
356 //i know that i should not jsut comment this code,
357 //but there is a lot of important logic in there, so
358 //i am leaving it in because we won't forget that it
361 void EngineController::play( const MetaBundle &bundle, uint offset )
365 KUrl url = bundle.url();
366 // don't destroy connection if we need to change station
367 if( url.protocol() != "lastfm" && LastFm::Controller::instance()->isPlaying() )
370 LastFm::Controller::instance()->playbackStopped();
373 //Holds the time since we started trying to play non-existent files
374 //so we know when to abort
375 static QTime failure_time;
376 if ( !m_playFailureCount )
377 failure_time.start();
379 debug() << "Loading URL: " << url.url();
380 m_lastMetadata.clear();
382 //TODO bummer why'd I do it this way? it should _not_ be in play!
383 //let Amarok know that the previous track is no longer playing
384 if ( m_timer->isActive() )
385 trackEnded( trackPosition(), m_bundle.length() * 1000, "change" );
387 if ( url.isLocalFile() ) {
388 // does the file really exist? the playlist entry might be old
389 if ( ! QFile::exists( url.path()) ) {
390 //debug() << " file >" << url.path() << "< does not exist!";
391 Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") );
392 goto some_kind_of_failure;
397 if( url.protocol() == "cdda" )
398 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
400 Amarok::StatusBar::instance()->shortMessage( i18n("Connecting to stream source...") );
401 debug() << "Connecting to protocol: " << url.protocol();
404 // WebDAV protocol is HTTP with extensions (and the "webdav" scheme
405 // is a KDE-ism anyway). Most engines cope with HTTP streaming, but
406 // not through KIO, so they don't support KDE-isms.
407 if ( url.protocol() == "webdav" )
408 url.setProtocol( "http" );
409 else if ( url.protocol() == "webdavs" )
410 url.setProtocol( "https" );
412 // streams from last.fm should be handled by our proxy, in order to authenticate with the server
413 else if ( url.protocol() == "lastfm" )
415 if( LastFm::Controller::instance()->isPlaying() )
417 LastFm::Controller::instance()->getService()->changeStation( url.url() );
418 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
419 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
424 url = LastFm::Controller::instance()->getNewProxy( url.url() );
425 if( url.isEmpty() ) goto some_kind_of_failure;
428 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
429 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
431 debug() << "New URL is " << url.url();
433 else if (url.protocol() == "daap" )
435 KUrl newUrl = MediaBrowser::instance()->getProxyUrl( url );
436 if( !newUrl.isEmpty() )
445 if( m_engine->load( url, url.protocol() == "http" || url.protocol() == "rtsp" ) )
447 //assign bundle now so that it is available when the engine
448 //emits stateChanged( Playing )
449 if( !m_bundle.url().path().isEmpty() ) //wasn't playing before
450 m_previousUrl = m_bundle.url();
452 m_previousUrl = bundle.url();
455 if( m_engine->play( offset ) )
457 //Reset failure count as we are now successfully playing a song
458 m_playFailureCount = 0;
460 // Ask engine for track length, if available. It's more reliable than TagLib.
461 const uint trackLength = m_engine->length() / 1000;
462 if ( trackLength ) m_bundle.setLength( trackLength );
464 m_xFadeThisTrack = !m_engine->isStream() && !(url.protocol() == "cdda") &&
465 m_bundle.length()*1000 - offset - AmarokConfig::crossfadeLength()*2 > 0;
467 newMetaDataNotify( m_bundle, true );
472 some_kind_of_failure:
473 debug() << "Failed to play this track.";
475 ++m_playFailureCount;
477 //Code to skip to next track if playback fails:
479 // The failure counter is reset if a track plays successfully or if playback is
480 // stopped, for whatever reason.
481 // For normal playback, the attempt to play is stopped at the end of the playlist
482 // For repeat playlist , a whole playlist worth of songs is tried
483 // For repeat album, the number of songs tried is the number of tracks from the
484 // album that are in the playlist.
485 // For repeat track, no attempts are made
486 // To prevent GUI freezes we don't try to play again after 0.5s of failure
487 int totalTracks = Playlist::instance()->totalTrackCount();
488 int currentTrack = Playlist::instance()->currentTrackIndex();
489 if ( ( ( Amarok::repeatPlaylist() && static_cast<int>(m_playFailureCount) < totalTracks )
490 || ( Amarok::repeatNone() && currentTrack != totalTracks - 1 )
491 || ( Amarok::repeatAlbum() && m_playFailureCount < Playlist::instance()->repeatAlbumTrackCount() ) )
492 && failure_time.elapsed() < 500 )
495 debug() << "Skipping to next track.";
497 // The test for loaded must be done _before_ next is called
498 if ( !m_engine->loaded() )
500 //False gives behaviour as if track played successfully
502 QTimer::singleShot( 0, this, SLOT(play()) );
506 //False gives behaviour as if track played successfully
512 //Stop playback, including resetting failure count (as all new failures are
513 //treated as independent after playback is stopped)
519 void EngineController::pause() //SLOT
521 if ( m_engine
->loaded() && !LastFm::Controller::instance()->isPlaying() )
526 void EngineController::stop() //SLOT
528 //Reset failure counter as after stop, everything else is unrelated
529 m_playFailureCount
= 0;
531 //let Amarok know that the previous track is no longer playing
533 trackEnded( trackPosition(), m_currentTrack
->length() * 1000, "stop" );
535 //Remove requirement for track to be loaded for stop to be called (fixes gltiches
536 //where stop never properly happens if call to m_engine->load fails in play)
537 //if ( m_engine->loaded() )
543 void EngineController::playPause() //SLOT
545 //this is used by the TrayIcon, PlayPauseAction and DCOP
547 if( m_engine
->state() == Engine::Playing
)
551 else if( m_engine
->state() == Engine::Paused
)
553 if ( m_engine
->loaded() )
561 void EngineController::seek( int ms
) //SLOT
563 Meta::TrackPtr track
= currentTrack();
566 if( track
->length() > 0 )
568 trackPositionChangedNotify( ms
, true ); /* User seek */
569 engine()->seek( ms
);
574 void EngineController::seekRelative( int ms
) //SLOT
576 if( m_engine
->state() != Engine::Empty
)
578 int newPos
= m_engine
->position() + ms
;
579 seek( newPos
<= 0 ? 1 : newPos
);
584 void EngineController::seekForward( int ms
)
590 void EngineController::seekBackward( int ms
)
596 int EngineController::increaseVolume( int ticks
) //SLOT
598 return setVolume( m_engine
->volume() + ticks
);
602 int EngineController::decreaseVolume( int ticks
) //SLOT
604 return setVolume( m_engine
->volume() - ticks
);
608 int EngineController::setVolume( int percent
) //SLOT
612 if( percent
< 0 ) percent
= 0;
613 if( percent
> 100 ) percent
= 100;
615 if( (uint
)percent
!= m_engine
->volume() )
617 m_engine
->setVolume( (uint
)percent
);
619 percent
= m_engine
->volume();
620 AmarokConfig::setMasterVolume( percent
);
621 volumeChangedNotify( percent
);
626 volumeChangedNotify( percent
);
629 return m_engine
->volume();
633 void EngineController::mute() //SLOT
635 if( m_muteVolume
== 0 )
637 int saveVolume
= m_engine
->volume();
639 m_muteVolume
= saveVolume
;
643 setVolume( m_muteVolume
);
649 EngineController::currentTrack() const
651 return m_engine
->state() == Engine::Empty
? Meta::TrackPtr() : m_currentTrack
;
654 //do we actually need this method?
656 EngineController::trackLength() const
658 Meta::TrackPtr track
= currentTrack();
660 return track
->length() * 1000;
665 //do we actually need this method?
667 EngineController::playingURL() const
669 Meta::TrackPtr track
= currentTrack();
671 return track
->playableUrl();
678 EngineController::slotEngineMetaData( const QHash
<qint64
, QString
> &newMetaData
)
680 bool trackChange
= m_currentTrack
->playableUrl().isLocalFile();
681 newMetaDataNotify( newMetaData
, trackChange
);
685 //////////////////////////////////////////////////////////////////////////////////////////
687 //////////////////////////////////////////////////////////////////////////////////////////
689 void EngineController::slotMainTimer() //SLOT
691 const uint position
= trackPosition();
693 trackPositionChangedNotify( position
);
696 if ( m_engine
->state() == Engine::Playing
&&
697 AmarokConfig::crossfade() && m_xFadeThisTrack
&&
698 m_engine
->hasPluginProperty( "HasCrossfade" ) &&
699 //Port 2.0 Playlist::instance()->stopAfterMode() != Playlist::StopAfterCurrent &&
700 ( (uint
) AmarokConfig::crossfadeType() == 0 || //Always or...
701 (uint
) AmarokConfig::crossfadeType() == 1 ) && //...automatic track change only
702 ( The::playlistModel()->activeRow() < The::playlistModel()->rowCount() ) &&
703 m_currentTrack
->length()*1000 - position
< (uint
) AmarokConfig::crossfadeLength() )
705 debug() << "Crossfading to next track...\n";
706 m_engine
->setXFadeNextTrack( true );
709 else if ( m_engine
->state() == Engine::Playing
&&
710 AmarokConfig::fadeout() &&
711 // Port 2.0 Playlist::instance()->stopAfterMode() == Playlist::StopAfterCurrent &&
712 m_currentTrack
->length()*1000 - position
< (uint
) AmarokConfig::fadeoutLength() )
719 void EngineController::slotTrackEnded() //SLOT
721 if ( AmarokConfig::trackDelayLength() > 0 )
726 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackDone()) );
735 void EngineController::slotStateChanged( Engine::State newState
) //SLOT
749 case Engine::Playing
:
751 m_timer
->start( MAIN_TIMER
);
758 stateChangedNotify( newState
);
761 uint
EngineController::trackPosition() const
763 const uint buffertime
= 5000; // worked for me with xine engine over 1 mbit dsl
766 uint pos
= m_engine
->position();
770 if( m_positionOffset
+ buffertime
<= pos
)
771 return pos
- m_positionOffset
- buffertime
;
772 if( m_lastPositionOffset
+ buffertime
<= pos
)
773 return pos
- m_lastPositionOffset
- buffertime
;
778 #include "enginecontroller.moc"