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 "mediabrowser.h"
25 #include "meta/meta.h"
27 #include "playlistloader.h"
28 #include "pluginmanager.h"
29 #include "statusbar.h"
31 #include <KApplication>
32 #include <kio/global.h>
34 #include <KMessageBox>
44 EngineController::ExtensionCache
EngineController::s_extensionCache
;
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
;
58 EngineController::EngineController()
63 , m_xFadeThisTrack( false )
64 , m_timer( new QTimer( this ) )
65 , m_playFailureCount( 0 )
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 //////////////////////////////////////////////////////////////////////////////////////////
83 //////////////////////////////////////////////////////////////////////////////////////////
86 EngineController::loadEngine() //static
88 /// always returns a valid pointer to EngineBase
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
);
137 #include <q3valuevector.h>
139 EngineController::loadEngine( const QString
&engineName
)
141 /// always returns a valid plugin (exits if it can't get one)
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] );
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
);
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()) );
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
> ) ) );
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>"
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
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
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() )
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
);
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" )
274 , KStandardGuiItem::no()
275 , "codecInstallWarning" ) == KMessageBox::Yes
)
277 KRun::runCommand(installScript
, 0);
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
);
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
);
312 //////////////////////////////////////////////////////////////////////////////////////////
314 //////////////////////////////////////////////////////////////////////////////////////////
316 void EngineController::previous() //SLOT
318 emit
orderPrevious();
322 void EngineController::next( bool forceNext
) //SLOT
324 m_previousUrl
= m_currentTrack
->url();
326 emit
orderNext(forceNext
);
330 void EngineController::play() //SLOT
332 if ( m_engine
->state() == Engine::Paused
)
336 else emit
orderCurrent();
339 void EngineController::play( const Meta::TrackPtr
& track
, uint offset
)
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
) )
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
359 void EngineController::play( const MetaBundle &bundle, uint offset )
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() )
368 LastFm::Controller::instance()->playbackStopped();
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;
395 if( url.protocol() == "cdda" )
396 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
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& ) ) );
422 url = LastFm::Controller::instance()->getNewProxy( url.url() );
423 if( url.isEmpty() ) goto some_kind_of_failure;
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() )
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();
450 m_previousUrl = bundle.url();
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 );
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
500 QTimer::singleShot( 0, this, SLOT(play()) );
504 //False gives behaviour as if track played successfully
510 //Stop playback, including resetting failure count (as all new failures are
511 //treated as independent after playback is stopped)
517 void EngineController::pause() //SLOT
519 if ( m_engine
->loaded() && !LastFm::Controller::instance()->isPlaying() )
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
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() )
541 void EngineController::playPause() //SLOT
543 //this is used by the TrayIcon, PlayPauseAction and DCOP
545 if( m_engine
->state() == Engine::Playing
)
549 else if( m_engine
->state() == Engine::Paused
)
551 if ( m_engine
->loaded() )
559 void EngineController::seek( int ms
) //SLOT
561 Meta::TrackPtr track
= currentTrack();
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
)
588 void EngineController::seekBackward( int 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
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
);
624 volumeChangedNotify( percent
);
627 return m_engine
->volume();
631 void EngineController::mute() //SLOT
633 if( m_muteVolume
== 0 )
635 int saveVolume
= m_engine
->volume();
637 m_muteVolume
= saveVolume
;
641 setVolume( m_muteVolume
);
647 EngineController::currentTrack() const
649 return m_engine
->state() == Engine::Empty
? Meta::TrackPtr() : m_currentTrack
;
652 //do we actually need this method?
654 EngineController::trackLength() const
656 Meta::TrackPtr track
= currentTrack();
658 return track
->length() * 1000;
663 //do we actually need this method?
665 EngineController::playingURL() const
667 Meta::TrackPtr track
= currentTrack();
669 return track
->playableUrl();
676 EngineController::slotEngineMetaData( const QHash
<qint64
, QString
> &newMetaData
)
678 bool trackChange
= m_currentTrack
->playableUrl().isLocalFile();
679 newMetaDataNotify( newMetaData
, trackChange
);
683 //////////////////////////////////////////////////////////////////////////////////////////
685 //////////////////////////////////////////////////////////////////////////////////////////
687 void EngineController::slotMainTimer() //SLOT
689 const uint position
= trackPosition();
691 trackPositionChangedNotify( position
);
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 );
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() )
717 void EngineController::slotTrackEnded() //SLOT
719 if ( AmarokConfig::trackDelayLength() > 0 )
724 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackDone()) );
733 void EngineController::slotStateChanged( Engine::State newState
) //SLOT
747 case Engine::Playing
:
749 m_timer
->start( MAIN_TIMER
);
756 stateChangedNotify( newState
);
759 uint
EngineController::trackPosition() const
761 const uint buffertime
= 5000; // worked for me with xine engine over 1 mbit dsl
764 uint pos
= m_engine
->position();
768 if( m_positionOffset
+ buffertime
<= pos
)
769 return pos
- m_positionOffset
- buffertime
;
770 if( m_lastPositionOffset
+ buffertime
<= pos
)
771 return pos
- m_lastPositionOffset
- buffertime
;
776 #include "enginecontroller.moc"