|
|
|
/***************************************************************************
|
|
|
|
* Copyright (C) 2004 Frederik Holljen <fh@ez.no> *
|
|
|
|
* (C) 2004,5 Max Howell <max.howell@methylblue.com> *
|
|
|
|
* (C) 2004,5 Mark Kretschmann *
|
|
|
|
* (C) 2006 Ian Monroe *
|
|
|
|
* *
|
|
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
|
|
* it under the terms of the GNU General Public License as published by *
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
|
|
* (at your option) any later version. *
|
|
|
|
* *
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
#define DEBUG_PREFIX "controller"
|
|
|
|
|
|
|
|
#include "amarok.h"
|
|
|
|
#include "amarokconfig.h"
|
|
|
|
#include "debug.h"
|
|
|
|
#include "enginebase.h"
|
|
|
|
#include "enginecontroller.h"
|
|
|
|
#include "lastfm.h"
|
|
|
|
#include "mediabrowser.h"
|
|
|
|
#include "playlist.h"
|
|
|
|
#include "playlistloader.h"
|
|
|
|
#include "pluginmanager.h"
|
|
|
|
#include "statusbar.h"
|
|
|
|
|
|
|
|
#include <tqfile.h>
|
|
|
|
#include <tqobjectlist.h>
|
|
|
|
#include <tqtimer.h>
|
|
|
|
|
|
|
|
#include <tdeapplication.h>
|
|
|
|
#include <tdefileitem.h>
|
|
|
|
#include <tdeio/global.h>
|
|
|
|
#include <tdeio/job.h>
|
|
|
|
#include <tdemessagebox.h>
|
|
|
|
#include <krun.h>
|
|
|
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
|
|
|
|
|
|
|
EngineController::ExtensionCache EngineController::s_extensionCache;
|
|
|
|
|
|
|
|
|
|
|
|
EngineController*
|
|
|
|
EngineController::instance()
|
|
|
|
{
|
|
|
|
//will only be instantiated the first time this function is called
|
|
|
|
//will work with the inline directive
|
|
|
|
static EngineController Instance;
|
|
|
|
|
|
|
|
return &Instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EngineController::EngineController()
|
|
|
|
: m_engine( 0 )
|
|
|
|
, m_voidEngine( 0 )
|
|
|
|
, m_delayTime( 0 )
|
|
|
|
, m_muteVolume( 0 )
|
|
|
|
, m_xFadeThisTrack( false )
|
|
|
|
, m_timer( new TQTimer( this ) )
|
|
|
|
, m_playFailureCount( 0 )
|
|
|
|
, m_lastFm( false )
|
|
|
|
, m_positionOffset( 0 )
|
|
|
|
, m_lastPositionOffset( 0 )
|
|
|
|
{
|
|
|
|
m_voidEngine = m_engine = loadEngine( "void-engine" );
|
|
|
|
|
|
|
|
connect( m_timer, TQT_SIGNAL( timeout() ), TQT_SLOT( slotMainTimer() ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
EngineController::~EngineController()
|
|
|
|
{
|
|
|
|
DEBUG_FUNC_INFO //we like to know when singletons are destroyed
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// PUBLIC
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
EngineBase*
|
|
|
|
EngineController::loadEngine() //static
|
|
|
|
{
|
|
|
|
/// always returns a valid pointer to EngineBase
|
|
|
|
|
|
|
|
DEBUG_BLOCK
|
|
|
|
//TODO remember song position, and resume playback
|
|
|
|
|
|
|
|
// new engine, new ext cache required
|
|
|
|
extensionCache().clear();
|
|
|
|
|
|
|
|
if( m_engine != m_voidEngine ) {
|
|
|
|
EngineBase *oldEngine = m_engine;
|
|
|
|
|
|
|
|
// we assign this first for thread-safety,
|
|
|
|
// EngineController::engine() must always return an engine!
|
|
|
|
m_engine = m_voidEngine;
|
|
|
|
|
|
|
|
// we unload the old engine first because there are a number of
|
|
|
|
// bugs associated with keeping one engine loaded while loading
|
|
|
|
// another, eg xine-engine can't init(), and aRts-engine crashes
|
|
|
|
PluginManager::unload( oldEngine );
|
|
|
|
|
|
|
|
// the engine is not required to do this when we unload it but
|
|
|
|
// we need to do it to ensure Amarok looks correct.
|
|
|
|
// We don't do this for the void-engine because that
|
|
|
|
// means Amarok sets all components to empty on startup, which is
|
|
|
|
// their responsibility.
|
|
|
|
slotStateChanged( Engine::Empty );
|
|
|
|
}
|
|
|
|
|
|
|
|
m_engine = loadEngine( AmarokConfig::soundSystem() );
|
|
|
|
|
|
|
|
const TQString engineName = PluginManager::getService( m_engine )->property( "X-TDE-Amarok-name" ).toString();
|
|
|
|
|
|
|
|
if( !AmarokConfig::soundSystem().isEmpty() && engineName != AmarokConfig::soundSystem() ) {
|
|
|
|
//AmarokConfig::soundSystem() is empty on the first-ever-run
|
|
|
|
|
|
|
|
Amarok::StatusBar::instance()->longMessage( i18n(
|
|
|
|
"Sorry, the '%1' could not be loaded, instead we have loaded the '%2'." )
|
|
|
|
.arg( AmarokConfig::soundSystem() )
|
|
|
|
.arg( engineName ),
|
|
|
|
KDE::StatusBar::Sorry );
|
|
|
|
|
|
|
|
AmarokConfig::setSoundSystem( engineName );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Important: Make sure soundSystem is not empty
|
|
|
|
if( AmarokConfig::soundSystem().isEmpty() )
|
|
|
|
AmarokConfig::setSoundSystem( engineName );
|
|
|
|
|
|
|
|
return m_engine;
|
|
|
|
}
|
|
|
|
|
|
|
|
#include <tqvaluevector.h>
|
|
|
|
EngineBase*
|
|
|
|
EngineController::loadEngine( const TQString &engineName )
|
|
|
|
{
|
|
|
|
/// always returns a valid plugin (exits if it can't get one)
|
|
|
|
|
|
|
|
DEBUG_BLOCK
|
|
|
|
|
|
|
|
TQString query = "[X-TDE-Amarok-plugintype] == 'engine' and [X-TDE-Amarok-name] != '%1'";
|
|
|
|
TDETrader::OfferList offers = PluginManager::query( query.arg( engineName ) );
|
|
|
|
|
|
|
|
// sort by rank, TQValueList::operator[] is O(n), so this is quite inefficient
|
|
|
|
#define rank( x ) (x)->property( "X-TDE-Amarok-rank" ).toInt()
|
|
|
|
for( int n = offers.count()-1, i = 0; i < n; i++ )
|
|
|
|
for( int j = n; j > i; j-- )
|
|
|
|
if( rank( offers[j] ) > rank( offers[j-1] ) )
|
|
|
|
tqSwap( offers[j], offers[j-1] );
|
|
|
|
#undef rank
|
|
|
|
|
|
|
|
// this is the actual engine we want
|
|
|
|
query = "[X-TDE-Amarok-plugintype] == 'engine' and [X-TDE-Amarok-name] == '%1'";
|
|
|
|
offers = PluginManager::query( query.arg( engineName ) ) + offers;
|
|
|
|
|
|
|
|
foreachType( TDETrader::OfferList, offers ) {
|
|
|
|
Amarok::Plugin *plugin = PluginManager::createFromService( *it );
|
|
|
|
|
|
|
|
if( plugin ) {
|
|
|
|
TQObject *bar = TQT_TQOBJECT(Amarok::StatusBar::instance());
|
|
|
|
EngineBase *engine = static_cast<EngineBase*>( plugin );
|
|
|
|
|
|
|
|
connect( engine, TQT_SIGNAL(stateChanged( Engine::State )),
|
|
|
|
this, TQT_SLOT(slotStateChanged( Engine::State )) );
|
|
|
|
connect( engine, TQT_SIGNAL(trackEnded()),
|
|
|
|
this, TQT_SLOT(slotTrackEnded()) );
|
|
|
|
if( bar )
|
|
|
|
{
|
|
|
|
connect( engine, TQT_SIGNAL(statusText( const TQString& )),
|
|
|
|
bar, TQT_SLOT(shortMessage( const TQString& )) );
|
|
|
|
connect( engine, TQT_SIGNAL(infoMessage( const TQString& )),
|
|
|
|
bar, TQT_SLOT(longMessage( const TQString& )) );
|
|
|
|
}
|
|
|
|
connect( engine, TQT_SIGNAL(metaData( const Engine::SimpleMetaBundle& )),
|
|
|
|
this, TQT_SLOT(slotEngineMetaData( const Engine::SimpleMetaBundle& )) );
|
|
|
|
connect( engine, TQT_SIGNAL(showConfigDialog( const TQCString& )),
|
|
|
|
kapp, TQT_SLOT(slotConfigAmarok( const TQCString& )) );
|
|
|
|
|
|
|
|
if( engine->init() )
|
|
|
|
return engine;
|
|
|
|
else
|
|
|
|
warning() << "Could not init() an engine\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
KRun::runCommand( "tdebuildsycoca" );
|
|
|
|
|
|
|
|
KMessageBox::error( 0, i18n(
|
|
|
|
"<p>Amarok could not find any sound-engine plugins. "
|
|
|
|
"Amarok is now updating the TDE configuration database. Please wait a couple of minutes, then restart Amarok.</p>"
|
|
|
|
"<p>If this does not help, "
|
|
|
|
"it is likely that Amarok is installed under the wrong prefix, please fix your installation using:<pre>"
|
|
|
|
"$ cd /path/to/amarok/source-code/<br>"
|
|
|
|
"$ su -c \"make uninstall\"<br>"
|
|
|
|
"$ ./configure --prefix=`tde-config --prefix` && su -c \"make install\"<br>"
|
|
|
|
"$ tdebuildsycoca<br>"
|
|
|
|
"$ amarok</pre>"
|
|
|
|
"More information can be found in the README file. For further assistance join us at #amarok on irc.freenode.net.</p>" ) );
|
|
|
|
|
|
|
|
// don't use TQApplication::exit, as the eventloop may not have started yet
|
|
|
|
std::exit( EXIT_SUCCESS );
|
|
|
|
|
|
|
|
// Not executed, just here to prevent compiler warning
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool EngineController::canDecode( const KURL &url ) //static
|
|
|
|
{
|
|
|
|
//NOTE this function must be thread-safe
|
|
|
|
//TODO a KFileItem version? <- presumably so we can mimetype check
|
|
|
|
|
|
|
|
const TQString fileName = url.fileName();
|
|
|
|
const TQString ext = Amarok::extension( fileName );
|
|
|
|
|
|
|
|
if ( PlaylistFile::isPlaylistFile( fileName ) ) return false;
|
|
|
|
|
|
|
|
// Ignore protocols "fetchcover" and "musicbrainz", they're not local but we don't really want them in the playlist :)
|
|
|
|
if ( url.protocol() == "fetchcover" || url.protocol() == "musicbrainz" ) return false;
|
|
|
|
|
|
|
|
// Accept non-local files, since we can't test them for validity at this point
|
|
|
|
// TODO actually, only accept unconditionally http stuff
|
|
|
|
// TODO this actually makes things like "Blarrghgjhjh:!!!" automatically get inserted
|
|
|
|
// into the playlist
|
|
|
|
// TODO remove for Amarok 1.3 and above silly checks, instead check for http type servers
|
|
|
|
if ( !url.isLocalFile() ) return true;
|
|
|
|
|
|
|
|
// If extension is already in the cache, return cache result
|
|
|
|
if ( extensionCache().contains( ext ) )
|
|
|
|
return s_extensionCache[ext];
|
|
|
|
|
|
|
|
// If file has 0 bytes, ignore it and return false, not to infect the cache with corrupt files.
|
|
|
|
// TODO also ignore files that are too small?
|
|
|
|
KFileItem f( KFileItem::Unknown, KFileItem::Unknown, url, false );
|
|
|
|
if ( !f.size() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const bool valid = engine()->canDecode( url );
|
|
|
|
|
|
|
|
if( engine() != EngineController::instance()->m_voidEngine )
|
|
|
|
{
|
|
|
|
//we special case this as otherwise users hate us
|
|
|
|
if ( !valid && ext.lower() == "mp3"){
|
|
|
|
TQCustomEvent * e = new TQCustomEvent( 2000 );
|
|
|
|
TQApplication::postEvent( Amarok::StatusBar::instance(), e );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache this result for the next lookup
|
|
|
|
if ( !ext.isEmpty() )
|
|
|
|
extensionCache().insert( ext, valid );
|
|
|
|
}
|
|
|
|
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
|
|
|
|
void EngineController::unplayableNotification() {
|
|
|
|
|
|
|
|
if( !installDistroCodec(AmarokConfig::soundSystem()))
|
|
|
|
Amarok::StatusBar::instance()->longMessageThreadSafe(
|
|
|
|
i18n( "<p>The %1 claims it <b>cannot</b> play MP3 files."
|
|
|
|
"<p>You may want to choose a different engine from the <i>Configure Dialog</i>, or examine "
|
|
|
|
"the installation of the multimedia-framework that the current engine uses. "
|
|
|
|
"<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok HandBook</i>." )
|
|
|
|
.arg( AmarokConfig::soundSystem() ), KDE::StatusBar::Error );
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EngineController::installDistroCodec( const TQString& engine /*Filetype type*/)
|
|
|
|
{
|
|
|
|
KService::Ptr service = TDETrader::self()->query( "Amarok/CodecInstall"
|
|
|
|
, TQString("[X-TDE-Amarok-codec] == 'mp3' and [X-TDE-Amarok-engine] == '%1'").arg(engine) ).first();
|
|
|
|
if( service )
|
|
|
|
{
|
|
|
|
TQString installScript = service->exec();
|
|
|
|
if( !installScript.isNull() ) //just a sanity check
|
|
|
|
{
|
|
|
|
KGuiItem installButton( i18n( "Install MP3 Support" ) );
|
|
|
|
if(KMessageBox::questionYesNo(PlaylistWindow::self()
|
|
|
|
, i18n("Amarok currently cannot play MP3 files.")
|
|
|
|
, i18n( "No MP3 Support" )
|
|
|
|
, installButton
|
|
|
|
, KStdGuiItem::no()
|
|
|
|
, "codecInstallWarning" ) == KMessageBox::Yes )
|
|
|
|
{
|
|
|
|
KRun::runCommand(installScript);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void EngineController::restoreSession()
|
|
|
|
{
|
|
|
|
//here we restore the session
|
|
|
|
//however, do note, this is always done, KDE session management is not involved
|
|
|
|
|
|
|
|
if( !AmarokConfig::resumeTrack().isEmpty() )
|
|
|
|
{
|
|
|
|
const KURL url = AmarokConfig::resumeTrack();
|
|
|
|
|
|
|
|
play( MetaBundle( url ), AmarokConfig::resumeTime() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::endSession()
|
|
|
|
{
|
|
|
|
//only update song stats, when we're not going to resume it
|
|
|
|
if ( !AmarokConfig::resumePlayback() )
|
|
|
|
{
|
|
|
|
trackEnded( trackPosition(), m_bundle.length() * 1000, "quit" );
|
|
|
|
}
|
|
|
|
|
|
|
|
PluginManager::unload( m_voidEngine );
|
|
|
|
m_voidEngine = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// PUBLIC SLOTS
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
void EngineController::previous() //SLOT
|
|
|
|
{
|
|
|
|
emit orderPrevious();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::next( bool forceNext ) //SLOT
|
|
|
|
{
|
|
|
|
m_previousUrl = m_bundle.url();
|
|
|
|
m_isTiming = false;
|
|
|
|
emit orderNext(forceNext);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::play() //SLOT
|
|
|
|
{
|
|
|
|
if ( m_engine->state() == Engine::Paused )
|
|
|
|
{
|
|
|
|
m_engine->unpause();
|
|
|
|
}
|
|
|
|
else emit orderCurrent();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::play( const MetaBundle &bundle, uint offset )
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
|
|
|
|
KURL url = bundle.url();
|
|
|
|
// don't destroy connection if we need to change station
|
|
|
|
if( url.protocol() != "lastfm" && LastFm::Controller::instance()->isPlaying() )
|
|
|
|
{
|
|
|
|
m_engine->stop();
|
|
|
|
LastFm::Controller::instance()->playbackStopped();
|
|
|
|
}
|
|
|
|
m_lastFm = false;
|
|
|
|
//Holds the time since we started trying to play non-existent files
|
|
|
|
//so we know when to abort
|
|
|
|
static TQTime failure_time;
|
|
|
|
if ( !m_playFailureCount )
|
|
|
|
failure_time.start();
|
|
|
|
|
|
|
|
debug() << "Loading URL: " << url.url() << endl;
|
|
|
|
m_lastMetadata.clear();
|
|
|
|
|
|
|
|
//TODO bummer why'd I do it this way? it should _not_ be in play!
|
|
|
|
//let Amarok know that the previous track is no longer playing
|
|
|
|
if ( m_timer->isActive() )
|
|
|
|
trackEnded( trackPosition(), m_bundle.length() * 1000, "change" );
|
|
|
|
|
|
|
|
if ( url.isLocalFile() ) {
|
|
|
|
// does the file really exist? the playlist entry might be old
|
|
|
|
if ( ! TQFile::exists( url.path()) ) {
|
|
|
|
//debug() << " file >" << url.path() << "< does not exist!" << endl;
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") );
|
|
|
|
goto some_kind_of_failure;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if( url.protocol() == "cdda" )
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("Starting CD Audio track...") );
|
|
|
|
else
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("Connecting to stream source...") );
|
|
|
|
debug() << "Connecting to protocol: " << url.protocol() << endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebDAV protocol is HTTP with extensions (and the "webdav" scheme
|
|
|
|
// is a KDE-ism anyway). Most engines cope with HTTP streaming, but
|
|
|
|
// not through KIO, so they don't support KDE-isms.
|
|
|
|
if ( url.protocol() == "webdav" )
|
|
|
|
url.setProtocol( "http" );
|
|
|
|
else if ( url.protocol() == "webdavs" )
|
|
|
|
url.setProtocol( "https" );
|
|
|
|
|
|
|
|
// streams from last.fm should be handled by our proxy, in order to authenticate with the server
|
|
|
|
else if ( url.protocol() == "lastfm" )
|
|
|
|
{
|
|
|
|
if( LastFm::Controller::instance()->isPlaying() )
|
|
|
|
{
|
|
|
|
if (LastFm::Controller::instance()->changeStation( url.url() ) == -1)
|
|
|
|
// Request was canceled, return immediately.
|
|
|
|
return;
|
|
|
|
connect( m_engine, TQT_SIGNAL( lastFmTrackChange() ), LastFm::Controller::instance()->getService()
|
|
|
|
, TQT_SLOT( requestMetaData() ) );
|
|
|
|
connect( LastFm::Controller::instance()->getService(), TQT_SIGNAL( metaDataResult( const MetaBundle& ) ),
|
|
|
|
this, TQT_SLOT( slotStreamMetaData( const MetaBundle& ) ) );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
url = LastFm::Controller::instance()->getNewProxy( url.url(), m_engine->lastFmProxyRequired() );
|
|
|
|
if( url.isEmpty() )
|
|
|
|
goto some_kind_of_failure;
|
|
|
|
else if ( !url.isValid() && url.url() == "lastfm://" )
|
|
|
|
// Request was canceled, return immediately.
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_lastFm = true;
|
|
|
|
connect( m_engine, TQT_SIGNAL( lastFmTrackChange() ), LastFm::Controller::instance()->getService()
|
|
|
|
, TQT_SLOT( requestMetaData() ) );
|
|
|
|
connect( LastFm::Controller::instance()->getService(), TQT_SIGNAL( metaDataResult( const MetaBundle& ) ),
|
|
|
|
this, TQT_SLOT( slotStreamMetaData( const MetaBundle& ) ) );
|
|
|
|
}
|
|
|
|
debug() << "New URL is " << url.url() << endl;
|
|
|
|
}
|
|
|
|
else if (url.protocol() == "daap" )
|
|
|
|
{
|
|
|
|
KURL newUrl = MediaBrowser::instance()->getProxyUrl( url );
|
|
|
|
if( !newUrl.isEmpty() )
|
|
|
|
{
|
|
|
|
debug() << newUrl << endl;
|
|
|
|
url = newUrl;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_engine->load( url, url.protocol() == "http" || url.protocol() == "rtsp" ) )
|
|
|
|
{
|
|
|
|
//assign bundle now so that it is available when the engine
|
|
|
|
//emits stateChanged( Playing )
|
|
|
|
if( !m_bundle.url().path().isEmpty() ) //wasn't playing before
|
|
|
|
m_previousUrl = m_bundle.url();
|
|
|
|
else
|
|
|
|
m_previousUrl = bundle.url();
|
|
|
|
m_bundle = bundle;
|
|
|
|
|
|
|
|
if( m_engine->play( offset ) )
|
|
|
|
{
|
|
|
|
//Reset failure count as we are now successfully playing a song
|
|
|
|
m_playFailureCount = 0;
|
|
|
|
|
|
|
|
// Ask engine for track length, if available. It's more reliable than TagLib.
|
|
|
|
const uint trackLength = m_engine->length() / 1000;
|
|
|
|
if ( trackLength ) m_bundle.setLength( trackLength );
|
|
|
|
|
|
|
|
m_xFadeThisTrack = !m_engine->isStream() && !(url.protocol() == "cdda") &&
|
|
|
|
m_bundle.length()*1000 - offset - AmarokConfig::crossfadeLength()*2 > 0;
|
|
|
|
|
|
|
|
newMetaDataNotify( m_bundle, true /* track change */ );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
some_kind_of_failure:
|
|
|
|
debug() << "Failed to play this track." << endl;
|
|
|
|
|
|
|
|
++m_playFailureCount;
|
|
|
|
|
|
|
|
//Code to skip to next track if playback fails:
|
|
|
|
//
|
|
|
|
//* The failure counter is reset if a track plays successfully or if playback is
|
|
|
|
// stopped, for whatever reason.
|
|
|
|
//* For normal playback, the attempt to play is stopped at the end of the playlist
|
|
|
|
//* For repeat playlist , a whole playlist worth of songs is tried
|
|
|
|
//* For repeat album, the number of songs tried is the number of tracks from the
|
|
|
|
// album that are in the playlist.
|
|
|
|
//* For repeat track, no attempts are made
|
|
|
|
//* For the nmm engine, no attempts are made (necessary? / FIXME)
|
|
|
|
//* To prevent GUI freezes we don't try to play again after 0.5s of failure
|
|
|
|
int totalTracks = Playlist::instance()->totalTrackCount();
|
|
|
|
int currentTrack = Playlist::instance()->currentTrackIndex();
|
|
|
|
if ( ( ( Amarok::repeatPlaylist() && static_cast<int>(m_playFailureCount) < totalTracks )
|
|
|
|
|| ( Amarok::repeatNone() && currentTrack != totalTracks - 1 )
|
|
|
|
|| ( Amarok::repeatAlbum() && m_playFailureCount < Playlist::instance()->repeatAlbumTrackCount() ) )
|
|
|
|
&& AmarokConfig::soundSystem() != "nmm-engine"
|
|
|
|
&& failure_time.elapsed() < 500 )
|
|
|
|
{
|
|
|
|
|
|
|
|
debug() << "Skipping to next track." << endl;
|
|
|
|
|
|
|
|
// The test for loaded must be done _before_ next is called
|
|
|
|
if ( !m_engine->loaded() )
|
|
|
|
{
|
|
|
|
//False gives behaviour as if track played successfully
|
|
|
|
next( false );
|
|
|
|
TQTimer::singleShot( 0, this, TQT_SLOT(play()) );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//False gives behaviour as if track played successfully
|
|
|
|
next( false );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//Stop playback, including resetting failure count (as all new failures are
|
|
|
|
//treated as independent after playback is stopped)
|
|
|
|
stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::pause() //SLOT
|
|
|
|
{
|
|
|
|
if ( m_engine->loaded() && !LastFm::Controller::instance()->isPlaying() )
|
|
|
|
m_engine->pause();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::stop() //SLOT
|
|
|
|
{
|
|
|
|
//Reset failure counter as after stop, everything else is unrelated
|
|
|
|
m_playFailureCount = 0;
|
|
|
|
|
|
|
|
//let Amarok know that the previous track is no longer playing
|
|
|
|
trackEnded( trackPosition(), m_bundle.length() * 1000, "stop" );
|
|
|
|
|
|
|
|
//Remove requirement for track to be loaded for stop to be called (fixes gltiches
|
|
|
|
//where stop never properly happens if call to m_engine->load fails in play)
|
|
|
|
//if ( m_engine->loaded() )
|
|
|
|
m_engine->stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::playPause() //SLOT
|
|
|
|
{
|
|
|
|
//this is used by the TrayIcon, PlayPauseAction and DCOP
|
|
|
|
|
|
|
|
if( m_engine->state() == Engine::Playing )
|
|
|
|
{
|
|
|
|
pause();
|
|
|
|
}
|
|
|
|
else if( m_engine->state() == Engine::Paused )
|
|
|
|
{
|
|
|
|
if ( m_engine->loaded() )
|
|
|
|
m_engine->unpause();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
play();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::seek( int ms ) //SLOT
|
|
|
|
{
|
|
|
|
if( bundle().length() > 0 )
|
|
|
|
{
|
|
|
|
trackPositionChangedNotify( ms, true ); /* User seek */
|
|
|
|
engine()->seek( ms );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::seekRelative( int ms ) //SLOT
|
|
|
|
{
|
|
|
|
if( m_engine->state() != Engine::Empty )
|
|
|
|
{
|
|
|
|
int newPos = m_engine->position() + ms;
|
|
|
|
seek( newPos <= 0 ? 1 : newPos );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::seekForward( int ms )
|
|
|
|
{
|
|
|
|
seekRelative( ms );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::seekBackward( int ms )
|
|
|
|
{
|
|
|
|
seekRelative( -ms );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int EngineController::increaseVolume( int ticks ) //SLOT
|
|
|
|
{
|
|
|
|
return setVolume( m_engine->volume() + ticks );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int EngineController::decreaseVolume( int ticks ) //SLOT
|
|
|
|
{
|
|
|
|
return setVolume( m_engine->volume() - ticks );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int EngineController::setVolume( int percent ) //SLOT
|
|
|
|
{
|
|
|
|
m_muteVolume = 0;
|
|
|
|
|
|
|
|
if( percent < 0 ) percent = 0;
|
|
|
|
if( percent > 100 ) percent = 100;
|
|
|
|
|
|
|
|
if( (uint)percent != m_engine->volume() )
|
|
|
|
{
|
|
|
|
m_engine->setVolume( (uint)percent );
|
|
|
|
|
|
|
|
percent = m_engine->volume();
|
|
|
|
AmarokConfig::setMasterVolume( percent );
|
|
|
|
volumeChangedNotify( percent );
|
|
|
|
return percent;
|
|
|
|
}
|
|
|
|
else // Still notify
|
|
|
|
{
|
|
|
|
volumeChangedNotify( percent );
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_engine->volume();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::mute() //SLOT
|
|
|
|
{
|
|
|
|
if( m_muteVolume == 0 )
|
|
|
|
{
|
|
|
|
int saveVolume = m_engine->volume();
|
|
|
|
setVolume( 0 );
|
|
|
|
m_muteVolume = saveVolume;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
setVolume( m_muteVolume );
|
|
|
|
m_muteVolume = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const MetaBundle&
|
|
|
|
EngineController::bundle() const
|
|
|
|
{
|
|
|
|
static MetaBundle null;
|
|
|
|
return m_engine->state() == Engine::Empty ? null : m_bundle;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::slotStreamMetaData( const MetaBundle &bundle ) //SLOT
|
|
|
|
{
|
|
|
|
// Prevent spamming by ignoring repeated identical data (some servers repeat it every 10 seconds)
|
|
|
|
if ( m_lastMetadata.contains( bundle ) )
|
|
|
|
return;
|
|
|
|
|
|
|
|
// We compare the new item with the last two items, because mth.house currently cycles
|
|
|
|
// two messages alternating, which gets very annoying
|
|
|
|
if ( m_lastMetadata.count() == 2 )
|
|
|
|
m_lastMetadata.pop_front();
|
|
|
|
|
|
|
|
m_lastMetadata << bundle;
|
|
|
|
|
|
|
|
m_previousUrl = m_bundle.url();
|
|
|
|
m_bundle = bundle;
|
|
|
|
m_lastPositionOffset = m_positionOffset;
|
|
|
|
if( m_lastFm )
|
|
|
|
m_positionOffset = m_engine->position();
|
|
|
|
else
|
|
|
|
m_positionOffset = 0;
|
|
|
|
newMetaDataNotify( m_bundle, false /* not a new track */ );
|
|
|
|
}
|
|
|
|
|
|
|
|
void EngineController::currentTrackMetaDataChanged( const MetaBundle& bundle )
|
|
|
|
{
|
|
|
|
m_previousUrl = m_bundle.url();
|
|
|
|
m_bundle = bundle;
|
|
|
|
newMetaDataNotify( bundle, false /* no track change */ );
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// PRIVATE SLOTS
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
void EngineController::slotEngineMetaData( const Engine::SimpleMetaBundle &simpleBundle ) //SLOT
|
|
|
|
{
|
|
|
|
if ( !m_bundle.url().isLocalFile() )
|
|
|
|
{
|
|
|
|
MetaBundle bundle = m_bundle;
|
|
|
|
bundle.setArtist( simpleBundle.artist );
|
|
|
|
bundle.setTitle( simpleBundle.title );
|
|
|
|
bundle.setComment( simpleBundle.comment );
|
|
|
|
bundle.setAlbum( simpleBundle.album );
|
|
|
|
|
|
|
|
if( !simpleBundle.genre.isEmpty() )
|
|
|
|
bundle.setGenre( simpleBundle.genre );
|
|
|
|
if( !simpleBundle.bitrate.isEmpty() )
|
|
|
|
bundle.setBitrate( simpleBundle.bitrate.toInt() );
|
|
|
|
if( !simpleBundle.samplerate.isEmpty() )
|
|
|
|
bundle.setSampleRate( simpleBundle.samplerate.toInt() );
|
|
|
|
if( !simpleBundle.year.isEmpty() )
|
|
|
|
bundle.setYear( simpleBundle.year.toInt() );
|
|
|
|
if( !simpleBundle.tracknr.isEmpty() )
|
|
|
|
bundle.setTrack( simpleBundle.tracknr.toInt() );
|
|
|
|
|
|
|
|
slotStreamMetaData( bundle );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::slotMainTimer() //SLOT
|
|
|
|
{
|
|
|
|
const uint position = trackPosition();
|
|
|
|
|
|
|
|
trackPositionChangedNotify( position );
|
|
|
|
|
|
|
|
// Crossfading
|
|
|
|
if ( m_engine->state() == Engine::Playing &&
|
|
|
|
AmarokConfig::crossfade() && m_xFadeThisTrack &&
|
|
|
|
m_engine->hasPluginProperty( "HasCrossfade" ) &&
|
|
|
|
Playlist::instance()->stopAfterMode() != Playlist::StopAfterCurrent &&
|
|
|
|
( (uint) AmarokConfig::crossfadeType() == 0 || //Always or...
|
|
|
|
(uint) AmarokConfig::crossfadeType() == 1 ) && //...automatic track change only
|
|
|
|
Playlist::instance()->isTrackAfter() &&
|
|
|
|
m_bundle.length()*1000 - position < (uint) AmarokConfig::crossfadeLength() )
|
|
|
|
{
|
|
|
|
debug() << "Crossfading to next track...\n";
|
|
|
|
m_engine->setXFadeNextTrack( true );
|
|
|
|
trackFinished();
|
|
|
|
}
|
|
|
|
else if ( m_engine->state() == Engine::Playing &&
|
|
|
|
AmarokConfig::fadeout() &&
|
|
|
|
Playlist::instance()->stopAfterMode() == Playlist::StopAfterCurrent &&
|
|
|
|
m_bundle.length()*1000 - position < (uint) AmarokConfig::fadeoutLength() )
|
|
|
|
{
|
|
|
|
m_engine->stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::slotTrackEnded() //SLOT
|
|
|
|
{
|
|
|
|
if ( AmarokConfig::trackDelayLength() > 0 )
|
|
|
|
{
|
|
|
|
//FIXME not perfect
|
|
|
|
if ( !m_isTiming )
|
|
|
|
{
|
|
|
|
TQTimer::singleShot( AmarokConfig::trackDelayLength(), this, TQT_SLOT(trackFinished()) );
|
|
|
|
m_isTiming = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
else trackFinished();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void EngineController::slotStateChanged( Engine::State newState ) //SLOT
|
|
|
|
{
|
|
|
|
|
|
|
|
switch( newState )
|
|
|
|
{
|
|
|
|
case Engine::Empty:
|
|
|
|
|
|
|
|
//FALL THROUGH...
|
|
|
|
|
|
|
|
case Engine::Paused:
|
|
|
|
|
|
|
|
m_timer->stop();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Engine::Playing:
|
|
|
|
|
|
|
|
m_timer->start( MAIN_TIMER );
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
;
|
|
|
|
}
|
|
|
|
|
|
|
|
stateChangedNotify( newState );
|
|
|
|
}
|
|
|
|
|
|
|
|
uint EngineController::trackPosition() const
|
|
|
|
{
|
|
|
|
const uint buffertime = 5000; // worked for me with xine engine over 1 mbit dsl
|
|
|
|
if( !m_engine )
|
|
|
|
return 0;
|
|
|
|
uint pos = m_engine->position();
|
|
|
|
if( !m_lastFm )
|
|
|
|
return pos;
|
|
|
|
|
|
|
|
if( m_positionOffset + buffertime <= pos )
|
|
|
|
return pos - m_positionOffset - buffertime;
|
|
|
|
if( m_lastPositionOffset + buffertime <= pos )
|
|
|
|
return pos - m_lastPositionOffset - buffertime;
|
|
|
|
return pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#include "enginecontroller.moc"
|