some more work on collabsible albums. I think I will need to optimize the playlist...
[amarok.git] / src / enginecontroller.cpp
blobb1fd47eb316e12cbd0dd4b63c0d70c8e9b22597e
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 "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>
34 #include <KIO/Job>
35 #include <KMessageBox>
36 #include <KRun>
38 #include <QByteArray>
39 #include <QFile>
40 #include <QTimer>
42 #include <cstdlib>
45 EngineController::ExtensionCache EngineController::s_extensionCache;
48 EngineController*
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;
55 return &Instance;
59 EngineController::EngineController()
60 : m_engine( 0 )
61 , m_voidEngine( 0 )
62 , m_delayTime( 0 )
63 , m_muteVolume( 0 )
64 , m_xFadeThisTrack( false )
65 , m_timer( new QTimer( this ) )
66 , m_playFailureCount( 0 )
67 , m_lastFm( false )
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 //////////////////////////////////////////////////////////////////////////////////////////
83 // PUBLIC
84 //////////////////////////////////////////////////////////////////////////////////////////
86 EngineBase*
87 EngineController::loadEngine() //static
89 /// always returns a valid pointer to EngineBase
91 DEBUG_BLOCK
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 );
135 return m_engine;
138 #include <q3valuevector.h>
139 EngineBase*
140 EngineController::loadEngine( const QString &engineName )
142 /// always returns a valid plugin (exits if it can't get one)
144 DEBUG_BLOCK
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] );
155 #undef rank
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 );
164 if( plugin ) {
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()) );
172 if( bar )
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> ) ) );
183 if( engine->init() )
184 return engine;
185 else
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>"
200 "$ kbuildsycoca<br>"
201 "$ amarok</pre>"
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
208 return 0;
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 );
219 //Port 2.0
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
228 // into the playlist
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() )
239 return false;
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 );
258 return 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" )
275 , installButton
276 , KStandardGuiItem::no()
277 , "codecInstallWarning" ) == KMessageBox::Yes )
279 KRun::runCommand(installScript, 0);
280 return true;
284 return false;
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 );
296 if( track )
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 );
311 m_voidEngine = 0;
314 //////////////////////////////////////////////////////////////////////////////////////////
315 // PUBLIC SLOTS
316 //////////////////////////////////////////////////////////////////////////////////////////
318 void EngineController::previous() //SLOT
320 emit orderPrevious();
324 void EngineController::next( bool forceNext ) //SLOT
326 m_previousUrl = m_currentTrack->url();
327 m_isTiming = false;
328 emit orderNext(forceNext);
332 void EngineController::play() //SLOT
334 if ( m_engine->state() == Engine::Paused )
336 m_engine->unpause();
338 else emit orderCurrent();
341 void EngineController::play( const Meta::TrackPtr& track, uint offset )
343 DEBUG_BLOCK
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 ) )
352 newTrackPlaying();
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
359 //exists that way
361 void EngineController::play( const MetaBundle &bundle, uint offset )
363 DEBUG_BLOCK
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() )
369 m_engine->stop();
370 LastFm::Controller::instance()->playbackStopped();
372 m_lastFm = false;
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;
395 else
397 if( url.protocol() == "cdda" )
398 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
399 else
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& ) ) );
420 return;
422 else
424 url = LastFm::Controller::instance()->getNewProxy( url.url() );
425 if( url.isEmpty() ) goto some_kind_of_failure;
426 m_lastFm = true;
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() )
438 debug() << newUrl;
439 url = newUrl;
441 else
442 return;
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();
451 else
452 m_previousUrl = bundle.url();
453 m_bundle = bundle;
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 );
468 return;
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
501 next( false );
502 QTimer::singleShot( 0, this, SLOT(play()) );
504 else
506 //False gives behaviour as if track played successfully
507 next( false );
510 else
512 //Stop playback, including resetting failure count (as all new failures are
513 //treated as independent after playback is stopped)
514 stop();
519 void EngineController::pause() //SLOT
521 if ( m_engine->loaded() && !LastFm::Controller::instance()->isPlaying() )
522 m_engine->pause();
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
532 if( m_currentTrack )
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() )
538 m_engine->stop();
543 void EngineController::playPause() //SLOT
545 //this is used by the TrayIcon, PlayPauseAction and DCOP
547 if( m_engine->state() == Engine::Playing )
549 pause();
551 else if( m_engine->state() == Engine::Paused )
553 if ( m_engine->loaded() )
554 m_engine->unpause();
556 else
557 play();
561 void EngineController::seek( int ms ) //SLOT
563 Meta::TrackPtr track = currentTrack();
564 if( !track )
565 return;
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 )
586 seekRelative( ms );
590 void EngineController::seekBackward( int ms )
592 seekRelative( -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
610 m_muteVolume = 0;
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 );
622 return percent;
624 else // Still notify
626 volumeChangedNotify( percent );
629 return m_engine->volume();
633 void EngineController::mute() //SLOT
635 if( m_muteVolume == 0 )
637 int saveVolume = m_engine->volume();
638 setVolume( 0 );
639 m_muteVolume = saveVolume;
641 else
643 setVolume( m_muteVolume );
644 m_muteVolume = 0;
648 Meta::TrackPtr
649 EngineController::currentTrack() const
651 return m_engine->state() == Engine::Empty ? Meta::TrackPtr() : m_currentTrack;
654 //do we actually need this method?
655 uint
656 EngineController::trackLength() const
658 Meta::TrackPtr track = currentTrack();
659 if( track )
660 return track->length() * 1000;
661 else
662 return 0;
665 //do we actually need this method?
666 KUrl
667 EngineController::playingURL() const
669 Meta::TrackPtr track = currentTrack();
670 if( track )
671 return track->playableUrl();
672 else
673 return KUrl();
677 void
678 EngineController::slotEngineMetaData( const QHash<qint64, QString> &newMetaData )
680 bool trackChange = m_currentTrack->playableUrl().isLocalFile();
681 newMetaDataNotify( newMetaData, trackChange );
685 //////////////////////////////////////////////////////////////////////////////////////////
686 // PRIVATE SLOTS
687 //////////////////////////////////////////////////////////////////////////////////////////
689 void EngineController::slotMainTimer() //SLOT
691 const uint position = trackPosition();
693 trackPositionChangedNotify( position );
695 // Crossfading
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 );
707 trackDone();
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() )
714 m_engine->stop();
719 void EngineController::slotTrackEnded() //SLOT
721 if ( AmarokConfig::trackDelayLength() > 0 )
723 //FIXME not perfect
724 if ( !m_isTiming )
726 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackDone()) );
727 m_isTiming = true;
731 else trackDone();
735 void EngineController::slotStateChanged( Engine::State newState ) //SLOT
738 switch( newState )
740 case Engine::Empty:
742 //FALL THROUGH...
744 case Engine::Paused:
746 m_timer->stop();
747 break;
749 case Engine::Playing:
751 m_timer->start( MAIN_TIMER );
752 break;
754 default:
758 stateChangedNotify( newState );
761 uint EngineController::trackPosition() const
763 const uint buffertime = 5000; // worked for me with xine engine over 1 mbit dsl
764 if( !m_engine )
765 return 0;
766 uint pos = m_engine->position();
767 if( !m_lastFm )
768 return pos;
770 if( m_positionOffset + buffertime <= pos )
771 return pos - m_positionOffset - buffertime;
772 if( m_lastPositionOffset + buffertime <= pos )
773 return pos - m_lastPositionOffset - buffertime;
774 return pos;
778 #include "enginecontroller.moc"