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"
17 #include "amarokconfig.h"
19 #include "enginebase.h"
20 #include "enginecontroller.h"
22 #include "mediabrowser.h"
24 #include "playlistloader.h"
25 #include "pluginmanager.h"
26 #include "statusbar.h"
32 #include <kapplication.h>
33 #include <kio/global.h>
35 #include <kmessagebox.h>
41 EngineController::ExtensionCache
EngineController::s_extensionCache
;
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
;
55 EngineController::EngineController()
60 , m_xFadeThisTrack( false )
61 , m_timer( new QTimer( this ) )
62 , m_playFailureCount( 0 )
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 //////////////////////////////////////////////////////////////////////////////////////////
80 //////////////////////////////////////////////////////////////////////////////////////////
83 EngineController::loadEngine() //static
85 /// always returns a valid pointer to EngineBase
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
);
134 #include <q3valuevector.h>
136 EngineController::loadEngine( const QString
&engineName
)
138 /// always returns a valid plugin (exits if it can't get one)
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] );
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
);
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()) );
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
& )) );
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>"
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
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
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() )
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
);
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" )
272 , KStandardGuiItem::no()
273 , "codecInstallWarning" ) == KMessageBox::Yes
)
275 KRun::runCommand(installScript
);
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
);
309 //////////////////////////////////////////////////////////////////////////////////////////
311 //////////////////////////////////////////////////////////////////////////////////////////
313 void EngineController::previous() //SLOT
315 emit
orderPrevious();
319 void EngineController::next( bool forceNext
) //SLOT
321 m_previousUrl
= m_bundle
.url();
323 emit
orderNext(forceNext
);
327 void EngineController::play() //SLOT
329 if ( m_engine
->state() == Engine::Paused
)
333 else emit
orderCurrent();
337 void EngineController::play( const MetaBundle
&bundle
, uint offset
)
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() )
346 LastFm::Controller::instance()->playbackStopped();
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
;
373 if( url
.protocol() == "cdda" )
374 Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
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
& ) ) );
400 url
= LastFm::Controller::instance()->getNewProxy( url
.url() );
401 if( url
.isEmpty() ) goto some_kind_of_failure
;
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
;
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();
428 m_previousUrl
= bundle
.url();
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 */ );
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
478 QTimer::singleShot( 0, this, SLOT(play()) );
482 //False gives behaviour as if track played successfully
488 //Stop playback, including resetting failure count (as all new failures are
489 //treated as independent after playback is stopped)
495 void EngineController::pause() //SLOT
497 if ( m_engine
->loaded() && !LastFm::Controller::instance()->isPlaying() )
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() )
517 void EngineController::playPause() //SLOT
519 //this is used by the TrayIcon, PlayPauseAction and DCOP
521 if( m_engine
->state() == Engine::Playing
)
525 else if( m_engine
->state() == Engine::Paused
)
527 if ( m_engine
->loaded() )
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
)
561 void EngineController::seekBackward( int 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
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
);
597 volumeChangedNotify( percent
);
600 return m_engine
->volume();
604 void EngineController::mute() //SLOT
606 if( m_muteVolume
== 0 )
608 int saveVolume
= m_engine
->volume();
610 m_muteVolume
= saveVolume
;
614 setVolume( m_muteVolume
);
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
) )
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();
643 m_lastPositionOffset
= m_positionOffset
;
645 m_positionOffset
= m_engine
->position();
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();
655 newMetaDataNotify( bundle
, false /* no track change */ );
658 //////////////////////////////////////////////////////////////////////////////////////////
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
);
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 );
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() )
718 void EngineController::slotTrackEnded() //SLOT
720 if ( AmarokConfig::trackDelayLength() > 0 )
725 QTimer::singleShot( AmarokConfig::trackDelayLength(), this, SLOT(trackFinished()) );
730 else trackFinished();
734 void EngineController::slotStateChanged( Engine::State newState
) //SLOT
748 case Engine::Playing
:
750 m_timer
->start( MAIN_TIMER
);
757 stateChangedNotify( newState
);
760 uint
EngineController::trackPosition() const
762 const uint buffertime
= 5000; // worked for me with xine engine over 1 mbit dsl
765 uint pos
= m_engine
->position();
769 if( m_positionOffset
+ buffertime
<= pos
)
770 return pos
- m_positionOffset
- buffertime
;
771 if( m_lastPositionOffset
+ buffertime
<= pos
)
772 return pos
- m_lastPositionOffset
- buffertime
;
777 #include "enginecontroller.moc"