Not crap after all...
[amarok.git] / src / enginecontroller.cpp
blob850bdb1871e6f18db36c8bf4fc890490c2b58d71
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 "amarok.h"
17 #include "amarokconfig.h"
18 #include "debug.h"
19 #include "enginebase.h"
20 #include "enginecontroller.h"
21 #include "lastfm.h"
22 #include "mediabrowser.h"
23 #include "playlist.h"
24 #include "playlistloader.h"
25 #include "pluginmanager.h"
26 #include "statusbar.h"
28 #include <QByteArray>
29 #include <QFile>
30 #include <QTimer>
32 #include <kapplication.h>
33 #include <kio/global.h>
34 #include <kio/job.h>
35 #include <kmessagebox.h>
36 #include <krun.h>
38 #include <cstdlib>
41 EngineController::ExtensionCache EngineController::s_extensionCache;
44 EngineController*
45 EngineController::instance()
47 //will only be instantiated the first time this function is called
48 //will work with the inline directive
49 static EngineController Instance;
51 return &Instance;
55 EngineController::EngineController()
56 : m_engine( 0 )
57 , m_voidEngine( 0 )
58 , m_delayTime( 0 )
59 , m_muteVolume( 0 )
60 , m_xFadeThisTrack( false )
61 , m_timer( new QTimer( this ) )
62 , m_playFailureCount( 0 )
63 , m_lastFm( false )
64 , m_positionOffset( 0 )
65 , m_lastPositionOffset( 0 )
67 m_voidEngine = m_engine = loadEngine( "void-engine" );
69 connect( m_timer, SIGNAL( timeout() ), SLOT( slotMainTimer() ) );
72 EngineController::~EngineController()
74 DEBUG_FUNC_INFO //we like to know when singletons are destroyed
78 //////////////////////////////////////////////////////////////////////////////////////////
79 // PUBLIC
80 //////////////////////////////////////////////////////////////////////////////////////////
82 EngineBase*
83 EngineController::loadEngine() //static
85 /// always returns a valid pointer to EngineBase
87 DEBUG_BLOCK
88 //TODO remember song position, and resume playback
90 // new engine, new ext cache required
91 extensionCache().clear();
93 if( m_engine != m_voidEngine ) {
94 EngineBase *oldEngine = m_engine;
96 // we assign this first for thread-safety,
97 // EngineController::engine() must always return an engine!
98 m_engine = m_voidEngine;
100 // we unload the old engine first because there are a number of
101 // bugs associated with keeping one engine loaded while loading
102 // another, eg xine-engine can't init(), and aRts-engine crashes
103 PluginManager::unload( oldEngine );
105 // the engine is not required to do this when we unload it but
106 // we need to do it to ensure Amarok looks correct.
107 // We don't do this for the void-engine because that
108 // means Amarok sets all components to empty on startup, which is
109 // their responsibility.
110 slotStateChanged( Engine::Empty );
113 m_engine = loadEngine( AmarokConfig::soundSystem() );
115 const QString engineName = PluginManager::getService( m_engine )->property( "X-KDE-Amarok-name" ).toString();
117 if( !AmarokConfig::soundSystem().isEmpty() && engineName != AmarokConfig::soundSystem() ) {
118 //AmarokConfig::soundSystem() is empty on the first-ever-run
120 Amarok::StatusBar::instance()->longMessageThreadSafe( i18n(
121 "Sorry, the '%1' could not be loaded, instead we have loaded the '%2'.", AmarokConfig::soundSystem(), engineName ),
122 KDE::StatusBar::Sorry );
124 AmarokConfig::setSoundSystem( engineName );
127 // Important: Make sure soundSystem is not empty
128 if( AmarokConfig::soundSystem().isEmpty() )
129 AmarokConfig::setSoundSystem( engineName );
131 return m_engine;
134 #include <q3valuevector.h>
135 EngineBase*
136 EngineController::loadEngine( const QString &engineName )
138 /// always returns a valid plugin (exits if it can't get one)
140 DEBUG_BLOCK
142 QString query = "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] != '%1'";
143 KService::List offers = PluginManager::query( query.arg( engineName ) );
145 // sort by rank, QValueList::operator[] is O(n), so this is quite inefficient
146 #define rank( x ) (x)->property( "X-KDE-Amarok-rank" ).toInt()
147 for( int n = offers.count()-1, i = 0; i < n; i++ )
148 for( int j = n; j > i; j-- )
149 if( rank( offers[j] ) > rank( offers[j-1] ) )
150 qSwap( offers[j], offers[j-1] );
151 #undef rank
153 // this is the actual engine we want
154 query = "[X-KDE-Amarok-plugintype] == 'engine' and [X-KDE-Amarok-name] == '%1'";
155 offers = PluginManager::query( query.arg( engineName ) ) + offers;
157 oldForeachType( KService::List, offers ) {
158 Amarok::Plugin *plugin = PluginManager::createFromService( *it );
160 if( plugin ) {
161 QObject *bar = Amarok::StatusBar::instance();
162 EngineBase *engine = static_cast<EngineBase*>( plugin );
164 connect( engine, SIGNAL(stateChanged( Engine::State )),
165 this, SLOT(slotStateChanged( Engine::State )) );
166 connect( engine, SIGNAL(trackEnded()),
167 this, SLOT(slotTrackEnded()) );
168 if( bar )
170 connect( engine, SIGNAL(statusText( const QString& )),
171 bar, SLOT(shortMessage( const QString& )) );
172 connect( engine, SIGNAL(infoMessage( const QString& )),
173 bar, SLOT(longMessage( const QString& )) );
175 connect( engine, SIGNAL(metaData( const Engine::SimpleMetaBundle& )),
176 this, SLOT(slotEngineMetaData( const Engine::SimpleMetaBundle& )) );
177 connect( engine, SIGNAL(showConfigDialog( const QByteArray& )),
178 kapp, SLOT(slotConfigAmarok( const QByteArray& )) );
180 if( engine->init() )
181 return engine;
182 else
183 warning() << "Could not init() an engine\n";
187 KRun::runCommand( "kbuildsycoca4" );
189 KMessageBox::error( 0, i18n(
190 "<p>Amarok could not find any sound-engine plugins. "
191 "Amarok is now updating the KDE configuration database. Please wait a couple of minutes, then restart Amarok.</p>"
192 "<p>If this does not help, "
193 "it is likely that Amarok is installed under the wrong prefix, please fix your installation using:<pre>"
194 "$ cd /path/to/amarok/source-code/<br>"
195 "$ su -c \"make uninstall\"<br>"
196 "$ ./configure --prefix=`kde-config --prefix` && su -c \"make install\"<br>"
197 "$ kbuildsycoca<br>"
198 "$ amarok</pre>"
199 "More information can be found in the README file. For further assistance join us at #amarok on irc.freenode.net.</p>" ) );
201 // don't use QApplication::exit, as the eventloop may not have started yet
202 std::exit( EXIT_SUCCESS );
204 // Not executed, just here to prevent compiler warning
205 return 0;
209 bool EngineController::canDecode( const KUrl &url ) //static
211 //NOTE this function must be thread-safe
213 const QString fileName = url.fileName();
214 const QString ext = Amarok::extension( fileName );
216 if ( PlaylistFile::isPlaylistFile( fileName ) ) return false;
218 // Ignore protocols "fetchcover" and "musicbrainz", they're not local but we don't really want them in the playlist :)
219 if ( url.protocol() == "fetchcover" || url.protocol() == "musicbrainz" ) return false;
221 // Accept non-local files, since we can't test them for validity at this point
222 // TODO actually, only accept unconditionally http stuff
223 // TODO this actually makes things like "Blarrghgjhjh:!!!" automatically get inserted
224 // into the playlist
225 // TODO remove for Amarok 1.3 and above silly checks, instead check for http type servers
226 if ( !url.isLocalFile() ) return true;
228 // If extension is already in the cache, return cache result
229 if ( extensionCache().contains( ext ) )
230 return s_extensionCache[ext];
232 // If file has 0 bytes, ignore it and return false, not to infect the cache with corrupt files.
233 // TODO also ignore files that are too small?
234 if ( !QFileInfo(url.path()).size() )
235 return false;
237 const bool valid = engine()->canDecode( url );
239 if( engine() != EngineController::instance()->m_voidEngine )
241 //we special case this as otherwise users hate us
242 if ( !valid && ext.toLower() == "mp3" && !installDistroCodec(AmarokConfig::soundSystem()) )
243 Amarok::StatusBar::instance()->longMessageThreadSafe(
244 i18n( "<p>The %1 claims it <b>cannot</b> play MP3 files."
245 "<p>You may want to choose a different engine from the <i>Configure Dialog</i>, or examine "
246 "the installation of the multimedia-framework that the current engine uses. "
247 "<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok HandBook</i>.", AmarokConfig::soundSystem() ), KDE::StatusBar::Error );
249 // Cache this result for the next lookup
250 if ( !ext.isEmpty() )
251 extensionCache().insert( ext, valid );
254 return valid;
257 bool EngineController::installDistroCodec( const QString& engine /*Filetype type*/)
259 KService::List services = KServiceTypeTrader::self()->query( "Amarok/CodecInstall"
260 , QString("[X-KDE-Amarok-codec] == 'mp3' and [X-KDE-Amarok-engine] == '%1'").arg(engine) );
261 if( !services.isEmpty() )
263 KService::Ptr service = services.first(); //list is not empty
264 QString installScript = service->exec();
265 if( !installScript.isNull() ) //just a sanity check
267 KGuiItem installButton("Install MP3 Support");
268 if(KMessageBox::questionYesNo(PlaylistWindow::self()
269 , i18n("Amarok currently cannot play MP3 files.")
270 , i18n( "No MP3 Support" )
271 , installButton
272 , KStandardGuiItem::no()
273 , "codecInstallWarning" ) == KMessageBox::Yes )
275 KRun::runCommand(installScript);
276 return true;
280 return false;
283 void EngineController::restoreSession()
285 //here we restore the session
286 //however, do note, this is always done, KDE session management is not involved
288 if( !AmarokConfig::resumeTrack().isEmpty() )
290 const KUrl url = AmarokConfig::resumeTrack();
292 play( MetaBundle( url ), AmarokConfig::resumeTime() );
297 void EngineController::endSession()
299 //only update song stats, when we're not going to resume it
300 if ( !AmarokConfig::resumePlayback() )
302 trackEnded( trackPosition(), m_bundle.length() * 1000, "quit" );
305 PluginManager::unload( m_voidEngine );
306 m_voidEngine = 0;
309 //////////////////////////////////////////////////////////////////////////////////////////
310 // PUBLIC SLOTS
311 //////////////////////////////////////////////////////////////////////////////////////////
313 void EngineController::previous() //SLOT
315 emit orderPrevious();
319 void EngineController::next( bool forceNext ) //SLOT
321 m_previousUrl = m_bundle.url();
322 m_isTiming = false;
323 emit orderNext(forceNext);
327 void EngineController::play() //SLOT
329 if ( m_engine->state() == Engine::Paused )
331 m_engine->unpause();
333 else emit orderCurrent();
337 void EngineController::play( const MetaBundle &bundle, uint offset )
339 DEBUG_BLOCK
341 KUrl url = bundle.url();
342 // don't destroy connection if we need to change station
343 if( url.protocol() != "lastfm" && LastFm::Controller::instance()->isPlaying() )
345 m_engine->stop();
346 LastFm::Controller::instance()->playbackStopped();
348 m_lastFm = false;
349 //Holds the time since we started trying to play non-existent files
350 //so we know when to abort
351 static QTime failure_time;
352 if ( !m_playFailureCount )
353 failure_time.start();
355 debug() << "Loading URL: " << url.url() << endl;
356 m_lastMetadata.clear();
358 //TODO bummer why'd I do it this way? it should _not_ be in play!
359 //let Amarok know that the previous track is no longer playing
360 if ( m_timer->isActive() )
361 trackEnded( trackPosition(), m_bundle.length() * 1000, "change" );
363 if ( url.isLocalFile() ) {
364 // does the file really exist? the playlist entry might be old
365 if ( ! QFile::exists( url.path()) ) {
366 //debug() << " file >" << url.path() << "< does not exist!" << endl;
367 Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") );
368 goto some_kind_of_failure;
371 else
373 if( url.protocol() == "cdda" )
374 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
375 else
376 Amarok::StatusBar::instance()->shortMessage( i18n("Connecting to stream source...") );
377 debug() << "Connecting to protocol: " << url.protocol() << endl;
380 // WebDAV protocol is HTTP with extensions (and the "webdav" scheme
381 // is a KDE-ism anyway). Most engines cope with HTTP streaming, but
382 // not through KIO, so they don't support KDE-isms.
383 if ( url.protocol() == "webdav" )
384 url.setProtocol( "http" );
385 else if ( url.protocol() == "webdavs" )
386 url.setProtocol( "https" );
388 // streams from last.fm should be handled by our proxy, in order to authenticate with the server
389 else if ( url.protocol() == "lastfm" )
391 if( LastFm::Controller::instance()->isPlaying() )
393 LastFm::Controller::instance()->getService()->changeStation( url.url() );
394 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
395 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
396 return;
398 else
400 url = LastFm::Controller::instance()->getNewProxy( url.url() );
401 if( url.isEmpty() ) goto some_kind_of_failure;
402 m_lastFm = true;
404 connect( LastFm::Controller::instance()->getService(), SIGNAL( metaDataResult( const MetaBundle& ) ),
405 this, SLOT( slotStreamMetaData( const MetaBundle& ) ) );
407 debug() << "New URL is " << url.url() << endl;
409 else if (url.protocol() == "daap" )
411 KUrl newUrl = MediaBrowser::instance()->getProxyUrl( url );
412 if( !newUrl.isEmpty() )
414 debug() << newUrl << endl;
415 url = newUrl;
417 else
418 return;
421 if( m_engine->load( url, url.protocol() == "http" || url.protocol() == "rtsp" ) )
423 //assign bundle now so that it is available when the engine
424 //emits stateChanged( Playing )
425 if( !m_bundle.url().path().isEmpty() ) //wasn't playing before
426 m_previousUrl = m_bundle.url();
427 else
428 m_previousUrl = bundle.url();
429 m_bundle = bundle;
431 if( m_engine->play( offset ) )
433 //Reset failure count as we are now successfully playing a song
434 m_playFailureCount = 0;
436 // Ask engine for track length, if available. It's more reliable than TagLib.
437 const uint trackLength = m_engine->length() / 1000;
438 if ( trackLength ) m_bundle.setLength( trackLength );
440 m_xFadeThisTrack = !m_engine->isStream() && !(url.protocol() == "cdda") &&
441 m_bundle.length()*1000 - offset - AmarokConfig::crossfadeLength()*2 > 0;
443 newMetaDataNotify( m_bundle, true /* track change */ );
444 return;
448 some_kind_of_failure:
449 debug() << "Failed to play this track." << endl;
451 ++m_playFailureCount;
453 //Code to skip to next track if playback fails:
455 //* The failure counter is reset if a track plays successfully or if playback is
456 // stopped, for whatever reason.
457 //* For normal playback, the attempt to play is stopped at the end of the playlist
458 //* For repeat playlist , a whole playlist worth of songs is tried
459 //* For repeat album, the number of songs tried is the number of tracks from the
460 // album that are in the playlist.
461 //* For repeat track, no attempts are made
462 //* To prevent GUI freezes we don't try to play again after 0.5s of failure
463 int totalTracks = Playlist::instance()->totalTrackCount();
464 int currentTrack = Playlist::instance()->currentTrackIndex();
465 if ( ( ( Amarok::repeatPlaylist() && static_cast<int>(m_playFailureCount) < totalTracks )
466 || ( Amarok::repeatNone() && currentTrack != totalTracks - 1 )
467 || ( Amarok::repeatAlbum() && m_playFailureCount < Playlist::instance()->repeatAlbumTrackCount() ) )
468 && failure_time.elapsed() < 500 )
471 debug() << "Skipping to next track." << endl;
473 // The test for loaded must be done _before_ next is called
474 if ( !m_engine->loaded() )
476 //False gives behaviour as if track played successfully
477 next( false );
478 QTimer::singleShot( 0, this, SLOT(play()) );
480 else
482 //False gives behaviour as if track played successfully
483 next( false );
486 else
488 //Stop playback, including resetting failure count (as all new failures are
489 //treated as independent after playback is stopped)
490 stop();
495 void EngineController::pause() //SLOT
497 if ( m_engine->loaded() && !LastFm::Controller::instance()->isPlaying() )
498 m_engine->pause();
502 void EngineController::stop() //SLOT
504 //Reset failure counter as after stop, everything else is unrelated
505 m_playFailureCount = 0;
507 //let Amarok know that the previous track is no longer playing
508 trackEnded( trackPosition(), m_bundle.length() * 1000, "stop" );
510 //Remove requirement for track to be loaded for stop to be called (fixes gltiches
511 //where stop never properly happens if call to m_engine->load fails in play)
512 //if ( m_engine->loaded() )
513 m_engine->stop();
517 void EngineController::playPause() //SLOT
519 //this is used by the TrayIcon, PlayPauseAction and DCOP
521 if( m_engine->state() == Engine::Playing )
523 pause();
525 else if( m_engine->state() == Engine::Paused )
527 if ( m_engine->loaded() )
528 m_engine->unpause();
530 else
531 play();
535 void EngineController::seek( int ms ) //SLOT
537 if( bundle().length() > 0 )
539 trackPositionChangedNotify( ms, true ); /* User seek */
540 engine()->seek( ms );
545 void EngineController::seekRelative( int ms ) //SLOT
547 if( m_engine->state() != Engine::Empty )
549 int newPos = m_engine->position() + ms;
550 seek( newPos <= 0 ? 1 : newPos );
555 void EngineController::seekForward( int ms )
557 seekRelative( ms );
561 void EngineController::seekBackward( int ms )
563 seekRelative( -ms );
567 int EngineController::increaseVolume( int ticks ) //SLOT
569 return setVolume( m_engine->volume() + ticks );
573 int EngineController::decreaseVolume( int ticks ) //SLOT
575 return setVolume( m_engine->volume() - ticks );
579 int EngineController::setVolume( int percent ) //SLOT
581 m_muteVolume = 0;
583 if( percent < 0 ) percent = 0;
584 if( percent > 100 ) percent = 100;
586 if( (uint)percent != m_engine->volume() )
588 m_engine->setVolume( (uint)percent );
590 percent = m_engine->volume();
591 AmarokConfig::setMasterVolume( percent );
592 volumeChangedNotify( percent );
593 return percent;
595 else // Still notify
597 volumeChangedNotify( percent );
600 return m_engine->volume();
604 void EngineController::mute() //SLOT
606 if( m_muteVolume == 0 )
608 int saveVolume = m_engine->volume();
609 setVolume( 0 );
610 m_muteVolume = saveVolume;
612 else
614 setVolume( m_muteVolume );
615 m_muteVolume = 0;
620 const MetaBundle&
621 EngineController::bundle() const
623 static MetaBundle null;
624 return m_engine->state() == Engine::Empty ? null : m_bundle;
628 void EngineController::slotStreamMetaData( const MetaBundle &bundle ) //SLOT
630 // Prevent spamming by ignoring repeated identical data (some servers repeat it every 10 seconds)
631 if ( m_lastMetadata.contains( bundle ) )
632 return;
634 // We compare the new item with the last two items, because mth.house currently cycles
635 // two messages alternating, which gets very annoying
636 if ( m_lastMetadata.count() == 2 )
637 m_lastMetadata.pop_front();
639 m_lastMetadata << bundle;
641 m_previousUrl = m_bundle.url();
642 m_bundle = bundle;
643 m_lastPositionOffset = m_positionOffset;
644 if( m_lastFm )
645 m_positionOffset = m_engine->position();
646 else
647 m_positionOffset = 0;
648 newMetaDataNotify( m_bundle, false /* not a new track */ );
651 void EngineController::currentTrackMetaDataChanged( const MetaBundle& bundle )
653 m_previousUrl = m_bundle.url();
654 m_bundle = bundle;
655 newMetaDataNotify( bundle, false /* no track change */ );
658 //////////////////////////////////////////////////////////////////////////////////////////
659 // PRIVATE SLOTS
660 //////////////////////////////////////////////////////////////////////////////////////////
662 void EngineController::slotEngineMetaData( const Engine::SimpleMetaBundle &simpleBundle ) //SLOT
664 if ( !m_bundle.url().isLocalFile() )
666 MetaBundle bundle = m_bundle;
667 bundle.setArtist( simpleBundle.artist );
668 bundle.setTitle( simpleBundle.title );
669 bundle.setComment( simpleBundle.comment );
670 bundle.setAlbum( simpleBundle.album );
672 if( !simpleBundle.genre.isEmpty() )
673 bundle.setGenre( simpleBundle.genre );
674 if( !simpleBundle.bitrate.isEmpty() )
675 bundle.setBitrate( simpleBundle.bitrate.toInt() );
676 if( !simpleBundle.samplerate.isEmpty() )
677 bundle.setSampleRate( simpleBundle.samplerate.toInt() );
678 if( !simpleBundle.year.isEmpty() )
679 bundle.setYear( simpleBundle.year.toInt() );
680 if( !simpleBundle.tracknr.isEmpty() )
681 bundle.setTrack( simpleBundle.tracknr.toInt() );
683 slotStreamMetaData( bundle );
688 void EngineController::slotMainTimer() //SLOT
690 const uint position = trackPosition();
692 trackPositionChangedNotify( position );
694 // Crossfading
695 if ( m_engine->state() == Engine::Playing &&
696 AmarokConfig::crossfade() && m_xFadeThisTrack &&
697 m_engine->hasPluginProperty( "HasCrossfade" ) &&
698 Playlist::instance()->stopAfterMode() != Playlist::StopAfterCurrent &&
699 ( (uint) AmarokConfig::crossfadeType() == 0 || //Always or...
700 (uint) AmarokConfig::crossfadeType() == 1 ) && //...automatic track change only
701 Playlist::instance()->isTrackAfter() &&
702 m_bundle.length()*1000 - position < (uint) AmarokConfig::crossfadeLength() )
704 debug() << "Crossfading to next track...\n";
705 m_engine->setXFadeNextTrack( true );
706 trackFinished();
708 else if ( m_engine->state() == Engine::Playing &&
709 AmarokConfig::fadeout() &&
710 Playlist::instance()->stopAfterMode() == Playlist::StopAfterCurrent &&
711 m_bundle.length()*1000 - position < (uint) AmarokConfig::fadeoutLength() )
713 m_engine->stop();
718 void EngineController::slotTrackEnded() //SLOT
720 if ( AmarokConfig::trackDelayLength() > 0 )
722 //FIXME not perfect
723 if ( !m_isTiming )
725 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackFinished()) );
726 m_isTiming = true;
730 else trackFinished();
734 void EngineController::slotStateChanged( Engine::State newState ) //SLOT
737 switch( newState )
739 case Engine::Empty:
741 //FALL THROUGH...
743 case Engine::Paused:
745 m_timer->stop();
746 break;
748 case Engine::Playing:
750 m_timer->start( MAIN_TIMER );
751 break;
753 default:
757 stateChangedNotify( newState );
760 uint EngineController::trackPosition() const
762 const uint buffertime = 5000; // worked for me with xine engine over 1 mbit dsl
763 if( !m_engine )
764 return 0;
765 uint pos = m_engine->position();
766 if( !m_lastFm )
767 return pos;
769 if( m_positionOffset + buffertime <= pos )
770 return pos - m_positionOffset - buffertime;
771 if( m_lastPositionOffset + buffertime <= pos )
772 return pos - m_lastPositionOffset - buffertime;
773 return pos;
777 #include "enginecontroller.moc"