Temporarily disable showing the mediabrowser as jefferais commit caused Amarok to...
[amarok.git] / src / enginecontroller.cpp
blob6ac7ae05c3436578f96b8a809302654423d21507
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 *
6 * *
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. *
11 * *
12 ***************************************************************************/
14 #define DEBUG_PREFIX "controller"
16 #include "enginecontroller.h"
18 #include "amarok.h"
19 #include "amarokconfig.h"
20 #include "collection/collectionmanager.h"
21 #include "debug.h"
22 #include "enginebase.h"
23 #include "lastfm.h"
24 #include "mediabrowser.h"
25 #include "meta/meta.h"
26 #include "playlist.h"
27 #include "playlistloader.h"
28 #include "pluginmanager.h"
29 #include "statusbar.h"
31 #include <KApplication>
32 #include <kio/global.h>
33 #include <KIO/Job>
34 #include <KMessageBox>
35 #include <KRun>
37 #include <QByteArray>
38 #include <QFile>
39 #include <QTimer>
41 #include <cstdlib>
44 EngineController::ExtensionCache EngineController::s_extensionCache;
47 EngineController*
48 EngineController::instance()
50 //will only be instantiated the first time this function is called
51 //will work with the inline directive
52 static EngineController Instance;
54 return &Instance;
58 EngineController::EngineController()
59 : m_engine( 0 )
60 , m_voidEngine( 0 )
61 , m_delayTime( 0 )
62 , m_muteVolume( 0 )
63 , m_xFadeThisTrack( false )
64 , m_timer( new QTimer( this ) )
65 , m_playFailureCount( 0 )
66 , m_lastFm( false )
67 , m_positionOffset( 0 )
68 , m_lastPositionOffset( 0 )
70 m_voidEngine = m_engine = loadEngine( "void-engine" );
72 connect( m_timer, SIGNAL( timeout() ), SLOT( slotMainTimer() ) );
75 EngineController::~EngineController()
77 DEBUG_FUNC_INFO //we like to know when singletons are destroyed
81 //////////////////////////////////////////////////////////////////////////////////////////
82 // PUBLIC
83 //////////////////////////////////////////////////////////////////////////////////////////
85 EngineBase*
86 EngineController::loadEngine() //static
88 /// always returns a valid pointer to EngineBase
90 DEBUG_BLOCK
91 //TODO remember song position, and resume playback
93 // new engine, new ext cache required
94 extensionCache().clear();
96 if( m_engine != m_voidEngine ) {
97 EngineBase *oldEngine = m_engine;
99 // we assign this first for thread-safety,
100 // EngineController::engine() must always return an engine!
101 m_engine = m_voidEngine;
103 // we unload the old engine first because there are a number of
104 // bugs associated with keeping one engine loaded while loading
105 // another, eg xine-engine can't init(), and aRts-engine crashes
106 PluginManager::unload( oldEngine );
108 // the engine is not required to do this when we unload it but
109 // we need to do it to ensure Amarok looks correct.
110 // We don't do this for the void-engine because that
111 // means Amarok sets all components to empty on startup, which is
112 // their responsibility.
113 slotStateChanged( Engine::Empty );
116 m_engine = loadEngine( AmarokConfig::soundSystem() );
118 const QString engineName = PluginManager::getService( m_engine )->property( "X-KDE-Amarok-name" ).toString();
120 if( !AmarokConfig::soundSystem().isEmpty() && engineName != AmarokConfig::soundSystem() ) {
121 //AmarokConfig::soundSystem() is empty on the first-ever-run
123 Amarok::StatusBar::instance()->longMessageThreadSafe( i18n(
124 "Sorry, the '%1' could not be loaded, instead we have loaded the '%2'.", AmarokConfig::soundSystem(), engineName ),
125 KDE::StatusBar::Sorry );
127 AmarokConfig::setSoundSystem( engineName );
130 // Important: Make sure soundSystem is not empty
131 if( AmarokConfig::soundSystem().isEmpty() )
132 AmarokConfig::setSoundSystem( engineName );
134 return m_engine;
137 #include <q3valuevector.h>
138 EngineBase*
139 EngineController::loadEngine( const QString &engineName )
141 /// always returns a valid plugin (exits if it can't get one)
143 DEBUG_BLOCK
145 QString query = "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] != '%1'";
146 KService::List offers = PluginManager::query( query.arg( engineName ) );
148 // sort by rank, QValueList::operator[] is O(n), so this is quite inefficient
149 #define rank( x ) (x)->property( "X-KDE-Amarok-rank" ).toInt()
150 for( int n = offers.count()-1, i = 0; i < n; i++ )
151 for( int j = n; j > i; j-- )
152 if( rank( offers[j] ) > rank( offers[j-1] ) )
153 qSwap( offers[j], offers[j-1] );
154 #undef rank
156 // this is the actual engine we want
157 query = "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] == '%1'";
158 offers = PluginManager::query( query.arg( engineName ) ) + offers;
160 foreach( KService::Ptr service, offers ) {
161 Amarok::Plugin *plugin = PluginManager::createFromService( service );
163 if( plugin ) {
164 QObject *bar = Amarok::StatusBar::instance();
165 EngineBase *engine = static_cast<EngineBase*>( plugin );
167 connect( engine, SIGNAL(stateChanged( Engine::State )),
168 this, SLOT(slotStateChanged( Engine::State )) );
169 connect( engine, SIGNAL(trackEnded()),
170 this, SLOT(slotTrackEnded()) );
171 if( bar )
173 connect( engine, SIGNAL(statusText( const QString& )),
174 bar, SLOT(shortMessage( const QString& )) );
175 connect( engine, SIGNAL(infoMessage( const QString& )),
176 bar, SLOT(longMessage( const QString& )) );
178 connect( engine, SIGNAL(showConfigDialog( const QByteArray& )),
179 kapp, SLOT(slotConfigAmarok( const QByteArray& )) );
180 connect( engine, SIGNAL( metaData( QHash<qint64, QString> ) ), SLOT( slotEngineMetaData( QHash<qint64, QString> ) ) );
182 if( engine->init() )
183 return engine;
184 else
185 warning() << "Could not init() an engine\n";
189 KRun::runCommand( "kbuildsycoca4", 0 );
191 KMessageBox::error( 0, i18n(
192 "<p>Amarok could not find any sound-engine plugins. "
193 "Amarok is now updating the KDE configuration database. Please wait a couple of minutes, then restart Amarok.</p>"
194 "<p>If this does not help, "
195 "it is likely that Amarok is installed under the wrong prefix, please fix your installation using:<pre>"
196 "$ cd /path/to/amarok/source-code/<br>"
197 "$ su -c \"make uninstall\"<br>"
198 "$ ./configure --prefix=`kde-config --prefix` && su -c \"make install\"<br>"
199 "$ kbuildsycoca<br>"
200 "$ amarok</pre>"
201 "More information can be found in the README file. For further assistance join us at #amarok on irc.freenode.net.</p>" ) );
203 // don't use QApplication::exit, as the eventloop may not have started yet
204 std::exit( EXIT_SUCCESS );
206 // Not executed, just here to prevent compiler warning
207 return 0;
211 bool EngineController::canDecode( const KUrl &url ) //static
213 //NOTE this function must be thread-safe
215 const QString fileName = url.fileName();
216 const QString ext = Amarok::extension( fileName );
218 if ( PlaylistFile::isPlaylistFile( fileName ) ) return false;
220 // Ignore protocols "fetchcover" and "musicbrainz", they're not local but we don't really want them in the playlist :)
221 if ( url.protocol() == "fetchcover" || url.protocol() == "musicbrainz" ) return false;
223 // Accept non-local files, since we can't test them for validity at this point
224 // TODO actually, only accept unconditionally http stuff
225 // TODO this actually makes things like "Blarrghgjhjh:!!!" automatically get inserted
226 // into the playlist
227 // TODO remove for Amarok 1.3 and above silly checks, instead check for http type servers
228 if ( !url.isLocalFile() ) return true;
230 // If extension is already in the cache, return cache result
231 if ( extensionCache().contains( ext ) )
232 return s_extensionCache[ext];
234 // If file has 0 bytes, ignore it and return false, not to infect the cache with corrupt files.
235 // TODO also ignore files that are too small?
236 if ( !QFileInfo(url.path()).size() )
237 return false;
239 const bool valid = engine()->canDecode( url );
241 if( engine() != EngineController::instance()->m_voidEngine )
243 //we special case this as otherwise users hate us
244 if ( !valid && ext.toLower() == "mp3" && !installDistroCodec(AmarokConfig::soundSystem()) )
245 Amarok::StatusBar::instance()->longMessageThreadSafe(
246 i18n( "<p>The %1 claims it <b>cannot</b> play MP3 files."
247 "<p>You may want to choose a different engine from the <i>Configure Dialog</i>, or examine "
248 "the installation of the multimedia-framework that the current engine uses. "
249 "<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok HandBook</i>.", AmarokConfig::soundSystem() ), KDE::StatusBar::Error );
251 // Cache this result for the next lookup
252 if ( !ext.isEmpty() )
253 extensionCache().insert( ext, valid );
256 return valid;
259 bool EngineController::installDistroCodec( const QString& engine /*Filetype type*/)
261 KService::List services = KServiceTypeTrader::self()->query( "Amarok/CodecInstall"
262 , QString("[X-KDE-Amarok-codec] == 'mp3' and [X-KDE-Amarok-engine] == '%1'").arg(engine) );
263 if( !services.isEmpty() )
265 KService::Ptr service = services.first(); //list is not empty
266 QString installScript = service->exec();
267 if( !installScript.isNull() ) //just a sanity check
269 KGuiItem installButton("Install MP3 Support");
270 if(KMessageBox::questionYesNo(MainWindow::self()
271 , i18n("Amarok currently cannot play MP3 files.")
272 , i18n( "No MP3 Support" )
273 , installButton
274 , KStandardGuiItem::no()
275 , "codecInstallWarning" ) == KMessageBox::Yes )
277 KRun::runCommand(installScript, 0);
278 return true;
282 return false;
285 void EngineController::restoreSession()
287 //here we restore the session
288 //however, do note, this is always done, KDE session management is not involved
290 if( !AmarokConfig::resumeTrack().isEmpty() )
292 const KUrl url = AmarokConfig::resumeTrack();
293 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
294 if( track )
295 play( track, AmarokConfig::resumeTime() );
300 void EngineController::endSession()
302 //only update song stats, when we're not going to resume it
303 if ( !AmarokConfig::resumePlayback() && m_currentTrack )
305 trackEnded( trackPosition(), m_currentTrack->length() * 1000, "quit" );
308 PluginManager::unload( m_voidEngine );
309 m_voidEngine = 0;
312 //////////////////////////////////////////////////////////////////////////////////////////
313 // PUBLIC SLOTS
314 //////////////////////////////////////////////////////////////////////////////////////////
316 void EngineController::previous() //SLOT
318 emit orderPrevious();
322 void EngineController::next( bool forceNext ) //SLOT
324 m_previousUrl = m_currentTrack->url();
325 m_isTiming = false;
326 emit orderNext(forceNext);
330 void EngineController::play() //SLOT
332 if ( m_engine->state() == Engine::Paused )
334 m_engine->unpause();
336 else emit orderCurrent();
339 void EngineController::play( const Meta::TrackPtr& track, uint offset )
341 DEBUG_BLOCK
342 m_currentTrack = track;
343 KUrl url = track->playableUrl();
344 if( m_engine->load( url, url.protocol() == "http" || url.protocol() == "rtsp" ) )
346 if( m_engine->play( offset ) )
350 newTrackPlaying();
354 //i know that i should not jsut comment this code,
355 //but there is a lot of important logic in there, so
356 //i am leaving it in because we won't forget that it
357 //exists that way
359 void EngineController::play( const MetaBundle &bundle, uint offset )
361 DEBUG_BLOCK
363 KUrl url = bundle.url();
364 // don't destroy connection if we need to change station
365 if( url.protocol() != "lastfm" && LastFm::Controller::instance()->isPlaying() )
367 m_engine->stop();
368 LastFm::Controller::instance()->playbackStopped();
370 m_lastFm = false;
371 //Holds the time since we started trying to play non-existent files
372 //so we know when to abort
373 static QTime failure_time;
374 if ( !m_playFailureCount )
375 failure_time.start();
377 debug() << "Loading URL: " << url.url();
378 m_lastMetadata.clear();
380 //TODO bummer why'd I do it this way? it should _not_ be in play!
381 //let Amarok know that the previous track is no longer playing
382 if ( m_timer->isActive() )
383 trackEnded( trackPosition(), m_bundle.length() * 1000, "change" );
385 if ( url.isLocalFile() ) {
386 // does the file really exist? the playlist entry might be old
387 if ( ! QFile::exists( url.path()) ) {
388 //debug() << " file >" << url.path() << "< does not exist!";
389 Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") );
390 goto some_kind_of_failure;
393 else
395 if( url.protocol() == "cdda" )
396 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
397 else
398 Amarok::StatusBar::instance()->shortMessage( i18n("Connecting to stream source...") );
399 debug() << "Connecting to protocol: " << url.protocol();
402 // WebDAV protocol is HTTP with extensions (and the "webdav" scheme
403 // is a KDE-ism anyway). Most engines cope with HTTP streaming, but
404 // not through KIO, so they don't support KDE-isms.
405 if ( url.protocol() == "webdav" )
406 url.setProtocol( "http" );
407 else if ( url.protocol() == "webdavs" )
408 url.setProtocol( "https" );
410 // streams from last.fm should be handled by our proxy, in order to authenticate with the server
411 else if ( url.protocol() == "lastfm" )
413 if( LastFm::Controller::instance()->isPlaying() )
415 LastFm::Controller::instance()->getService()->changeStation( url.url() );
416 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
417 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
418 return;
420 else
422 url = LastFm::Controller::instance()->getNewProxy( url.url() );
423 if( url.isEmpty() ) goto some_kind_of_failure;
424 m_lastFm = true;
426 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
427 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
429 debug() << "New URL is " << url.url();
431 else if (url.protocol() == "daap" )
433 KUrl newUrl = MediaBrowser::instance()->getProxyUrl( url );
434 if( !newUrl.isEmpty() )
436 debug() << newUrl;
437 url = newUrl;
439 else
440 return;
443 if( m_engine->load( url, url.protocol() == "http" || url.protocol() == "rtsp" ) )
445 //assign bundle now so that it is available when the engine
446 //emits stateChanged( Playing )
447 if( !m_bundle.url().path().isEmpty() ) //wasn't playing before
448 m_previousUrl = m_bundle.url();
449 else
450 m_previousUrl = bundle.url();
451 m_bundle = bundle;
453 if( m_engine->play( offset ) )
455 //Reset failure count as we are now successfully playing a song
456 m_playFailureCount = 0;
458 // Ask engine for track length, if available. It's more reliable than TagLib.
459 const uint trackLength = m_engine->length() / 1000;
460 if ( trackLength ) m_bundle.setLength( trackLength );
462 m_xFadeThisTrack = !m_engine->isStream() && !(url.protocol() == "cdda") &&
463 m_bundle.length()*1000 - offset - AmarokConfig::crossfadeLength()*2 > 0;
465 newMetaDataNotify( m_bundle, true );
466 return;
470 some_kind_of_failure:
471 debug() << "Failed to play this track.";
473 ++m_playFailureCount;
475 //Code to skip to next track if playback fails:
477 // The failure counter is reset if a track plays successfully or if playback is
478 // stopped, for whatever reason.
479 // For normal playback, the attempt to play is stopped at the end of the playlist
480 // For repeat playlist , a whole playlist worth of songs is tried
481 // For repeat album, the number of songs tried is the number of tracks from the
482 // album that are in the playlist.
483 // For repeat track, no attempts are made
484 // To prevent GUI freezes we don't try to play again after 0.5s of failure
485 int totalTracks = Playlist::instance()->totalTrackCount();
486 int currentTrack = Playlist::instance()->currentTrackIndex();
487 if ( ( ( Amarok::repeatPlaylist() && static_cast<int>(m_playFailureCount) < totalTracks )
488 || ( Amarok::repeatNone() && currentTrack != totalTracks - 1 )
489 || ( Amarok::repeatAlbum() && m_playFailureCount < Playlist::instance()->repeatAlbumTrackCount() ) )
490 && failure_time.elapsed() < 500 )
493 debug() << "Skipping to next track.";
495 // The test for loaded must be done _before_ next is called
496 if ( !m_engine->loaded() )
498 //False gives behaviour as if track played successfully
499 next( false );
500 QTimer::singleShot( 0, this, SLOT(play()) );
502 else
504 //False gives behaviour as if track played successfully
505 next( false );
508 else
510 //Stop playback, including resetting failure count (as all new failures are
511 //treated as independent after playback is stopped)
512 stop();
517 void EngineController::pause() //SLOT
519 if ( m_engine->loaded() && !LastFm::Controller::instance()->isPlaying() )
520 m_engine->pause();
524 void EngineController::stop() //SLOT
526 //Reset failure counter as after stop, everything else is unrelated
527 m_playFailureCount = 0;
529 //let Amarok know that the previous track is no longer playing
530 if( m_currentTrack )
531 trackEnded( trackPosition(), m_currentTrack->length() * 1000, "stop" );
533 //Remove requirement for track to be loaded for stop to be called (fixes gltiches
534 //where stop never properly happens if call to m_engine->load fails in play)
535 //if ( m_engine->loaded() )
536 m_engine->stop();
541 void EngineController::playPause() //SLOT
543 //this is used by the TrayIcon, PlayPauseAction and DCOP
545 if( m_engine->state() == Engine::Playing )
547 pause();
549 else if( m_engine->state() == Engine::Paused )
551 if ( m_engine->loaded() )
552 m_engine->unpause();
554 else
555 play();
559 void EngineController::seek( int ms ) //SLOT
561 Meta::TrackPtr track = currentTrack();
562 if( !track )
563 return;
564 if( track->length() > 0 )
566 trackPositionChangedNotify( ms, true ); /* User seek */
567 engine()->seek( ms );
572 void EngineController::seekRelative( int ms ) //SLOT
574 if( m_engine->state() != Engine::Empty )
576 int newPos = m_engine->position() + ms;
577 seek( newPos <= 0 ? 1 : newPos );
582 void EngineController::seekForward( int ms )
584 seekRelative( ms );
588 void EngineController::seekBackward( int ms )
590 seekRelative( -ms );
594 int EngineController::increaseVolume( int ticks ) //SLOT
596 return setVolume( m_engine->volume() + ticks );
600 int EngineController::decreaseVolume( int ticks ) //SLOT
602 return setVolume( m_engine->volume() - ticks );
606 int EngineController::setVolume( int percent ) //SLOT
608 m_muteVolume = 0;
610 if( percent < 0 ) percent = 0;
611 if( percent > 100 ) percent = 100;
613 if( (uint)percent != m_engine->volume() )
615 m_engine->setVolume( (uint)percent );
617 percent = m_engine->volume();
618 AmarokConfig::setMasterVolume( percent );
619 volumeChangedNotify( percent );
620 return percent;
622 else // Still notify
624 volumeChangedNotify( percent );
627 return m_engine->volume();
631 void EngineController::mute() //SLOT
633 if( m_muteVolume == 0 )
635 int saveVolume = m_engine->volume();
636 setVolume( 0 );
637 m_muteVolume = saveVolume;
639 else
641 setVolume( m_muteVolume );
642 m_muteVolume = 0;
646 Meta::TrackPtr
647 EngineController::currentTrack() const
649 return m_engine->state() == Engine::Empty ? Meta::TrackPtr() : m_currentTrack;
652 //do we actually need this method?
653 uint
654 EngineController::trackLength() const
656 Meta::TrackPtr track = currentTrack();
657 if( track )
658 return track->length() * 1000;
659 else
660 return 0;
663 //do we actually need this method?
664 KUrl
665 EngineController::playingURL() const
667 Meta::TrackPtr track = currentTrack();
668 if( track )
669 return track->playableUrl();
670 else
671 return KUrl();
675 void
676 EngineController::slotEngineMetaData( const QHash<qint64, QString> &newMetaData )
678 bool trackChange = m_currentTrack->playableUrl().isLocalFile();
679 newMetaDataNotify( newMetaData, trackChange );
683 //////////////////////////////////////////////////////////////////////////////////////////
684 // PRIVATE SLOTS
685 //////////////////////////////////////////////////////////////////////////////////////////
687 void EngineController::slotMainTimer() //SLOT
689 const uint position = trackPosition();
691 trackPositionChangedNotify( position );
693 // Crossfading
694 if ( m_engine->state() == Engine::Playing &&
695 AmarokConfig::crossfade() && m_xFadeThisTrack &&
696 m_engine->hasPluginProperty( "HasCrossfade" ) &&
697 Playlist::instance()->stopAfterMode() != Playlist::StopAfterCurrent &&
698 ( (uint) AmarokConfig::crossfadeType() == 0 || //Always or...
699 (uint) AmarokConfig::crossfadeType() == 1 ) && //...automatic track change only
700 Playlist::instance()->isTrackAfter() &&
701 m_currentTrack->length()*1000 - position < (uint) AmarokConfig::crossfadeLength() )
703 debug() << "Crossfading to next track...\n";
704 m_engine->setXFadeNextTrack( true );
705 trackDone();
707 else if ( m_engine->state() == Engine::Playing &&
708 AmarokConfig::fadeout() &&
709 Playlist::instance()->stopAfterMode() == Playlist::StopAfterCurrent &&
710 m_currentTrack->length()*1000 - position < (uint) AmarokConfig::fadeoutLength() )
712 m_engine->stop();
717 void EngineController::slotTrackEnded() //SLOT
719 if ( AmarokConfig::trackDelayLength() > 0 )
721 //FIXME not perfect
722 if ( !m_isTiming )
724 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackDone()) );
725 m_isTiming = true;
729 else trackDone();
733 void EngineController::slotStateChanged( Engine::State newState ) //SLOT
736 switch( newState )
738 case Engine::Empty:
740 //FALL THROUGH...
742 case Engine::Paused:
744 m_timer->stop();
745 break;
747 case Engine::Playing:
749 m_timer->start( MAIN_TIMER );
750 break;
752 default:
756 stateChangedNotify( newState );
759 uint EngineController::trackPosition() const
761 const uint buffertime = 5000; // worked for me with xine engine over 1 mbit dsl
762 if( !m_engine )
763 return 0;
764 uint pos = m_engine->position();
765 if( !m_lastFm )
766 return pos;
768 if( m_positionOffset + buffertime <= pos )
769 return pos - m_positionOffset - buffertime;
770 if( m_lastPositionOffset + buffertime <= pos )
771 return pos - m_lastPositionOffset - buffertime;
772 return pos;
776 #include "enginecontroller.moc"