diff --git a/src/EngineController.cpp b/src/EngineController.cpp index e3633a551f..8f3d70a2cc 100644 --- a/src/EngineController.cpp +++ b/src/EngineController.cpp @@ -1,1370 +1,1376 @@ /**************************************************************************************** * Copyright (c) 2004 Frederik Holljen * * Copyright (c) 2004,2005 Max Howell * * Copyright (c) 2004-2013 Mark Kretschmann * * Copyright (c) 2006,2008 Ian Monroe * * Copyright (c) 2008 Jason A. Donenfeld * * Copyright (c) 2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Artur Szymiec * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "EngineController" #include "EngineController.h" #include "MainWindow.h" #include "amarokconfig.h" #include "core-impl/collections/support/CollectionManager.h" #include "core/capabilities/MultiPlayableCapability.h" #include "core/capabilities/MultiSourceCapability.h" #include "core/capabilities/SourceInfoCapability.h" #include "core/logger/Logger.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaConstants.h" #include "core/meta/support/MetaUtility.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "playback/DelayedDoers.h" #include "playback/Fadeouter.h" #include "playback/PowerManager.h" #include "playlist/PlaylistActions.h" #include #include #include #include #include #include #include #include #include +#include + + // for slotMetaDataChanged() typedef QPair FieldPair; namespace The { EngineController* engineController() { return EngineController::instance(); } } EngineController * EngineController::instance() { return Amarok::Components::engineController(); } EngineController::EngineController() : m_boundedPlayback( 0 ) , m_multiPlayback( 0 ) , m_multiSource( 0 ) , m_playWhenFetched( true ) , m_volume( 0 ) , m_currentAudioCdTrack( 0 ) , m_pauseTimer( new QTimer( this ) ) , m_lastStreamStampPosition( -1 ) , m_ignoreVolumeChangeAction ( false ) , m_ignoreVolumeChangeObserve ( false ) , m_tickInterval( 0 ) , m_lastTickPosition( -1 ) , m_lastTickCount( 0 ) , m_mutex( QMutex::Recursive ) { DEBUG_BLOCK // ensure this object is created in a main thread Q_ASSERT( thread() == QCoreApplication::instance()->thread() ); connect( this, &EngineController::fillInSupportedMimeTypes, this, &EngineController::slotFillInSupportedMimeTypes ); connect( this, &EngineController::trackFinishedPlaying, this, &EngineController::slotTrackFinishedPlaying ); new PowerManager( this ); // deals with inhibiting suspend etc. m_pauseTimer->setSingleShot( true ); connect( m_pauseTimer, &QTimer::timeout, this, &EngineController::slotPause ); m_equalizerController = new EqualizerController( this ); } EngineController::~EngineController() { DEBUG_BLOCK //we like to know when singletons are destroyed // don't do any of the after-processing that normally happens when // the media is stopped - that's what endSession() is for if( m_media ) { m_media->blockSignals(true); m_media->stop(); } delete m_boundedPlayback; m_boundedPlayback = 0; delete m_multiPlayback; // need to get a new instance of multi if played again m_multiPlayback = 0; delete m_media.data(); delete m_audio.data(); delete m_audioDataOutput.data(); } void EngineController::initializePhonon() { DEBUG_BLOCK m_path.disconnect(); m_dataPath.disconnect(); // QWeakPointers reset themselves to null if the object is deleted delete m_media.data(); delete m_controller.data(); delete m_audio.data(); delete m_audioDataOutput.data(); delete m_preamp.data(); delete m_fader.data(); using namespace Phonon; PERF_LOG( "EngineController: loading phonon objects" ) m_media = new MediaObject( this ); // Enable zeitgeist support on linux //TODO: make this configurable by the user. m_media->setProperty( "PlaybackTracking", true ); m_audio = new AudioOutput( MusicCategory, this ); m_audioDataOutput = new AudioDataOutput( this ); m_audioDataOutput->setDataSize( DATAOUTPUT_DATA_SIZE ); // The number of samples that Phonon sends per signal m_path = createPath( m_media.data(), m_audio.data() ); m_controller = new MediaController( m_media.data() ); m_equalizerController->initialize( m_path ); // HACK we turn off replaygain manually on OSX, until the phonon coreaudio backend is fixed. // as the default is specified in the .cfg file, we can't just tell it to be a different default on OSX #ifdef Q_WS_MAC AmarokConfig::setReplayGainMode( AmarokConfig::EnumReplayGainMode::Off ); AmarokConfig::setFadeoutOnStop( false ); #endif // we now try to create pre-amp unconditionally, however we check that it is valid. // So now m_preamp is null equals not available at all QScopedPointer preamp( new VolumeFaderEffect( this ) ); if( preamp->isValid() ) { m_preamp = preamp.take(); m_path.insertEffect( m_preamp.data() ); } QScopedPointer fader( new VolumeFaderEffect( this ) ); if( fader->isValid() ) { fader->setFadeCurve( VolumeFaderEffect::Fade9Decibel ); m_fader = fader.take(); m_path.insertEffect( m_fader.data() ); m_dataPath = createPath( m_fader.data(), m_audioDataOutput.data() ); } else m_dataPath = createPath( m_media.data(), m_audioDataOutput.data() ); m_media.data()->setTickInterval( 100 ); m_tickInterval = m_media.data()->tickInterval(); debug() << "Tick Interval (actual): " << m_tickInterval; PERF_LOG( "EngineController: loaded phonon objects" ) // Get the next track when there is 2 seconds left on the current one. m_media.data()->setPrefinishMark( 2000 ); connect( m_media.data(), &MediaObject::finished, this, &EngineController::slotFinished ); connect( m_media.data(), &MediaObject::aboutToFinish, this, &EngineController::slotAboutToFinish ); connect( m_media.data(), &MediaObject::metaDataChanged, this, &EngineController::slotMetaDataChanged ); connect( m_media.data(), &MediaObject::stateChanged, this, &EngineController::slotStateChanged ); connect( m_media.data(), &MediaObject::tick, this, &EngineController::slotTick ); connect( m_media.data(), &MediaObject::totalTimeChanged, this, &EngineController::slotTrackLengthChanged ); connect( m_media.data(), &MediaObject::currentSourceChanged, this, &EngineController::slotNewTrackPlaying ); connect( m_media.data(), &MediaObject::seekableChanged, this, &EngineController::slotSeekableChanged ); connect( m_audio.data(), &AudioOutput::volumeChanged, this, &EngineController::slotVolumeChanged ); connect( m_audio.data(), &AudioOutput::mutedChanged, this, &EngineController::slotMutedChanged ); connect( m_audioDataOutput.data(), &AudioDataOutput::dataReady, this, &EngineController::audioDataReady ); connect( m_controller.data(), &MediaController::titleChanged, this, &EngineController::slotTitleChanged ); // Read the volume from phonon m_volume = qBound( 0, qRound(m_audio.data()->volume()*100), 100 ); if( m_currentTrack ) { unsubscribeFrom( m_currentTrack ); m_currentTrack.clear(); } if( m_currentAlbum ) { unsubscribeFrom( m_currentAlbum ); m_currentAlbum.clear(); } } ////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC ////////////////////////////////////////////////////////////////////////////////////////// QStringList EngineController::supportedMimeTypes() { // this ensures that slotFillInSupportedMimeTypes() is called in the main thread. It // will be called directly if we are called in the main thread (so that no deadlock // can occur) and indirectly if we are called in non-main thread. emit fillInSupportedMimeTypes(); // ensure slotFillInSupportedMimeTypes() called above has already finished: m_supportedMimeTypesSemaphore.acquire(); return m_supportedMimeTypes; } void EngineController::slotFillInSupportedMimeTypes() { // we assume non-empty == already filled in if( !m_supportedMimeTypes.isEmpty() ) { // unblock waiting for the semaphore in supportedMimeTypes(): m_supportedMimeTypesSemaphore.release(); return; } QRegExp avFilter( "^(audio|video)/", Qt::CaseInsensitive ); m_supportedMimeTypes = Phonon::BackendCapabilities::availableMimeTypes().filter( avFilter ); // Add whitelist hacks // MP4 Audio Books have a different extension that KFileItem/Phonon don't grok if( !m_supportedMimeTypes.contains( "audio/x-m4b" ) ) m_supportedMimeTypes << "audio/x-m4b"; // technically, "audio/flac" is not a valid mimetype (not on IANA list), but some things expect it if( m_supportedMimeTypes.contains( "audio/x-flac" ) && !m_supportedMimeTypes.contains( "audio/flac" ) ) m_supportedMimeTypes << "audio/flac"; // technically, "audio/mp4" is the official mime type, but sometimes Phonon returns audio/x-m4a if( m_supportedMimeTypes.contains( "audio/x-m4a" ) && !m_supportedMimeTypes.contains( "audio/mp4" ) ) m_supportedMimeTypes << "audio/mp4"; // unblock waiting for the semaphore in supportedMimeTypes(). We can over-shoot // resource number so that next call to supportedMimeTypes won't have to // wait for main loop; this is however just an optimization and we could have safely // released just one resource. Note that this code-path is reached only once, so // overflow cannot happen. m_supportedMimeTypesSemaphore.release( 100000 ); } void EngineController::restoreSession() { //here we restore the session //however, do note, this is always done, KDE session management is not involved if( AmarokConfig::resumePlayback() ) { const QUrl url = QUrl::fromUserInput(AmarokConfig::resumeTrack()); Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url ); // Only give a resume time for local files, because resuming remote protocols can have weird side effects. // See: http://bugs.kde.org/show_bug.cgi?id=172897 if( url.isLocalFile() ) play( track, AmarokConfig::resumeTime(), AmarokConfig::resumePaused() ); else play( track, 0, AmarokConfig::resumePaused() ); } } void EngineController::endSession() { //only update song stats, when we're not going to resume it if ( !AmarokConfig::resumePlayback() && m_currentTrack ) { emit stopped( trackPositionMs(), m_currentTrack->length() ); unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); emit trackChanged( Meta::TrackPtr( 0 ) ); } emit sessionEnded( AmarokConfig::resumePlayback() && m_currentTrack ); } EqualizerController* EngineController::equalizerController() const { return m_equalizerController; } ////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC SLOTS ////////////////////////////////////////////////////////////////////////////////////////// void EngineController::play() //SLOT { DEBUG_BLOCK if( isPlaying() ) return; if( isPaused() ) { if( m_currentTrack && m_currentTrack->type() == "stream" ) { debug() << "This is a stream that cannot be resumed after pausing. Restarting instead."; play( m_currentTrack ); return; } else { m_pauseTimer->stop(); if( supportsFadeout() ) m_fader->setVolume( 1.0 ); m_media->play(); emit trackPlaying( m_currentTrack ); return; } } The::playlistActions()->play(); } void EngineController::play( Meta::TrackPtr track, uint offset, bool startPaused ) { DEBUG_BLOCK if( !track ) // Guard return; // clear the current track without sending playbackEnded or trackChangeNotify yet stop( /* forceInstant */ true, /* playingWillContinue */ true ); // we grant exclusive access to setting new m_currentTrack to newTrackPlaying() m_nextTrack = track; debug() << "play: bounded is "<name(); m_boundedPlayback = track->create(); m_multiPlayback = track->create(); track->prepareToPlay(); m_nextUrl = track->playableUrl(); if( m_multiPlayback ) { connect( m_multiPlayback, &Capabilities::MultiPlayableCapability::playableUrlFetched, this, &EngineController::slotPlayableUrlFetched ); m_multiPlayback->fetchFirst(); } else if( m_boundedPlayback ) { debug() << "Starting bounded playback of url " << track->playableUrl() << " at position " << m_boundedPlayback->startPosition(); playUrl( track->playableUrl(), m_boundedPlayback->startPosition(), startPaused ); } else { debug() << "Just a normal, boring track... :-P"; playUrl( track->playableUrl(), offset, startPaused ); } } void EngineController::replay() // slot { DEBUG_BLOCK seekTo( 0 ); emit trackPositionChanged( 0, true ); } void EngineController::playUrl( const QUrl &url, uint offset, bool startPaused ) { DEBUG_BLOCK m_media->stop(); debug() << "URL: " << url << url.url(); debug() << "Offset: " << offset; m_currentAudioCdTrack = 0; if( url.scheme() == "audiocd" ) { QStringList pathItems = url.path().split( '/', QString::KeepEmptyParts ); if( pathItems.count() != 3 ) { error() << __PRETTY_FUNCTION__ << url.url() << "is not in expected format"; return; } bool ok = false; int trackNumber = pathItems.at( 2 ).toInt( &ok ); if( !ok || trackNumber <= 0 ) { error() << __PRETTY_FUNCTION__ << "failed to get positive track number from" << url.url(); return; } QString device = QUrlQuery(url).queryItemValue( "device" ); m_media->setCurrentSource( Phonon::MediaSource( Phonon::Cd, device ) ); m_currentAudioCdTrack = trackNumber; } else { // keep in sync with setNextTrack(), slotPlayableUrlFetched() m_media->setCurrentSource( url ); } m_media->clearQueue(); if( m_currentAudioCdTrack ) { // call to play() is asynchronous and ->setCurrentTitle() can be only called on // playing, buffering or paused media. m_media->pause(); DelayedTrackChanger *trackChanger = new DelayedTrackChanger( m_media.data(), m_controller.data(), m_currentAudioCdTrack, offset, startPaused ); connect( trackChanger, &DelayedTrackChanger::trackPositionChanged, this, &EngineController::trackPositionChanged ); } else if( offset ) { // call to play() is asynchronous and ->seek() can be only called on playing, // buffering or paused media. Calling play() would lead to audible glitches, // so call pause() that doesn't suffer from such problem. m_media->pause(); DelayedSeeker *seeker = new DelayedSeeker( m_media.data(), offset, startPaused ); connect( seeker, &DelayedSeeker::trackPositionChanged, this, &EngineController::trackPositionChanged ); } else { if( startPaused ) { m_media->pause(); } else { m_pauseTimer->stop(); if( supportsFadeout() ) m_fader->setVolume( 1.0 ); m_media->play(); } } } void EngineController::pause() //SLOT { if( supportsFadeout() && AmarokConfig::fadeoutOnPause() ) { m_fader->fadeOut( AmarokConfig::fadeoutLength() ); m_pauseTimer->start( AmarokConfig::fadeoutLength() + 500 ); return; } slotPause(); } void EngineController::slotPause() { if( supportsFadeout() && AmarokConfig::fadeoutOnPause() ) { // Reset VolumeFaderEffect to full volume m_fader->setVolume( 1.0 ); // Wait a bit before pausing the pipeline. Necessary for the new fader setting to take effect. QTimer::singleShot( 1000, m_media.data(), &Phonon::MediaObject::pause ); } else { m_media->pause(); } emit paused(); } void EngineController::stop( bool forceInstant, bool playingWillContinue ) //SLOT { DEBUG_BLOCK /* Only do fade-out when all conditions are met: * a) instant stop is not requested * b) we aren't already in a fadeout * c) we are currently playing (not paused etc.) * d) Amarok is configured to fadeout at all * e) configured fadeout length is positive * f) Phonon fader to do it is actually available */ bool doFadeOut = !forceInstant && !m_fadeouter && m_media->state() == Phonon::PlayingState && AmarokConfig::fadeoutOnStop() && AmarokConfig::fadeoutLength() > 0 && m_fader; // let Amarok know that the previous track is no longer playing; if we will fade-out // ::stop() is called after the fade by Fadeouter. if( m_currentTrack && !doFadeOut ) { unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); const qint64 pos = trackPositionMs(); // updateStreamLength() intentionally not here, we're probably in the middle of a track const qint64 length = trackLength(); emit trackFinishedPlaying( m_currentTrack, pos / qMax( length, pos ) ); m_currentTrack = 0; m_currentAlbum = 0; if( !playingWillContinue ) { emit stopped( pos, length ); emit trackChanged( m_currentTrack ); } } { QMutexLocker locker( &m_mutex ); delete m_boundedPlayback; m_boundedPlayback = 0; delete m_multiPlayback; // need to get a new instance of multi if played again m_multiPlayback = 0; m_multiSource.reset(); m_nextTrack.clear(); m_nextUrl.clear(); m_media->clearQueue(); } if( doFadeOut ) { m_fadeouter = new Fadeouter( m_media, m_fader, AmarokConfig::fadeoutLength() ); // even though we don't pass forceInstant, doFadeOut will be false because // m_fadeouter will be still valid connect( m_fadeouter.data(), &Fadeouter::fadeoutFinished, this, &EngineController::regularStop ); } else { m_media->stop(); m_media->setCurrentSource( Phonon::MediaSource() ); } } void EngineController::regularStop() { stop( false, false ); } bool EngineController::isPaused() const { return m_media->state() == Phonon::PausedState; } bool EngineController::isPlaying() const { return !isPaused() && !isStopped(); } bool EngineController::isStopped() const { return m_media->state() == Phonon::StoppedState || m_media->state() == Phonon::LoadingState || m_media->state() == Phonon::ErrorState; } void EngineController::playPause() //SLOT { DEBUG_BLOCK debug() << "PlayPause: EngineController state" << m_media->state(); if( isPlaying() ) pause(); else play(); } void EngineController::seekTo( int ms ) //SLOT { DEBUG_BLOCK if( m_media->isSeekable() ) { debug() << "seek to: " << ms; int seekTo; if( m_boundedPlayback ) { seekTo = m_boundedPlayback->startPosition() + ms; if( seekTo < m_boundedPlayback->startPosition() ) seekTo = m_boundedPlayback->startPosition(); else if( seekTo > m_boundedPlayback->startPosition() + trackLength() ) seekTo = m_boundedPlayback->startPosition() + trackLength(); } else seekTo = ms; m_media->seek( static_cast( seekTo ) ); emit trackPositionChanged( seekTo, true ); /* User seek */ } else debug() << "Stream is not seekable."; } void EngineController::seekBy( int ms ) //SLOT { qint64 newPos = m_media->currentTime() + ms; seekTo( newPos <= 0 ? 0 : newPos ); } int EngineController::increaseVolume( int ticks ) //SLOT { return setVolume( volume() + ticks ); } int EngineController::decreaseVolume( int ticks ) //SLOT { return setVolume( volume() - ticks ); } int EngineController::setVolume( int percent ) //SLOT { percent = qBound( 0, percent, 100 ); m_volume = percent; const qreal volume = percent / 100.0; if ( !m_ignoreVolumeChangeAction && m_audio->volume() != volume ) { m_ignoreVolumeChangeObserve = true; m_audio->setVolume( volume ); AmarokConfig::setMasterVolume( percent ); emit volumeChanged( percent ); } m_ignoreVolumeChangeAction = false; return percent; } int EngineController::volume() const { return m_volume; } bool EngineController::isMuted() const { return m_audio->isMuted(); } void EngineController::setMuted( bool mute ) //SLOT { m_audio->setMuted( mute ); // toggle mute if( !isMuted() ) setVolume( m_volume ); AmarokConfig::setMuteState( mute ); emit muteStateChanged( mute ); } void EngineController::toggleMute() //SLOT { setMuted( !isMuted() ); } Meta::TrackPtr EngineController::currentTrack() const { return m_currentTrack; } qint64 EngineController::trackLength() const { //When starting a last.fm stream, Phonon still shows the old track's length--trust //Meta::Track over Phonon if( m_currentTrack && m_currentTrack->length() > 0 ) return m_currentTrack->length(); else return m_media->totalTime(); //may return -1 } void EngineController::setNextTrack( Meta::TrackPtr track ) { DEBUG_BLOCK if( !track ) return; track->prepareToPlay(); QUrl url = track->playableUrl(); if( url.isEmpty() ) return; QMutexLocker locker( &m_mutex ); if( isPlaying() ) { m_media->clearQueue(); // keep in sync with playUrl(), slotPlayableUrlFetched() if( url.scheme() != "audiocd" ) // we don't support gapless for CD, bug 305708 m_media->enqueue( url ); m_nextTrack = track; m_nextUrl = url; } else play( track ); } bool EngineController::isStream() { Phonon::MediaSource::Type type = Phonon::MediaSource::Invalid; if( m_media ) // type is determined purely from the MediaSource constructor used in // setCurrentSource(). For streams we use the QUrl one, see playUrl() type = m_media->currentSource().type(); return type == Phonon::MediaSource::Url || type == Phonon::MediaSource::Stream; } bool EngineController::isSeekable() const { if( m_media ) return m_media->isSeekable(); return false; } int EngineController::trackPosition() const { return trackPositionMs() / 1000; } qint64 EngineController::trackPositionMs() const { return m_media->currentTime(); } bool EngineController::supportsFadeout() const { return m_fader; } bool EngineController::supportsGainAdjustments() const { return m_preamp; } bool EngineController::supportsAudioDataOutput() const { const Phonon::AudioDataOutput out; return out.isValid(); } ////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE SLOTS ////////////////////////////////////////////////////////////////////////////////////////// void EngineController::slotTick( qint64 position ) { if( m_boundedPlayback ) { qint64 newPosition = position; emit trackPositionChanged( static_cast( position - m_boundedPlayback->startPosition() ), false ); // Calculate a better position. Sometimes the position doesn't update // with a good resolution (for example, 1 sec for TrueAudio files in the // Xine-1.1.18 backend). This tick function, in those cases, just gets // called multiple times with the same position. We count how many // times this has been called prior, and adjust for it. if( position == m_lastTickPosition ) newPosition += ++m_lastTickCount * m_tickInterval; else m_lastTickCount = 0; m_lastTickPosition = position; //don't go beyond the stop point if( newPosition >= m_boundedPlayback->endPosition() ) { slotAboutToFinish(); } } else { m_lastTickPosition = position; emit trackPositionChanged( static_cast( position ), false ); } } void EngineController::slotAboutToFinish() { DEBUG_BLOCK if( m_fadeouter ) { debug() << "slotAboutToFinish(): a fadeout is in progress, don't queue new track"; return; } if( m_multiPlayback ) { DEBUG_LINE_INFO m_mutex.lock(); m_playWhenFetched = false; m_mutex.unlock(); m_multiPlayback->fetchNext(); debug() << "The queue has: " << m_media->queue().size() << " tracks in it"; } else if( m_multiSource ) { debug() << "source finished, lets get the next one"; QUrl nextSource = m_multiSource->nextUrl(); if( !nextSource.isEmpty() ) { //more sources m_mutex.lock(); m_playWhenFetched = false; m_mutex.unlock(); debug() << "playing next source: " << nextSource; slotPlayableUrlFetched( nextSource ); } else if( m_media->queue().isEmpty() ) { debug() << "no more sources, skip to next track"; m_multiSource.reset(); // don't cofuse slotFinished The::playlistActions()->requestNextTrack(); } } else if( m_boundedPlayback ) { debug() << "finished a track that consists of part of another track, go to next track even if this url is technically not done yet"; //stop this track, now, as the source track might go on and on, and //there might not be any more tracks in the playlist... stop( true ); The::playlistActions()->requestNextTrack(); } else if( m_media->queue().isEmpty() ) The::playlistActions()->requestNextTrack(); } void EngineController::slotFinished() { DEBUG_BLOCK // paranoia checking, m_currentTrack shouldn't really be null if( m_currentTrack ) { debug() << "Track finished completely, updating statistics"; unsubscribeFrom( m_currentTrack ); // don't bother with trackMetadataChanged() stampStreamTrackLength(); // update track length in stream for accurate scrobbling emit trackFinishedPlaying( m_currentTrack, 1.0 ); subscribeTo( m_currentTrack ); } if( !m_multiPlayback && !m_multiSource ) { // again. at this point the track is finished so it's trackPositionMs is 0 if( !m_nextTrack && m_nextUrl.isEmpty() ) emit stopped( m_currentTrack ? m_currentTrack->length() : 0, m_currentTrack ? m_currentTrack->length() : 0 ); unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); m_currentTrack = 0; m_currentAlbum = 0; if( !m_nextTrack && m_nextUrl.isEmpty() ) // we will the trackChanged signal later emit trackChanged( Meta::TrackPtr() ); m_media->setCurrentSource( Phonon::MediaSource() ); } m_mutex.lock(); // in case setNextTrack is being handled right now. // Non-local urls are not enqueued so we must play them explicitly. if( m_nextTrack ) { DEBUG_LINE_INFO play( m_nextTrack ); } else if( !m_nextUrl.isEmpty() ) { DEBUG_LINE_INFO playUrl( m_nextUrl, 0 ); } else { DEBUG_LINE_INFO // possibly we are waiting for a fetch m_playWhenFetched = true; } m_mutex.unlock(); } static const qreal log10over20 = 0.1151292546497022842; // ln(10) / 20 void EngineController::slotNewTrackPlaying( const Phonon::MediaSource &source ) { DEBUG_BLOCK if( source.type() == Phonon::MediaSource::Empty ) { debug() << "Empty MediaSource (engine stop)"; return; } if( m_currentTrack ) { unsubscribeFrom( m_currentTrack ); if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); } // only update stats if we are called for something new, some phonon back-ends (at // least phonon-gstreamer-4.6.1) call slotNewTrackPlaying twice with the same source if( m_currentTrack && ( m_nextTrack || !m_nextUrl.isEmpty() ) ) { debug() << "Previous track finished completely, updating statistics"; stampStreamTrackLength(); // update track length in stream for accurate scrobbling emit trackFinishedPlaying( m_currentTrack, 1.0 ); if( m_multiSource ) // advance source of a multi-source track m_multiSource->setSource( m_multiSource->current() + 1 ); } m_nextUrl.clear(); if( m_nextTrack ) { // already unsubscribed m_currentTrack = m_nextTrack; m_nextTrack.clear(); m_multiSource.reset( m_currentTrack->create() ); if( m_multiSource ) { debug() << "Got a MultiSource Track with" << m_multiSource->sources().count() << "sources"; connect( m_multiSource.data(), &Capabilities::MultiSourceCapability::urlChanged, this, &EngineController::slotPlayableUrlFetched ); } } if( m_currentTrack && AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off ) { Meta::ReplayGainTag mode; // gain is usually negative (but may be positive) mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track) ? Meta::ReplayGain_Track_Gain : Meta::ReplayGain_Album_Gain; qreal gain = m_currentTrack->replayGain( mode ); // peak is usually positive and smaller than gain (but may be negative) mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track) ? Meta::ReplayGain_Track_Peak : Meta::ReplayGain_Album_Peak; qreal peak = m_currentTrack->replayGain( mode ); if( gain + peak > 0.0 ) { debug() << "Gain of" << gain << "would clip at absolute peak of" << gain + peak; gain -= gain + peak; } if( m_preamp ) { debug() << "Using gain of" << gain << "with relative peak of" << peak; // we calculate the volume change ourselves, because m_preamp->setVolumeDecibel is // a little confused about minus signs m_preamp->setVolume( qExp( gain * log10over20 ) ); } else warning() << "Would use gain of" << gain << ", but current Phonon backend" << "doesn't seem to support pre-amplifier (VolumeFaderEffect)"; } else if( m_preamp ) { m_preamp->setVolume( 1.0 ); } bool useTrackWithinStreamDetection = false; if( m_currentTrack ) { subscribeTo( m_currentTrack ); Meta::AlbumPtr m_currentAlbum = m_currentTrack->album(); if( m_currentAlbum ) subscribeTo( m_currentAlbum ); /** We only use detect-tracks-in-stream for tracks that have stream type * (exactly, we purposely exclude stream/lastfm) *and* that don't have length * already filled in. Bug 311852 */ if( m_currentTrack->type() == "stream" && m_currentTrack->length() == 0 ) useTrackWithinStreamDetection = true; } m_lastStreamStampPosition = useTrackWithinStreamDetection ? 0 : -1; emit trackChanged( m_currentTrack ); emit trackPlaying( m_currentTrack ); } void EngineController::slotStateChanged( Phonon::State newState, Phonon::State oldState ) //SLOT { debug() << "slotStateChanged from" << oldState << "to" << newState; static const int maxErrors = 5; static int errorCount = 0; // Sanity checks: if( newState == oldState ) return; if( newState == Phonon::ErrorState ) // If media is borked, skip to next track { emit trackError( m_currentTrack ); warning() << "Phonon failed to play this URL. Error: " << m_media->errorString(); warning() << "Forcing phonon engine reinitialization."; /* In case of error Phonon MediaObject automatically switches to KioMediaSource, which cause problems: runs StopAfterCurrentTrack mode, force PlayPause button to reply the track (can't be paused). So we should reinitiate Phonon after each Error. */ initializePhonon(); errorCount++; if ( errorCount >= maxErrors ) { // reset error count errorCount = 0; Amarok::Logger::longMessage( i18n( "Too many errors encountered in playlist. Playback stopped." ), Amarok::Logger::Warning ); error() << "Stopping playlist."; } else // and start the next song, even if the current failed to start playing The::playlistActions()->requestUserNextTrack(); } else if( newState == Phonon::PlayingState ) { errorCount = 0; emit playbackStateChanged(); } else if( newState == Phonon::StoppedState || newState == Phonon::PausedState ) { emit playbackStateChanged(); } } void EngineController::slotPlayableUrlFetched( const QUrl &url ) { DEBUG_BLOCK debug() << "Fetched url: " << url; if( url.isEmpty() ) { DEBUG_LINE_INFO The::playlistActions()->requestNextTrack(); return; } if( !m_playWhenFetched ) { DEBUG_LINE_INFO m_mutex.lock(); m_media->clearQueue(); // keep synced with setNextTrack(), playUrl() m_media->enqueue( url ); m_nextTrack.clear(); m_nextUrl = url; debug() << "The next url we're playing is: " << m_nextUrl; // reset this flag each time m_playWhenFetched = true; m_mutex.unlock(); } else { DEBUG_LINE_INFO m_mutex.lock(); playUrl( url, 0 ); m_mutex.unlock(); } } void EngineController::slotTrackLengthChanged( qint64 milliseconds ) { debug() << "slotTrackLengthChanged(" << milliseconds << ")"; emit trackLengthChanged( ( !m_multiPlayback || !m_boundedPlayback ) ? trackLength() : milliseconds ); } void EngineController::slotMetaDataChanged() { QVariantMap meta; meta.insert( Meta::Field::URL, m_media->currentSource().url() ); static const QList fieldPairs = QList() << FieldPair( Phonon::ArtistMetaData, Meta::Field::ARTIST ) << FieldPair( Phonon::AlbumMetaData, Meta::Field::ALBUM ) << FieldPair( Phonon::TitleMetaData, Meta::Field::TITLE ) << FieldPair( Phonon::GenreMetaData, Meta::Field::GENRE ) << FieldPair( Phonon::TracknumberMetaData, Meta::Field::TRACKNUMBER ) << FieldPair( Phonon::DescriptionMetaData, Meta::Field::COMMENT ); foreach( const FieldPair &pair, fieldPairs ) { QStringList values = m_media->metaData( pair.first ); if( !values.isEmpty() ) meta.insert( pair.second, values.first() ); } // note: don't rely on m_currentTrack here. At least some Phonon backends first emit // totalTimeChanged(), then metaDataChanged() and only then currentSourceChanged() // which currently sets correct m_currentTrack. if( isInRecentMetaDataHistory( meta ) ) { // slotMetaDataChanged() triggered by phonon, but we've already seen // exactly the same metadata recently. Ignoring for now. return; } // following is an implementation of song end (and length) within a stream detection. // This normally fires minutes after the track has started playing so m_currentTrack // should be accurate if( m_currentTrack && m_lastStreamStampPosition >= 0 ) { stampStreamTrackLength(); emit trackFinishedPlaying( m_currentTrack, 1.0 ); // update track length to 0 because length emitted by stampStreamTrackLength() // is for the previous song meta.insert( Meta::Field::LENGTH, 0 ); } debug() << "slotMetaDataChanged(): new meta-data:" << meta; emit currentMetadataChanged( meta ); } void EngineController::slotSeekableChanged( bool seekable ) { emit seekableChanged( seekable ); } void EngineController::slotTitleChanged( int titleNumber ) { DEBUG_BLOCK if ( titleNumber != m_currentAudioCdTrack ) { The::playlistActions()->requestNextTrack(); slotFinished(); } } void EngineController::slotVolumeChanged( qreal newVolume ) { int percent = qBound( 0, qRound(newVolume * 100), 100 ); if ( !m_ignoreVolumeChangeObserve && m_volume != percent ) { m_ignoreVolumeChangeAction = true; m_volume = percent; AmarokConfig::setMasterVolume( percent ); emit volumeChanged( percent ); } else m_volume = percent; m_ignoreVolumeChangeObserve = false; } void EngineController::slotMutedChanged( bool mute ) { AmarokConfig::setMuteState( mute ); emit muteStateChanged( mute ); } void EngineController::slotTrackFinishedPlaying( Meta::TrackPtr track, double playedFraction ) { Q_ASSERT( track ); debug() << "slotTrackFinishedPlaying(" << ( track->artist() ? track->artist()->name() : QString( "[no artist]" ) ) << "-" << ( track->album() ? track->album()->name() : QString( "[no album]" ) ) << "-" << track->name() << "," << playedFraction << ")"; - track->finishedPlaying( playedFraction ); + + // Track::finishedPlaying is thread-safe and can take a long time to finish. + std::thread thread( &Meta::Track::finishedPlaying, track, playedFraction ); + thread.detach(); } void EngineController::metadataChanged( Meta::TrackPtr track ) { Meta::AlbumPtr album = m_currentTrack->album(); if( m_currentAlbum != album ) { if( m_currentAlbum ) unsubscribeFrom( m_currentAlbum ); m_currentAlbum = album; if( m_currentAlbum ) subscribeTo( m_currentAlbum ); } emit trackMetadataChanged( track ); } void EngineController::metadataChanged( Meta::AlbumPtr album ) { emit albumMetadataChanged( album ); } QString EngineController::prettyNowPlaying( bool progress ) const { Meta::TrackPtr track = currentTrack(); if( track ) { QString title = track->name().toHtmlEscaped(); QString prettyTitle = track->prettyName().toHtmlEscaped(); QString artist = track->artist() ? track->artist()->name().toHtmlEscaped() : QString(); QString album = track->album() ? track->album()->name().toHtmlEscaped() : QString(); // ugly because of translation requirements if( !title.isEmpty() && !artist.isEmpty() && !album.isEmpty() ) title = i18nc( "track by artist on album", "%1 by %2 on %3", title, artist, album ); else if( !title.isEmpty() && !artist.isEmpty() ) title = i18nc( "track by artist", "%1 by %2", title, artist ); else if( !album.isEmpty() ) // we try for pretty title as it may come out better title = i18nc( "track on album", "%1 on %2", prettyTitle, album ); else title = "" + prettyTitle + ""; if( title.isEmpty() ) title = i18n( "Unknown track" ); QScopedPointer sic( track->create() ); if( sic ) { QString source = sic->sourceName(); if( !source.isEmpty() ) title += ' ' + i18nc( "track from source", "from %1", source ); } if( track->length() > 0 ) { QString length = Meta::msToPrettyTime( track->length() ).toHtmlEscaped(); title += " ("; if( progress ) title += Meta::msToPrettyTime( m_lastTickPosition ).toHtmlEscaped() + '/'; title += length + ')'; } return title; } else return i18n( "No track playing" ); } bool EngineController::isInRecentMetaDataHistory( const QVariantMap &meta ) { // search for Metadata in history for( int i = 0; i < m_metaDataHistory.size(); i++) { if( m_metaDataHistory.at( i ) == meta ) // we already had that one -> spam! { m_metaDataHistory.move( i, 0 ); // move spam to the beginning of the list return true; } } if( m_metaDataHistory.size() == 12 ) m_metaDataHistory.removeLast(); m_metaDataHistory.insert( 0, meta ); return false; } void EngineController::stampStreamTrackLength() { if( m_lastStreamStampPosition < 0 ) return; qint64 currentPosition = trackPositionMs(); debug() << "stampStreamTrackLength(): m_lastStreamStampPosition:" << m_lastStreamStampPosition << "currentPosition:" << currentPosition; if( currentPosition == m_lastStreamStampPosition ) return; qint64 length = qMax( currentPosition - m_lastStreamStampPosition, qint64( 0 ) ); updateStreamLength( length ); m_lastStreamStampPosition = currentPosition; } void EngineController::updateStreamLength( qint64 length ) { if( !m_currentTrack ) { warning() << __PRETTY_FUNCTION__ << "called with cull m_currentTrack"; return; } // Last.fm scrobbling needs to know track length before it can scrobble: QVariantMap lengthMetaData; // we cannot use m_media->currentSource()->url() here because it is already empty, bug 309976 lengthMetaData.insert( Meta::Field::URL, QUrl( m_currentTrack->playableUrl() ) ); lengthMetaData.insert( Meta::Field::LENGTH, length ); debug() << "updateStreamLength(): emitting currentMetadataChanged(" << lengthMetaData << ")"; emit currentMetadataChanged( lengthMetaData ); } diff --git a/src/context/applets/currenttrack/plugin/CurrentEngine.cpp b/src/context/applets/currenttrack/plugin/CurrentEngine.cpp index 2cbbe73412..81cd04f7e7 100644 --- a/src/context/applets/currenttrack/plugin/CurrentEngine.cpp +++ b/src/context/applets/currenttrack/plugin/CurrentEngine.cpp @@ -1,283 +1,284 @@ /**************************************************************************************** * Copyright (c) 2007 Leo Franchi * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "CurrentEngine" #include "CurrentEngine.h" #include "EngineController.h" #include "core/support/Debug.h" #include "core/capabilities/SourceInfoCapability.h" #include "core/collections/Collection.h" #include "core/collections/QueryMaker.h" #include "core/meta/support/MetaUtility.h" #include "core/meta/Statistics.h" #include "core/support/Amarok.h" #include "core-impl/collections/support/CollectionManager.h" #include "covermanager/CoverCache.h" #include #include #include CurrentEngine::CurrentEngine( QObject* parent ) : QObject( parent ) , m_coverWidth( 0 ) , m_lastQueryMaker( Q_NULLPTR ) { EngineController* engine = The::engineController(); + // Connect queued to reduce interface stuttering. connect( engine, &EngineController::trackPlaying, - this, &CurrentEngine::slotTrackChanged ); + this, &CurrentEngine::slotTrackChanged, Qt::QueuedConnection ); connect( engine, &EngineController::stopped, - this, &CurrentEngine::stopped ); + this, &CurrentEngine::stopped, Qt::QueuedConnection ); connect( engine, &EngineController::trackMetadataChanged, - this, &CurrentEngine::slotTrackMetadataChanged ); + this, &CurrentEngine::slotTrackMetadataChanged, Qt::QueuedConnection ); connect( engine, &EngineController::albumMetadataChanged, - this, &CurrentEngine::slotAlbumMetadataChanged ); + this, &CurrentEngine::slotAlbumMetadataChanged, Qt::QueuedConnection ); } CurrentEngine::~CurrentEngine() { } void CurrentEngine::slotAlbumMetadataChanged( Meta::AlbumPtr album ) { DEBUG_BLOCK // disregard changes for other albums (BR: 306735) if( !m_currentTrack || m_currentTrack->album() != album ) return; QPixmap cover; if( album ) cover = The::coverCache()->getCover( album, m_coverWidth ); if( m_cover.cacheKey() != cover.cacheKey() ) { m_cover = cover; emit albumChanged(); } } void CurrentEngine::slotTrackMetadataChanged( Meta::TrackPtr track ) { if( !track ) return; update( track->album() ); emit trackChanged(); } void CurrentEngine::slotTrackChanged(Meta::TrackPtr track) { DEBUG_BLOCK if( !track || track == m_currentTrack ) return; m_currentTrack = track; slotTrackMetadataChanged( track ); } void CurrentEngine::stopped() { m_currentTrack.clear(); emit trackChanged(); m_cover = QPixmap(); // Collect data for the recently added albums m_albums.clear(); emit albumChanged(); Collections::QueryMaker *qm = CollectionManager::instance()->queryMaker(); qm->setAutoDelete( true ); qm->setQueryType( Collections::QueryMaker::Album ); qm->excludeFilter( Meta::valAlbum, QString(), true, true ); qm->orderBy( Meta::valCreateDate, true ); qm->limitMaxResultSize( Amarok::config("Albums Applet").readEntry("RecentlyAdded", 5) ); connect( qm, &Collections::QueryMaker::newAlbumsReady, this, &CurrentEngine::resultReady, Qt::QueuedConnection ); m_lastQueryMaker = qm; qm->run(); } void CurrentEngine::update( Meta::AlbumPtr album ) { m_lastQueryMaker = Q_NULLPTR; if( !album ) return; slotAlbumMetadataChanged( album ); Meta::TrackPtr track = The::engineController()->currentTrack(); if( !track ) return; Meta::ArtistPtr artist = track->artist(); // Prefer track artist to album artist BUG: 266682 if( !artist ) artist = album->albumArtist(); if( artist && !artist->name().isEmpty() ) { m_albums.clear(); // -- search the collection for albums with the same artist Collections::QueryMaker *qm = CollectionManager::instance()->queryMaker(); qm->setAutoDelete( true ); qm->addFilter( Meta::valArtist, artist->name(), true, true ); qm->setAlbumQueryMode( Collections::QueryMaker::AllAlbums ); qm->setQueryType( Collections::QueryMaker::Album ); connect( qm, &Collections::QueryMaker::newAlbumsReady, this, &CurrentEngine::resultReady, Qt::QueuedConnection ); m_lastQueryMaker = qm; qm->run(); } } void CurrentEngine::resultReady( const Meta::AlbumList &albums ) { if( sender() == m_lastQueryMaker ) m_albums << albums; } QString CurrentEngine::artist() const { if( !m_currentTrack ) return QString(); return m_currentTrack->artist()->prettyName(); } QString CurrentEngine::track() const { if( !m_currentTrack ) return QString(); return m_currentTrack->prettyName(); } QString CurrentEngine::album() const { if( !m_currentTrack ) return QString(); return m_currentTrack->album()->prettyName(); } int CurrentEngine::rating() const { if( !m_currentTrack ) return 0; return m_currentTrack->statistics()->rating(); } void CurrentEngine::setRating(int rating) { DEBUG_BLOCK debug() << "New rating:" << rating; if( !m_currentTrack ) return; if( rating == m_currentTrack->statistics()->rating() ) return; m_currentTrack->statistics()->setRating( rating ); emit trackChanged(); } int CurrentEngine::score() const { if( !m_currentTrack ) return 0; return m_currentTrack->statistics()->score(); } int CurrentEngine::length() const { if( !m_currentTrack ) return 0; return m_currentTrack->length(); } QString CurrentEngine::lastPlayed() const { if( !m_currentTrack ) return QString(); auto lastPlayed = m_currentTrack->statistics()->lastPlayed(); QString lastPlayedString; if( lastPlayed.isValid() ) lastPlayedString = KFormat().formatRelativeDateTime( lastPlayed, QLocale::ShortFormat ); else lastPlayedString = i18n( "Never" ); return lastPlayedString; } int CurrentEngine::timesPlayed() const { if( !m_currentTrack ) return 0; return m_currentTrack->statistics()->playCount(); } void CurrentEngine::setCoverWidth(int width) { if( m_coverWidth == width ) return; m_coverWidth = width; emit coverWidthChanged(); if( m_currentTrack ) slotAlbumMetadataChanged( m_currentTrack->album() ); } diff --git a/src/core-impl/collections/db/sql/SqlMeta.cpp b/src/core-impl/collections/db/sql/SqlMeta.cpp index b73382bb5d..7ace034eae 100644 --- a/src/core-impl/collections/db/sql/SqlMeta.cpp +++ b/src/core-impl/collections/db/sql/SqlMeta.cpp @@ -1,2247 +1,2252 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Daniel Winter * * Copyright (c) 2010 Ralf Engels * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SqlMeta" #include "SqlMeta.h" #include "amarokconfig.h" #include "SqlCapabilities.h" #include "SqlCollection.h" #include "SqlQueryMaker.h" #include "SqlRegistry.h" #include "SqlReadLabelCapability.h" #include "SqlWriteLabelCapability.h" #include "MetaTagLib.h" // for getting an embedded cover #include "amarokurls/BookmarkMetaActions.h" #include #include "core/meta/support/MetaUtility.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core/capabilities/BookmarkThisCapability.h" #include "core-impl/capabilities/AlbumActionsCapability.h" #include "core-impl/collections/db/MountPointManager.h" #include "core-impl/collections/support/ArtistHelper.h" #include "core-impl/collections/support/jobs/WriteTagsJob.h" #include "covermanager/CoverCache.h" #include "covermanager/CoverFetcher.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include + + // additional constants namespace Meta { static const qint64 valAlbumId = valCustom + 1; } using namespace Meta; QString SqlTrack::getTrackReturnValues() { //do not use any weird column names that contains commas: this will break getTrackReturnValuesCount() // NOTE: when changing this, always check that SqlTrack::TrackReturnIndex enum remains valid return "urls.id, urls.deviceid, urls.rpath, urls.directory, urls.uniqueid, " "tracks.id, tracks.title, tracks.comment, " "tracks.tracknumber, tracks.discnumber, " "statistics.score, statistics.rating, " "tracks.bitrate, tracks.length, " "tracks.filesize, tracks.samplerate, " "statistics.id, " "statistics.createdate, statistics.accessdate, " "statistics.playcount, tracks.filetype, tracks.bpm, " "tracks.createdate, tracks.modifydate, tracks.albumgain, tracks.albumpeakgain, " "tracks.trackgain, tracks.trackpeakgain, " "artists.name, artists.id, " // TODO: just reading the id should be sufficient "albums.name, albums.id, albums.artist, " // TODO: again here "genres.name, genres.id, " // TODO: again here "composers.name, composers.id, " // TODO: again here "years.name, years.id"; // TODO: again here } QString SqlTrack::getTrackJoinConditions() { return "LEFT JOIN tracks ON urls.id = tracks.url " "LEFT JOIN statistics ON urls.id = statistics.url " "LEFT JOIN artists ON tracks.artist = artists.id " "LEFT JOIN albums ON tracks.album = albums.id " "LEFT JOIN genres ON tracks.genre = genres.id " "LEFT JOIN composers ON tracks.composer = composers.id " "LEFT JOIN years ON tracks.year = years.id"; } int SqlTrack::getTrackReturnValueCount() { static int count = getTrackReturnValues().split( ',' ).count(); return count; } SqlTrack::SqlTrack( Collections::SqlCollection *collection, int deviceId, const QString &rpath, int directoryId, const QString &uidUrl ) : Track() , m_collection( collection ) , m_batchUpdate( 0 ) , m_writeFile( true ) , m_labelsInCache( false ) { m_batchUpdate = 1; // I don't want commits yet m_urlId = -1; // this will be set with the first database write m_trackId = -1; // this will be set with the first database write m_statisticsId = -1; setUrl( deviceId, rpath, directoryId ); m_url = QUrl::fromUserInput(m_cache.value( Meta::valUrl ).toString()); // SqlRegistry already has this url setUidUrl( uidUrl ); m_uid = m_cache.value( Meta::valUniqueId ).toString(); // SqlRegistry already has this uid // ensure that these values get a correct database id m_cache.insert( Meta::valAlbum, QVariant() ); m_cache.insert( Meta::valArtist, QVariant() ); m_cache.insert( Meta::valComposer, QVariant() ); m_cache.insert( Meta::valYear, QVariant() ); m_cache.insert( Meta::valGenre, QVariant() ); m_trackNumber = 0; m_discNumber = 0; m_score = 0; m_rating = 0; m_bitrate = 0; m_length = 0; m_filesize = 0; m_sampleRate = 0; m_playCount = 0; m_bpm = 0.0; m_createDate = QDateTime::currentDateTime(); m_cache.insert( Meta::valCreateDate, m_createDate ); // ensure that the created date is written the next time m_trackGain = 0.0; m_trackPeakGain = 0.0; m_albumGain = 0.0; m_albumPeakGain = 0.0; m_batchUpdate = 0; // reset in-batch-update without committing m_filetype = Amarok::Unknown; } SqlTrack::SqlTrack( Collections::SqlCollection *collection, const QStringList &result ) : Track() , m_collection( collection ) , m_batchUpdate( 0 ) , m_writeFile( true ) , m_labelsInCache( false ) { QStringList::ConstIterator iter = result.constBegin(); m_urlId = (*(iter++)).toInt(); Q_ASSERT( m_urlId > 0 && "refusing to create SqlTrack with non-positive urlId, please file a bug" ); m_deviceId = (*(iter++)).toInt(); Q_ASSERT( m_deviceId != 0 && "refusing to create SqlTrack with zero deviceId, please file a bug" ); m_rpath = *(iter++); m_directoryId = (*(iter++)).toInt(); Q_ASSERT( m_directoryId > 0 && "refusing to create SqlTrack with non-positive directoryId, please file a bug" ); m_url = QUrl::fromLocalFile( m_collection->mountPointManager()->getAbsolutePath( m_deviceId, m_rpath ) ); m_uid = *(iter++); m_trackId = (*(iter++)).toInt(); m_title = *(iter++); m_comment = *(iter++); m_trackNumber = (*(iter++)).toInt(); m_discNumber = (*(iter++)).toInt(); m_score = (*(iter++)).toDouble(); m_rating = (*(iter++)).toInt(); m_bitrate = (*(iter++)).toInt(); m_length = (*(iter++)).toInt(); m_filesize = (*(iter++)).toInt(); m_sampleRate = (*(iter++)).toInt(); m_statisticsId = (*(iter++)).toInt(); uint time = (*(iter++)).toUInt(); if( time > 0 ) m_firstPlayed = QDateTime::fromTime_t(time); time = (*(iter++)).toUInt(); if( time > 0 ) m_lastPlayed = QDateTime::fromTime_t(time); m_playCount = (*(iter++)).toInt(); m_filetype = Amarok::FileType( (*(iter++)).toInt() ); m_bpm = (*(iter++)).toFloat(); m_createDate = QDateTime::fromTime_t((*(iter++)).toUInt()); m_modifyDate = QDateTime::fromTime_t((*(iter++)).toUInt()); // if there is no track gain, we assume a gain of 0 // if there is no album gain, we use the track gain QString albumGain = *(iter++); QString albumPeakGain = *(iter++); m_trackGain = (*(iter++)).toDouble(); m_trackPeakGain = (*(iter++)).toDouble(); if ( albumGain.isEmpty() ) { m_albumGain = m_trackGain; m_albumPeakGain = m_trackPeakGain; } else { m_albumGain = albumGain.toDouble(); m_albumPeakGain = albumPeakGain.toDouble(); } SqlRegistry* registry = m_collection->registry(); QString artist = *(iter++); int artistId = (*(iter++)).toInt(); if( artistId > 0 ) m_artist = registry->getArtist( artistId, artist ); QString album = *(iter++); int albumId =(*(iter++)).toInt(); int albumArtistId = (*(iter++)).toInt(); if( albumId > 0 ) // sanity check m_album = registry->getAlbum( albumId, album, albumArtistId ); QString genre = *(iter++); int genreId = (*(iter++)).toInt(); if( genreId > 0 ) // sanity check m_genre = registry->getGenre( genreId, genre ); QString composer = *(iter++); int composerId = (*(iter++)).toInt(); if( composerId > 0 ) // sanity check m_composer = registry->getComposer( composerId, composer ); QString year = *(iter++); int yearId = (*(iter++)).toInt(); if( yearId > 0 ) // sanity check m_year = registry->getYear( year.toInt(), yearId ); //Q_ASSERT_X( iter == result.constEnd(), "SqlTrack( Collections::SqlCollection*, QStringList )", "number of expected fields did not match number of actual fields: expected " + result.size() ); } SqlTrack::~SqlTrack() { QWriteLocker locker( &m_lock ); if( !m_cache.isEmpty() ) warning() << "Destroying track with unwritten meta information." << m_title << "cache:" << m_cache; if( m_batchUpdate ) warning() << "Destroying track with unclosed batch update." << m_title; } QString SqlTrack::name() const { QReadLocker locker( &m_lock ); return m_title; } QString SqlTrack::prettyName() const { if ( !name().isEmpty() ) return name(); return prettyTitle( m_url.fileName() ); } void SqlTrack::setTitle( const QString &newTitle ) { QWriteLocker locker( &m_lock ); if ( m_title != newTitle ) commitIfInNonBatchUpdate( Meta::valTitle, newTitle ); } QUrl SqlTrack::playableUrl() const { QReadLocker locker( &m_lock ); return m_url; } QString SqlTrack::prettyUrl() const { QReadLocker locker( &m_lock ); return m_url.path(); } void SqlTrack::setUrl( int deviceId, const QString &rpath, int directoryId ) { QWriteLocker locker( &m_lock ); if( m_deviceId == deviceId && m_rpath == rpath && m_directoryId == directoryId ) return; m_deviceId = deviceId; m_rpath = rpath; m_directoryId = directoryId; commitIfInNonBatchUpdate( Meta::valUrl, m_collection->mountPointManager()->getAbsolutePath( m_deviceId, m_rpath ) ); } QString SqlTrack::uidUrl() const { QReadLocker locker( &m_lock ); return m_uid; } void SqlTrack::setUidUrl( const QString &uid ) { QWriteLocker locker( &m_lock ); // -- ensure that the uid starts with the collections protocol (amarok-sqltrackuid) QString newid = uid; QString protocol; if( m_collection ) protocol = m_collection->uidUrlProtocol()+"://"; if( !newid.startsWith( protocol ) ) newid.prepend( protocol ); m_cache.insert( Meta::valUniqueId, newid ); if( m_batchUpdate == 0 ) { debug() << "setting uidUrl manually...did you really mean to do this?"; commitIfInNonBatchUpdate(); } } QString SqlTrack::notPlayableReason() const { return localFileNotPlayableReason( playableUrl().toLocalFile() ); } bool SqlTrack::isEditable() const { QReadLocker locker( &m_lock ); QFile::Permissions p = QFile::permissions( m_url.path() ); const bool editable = ( p & QFile::WriteUser ) || ( p & QFile::WriteGroup ) || ( p & QFile::WriteOther ); return m_collection && QFile::exists( m_url.path() ) && editable; } Meta::AlbumPtr SqlTrack::album() const { QReadLocker locker( &m_lock ); return m_album; } void SqlTrack::setAlbum( const QString &newAlbum ) { QWriteLocker locker( &m_lock ); if( !m_album || m_album->name() != newAlbum ) commitIfInNonBatchUpdate( Meta::valAlbum, newAlbum ); } void SqlTrack::setAlbum( int albumId ) { QWriteLocker locker( &m_lock ); commitIfInNonBatchUpdate( Meta::valAlbumId, albumId ); } Meta::ArtistPtr SqlTrack::artist() const { QReadLocker locker( &m_lock ); return m_artist; } void SqlTrack::setArtist( const QString &newArtist ) { QWriteLocker locker( &m_lock ); if( !m_artist || m_artist->name() != newArtist ) commitIfInNonBatchUpdate( Meta::valArtist, newArtist ); } void SqlTrack::setAlbumArtist( const QString &newAlbumArtist ) { if( m_album.isNull() ) return; if( !newAlbumArtist.compare( "Various Artists", Qt::CaseInsensitive ) || !newAlbumArtist.compare( i18n( "Various Artists" ), Qt::CaseInsensitive ) ) { commitIfInNonBatchUpdate( Meta::valCompilation, true ); } else { m_cache.insert( Meta::valAlbumArtist, ArtistHelper::realTrackArtist( newAlbumArtist ) ); m_cache.insert( Meta::valCompilation, false ); commitIfInNonBatchUpdate(); } } Meta::ComposerPtr SqlTrack::composer() const { QReadLocker locker( &m_lock ); return m_composer; } void SqlTrack::setComposer( const QString &newComposer ) { QWriteLocker locker( &m_lock ); if( !m_composer || m_composer->name() != newComposer ) commitIfInNonBatchUpdate( Meta::valComposer, newComposer ); } Meta::YearPtr SqlTrack::year() const { QReadLocker locker( &m_lock ); return m_year; } void SqlTrack::setYear( int newYear ) { QWriteLocker locker( &m_lock ); if( !m_year || m_year->year() != newYear ) commitIfInNonBatchUpdate( Meta::valYear, newYear ); } Meta::GenrePtr SqlTrack::genre() const { QReadLocker locker( &m_lock ); return m_genre; } void SqlTrack::setGenre( const QString &newGenre ) { QWriteLocker locker( &m_lock ); if( !m_genre || m_genre->name() != newGenre ) commitIfInNonBatchUpdate( Meta::valGenre, newGenre ); } QString SqlTrack::type() const { QReadLocker locker( &m_lock ); return m_url.isLocalFile() ? Amarok::FileTypeSupport::toString( m_filetype ) // don't localize. This is used in different files to identify streams, see EngineController quirks : "stream"; } void SqlTrack::setType( Amarok::FileType newType ) { QWriteLocker locker( &m_lock ); if ( m_filetype != newType ) commitIfInNonBatchUpdate( Meta::valFormat, int(newType) ); } qreal SqlTrack::bpm() const { QReadLocker locker( &m_lock ); return m_bpm; } void SqlTrack::setBpm( const qreal newBpm ) { QWriteLocker locker( &m_lock ); if ( m_bpm != newBpm ) commitIfInNonBatchUpdate( Meta::valBpm, newBpm ); } QString SqlTrack::comment() const { QReadLocker locker( &m_lock ); return m_comment; } void SqlTrack::setComment( const QString &newComment ) { QWriteLocker locker( &m_lock ); if( newComment != m_comment ) commitIfInNonBatchUpdate( Meta::valComment, newComment ); } double SqlTrack::score() const { QReadLocker locker( &m_lock ); return m_score; } void SqlTrack::setScore( double newScore ) { QWriteLocker locker( &m_lock ); newScore = qBound( double(0), newScore, double(100) ); if( qAbs( newScore - m_score ) > 0.001 ) // we don't commit for minimal changes commitIfInNonBatchUpdate( Meta::valScore, newScore ); } int SqlTrack::rating() const { QReadLocker locker( &m_lock ); return m_rating; } void SqlTrack::setRating( int newRating ) { QWriteLocker locker( &m_lock ); newRating = qBound( 0, newRating, 10 ); if( newRating != m_rating ) commitIfInNonBatchUpdate( Meta::valRating, newRating ); } qint64 SqlTrack::length() const { QReadLocker locker( &m_lock ); return m_length; } void SqlTrack::setLength( qint64 newLength ) { QWriteLocker locker( &m_lock ); if( newLength != m_length ) commitIfInNonBatchUpdate( Meta::valLength, newLength ); } int SqlTrack::filesize() const { QReadLocker locker( &m_lock ); return m_filesize; } int SqlTrack::sampleRate() const { QReadLocker locker( &m_lock ); return m_sampleRate; } void SqlTrack::setSampleRate( int newSampleRate ) { QWriteLocker locker( &m_lock ); if( newSampleRate != m_sampleRate ) commitIfInNonBatchUpdate( Meta::valSamplerate, newSampleRate ); } int SqlTrack::bitrate() const { QReadLocker locker( &m_lock ); return m_bitrate; } void SqlTrack::setBitrate( int newBitrate ) { QWriteLocker locker( &m_lock ); if( newBitrate != m_bitrate ) commitIfInNonBatchUpdate( Meta::valBitrate, newBitrate ); } QDateTime SqlTrack::createDate() const { QReadLocker locker( &m_lock ); return m_createDate; } QDateTime SqlTrack::modifyDate() const { QReadLocker locker( &m_lock ); return m_modifyDate; } void SqlTrack::setModifyDate( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_modifyDate ) commitIfInNonBatchUpdate( Meta::valModified, newTime ); } int SqlTrack::trackNumber() const { QReadLocker locker( &m_lock ); return m_trackNumber; } void SqlTrack::setTrackNumber( int newTrackNumber ) { QWriteLocker locker( &m_lock ); if( newTrackNumber != m_trackNumber ) commitIfInNonBatchUpdate( Meta::valTrackNr, newTrackNumber ); } int SqlTrack::discNumber() const { QReadLocker locker( &m_lock ); return m_discNumber; } void SqlTrack::setDiscNumber( int newDiscNumber ) { QWriteLocker locker( &m_lock ); if( newDiscNumber != m_discNumber ) commitIfInNonBatchUpdate( Meta::valDiscNr, newDiscNumber ); } QDateTime SqlTrack::lastPlayed() const { QReadLocker locker( &m_lock ); return m_lastPlayed; } void SqlTrack::setLastPlayed( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_lastPlayed ) commitIfInNonBatchUpdate( Meta::valLastPlayed, newTime ); } QDateTime SqlTrack::firstPlayed() const { QReadLocker locker( &m_lock ); return m_firstPlayed; } void SqlTrack::setFirstPlayed( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_firstPlayed ) commitIfInNonBatchUpdate( Meta::valFirstPlayed, newTime ); } int SqlTrack::playCount() const { QReadLocker locker( &m_lock ); return m_playCount; } void SqlTrack::setPlayCount( const int newCount ) { QWriteLocker locker( &m_lock ); if( newCount != m_playCount ) commitIfInNonBatchUpdate( Meta::valPlaycount, newCount ); } qreal SqlTrack::replayGain( ReplayGainTag mode ) const { QReadLocker locker(&(const_cast(this)->m_lock)); switch( mode ) { case Meta::ReplayGain_Track_Gain: return m_trackGain; case Meta::ReplayGain_Track_Peak: return m_trackPeakGain; case Meta::ReplayGain_Album_Gain: return m_albumGain; case Meta::ReplayGain_Album_Peak: return m_albumPeakGain; } return 0.0; } void SqlTrack::setReplayGain( Meta::ReplayGainTag mode, qreal value ) { if( qAbs( value - replayGain( mode ) ) < 0.01 ) return; { QWriteLocker locker( &m_lock ); switch( mode ) { case Meta::ReplayGain_Track_Gain: m_cache.insert( Meta::valTrackGain, value ); break; case Meta::ReplayGain_Track_Peak: m_cache.insert( Meta::valTrackGainPeak, value ); break; case Meta::ReplayGain_Album_Gain: m_cache.insert( Meta::valAlbumGain, value ); break; case Meta::ReplayGain_Album_Peak: m_cache.insert( Meta::valAlbumGainPeak, value ); break; } commitIfInNonBatchUpdate(); } } void SqlTrack::beginUpdate() { QWriteLocker locker( &m_lock ); m_batchUpdate++; } void SqlTrack::endUpdate() { QWriteLocker locker( &m_lock ); Q_ASSERT( m_batchUpdate > 0 ); m_batchUpdate--; commitIfInNonBatchUpdate(); } void SqlTrack::commitIfInNonBatchUpdate( qint64 field, const QVariant &value ) { m_cache.insert( field, value ); commitIfInNonBatchUpdate(); } void SqlTrack::commitIfInNonBatchUpdate() { if( m_batchUpdate > 0 || m_cache.isEmpty() ) return; // nothing to do // debug() << "SqlTrack::commitMetaDataChanges " << m_cache; QString oldUid = m_uid; // for all the following objects we need to invalidate the cache and // notify the observers after the update AmarokSharedPointer oldArtist; AmarokSharedPointer newArtist; AmarokSharedPointer oldAlbum; AmarokSharedPointer newAlbum; AmarokSharedPointer oldComposer; AmarokSharedPointer newComposer; AmarokSharedPointer oldGenre; AmarokSharedPointer newGenre; AmarokSharedPointer oldYear; AmarokSharedPointer newYear; if( m_cache.contains( Meta::valFormat ) ) m_filetype = Amarok::FileType(m_cache.value( Meta::valFormat ).toInt()); if( m_cache.contains( Meta::valTitle ) ) m_title = m_cache.value( Meta::valTitle ).toString(); if( m_cache.contains( Meta::valComment ) ) m_comment = m_cache.value( Meta::valComment ).toString(); if( m_cache.contains( Meta::valScore ) ) m_score = m_cache.value( Meta::valScore ).toDouble(); if( m_cache.contains( Meta::valRating ) ) m_rating = m_cache.value( Meta::valRating ).toInt(); if( m_cache.contains( Meta::valLength ) ) m_length = m_cache.value( Meta::valLength ).toLongLong(); if( m_cache.contains( Meta::valSamplerate ) ) m_sampleRate = m_cache.value( Meta::valSamplerate ).toInt(); if( m_cache.contains( Meta::valBitrate ) ) m_bitrate = m_cache.value( Meta::valBitrate ).toInt(); if( m_cache.contains( Meta::valFirstPlayed ) ) m_firstPlayed = m_cache.value( Meta::valFirstPlayed ).toDateTime(); if( m_cache.contains( Meta::valLastPlayed ) ) m_lastPlayed = m_cache.value( Meta::valLastPlayed ).toDateTime(); if( m_cache.contains( Meta::valTrackNr ) ) m_trackNumber = m_cache.value( Meta::valTrackNr ).toInt(); if( m_cache.contains( Meta::valDiscNr ) ) m_discNumber = m_cache.value( Meta::valDiscNr ).toInt(); if( m_cache.contains( Meta::valPlaycount ) ) m_playCount = m_cache.value( Meta::valPlaycount ).toInt(); if( m_cache.contains( Meta::valCreateDate ) ) m_createDate = m_cache.value( Meta::valCreateDate ).toDateTime(); if( m_cache.contains( Meta::valModified ) ) m_modifyDate = m_cache.value( Meta::valModified ).toDateTime(); if( m_cache.contains( Meta::valTrackGain ) ) m_trackGain = m_cache.value( Meta::valTrackGain ).toDouble(); if( m_cache.contains( Meta::valTrackGainPeak ) ) m_trackPeakGain = m_cache.value( Meta::valTrackGainPeak ).toDouble(); if( m_cache.contains( Meta::valAlbumGain ) ) m_albumGain = m_cache.value( Meta::valAlbumGain ).toDouble(); if( m_cache.contains( Meta::valAlbumGainPeak ) ) m_albumPeakGain = m_cache.value( Meta::valAlbumGainPeak ).toDouble(); if( m_cache.contains( Meta::valUrl ) ) { // slight problem here: it is possible to set the url to the one of an already // existing track, which is forbidden by the database // At least the ScanResultProcessor handles this problem QUrl oldUrl = m_url; QUrl newUrl = QUrl::fromUserInput(m_cache.value( Meta::valUrl ).toString()); if( oldUrl != newUrl ) m_collection->registry()->updateCachedUrl( oldUrl.path(), newUrl.path() ); m_url = newUrl; // debug() << "m_cache contains a new URL, setting m_url to " << m_url << " from " << oldUrl; } if( m_cache.contains( Meta::valArtist ) ) { //invalidate cache of the old artist... oldArtist = static_cast(m_artist.data()); m_artist = m_collection->registry()->getArtist( m_cache.value( Meta::valArtist ).toString() ); //and the new one newArtist = static_cast(m_artist.data()); // if the current album is no compilation and we aren't changing // the album anyway, then we need to create a new album with the // new artist. if( m_album ) { bool supp = m_album->suppressImageAutoFetch(); m_album->setSuppressImageAutoFetch( true ); if( m_album->hasAlbumArtist() && m_album->albumArtist() == oldArtist && !m_cache.contains( Meta::valAlbum ) && !m_cache.contains( Meta::valAlbumId ) ) { m_cache.insert( Meta::valAlbum, m_album->name() ); } m_album->setSuppressImageAutoFetch( supp ); } } if( m_cache.contains( Meta::valAlbum ) || m_cache.contains( Meta::valAlbumId ) || m_cache.contains( Meta::valAlbumArtist ) ) { oldAlbum = static_cast(m_album.data()); if( m_cache.contains( Meta::valAlbumId ) ) m_album = m_collection->registry()->getAlbum( m_cache.value( Meta::valAlbumId ).toInt() ); else { // the album should remain a compilation after renaming it // TODO: we would need to use the artist helper QString newArtistName; if( m_cache.contains( Meta::valAlbumArtist ) ) newArtistName = m_cache.value( Meta::valAlbumArtist ).toString(); else if( oldAlbum && oldAlbum->isCompilation() && !oldAlbum->name().isEmpty() ) newArtistName.clear(); else if( oldAlbum && oldAlbum->hasAlbumArtist() ) newArtistName = oldAlbum->albumArtist()->name(); m_album = m_collection->registry()->getAlbum( m_cache.contains( Meta::valAlbum) ? m_cache.value( Meta::valAlbum ).toString() : oldAlbum->name(), newArtistName ); } newAlbum = static_cast(m_album.data()); // due to the complex logic with artist and albumId it can happen that // in the end we have the same album as before. if( newAlbum == oldAlbum ) { m_cache.remove( Meta::valAlbum ); m_cache.remove( Meta::valAlbumId ); m_cache.remove( Meta::valAlbumArtist ); oldAlbum.clear(); newAlbum.clear(); } } if( m_cache.contains( Meta::valComposer ) ) { oldComposer = static_cast(m_composer.data()); m_composer = m_collection->registry()->getComposer( m_cache.value( Meta::valComposer ).toString() ); newComposer = static_cast(m_composer.data()); } if( m_cache.contains( Meta::valGenre ) ) { oldGenre = static_cast(m_genre.data()); m_genre = m_collection->registry()->getGenre( m_cache.value( Meta::valGenre ).toString() ); newGenre = static_cast(m_genre.data()); } if( m_cache.contains( Meta::valYear ) ) { oldYear = static_cast(m_year.data()); m_year = m_collection->registry()->getYear( m_cache.value( Meta::valYear ).toInt() ); newYear = static_cast(m_year.data()); } if( m_cache.contains( Meta::valBpm ) ) m_bpm = m_cache.value( Meta::valBpm ).toDouble(); // --- write the file if( m_writeFile && AmarokConfig::writeBack() ) { Meta::Tag::writeTags( m_url.path(), m_cache, AmarokConfig::writeBackStatistics() ); // unique id may have changed QString uid = Meta::Tag::readTags( m_url.path() ).value( Meta::valUniqueId ).toString(); if( !uid.isEmpty() ) m_cache[ Meta::valUniqueId ] = m_collection->generateUidUrl( uid ); } // needs to be after writing to file; that may have changed generated uid if( m_cache.contains( Meta::valUniqueId ) ) { QString newUid = m_cache.value( Meta::valUniqueId ).toString(); if( oldUid != newUid && m_collection->registry()->updateCachedUid( oldUid, newUid ) ) m_uid = newUid; } //updating the fields might have changed the filesize //read the current filesize so that we can update the db QFile file( m_url.path() ); if( file.exists() ) { if( m_filesize != file.size() ) { m_cache.insert( Meta::valFilesize, file.size() ); m_filesize = file.size(); } } // --- add to the registry dirty list SqlRegistry *registry = 0; // prevent writing to the db when we don't know the directory, bug 322474. Note that // m_urlId is created by registry->commitDirtyTracks() if there is none. if( m_deviceId != 0 && m_directoryId > 0 ) { registry = m_collection->registry(); QMutexLocker locker2( ®istry->m_blockMutex ); registry->m_dirtyTracks.insert( Meta::SqlTrackPtr( this ) ); if( oldArtist ) registry->m_dirtyArtists.insert( oldArtist ); if( newArtist ) registry->m_dirtyArtists.insert( newArtist ); if( oldAlbum ) registry->m_dirtyAlbums.insert( oldAlbum ); if( newAlbum ) registry->m_dirtyAlbums.insert( newAlbum ); if( oldComposer ) registry->m_dirtyComposers.insert( oldComposer ); if( newComposer ) registry->m_dirtyComposers.insert( newComposer ); if( oldGenre ) registry->m_dirtyGenres.insert( oldGenre ); if( newGenre ) registry->m_dirtyGenres.insert( newGenre ); if( oldYear ) registry->m_dirtyYears.insert( oldYear ); if( newYear ) registry->m_dirtyYears.insert( newYear ); } else error() << Q_FUNC_INFO << "non-positive urlId, zero deviceId or non-positive" << "directoryId encountered in track" << m_url << "urlId:" << m_urlId << "deviceId:" << m_deviceId << "directoryId:" << m_directoryId << "- not writing back metadata" << "changes to the database."; m_lock.unlock(); // or else we provoke a deadlock // copy the image BUG: 203211 (we need to do it here or provoke a dead lock) if( oldAlbum && newAlbum ) { bool oldSupp = oldAlbum->suppressImageAutoFetch(); bool newSupp = newAlbum->suppressImageAutoFetch(); oldAlbum->setSuppressImageAutoFetch( true ); newAlbum->setSuppressImageAutoFetch( true ); if( oldAlbum->hasImage() && !newAlbum->hasImage() ) newAlbum->setImage( oldAlbum->imageLocation().path() ); oldAlbum->setSuppressImageAutoFetch( oldSupp ); newAlbum->setSuppressImageAutoFetch( newSupp ); } if( registry ) registry->commitDirtyTracks(); // calls notifyObservers() as appropriate else notifyObservers(); m_lock.lockForWrite(); // reset back to state it was during call if( m_uid != oldUid ) { updatePlaylistsToDb( m_cache, oldUid ); updateEmbeddedCoversToDb( m_cache, oldUid ); } // --- clean up m_cache.clear(); } void SqlTrack::updatePlaylistsToDb( const FieldHash &fields, const QString &oldUid ) { if( fields.isEmpty() ) return; // nothing to do auto storage = m_collection->sqlStorage(); QStringList tags; // keep this in sync with SqlPlaylist::saveTracks()! if( fields.contains( Meta::valUrl ) ) tags << QString( "url='%1'" ).arg( storage->escape( m_url.path() ) ); if( fields.contains( Meta::valTitle ) ) tags << QString( "title='%1'" ).arg( storage->escape( m_title ) ); if( fields.contains( Meta::valAlbum ) ) tags << QString( "album='%1'" ).arg( m_album ? storage->escape( m_album->prettyName() ) : "" ); if( fields.contains( Meta::valArtist ) ) tags << QString( "artist='%1'" ).arg( m_artist ? storage->escape( m_artist->prettyName() ) : "" ); if( fields.contains( Meta::valLength ) ) tags << QString( "length=%1").arg( QString::number( m_length ) ); if( fields.contains( Meta::valUniqueId ) ) { // SqlPlaylist mirrors uniqueid to url, update it too, bug 312128 tags << QString( "url='%1'" ).arg( storage->escape( m_uid ) ); tags << QString( "uniqueid='%1'" ).arg( storage->escape( m_uid ) ); } if( !tags.isEmpty() ) { QString update = "UPDATE playlist_tracks SET %1 WHERE uniqueid = '%2';"; update = update.arg( tags.join( ", " ), storage->escape( oldUid ) ); storage->query( update ); } } void SqlTrack::updateEmbeddedCoversToDb( const FieldHash &fields, const QString &oldUid ) { if( fields.isEmpty() ) return; // nothing to do auto storage = m_collection->sqlStorage(); QString tags; if( fields.contains( Meta::valUniqueId ) ) tags += QString( ",path='%1'" ).arg( storage->escape( m_uid ) ); if( !tags.isEmpty() ) { tags = tags.remove(0, 1); // the first character is always a ',' QString update = "UPDATE images SET %1 WHERE path = '%2';"; update = update.arg( tags, storage->escape( oldUid ) ); storage->query( update ); } } QString SqlTrack::prettyTitle( const QString &filename ) //static { QString s = filename; //just so the code is more readable //remove .part extension if it exists if (s.endsWith( ".part" )) s = s.left( s.length() - 5 ); //remove file extension, s/_/ /g and decode %2f-like sequences s = s.left( s.lastIndexOf( '.' ) ).replace( '_', ' ' ); s = QUrl::fromPercentEncoding( s.toLatin1() ); return s; } bool SqlTrack::inCollection() const { QReadLocker locker( &m_lock ); return m_trackId > 0; } Collections::Collection* SqlTrack::collection() const { return m_collection; } QString SqlTrack::cachedLyrics() const { /* We don't cache the string as it may be potentially very long */ QString query = QString( "SELECT lyrics FROM lyrics WHERE url = %1" ).arg( m_urlId ); QStringList result = m_collection->sqlStorage()->query( query ); if( result.isEmpty() ) return QString(); return result.first(); } void SqlTrack::setCachedLyrics( const QString &lyrics ) { QString query = QString( "SELECT count(*) FROM lyrics WHERE url = %1").arg( m_urlId ); const QStringList queryResult = m_collection->sqlStorage()->query( query ); if( queryResult.isEmpty() ) return; // error in the query? if( queryResult.first().toInt() == 0 ) { QString insert = QString( "INSERT INTO lyrics( url, lyrics ) VALUES ( %1, '%2' )" ) .arg( QString::number( m_urlId ), m_collection->sqlStorage()->escape( lyrics ) ); m_collection->sqlStorage()->insert( insert, "lyrics" ); } else { QString update = QString( "UPDATE lyrics SET lyrics = '%1' WHERE url = %2" ) .arg( m_collection->sqlStorage()->escape( lyrics ), QString::number( m_urlId ) ); m_collection->sqlStorage()->query( update ); } notifyObservers(); } bool SqlTrack::hasCapabilityInterface( Capabilities::Capability::Type type ) const { switch( type ) { case Capabilities::Capability::Actions: case Capabilities::Capability::Organisable: case Capabilities::Capability::BookmarkThis: case Capabilities::Capability::WriteTimecode: case Capabilities::Capability::LoadTimecode: case Capabilities::Capability::ReadLabel: case Capabilities::Capability::WriteLabel: case Capabilities::Capability::FindInSource: return true; default: return Track::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlTrack::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::Actions: { QList actions; //TODO These actions will hang around until m_collection is destructed. // Find a better parent to avoid this memory leak. //actions.append( new CopyToDeviceAction( m_collection, this ) ); return new Capabilities::ActionsCapability( actions ); } case Capabilities::Capability::Organisable: return new Capabilities::OrganiseCapabilityImpl( this ); case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkCurrentTrackPositionAction( 0 ) ); case Capabilities::Capability::WriteTimecode: return new Capabilities::TimecodeWriteCapabilityImpl( this ); case Capabilities::Capability::LoadTimecode: return new Capabilities::TimecodeLoadCapabilityImpl( this ); case Capabilities::Capability::ReadLabel: return new Capabilities::SqlReadLabelCapability( this, sqlCollection()->sqlStorage() ); case Capabilities::Capability::WriteLabel: return new Capabilities::SqlWriteLabelCapability( this, sqlCollection()->sqlStorage() ); case Capabilities::Capability::FindInSource: return new Capabilities::FindInSourceCapabilityImpl( this ); default: return Track::createCapabilityInterface( type ); } } void SqlTrack::addLabel( const QString &label ) { Meta::LabelPtr realLabel = m_collection->registry()->getLabel( label ); addLabel( realLabel ); } void SqlTrack::addLabel( const Meta::LabelPtr &label ) { AmarokSharedPointer sqlLabel = AmarokSharedPointer::dynamicCast( label ); if( !sqlLabel ) { Meta::LabelPtr tmp = m_collection->registry()->getLabel( label->name() ); sqlLabel = AmarokSharedPointer::dynamicCast( tmp ); } if( sqlLabel ) { QWriteLocker locker( &m_lock ); commitIfInNonBatchUpdate(); // we need to have a up-to-date m_urlId if( m_urlId <= 0 ) { warning() << "Track does not have an urlId."; return; } QString countQuery = "SELECT COUNT(*) FROM urls_labels WHERE url = %1 AND label = %2;"; QStringList countRs = m_collection->sqlStorage()->query( countQuery.arg( QString::number( m_urlId ), QString::number( sqlLabel->id() ) ) ); if( !countRs.isEmpty() && countRs.first().toInt() == 0 ) { QString insert = "INSERT INTO urls_labels(url,label) VALUES (%1,%2);"; m_collection->sqlStorage()->insert( insert.arg( QString::number( m_urlId ), QString::number( sqlLabel->id() ) ), "urls_labels" ); if( m_labelsInCache ) { m_labelsCache.append( Meta::LabelPtr::staticCast( sqlLabel ) ); } locker.unlock(); notifyObservers(); sqlLabel->invalidateCache(); } } } int SqlTrack::id() const { QReadLocker locker( &m_lock ); return m_trackId; } int SqlTrack::urlId() const { QReadLocker locker( &m_lock ); return m_urlId; } void SqlTrack::removeLabel( const Meta::LabelPtr &label ) { AmarokSharedPointer sqlLabel = AmarokSharedPointer::dynamicCast( label ); if( !sqlLabel ) { Meta::LabelPtr tmp = m_collection->registry()->getLabel( label->name() ); sqlLabel = AmarokSharedPointer::dynamicCast( tmp ); } if( sqlLabel ) { QString query = "DELETE FROM urls_labels WHERE label = %2 and url = (SELECT url FROM tracks WHERE id = %1);"; m_collection->sqlStorage()->query( query.arg( QString::number( m_trackId ), QString::number( sqlLabel->id() ) ) ); if( m_labelsInCache ) { m_labelsCache.removeAll( Meta::LabelPtr::staticCast( sqlLabel ) ); } notifyObservers(); sqlLabel->invalidateCache(); } } Meta::LabelList SqlTrack::labels() const { { QReadLocker locker( &m_lock ); if( m_labelsInCache ) return m_labelsCache; } if( !m_collection ) return Meta::LabelList(); // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Label ); qm->addMatch( Meta::TrackPtr( const_cast(this) ) ); qm->setBlocking( true ); qm->run(); { QWriteLocker locker( &m_lock ); m_labelsInCache = true; m_labelsCache = qm->labels(); delete qm; return m_labelsCache; } } TrackEditorPtr SqlTrack::editor() { return TrackEditorPtr( isEditable() ? this : 0 ); } StatisticsPtr SqlTrack::statistics() { return StatisticsPtr( this ); } void SqlTrack::remove() { QWriteLocker locker( &m_lock ); m_cache.clear(); locker.unlock(); m_collection->registry()->removeTrack( m_urlId, m_uid ); // -- inform all albums, artist, years #undef foreachInvalidateCache #define INVALIDATE_AND_UPDATE(X) if( X ) \ { \ X->invalidateCache(); \ X->notifyObservers(); \ } INVALIDATE_AND_UPDATE(static_cast(m_artist.data())); INVALIDATE_AND_UPDATE(static_cast(m_album.data())); INVALIDATE_AND_UPDATE(static_cast(m_composer.data())); INVALIDATE_AND_UPDATE(static_cast(m_genre.data())); INVALIDATE_AND_UPDATE(static_cast(m_year.data())); #undef INVALIDATE_AND_UPDATE m_artist = 0; m_album = 0; m_composer = 0; m_genre = 0; m_year = 0; m_urlId = 0; m_trackId = 0; m_statisticsId = 0; m_collection->collectionUpdated(); } //---------------------- class Artist -------------------------- SqlArtist::SqlArtist( Collections::SqlCollection *collection, int id, const QString &name ) : Artist() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } Meta::SqlArtist::~SqlArtist() { } void SqlArtist::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlArtist::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::ArtistPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } bool SqlArtist::hasCapabilityInterface( Capabilities::Capability::Type type ) const { switch( type ) { case Capabilities::Capability::BookmarkThis: return true; default: return Artist::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlArtist::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkArtistAction( 0, Meta::ArtistPtr( this ) ) ); default: return Artist::createCapabilityInterface( type ); } } //--------------- class Album --------------------------------- const QString SqlAlbum::AMAROK_UNSET_MAGIC = QString( "AMAROK_UNSET_MAGIC" ); SqlAlbum::SqlAlbum( Collections::SqlCollection *collection, int id, const QString &name, int artist ) : Album() , m_collection( collection ) , m_name( name ) , m_id( id ) , m_artistId( artist ) , m_imageId( -1 ) , m_hasImage( false ) , m_hasImageChecked( false ) , m_unsetImageId( -1 ) , m_tracksLoaded( false ) , m_suppressAutoFetch( false ) , m_mutex( QMutex::Recursive ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } Meta::SqlAlbum::~SqlAlbum() { CoverCache::invalidateAlbum( this ); } void SqlAlbum::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_hasImage = false; m_hasImageChecked = false; m_tracks.clear(); } TrackList SqlAlbum::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::AlbumPtr( this ) ); qm->orderBy( Meta::valDiscNr ); qm->orderBy( Meta::valTrackNr ); qm->orderBy( Meta::valTitle ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } // note for internal implementation: // if hasImage returns true then m_imagePath is set bool SqlAlbum::hasImage( int size ) const { Q_UNUSED(size); // we have every size if we have an image at all QMutexLocker locker( &m_mutex ); if( m_name.isEmpty() ) return false; if( !m_hasImageChecked ) { m_hasImageChecked = true; const_cast( this )->largeImagePath(); // The user has explicitly set no cover if( m_imagePath == AMAROK_UNSET_MAGIC ) m_hasImage = false; // if we don't have an image but it was not explicitly blocked else if( m_imagePath.isEmpty() ) { // Cover fetching runs in another thread. If there is a retrieved cover // then updateImage() gets called which updates the cache and alerts the // subscribers. We use queueAlbum() because this runs the fetch as a // background job and doesn't give an intruding popup asking for confirmation if( !m_suppressAutoFetch && !m_name.isEmpty() && AmarokConfig::autoGetCoverArt() ) CoverFetcher::instance()->queueAlbum( AlbumPtr(const_cast(this)) ); m_hasImage = false; } else m_hasImage = true; } return m_hasImage; } QImage SqlAlbum::image( int size ) const { QMutexLocker locker( &m_mutex ); if( !hasImage() ) return Meta::Album::image( size ); // findCachedImage looks for a scaled version of the fullsize image // which may have been saved on a previous lookup QString cachedImagePath; if( size <= 1 ) cachedImagePath = m_imagePath; else cachedImagePath = scaledDiskCachePath( size ); //FIXME this cache doesn't differentiate between shadowed/unshadowed // a image exists. just load it. if( !cachedImagePath.isEmpty() && QFile( cachedImagePath ).exists() ) { QImage image( cachedImagePath ); if( image.isNull() ) return Meta::Album::image( size ); return image; } // no cached scaled image exists. Have to create it QImage image; // --- embedded cover if( m_collection && m_imagePath.startsWith( m_collection->uidUrlProtocol() ) ) { // -- check if we have a track with the given path as uid Meta::TrackPtr track = m_collection->getTrackFromUid( m_imagePath ); if( track ) image = Meta::Tag::embeddedCover( track->playableUrl().path() ); } // --- a normal path if( image.isNull() ) image = QImage( m_imagePath ); if( image.isNull() ) return Meta::Album::image( size ); if( size > 1 && size < 1000 ) { image = image.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); - image.save( cachedImagePath, "PNG" ); + std::thread thread( QOverload::of( &QImage::save ), image, cachedImagePath, "PNG", -1 ); + thread.detach(); } return image; } QUrl SqlAlbum::imageLocation( int size ) { if( !hasImage() ) return QUrl(); // findCachedImage looks for a scaled version of the fullsize image // which may have been saved on a previous lookup if( size <= 1 ) return QUrl::fromLocalFile( m_imagePath ); QString cachedImagePath = scaledDiskCachePath( size ); if( cachedImagePath.isEmpty() ) return QUrl(); if( !QFile( cachedImagePath ).exists() ) { // If we don't have the location, it's possible that we haven't tried to find the image yet // So, let's look for it and just ignore the result QImage i = image( size ); Q_UNUSED( i ) } if( !QFile( cachedImagePath ).exists() ) return QUrl(); return QUrl::fromLocalFile(cachedImagePath); } void SqlAlbum::setImage( const QImage &image ) { // the unnamed album is special. it will never have an image if( m_name.isEmpty() ) return; QMutexLocker locker( &m_mutex ); if( image.isNull() ) return; // removeImage() will destroy all scaled cached versions of the artwork // and remove references from the database if required. removeImage(); QString path = largeDiskCachePath(); // make sure not to overwrite existing images while( QFile(path).exists() ) path += '_'; // not that nice but it shouldn't happen that often. - image.save( path, "JPG" ); + std::thread thread( QOverload::of( &QImage::save ), image, path, "JPG", -1 ); + thread.detach(); setImage( path ); locker.unlock(); notifyObservers(); // -- write back the album cover if allowed if( AmarokConfig::writeBackCover() ) { // - scale to cover to a sensible size QImage scaledImage( image ); if( scaledImage.width() > AmarokConfig::writeBackCoverDimensions() || scaledImage.height() > AmarokConfig::writeBackCoverDimensions() ) scaledImage = scaledImage.scaled( AmarokConfig::writeBackCoverDimensions(), AmarokConfig::writeBackCoverDimensions(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); // - set the image for each track Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { // the song needs to be at least one mb big or we won't set an image // that means that the new image will increase the file size by less than 2% if( metaTrack->filesize() > 1024l * 1024l ) { Meta::FieldHash fields; fields.insert( Meta::valImage, scaledImage ); WriteTagsJob *job = new WriteTagsJob( metaTrack->playableUrl().path(), fields ); QObject::connect( job, &WriteTagsJob::done, job, &WriteTagsJob::deleteLater ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(job) ); } // note: we might want to update the track file size after writing the image } } } void SqlAlbum::removeImage() { QMutexLocker locker( &m_mutex ); if( !hasImage() ) return; // Update the database image path // Set the album image to a magic value which will tell Amarok not to fetch it automatically const int unsetId = unsetImageId(); QString query = "UPDATE albums SET image = %1 WHERE id = %2"; m_collection->sqlStorage()->query( query.arg( QString::number( unsetId ), QString::number( m_id ) ) ); // From here on we check if there are any remaining references to that particular image in the database // If there aren't, then we should remove the image path from the database ( and possibly delete the file? ) // If there are, we need to leave it since other albums will reference this particular image path. // query = "SELECT count( albums.id ) FROM albums " "WHERE albums.image = %1"; QStringList res = m_collection->sqlStorage()->query( query.arg( QString::number( m_imageId ) ) ); if( !res.isEmpty() ) { int references = res.first().toInt(); // If there are no more references to this particular image, then we should clean up if( references <= 0 ) { query = "DELETE FROM images WHERE id = %1"; m_collection->sqlStorage()->query( query.arg( QString::number( m_imageId ) ) ); // remove the large cover only if it was cached. QDir largeCoverDir( Amarok::saveLocation( "albumcovers/large/" ) ); if( QFileInfo(m_imagePath).absoluteDir() == largeCoverDir ) QFile::remove( m_imagePath ); // remove all cache images QString key = md5sum( QString(), QString(), m_imagePath ); QDir cacheDir( Amarok::saveLocation( "albumcovers/cache/" ) ); QStringList cacheFilter; cacheFilter << QString( "*@" ) + key; QStringList cachedImages = cacheDir.entryList( cacheFilter ); foreach( const QString &image, cachedImages ) { bool r = QFile::remove( cacheDir.filePath( image ) ); debug() << "deleting cached image: " << image << " : " + ( r ? QString("ok") : QString("fail") ); } CoverCache::invalidateAlbum( this ); } } m_imageId = -1; m_imagePath.clear(); m_hasImage = false; m_hasImageChecked = true; locker.unlock(); notifyObservers(); } int SqlAlbum::unsetImageId() const { // Return the cached value if we have already done the lookup before if( m_unsetImageId >= 0 ) return m_unsetImageId; QString query = "SELECT id FROM images WHERE path = '%1'"; QStringList res = m_collection->sqlStorage()->query( query.arg( AMAROK_UNSET_MAGIC ) ); // We already have the AMAROK_UNSET_MAGIC variable in the database if( !res.isEmpty() ) { m_unsetImageId = res.first().toInt(); } else { // We need to create this value query = QString( "INSERT INTO images( path ) VALUES ( '%1' )" ) .arg( m_collection->sqlStorage()->escape( AMAROK_UNSET_MAGIC ) ); m_unsetImageId = m_collection->sqlStorage()->insert( query, "images" ); } return m_unsetImageId; } bool SqlAlbum::isCompilation() const { return !hasAlbumArtist(); } bool SqlAlbum::hasAlbumArtist() const { return !albumArtist().isNull(); } Meta::ArtistPtr SqlAlbum::albumArtist() const { if( m_artistId > 0 && !m_artist ) { const_cast( this )->m_artist = m_collection->registry()->getArtist( m_artistId ); } return m_artist; } QByteArray SqlAlbum::md5sum( const QString& artist, const QString& album, const QString& file ) const { // FIXME: All existing image stores have been invalidated. return QCryptographicHash::hash( artist.toLower().toUtf8() + QByteArray( "#" ) + album.toLower().toUtf8() + QByteArray( "?" ) + file.toUtf8(), QCryptographicHash::Md5 ).toHex(); } QString SqlAlbum::largeDiskCachePath() const { // IMPROVEMENT: the large disk cache path could be human readable const QString artist = hasAlbumArtist() ? albumArtist()->name() : QString(); if( artist.isEmpty() && m_name.isEmpty() ) return QString(); QDir largeCoverDir( Amarok::saveLocation( "albumcovers/large/" ) ); const QString key = md5sum( artist, m_name, QString() ); if( !key.isEmpty() ) return largeCoverDir.filePath( key ); return QString(); } QString SqlAlbum::scaledDiskCachePath( int size ) const { const QByteArray widthKey = QByteArray::number( size ) + '@'; QDir cacheCoverDir( Amarok::saveLocation( "albumcovers/cache/" ) ); QString key = md5sum( QString(), QString(), m_imagePath ); if( !cacheCoverDir.exists( widthKey + key ) ) { // the correct location is empty // check deprecated locations for the image cache and delete them // (deleting the scaled image cache is fine) const QString artist = hasAlbumArtist() ? albumArtist()->name() : QString(); if( artist.isEmpty() && m_name.isEmpty() ) ; // do nothing special else { QString oldKey; oldKey = md5sum( artist, m_name, m_imagePath ); if( cacheCoverDir.exists( widthKey + oldKey ) ) cacheCoverDir.remove( widthKey + oldKey ); oldKey = md5sum( artist, m_name, QString() ); if( cacheCoverDir.exists( widthKey + oldKey ) ) cacheCoverDir.remove( widthKey + oldKey ); } } return cacheCoverDir.filePath( widthKey + key ); } QString SqlAlbum::largeImagePath() { if( !m_collection ) return m_imagePath; // Look up in the database QString query = "SELECT images.id, images.path FROM images, albums WHERE albums.image = images.id AND albums.id = %1;"; // TODO: shouldn't we do a JOIN here? QStringList res = m_collection->sqlStorage()->query( query.arg( m_id ) ); if( !res.isEmpty() ) { m_imageId = res.at(0).toInt(); m_imagePath = res.at(1); // explicitly deleted image if( m_imagePath == AMAROK_UNSET_MAGIC ) return AMAROK_UNSET_MAGIC; // embedded image (e.g. id3v2 APIC // We store embedded images as unique ids in the database // we will get the real image later on from the track. if( m_imagePath.startsWith( m_collection->uidUrlProtocol()+"://" ) ) return m_imagePath; // normal file if( !m_imagePath.isEmpty() && QFile::exists( m_imagePath ) ) return m_imagePath; } // After a rescan we currently lose all image information, so we need // to check that we haven't already downloaded this image before. m_imagePath = largeDiskCachePath(); if( !m_imagePath.isEmpty() && QFile::exists( m_imagePath ) ) { setImage(m_imagePath); return m_imagePath; } m_imageId = -1; m_imagePath.clear(); return m_imagePath; } // note: we won't notify the observers. we are a private function. the caller must do that. void SqlAlbum::setImage( const QString &path ) { if( m_imagePath == path ) return; if( m_name.isEmpty() ) // the empty album never has an image return; QMutexLocker locker( &m_mutex ); QString imagePath = path; QString query = "SELECT id FROM images WHERE path = '%1'"; query = query.arg( m_collection->sqlStorage()->escape( imagePath ) ); QStringList res = m_collection->sqlStorage()->query( query ); if( res.isEmpty() ) { QString insert = QString( "INSERT INTO images( path ) VALUES ( '%1' )" ) .arg( m_collection->sqlStorage()->escape( imagePath ) ); m_imageId = m_collection->sqlStorage()->insert( insert, "images" ); } else m_imageId = res.first().toInt(); if( m_imageId >= 0 ) { query = QString("UPDATE albums SET image = %1 WHERE albums.id = %2" ) .arg( QString::number( m_imageId ), QString::number( m_id ) ); m_collection->sqlStorage()->query( query ); m_imagePath = imagePath; m_hasImage = true; m_hasImageChecked = true; CoverCache::invalidateAlbum( this ); } } /** Set the compilation flag. * Actually it does not cange this album but instead moves * the tracks to other albums (e.g. one with the same name which is a * compilation) * If the compilation flag is set to "false" then all songs * with different artists will be moved to other albums, possibly even * creating them. */ void SqlAlbum::setCompilation( bool compilation ) { if( m_name.isEmpty() ) return; if( isCompilation() == compilation ) { return; } else { m_collection->blockUpdatedSignal(); if( compilation ) { // get the new compilation album Meta::AlbumPtr metaAlbum = m_collection->registry()->getAlbum( name(), QString() ); AmarokSharedPointer sqlAlbum = AmarokSharedPointer::dynamicCast( metaAlbum ); Meta::FieldHash changes; changes.insert( Meta::valCompilation, 1); Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { SqlTrack* sqlTrack = static_cast(metaTrack.data()); // copy over the cover image if( sqlTrack->album()->hasImage() && !sqlAlbum->hasImage() ) sqlAlbum->setImage( sqlTrack->album()->imageLocation().path() ); // move the track sqlTrack->setAlbum( sqlAlbum->id() ); if( AmarokConfig::writeBack() ) Meta::Tag::writeTags( sqlTrack->playableUrl().path(), changes, AmarokConfig::writeBackStatistics() ); } /* TODO: delete all old tracks albums */ } else { Meta::FieldHash changes; changes.insert( Meta::valCompilation, 0); Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { SqlTrack* sqlTrack = static_cast(metaTrack.data()); Meta::ArtistPtr trackArtist = sqlTrack->artist(); // get the new album Meta::AlbumPtr metaAlbum = m_collection->registry()->getAlbum( sqlTrack->album()->name(), trackArtist ? ArtistHelper::realTrackArtist( trackArtist->name() ) : QString() ); AmarokSharedPointer sqlAlbum = AmarokSharedPointer::dynamicCast( metaAlbum ); // copy over the cover image if( sqlTrack->album()->hasImage() && !sqlAlbum->hasImage() ) sqlAlbum->setImage( sqlTrack->album()->imageLocation().path() ); // move the track sqlTrack->setAlbum( sqlAlbum->id() ); if( AmarokConfig::writeBack() ) Meta::Tag::writeTags( sqlTrack->playableUrl().path(), changes, AmarokConfig::writeBackStatistics() ); } /* TODO //step 5: delete the original album, if necessary */ } m_collection->unblockUpdatedSignal(); } } bool SqlAlbum::hasCapabilityInterface( Capabilities::Capability::Type type ) const { if( m_name.isEmpty() ) return false; switch( type ) { case Capabilities::Capability::Actions: case Capabilities::Capability::BookmarkThis: return true; default: return Album::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlAlbum::createCapabilityInterface( Capabilities::Capability::Type type ) { if( m_name.isEmpty() ) return 0; switch( type ) { case Capabilities::Capability::Actions: return new Capabilities::AlbumActionsCapability( Meta::AlbumPtr( this ) ); case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkAlbumAction( 0, Meta::AlbumPtr( this ) ) ); default: return Album::createCapabilityInterface( type ); } } //---------------SqlComposer--------------------------------- SqlComposer::SqlComposer( Collections::SqlCollection *collection, int id, const QString &name ) : Composer() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlComposer::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlComposer::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::ComposerPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlGenre--------------------------------- SqlGenre::SqlGenre( Collections::SqlCollection *collection, int id, const QString &name ) : Genre() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlGenre::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlGenre::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::GenrePtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlYear--------------------------------- SqlYear::SqlYear( Collections::SqlCollection *collection, int id, int year) : Year() , m_collection( collection ) , m_id( id ) , m_year( year ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlYear::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlYear::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::YearPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlLabel--------------------------------- SqlLabel::SqlLabel( Collections::SqlCollection *collection, int id, const QString &name ) : Label() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlLabel::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlLabel::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::LabelPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } diff --git a/src/core-impl/collections/db/sql/SqlScanResultProcessor.cpp b/src/core-impl/collections/db/sql/SqlScanResultProcessor.cpp index ab1ee38575..fc72394087 100644 --- a/src/core-impl/collections/db/sql/SqlScanResultProcessor.cpp +++ b/src/core-impl/collections/db/sql/SqlScanResultProcessor.cpp @@ -1,728 +1,737 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Seb Ruiz * * Copyright (c) 2009-2010 Jeff Mitchell * * Copyright (c) 2013 Ralf Engels * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SqlScanResultProcessor" #include "SqlScanResultProcessor.h" #include "MainWindow.h" #include "collectionscanner/Directory.h" #include "collectionscanner/Album.h" #include "collectionscanner/Track.h" #include "collectionscanner/Playlist.h" #include "core/support/Debug.h" #include "core-impl/collections/db/MountPointManager.h" #include "core-impl/collections/db/sql/SqlQueryMaker.h" #include "playlistmanager/PlaylistManager.h" #include +#include #include SqlScanResultProcessor::SqlScanResultProcessor( GenericScanManager* manager, Collections::SqlCollection* collection, QObject *parent ) : AbstractScanResultProcessor( manager, parent ) , m_collection( collection ) { } SqlScanResultProcessor::~SqlScanResultProcessor() { } void SqlScanResultProcessor::scanStarted( GenericScanManager::ScanType type ) { AbstractScanResultProcessor::scanStarted( type ); m_collection->sqlStorage()->clearLastErrors(); m_messages.clear(); } void SqlScanResultProcessor::scanSucceeded() { DEBUG_BLOCK; // we are blocking the updated signal for maximum of one second. m_blockedTime = QDateTime::currentDateTime(); blockUpdates(); urlsCacheInit(); // -- call the base implementation AbstractScanResultProcessor::scanSucceeded(); // -- error reporting m_messages.append( m_collection->sqlStorage()->getLastErrors() ); if( !m_messages.isEmpty() && qobject_cast(qApp) ) QTimer::singleShot(0, this, &SqlScanResultProcessor::displayMessages); // do in the UI thread unblockUpdates(); } void SqlScanResultProcessor::displayMessages() { QString errorList = m_messages.join( "
  • " ).replace( '\n', "
    " ); QString text = i18n( "
    • %1
    " "In most cases this means that not all of your tracks were imported.
    " "See " "Amarok Manual for information about duplicate tracks.", errorList ); KMessageBox::error( The::mainWindow(), text, i18n( "Errors During Collection Scan" ), KMessageBox::AllowLink ); m_messages.clear(); } void SqlScanResultProcessor::blockUpdates() { + DEBUG_BLOCK + m_collection->blockUpdatedSignal(); m_collection->registry()->blockDatabaseUpdate(); } void SqlScanResultProcessor::unblockUpdates() { + DEBUG_BLOCK + m_collection->registry()->unblockDatabaseUpdate(); m_collection->unblockUpdatedSignal(); } void SqlScanResultProcessor::message( const QString& message ) { m_messages.append( message ); } void SqlScanResultProcessor::commitDirectory( QSharedPointer directory ) { QString path = directory->path(); // a bit of paranoia: if( m_foundDirectories.contains( path ) ) warning() << "commitDirectory(): duplicate directory path" << path << "in" << "collectionscanner output. This shouldn't happen."; // getDirectory() updates the directory entry mtime: int dirId = m_collection->registry()->getDirectory( path, directory->mtime() ); // we never dereference key of m_directoryIds, it is safe to add it as a plain pointer m_directoryIds.insert( directory.data(), dirId ); m_foundDirectories.insert( path, dirId ); AbstractScanResultProcessor::commitDirectory( directory ); // --- unblock every 5 second. Maybe not really needed, but still nice if( m_blockedTime.secsTo( QDateTime::currentDateTime() ) >= 5 ) { unblockUpdates(); m_blockedTime = QDateTime::currentDateTime(); blockUpdates(); } } void SqlScanResultProcessor::commitAlbum( CollectionScanner::Album *album ) { debug() << "commitAlbum on"<name()<< "artist"<artist(); // --- get or create the album Meta::SqlAlbumPtr metaAlbum; metaAlbum = Meta::SqlAlbumPtr::staticCast( m_collection->getAlbum( album->name(), album->artist() ) ); if( !metaAlbum ) return; m_albumIds.insert( album, metaAlbum->id() ); // --- add all tracks foreach( CollectionScanner::Track *track, album->tracks() ) commitTrack( track, album ); // --- set the cover if we have one // we need to do this after the tracks are added in case of an embedded cover bool suppressAutoFetch = metaAlbum->suppressImageAutoFetch(); metaAlbum->setSuppressImageAutoFetch( true ); if( m_type == GenericScanManager::FullScan ) { if( !album->cover().isEmpty() ) { metaAlbum->removeImage(); metaAlbum->setImage( album->cover() ); } } else { if( !metaAlbum->hasImage() && !album->cover().isEmpty() ) metaAlbum->setImage( album->cover() ); } metaAlbum->setSuppressImageAutoFetch( suppressAutoFetch ); } void SqlScanResultProcessor::commitTrack( CollectionScanner::Track *track, CollectionScanner::Album *srcAlbum ) { // debug() << "commitTrack on"<title()<< "album"<name() << "dir:" << track->directory()->path()<directory(); Q_ASSERT( track ); Q_ASSERT( srcAlbum ); Q_ASSERT( m_directoryIds.contains( track->directory() ) ); int directoryId = m_directoryIds.value( track->directory() ); Q_ASSERT( m_albumIds.contains( srcAlbum ) ); int albumId = m_albumIds.value( srcAlbum ); QString uid = track->uniqueid(); if( uid.isEmpty() ) { warning() << "commitTrack(): got track with empty unique id from the scanner," << "not adding it"; m_messages.append( QString( "Not adding track %1 because it has no unique id." ). arg(track->path()) ); return; } uid = m_collection->generateUidUrl( uid ); int deviceId = m_collection->mountPointManager()->getIdForUrl( QUrl::fromUserInput(track->path()) ); QString rpath = m_collection->mountPointManager()->getRelativePath( deviceId, track->path() ); if( m_foundTracks.contains( uid ) ) { const UrlEntry old = m_urlsCache.value( m_uidCache.value( uid ) ); const char *pattern = I18N_NOOP( "Duplicates found, the second file will be ignored:\n%1\n%2" ); // we want translated version for GUI and non-translated for debug log warning() << "commitTrack():" << QString( pattern ).arg( old.path, track->path() ); m_messages.append( i18n( pattern, old.path, track->path() ) ); return; } Meta::SqlTrackPtr metaTrack; UrlEntry entry; // find an existing track by uid if( m_uidCache.contains( uid ) ) { // uid is sadly not unique. Try to find the best url id. int urlId = findBestUrlId( uid, track->path() ); Q_ASSERT( urlId > 0 ); Q_ASSERT( m_urlsCache.contains( urlId ) ); entry = m_urlsCache.value( urlId ); entry.path = track->path(); entry.directoryId = directoryId; metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->registry()->getTrack( urlId ) ); Q_ASSERT( metaTrack->urlId() == entry.id ); } // find an existing track by path else if( m_pathCache.contains( track->path() ) ) { int urlId = m_pathCache.value( track->path() ); Q_ASSERT( m_urlsCache.contains( urlId ) ); entry = m_urlsCache.value( urlId ); entry.uid = uid; entry.directoryId = directoryId; metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->registry()->getTrack( urlId ) ); Q_ASSERT( metaTrack->urlId() == entry.id ); } // create a new one else { static int autoDecrementId = -1; entry.id = autoDecrementId--; entry.path = track->path(); entry.uid = uid; entry.directoryId = directoryId; metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->getTrack( deviceId, rpath, directoryId, uid ) ); } if( !metaTrack ) { QString text = QString( "Something went wrong when importing track %1, metaTrack " "is null while it shouldn't be." ).arg( track->path() ); warning() << "commitTrack():" << text.toLocal8Bit().data(); m_messages.append( text ); return; } urlsCacheInsert( entry ); // removes the previous entry (by id) first if necessary m_foundTracks.insert( uid, entry.id ); // TODO: we need to check the modified date of the file agains the last updated of the file // to figure out if the track information was updated from outside Amarok. // In such a case we would fully reread all the information as if in a FullScan // -- set the values metaTrack->setWriteFile( false ); // no need to write the tags back metaTrack->beginUpdate(); metaTrack->setUidUrl( uid ); metaTrack->setUrl( deviceId, rpath, directoryId ); if( m_type == GenericScanManager::FullScan || !track->title().isEmpty() ) metaTrack->setTitle( track->title() ); if( m_type == GenericScanManager::FullScan || albumId != -1 ) metaTrack->setAlbum( albumId ); if( m_type == GenericScanManager::FullScan || !track->artist().isEmpty() ) metaTrack->setArtist( track->artist() ); if( m_type == GenericScanManager::FullScan || !track->composer().isEmpty() ) metaTrack->setComposer( track->composer() ); if( m_type == GenericScanManager::FullScan || track->year() >= 0 ) metaTrack->setYear( (track->year() >= 0) ? track->year() : 0 ); if( m_type == GenericScanManager::FullScan || !track->genre().isEmpty() ) metaTrack->setGenre( track->genre() ); metaTrack->setType( track->filetype() ); if( m_type == GenericScanManager::FullScan || track->bpm() >= 0 ) metaTrack->setBpm( track->bpm() ); if( m_type == GenericScanManager::FullScan || !track->comment().isEmpty() ) metaTrack->setComment( track->comment() ); if( (m_type == GenericScanManager::FullScan || metaTrack->score() == 0) && track->score() >= 0 ) metaTrack->setScore( track->score() ); if( (m_type == GenericScanManager::FullScan || metaTrack->rating() == 0.0) && track->rating() >= 0 ) metaTrack->setRating( track->rating() ); if( (m_type == GenericScanManager::FullScan || metaTrack->length() == 0) && track->length() >= 0 ) metaTrack->setLength( track->length() ); // the filesize is updated every time after the // file is changed. Doesn't make sense to set it. if( (m_type == GenericScanManager::FullScan || !metaTrack->modifyDate().isValid()) && track->modified().isValid() ) metaTrack->setModifyDate( track->modified() ); if( (m_type == GenericScanManager::FullScan || metaTrack->sampleRate() == 0) && track->samplerate() >= 0 ) metaTrack->setSampleRate( track->samplerate() ); if( (m_type == GenericScanManager::FullScan || metaTrack->bitrate() == 0) && track->bitrate() >= 0 ) metaTrack->setBitrate( track->bitrate() ); if( (m_type == GenericScanManager::FullScan || metaTrack->trackNumber() == 0) && track->track() >= 0 ) metaTrack->setTrackNumber( track->track() ); if( (m_type == GenericScanManager::FullScan || metaTrack->discNumber() == 0) && track->disc() >= 0 ) metaTrack->setDiscNumber( track->disc() ); if( m_type == GenericScanManager::FullScan && track->playcount() >= metaTrack->playCount() ) metaTrack->setPlayCount( track->playcount() ); Meta::ReplayGainTag modes[] = { Meta::ReplayGain_Track_Gain, Meta::ReplayGain_Track_Peak, Meta::ReplayGain_Album_Gain, Meta::ReplayGain_Album_Peak }; for( int i=0; i<4; i++ ) { if( track->replayGain( modes[i] ) != 0.0 ) metaTrack->setReplayGain( modes[i], track->replayGain( modes[i] ) ); } metaTrack->endUpdate(); metaTrack->setWriteFile( true ); } void SqlScanResultProcessor::deleteDeletedDirectories() { auto storage = m_collection->sqlStorage(); QList toCheck; switch( m_type ) { case GenericScanManager::FullScan: case GenericScanManager::UpdateScan: toCheck = mountedDirectories(); break; case GenericScanManager::PartialUpdateScan: toCheck = deletedDirectories(); } // -- check if the have been found during the scan foreach( const DirectoryEntry &e, toCheck ) { /* we need to match directories by their (absolute) path, otherwise following * scenario triggers statistics loss (bug 298275): * * 1. user relocates collection to different filesystem, but clones path structure * or toggles MassStorageDeviceHandler enabled in Config -> plugins. * 2. collectionscanner knows nothings about directory ids, so it doesn't detect * any track changes and emits a bunch of skipped (unchanged) dirs with no * tracks. * 3. SqlRegistry::getDirectory() called there from returns different directory id * then in past. * 4. deleteDeletedDirectories() is called, and if it operates on directory ids, * it happily removes _all_ directories, taking tracks with it. * 5. Tracks disappear from the UI until full rescan, stats, lyrics, labels are * lost forever. */ QString path = m_collection->mountPointManager()->getAbsolutePath( e.deviceId, e.dir ); bool deleteThisDir = false; if( !m_foundDirectories.contains( path ) ) deleteThisDir = true; else if( m_foundDirectories.value( path ) != e.dirId ) { int newDirId = m_foundDirectories.value( path ); // as a safety measure, we don't delete the old dir if relocation fails deleteThisDir = relocateTracksToNewDirectory( e.dirId, newDirId ); } if( deleteThisDir ) { deleteDeletedTracks( e.dirId ); QString query = QString( "DELETE FROM directories WHERE id = %1;" ).arg( e.dirId ); storage->query( query ); } } } void SqlScanResultProcessor::deleteDeletedTracksAndSubdirs( QSharedPointer directory ) { Q_ASSERT( m_directoryIds.contains( directory.data() ) ); int directoryId = m_directoryIds.value( directory.data() ); // only deletes tracks directly in this dir deleteDeletedTracks( directoryId ); // trigger deletion of deleted subdirectories in deleteDeletedDirectories(): m_scannedDirectoryIds.insert( directoryId ); } void SqlScanResultProcessor::deleteDeletedTracks( int directoryId ) { // -- find all tracks QList urlIds = m_directoryCache.values( directoryId ); // -- check if the tracks have been found during the scan foreach( int urlId, urlIds ) { Q_ASSERT( m_urlsCache.contains( urlId ) ); const UrlEntry &entry = m_urlsCache[ urlId ]; Q_ASSERT( entry.directoryId == directoryId ); // we need to match both uid and url id, because uid is not unique if( !m_foundTracks.contains( entry.uid, entry.id ) ) { removeTrack( entry ); urlsCacheRemove( entry ); } } } int SqlScanResultProcessor::findBestUrlId( const QString &uid, const QString &path ) { QList urlIds = m_uidCache.values( uid ); if( urlIds.isEmpty() ) return -1; if( urlIds.size() == 1 ) return urlIds.at( 0 ); // normal operation foreach( int testedUrlId, urlIds ) { Q_ASSERT( m_urlsCache.contains( testedUrlId ) ); if( m_urlsCache[ testedUrlId ].path == path ) return testedUrlId; } warning() << "multiple url entries with uid" << uid << "found in the database, but" << "none with current path" << path << "Choosing blindly the last one out" << "of url id candidates" << urlIds; return urlIds.last(); } bool SqlScanResultProcessor::relocateTracksToNewDirectory( int oldDirId, int newDirId ) { QList urlIds = m_directoryCache.values( oldDirId ); if( urlIds.isEmpty() ) return true; // nothing to do MountPointManager *manager = m_collection->mountPointManager(); SqlRegistry *reg = m_collection->registry(); auto storage = m_collection->sqlStorage(); // sanity checking, not strictly needed, but imagine new device appearing in the // middle of the scan, so rather prevent db corruption: QStringList res = storage->query( QString( "SELECT deviceid FROM directories " "WHERE id = %1" ).arg( newDirId ) ); if( res.count() != 1 ) { warning() << "relocateTracksToNewDirectory(): no or multiple entries when" << "querying directory with id" << newDirId; return false; } int newDirDeviceId = res.at( 0 ).toInt(); foreach( int urlId, urlIds ) { Q_ASSERT( m_urlsCache.contains( urlId ) ); UrlEntry entry = m_urlsCache.value( urlId ); Meta::SqlTrackPtr track = Meta::SqlTrackPtr::staticCast( reg->getTrack( urlId ) ); Q_ASSERT( track ); // not strictly needed, but we want to sanity check it to prevent corrupt db int deviceId = manager->getIdForUrl( QUrl::fromUserInput(entry.path) ); if( newDirDeviceId != deviceId ) { warning() << "relocateTracksToNewDirectory(): device id from newDirId (" << res.at( 0 ).toInt() << ") and device id from mountPointManager (" << deviceId << ") don't match!"; return false; } QString rpath = manager->getRelativePath( deviceId, entry.path ); track->setUrl( deviceId, rpath, newDirId ); entry.directoryId = newDirId; urlsCacheInsert( entry ); // removes the previous entry (by id) first } return true; } void SqlScanResultProcessor::removeTrack( const UrlEntry &entry ) { debug() << "removeTrack(" << entry << ")"; // we used to skip track removal is m_messages wasn't empty, but that lead to to // tracks left laying around. We now hope that the result processor got better and // only removes tracks that should be removed. SqlRegistry *reg = m_collection->registry(); // we must get the track by id, uid is not unique Meta::SqlTrackPtr track = Meta::SqlTrackPtr::staticCast( reg->getTrack( entry.id ) ); Q_ASSERT( track->urlId() == entry.id ); track->remove(); } QList SqlScanResultProcessor::mountedDirectories() const { auto storage = m_collection->sqlStorage(); // -- get a list of all mounted device ids QList idList = m_collection->mountPointManager()->getMountedDeviceIds(); QString deviceIds; foreach( int id, idList ) { if ( !deviceIds.isEmpty() ) deviceIds += ','; deviceIds += QString::number( id ); } // -- get all (mounted) directories QString query = QString( "SELECT id, deviceid, dir FROM directories " "WHERE deviceid IN (%1)" ).arg( deviceIds ); QStringList res = storage->query( query ); QList result; for( int i = 0; i < res.count(); ) { DirectoryEntry e; e.dirId = res.at( i++ ).toInt(); e.deviceId = res.at( i++ ).toInt(); e.dir = res.at( i++ ); result << e; } return result; } QList SqlScanResultProcessor::deletedDirectories() const { auto storage = m_collection->sqlStorage(); QHash idToDirEntryMap; // for faster processing during filtering foreach( int directoryId, m_scannedDirectoryIds ) { QString query = QString( "SELECT deviceid, dir FROM directories WHERE id = %1" ) .arg( directoryId ); QStringList res = storage->query( query ); if( res.count() != 2 ) { warning() << "unexpected query result" << res << "in deletedDirectories()"; continue; } int deviceId = res.at( 0 ).toInt(); QString dir = res.at( 1 ); // select all child directories query = QString( "SELECT id, deviceid, dir FROM directories WHERE deviceid = %1 " "AND dir LIKE '%2_%'" ).arg( deviceId ).arg( storage->escape( dir ) ); res = storage->query( query ); for( int i = 0; i < res.count(); ) { DirectoryEntry e; e.dirId = res.at( i++ ).toInt(); e.deviceId = res.at( i++ ).toInt(); e.dir = res.at( i++ ); idToDirEntryMap.insert( e.dirId, e ); } } // now we must fileter-out all found directories *and their children*, because the // children are *not* in m_foundDirectories and deleteDeletedDirectories() would // remove them errorneously foreach( int foundDirectoryId, m_foundDirectories ) { if( idToDirEntryMap.contains( foundDirectoryId ) ) { int existingDeviceId = idToDirEntryMap[ foundDirectoryId ].deviceId; QString existingPath = idToDirEntryMap[ foundDirectoryId ].dir; idToDirEntryMap.remove( foundDirectoryId ); // now remove all children of the existing directory QMutableHashIterator it( idToDirEntryMap ); while( it.hasNext() ) { const DirectoryEntry &e = it.next().value(); if( e.deviceId == existingDeviceId && e.dir.startsWith( existingPath ) ) it.remove(); } } } return idToDirEntryMap.values(); } void SqlScanResultProcessor::urlsCacheInit() { + DEBUG_BLOCK + auto storage = m_collection->sqlStorage(); QString query = QString( "SELECT id, deviceid, rpath, directory, uniqueid FROM urls;"); QStringList res = storage->query( query ); for( int i = 0; i < res.count(); ) { int id = res.at(i++).toInt(); int deviceId = res.at(i++).toInt(); QString rpath = res.at(i++); int directoryId = res.at(i++).toInt(); QString uid = res.at(i++); QString path; if( deviceId ) path = m_collection->mountPointManager()->getAbsolutePath( deviceId, rpath ); else path = rpath; UrlEntry entry; entry.id = id; entry.path = path; entry.directoryId = directoryId; entry.uid = uid; if( !directoryId ) { warning() << "Found urls entry without directory. A phantom track. Removing" << path; removeTrack( entry ); continue; } urlsCacheInsert( entry ); + + QAbstractEventDispatcher::instance()->processEvents( QEventLoop::AllEvents ); } } void SqlScanResultProcessor::urlsCacheInsert( const UrlEntry &entry ) { // this case is normal operation if( m_urlsCache.contains( entry.id ) ) urlsCacheRemove( m_urlsCache[ entry.id ] ); // following shoudn't normally happen: if( m_pathCache.contains( entry.path ) ) { int oldId = m_pathCache.value( entry.path ); Q_ASSERT( m_urlsCache.contains( oldId ) ); const UrlEntry &old = m_urlsCache[ oldId ]; warning() << "urlsCacheInsert(): found duplicate in path. old" << old << "will be hidden by the new one in the cache:" << entry; } // this will signify error in this class: Q_ASSERT( !m_uidCache.contains( entry.uid, entry.id ) ); Q_ASSERT( !m_directoryCache.contains( entry.directoryId, entry.id ) ); m_urlsCache.insert( entry.id, entry ); m_uidCache.insert( entry.uid, entry.id ); m_pathCache.insert( entry.path, entry.id ); m_directoryCache.insert( entry.directoryId, entry.id ); } void SqlScanResultProcessor::cleanupMembers() { m_foundDirectories.clear(); m_foundTracks.clear(); m_scannedDirectoryIds.clear(); m_directoryIds.clear(); m_albumIds.clear(); m_urlsCache.clear(); m_uidCache.clear(); m_pathCache.clear(); m_directoryCache.clear(); AbstractScanResultProcessor::cleanupMembers(); } void SqlScanResultProcessor::urlsCacheRemove( const UrlEntry &entry ) { if( !m_urlsCache.contains( entry.id ) ) return; m_uidCache.remove( entry.uid, entry.id ); m_pathCache.remove( entry.path ); m_directoryCache.remove( entry.directoryId, entry.id ); m_urlsCache.remove( entry.id ); } QDebug operator<<( QDebug dbg, const SqlScanResultProcessor::UrlEntry &entry ) { dbg.nospace() << "Entry(id=" << entry.id << ", path=" << entry.path << ", dirId=" << entry.directoryId << ", uid=" << entry.uid << ")"; return dbg.space(); } diff --git a/src/core/meta/support/MetaUtility.cpp b/src/core/meta/support/MetaUtility.cpp index 66fc0111fc..feef7dbf45 100644 --- a/src/core/meta/support/MetaUtility.cpp +++ b/src/core/meta/support/MetaUtility.cpp @@ -1,466 +1,471 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "core/meta/support/MetaUtility.h" #include "core/capabilities/Capability.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/meta/TrackEditor.h" #include "core/support/Debug.h" #include #include #include #include #include static const QString XESAM_ALBUM = "http://freedesktop.org/standards/xesam/1.0/core#album"; static const QString XESAM_ALBUMARTIST = "http://freedesktop.org/standards/xesam/1.0/core#albumArtist"; static const QString XESAM_ARTIST = "http://freedesktop.org/standards/xesam/1.0/core#artist"; static const QString XESAM_BITRATE = "http://freedesktop.org/standards/xesam/1.0/core#audioBitrate"; static const QString XESAM_BPM = "http://freedesktop.org/standards/xesam/1.0/core#audioBPM"; static const QString XESAM_CODEC = "http://freedesktop.org/standards/xesam/1.0/core#audioCodec"; static const QString XESAM_COMMENT = "http://freedesktop.org/standards/xesam/1.0/core#comment"; static const QString XESAM_COMPOSER = "http://freedesktop.org/standards/xesam/1.0/core#composer"; static const QString XESAM_DISCNUMBER = "http://freedesktop.org/standards/xesam/1.0/core#discNumber"; static const QString XESAM_FILESIZE = "http://freedesktop.org/standards/xesam/1.0/core#size"; static const QString XESAM_GENRE = "http://freedesktop.org/standards/xesam/1.0/core#genre"; static const QString XESAM_LENGTH = "http://freedesktop.org/standards/xesam/1.0/core#mediaDuration"; static const QString XESAM_RATING = "http://freedesktop.org/standards/xesam/1.0/core#userRating"; static const QString XESAM_SAMPLERATE = "http://freedesktop.org/standards/xesam/1.0/core#audioSampleRate"; static const QString XESAM_TITLE = "http://freedesktop.org/standards/xesam/1.0/core#title"; static const QString XESAM_TRACKNUMBER = "http://freedesktop.org/standards/xesam/1.0/core#trackNumber"; static const QString XESAM_URL = "http://freedesktop.org/standards/xesam/1.0/core#url"; static const QString XESAM_YEAR = "http://freedesktop.org/standards/xesam/1.0/core#contentCreated"; static const QString XESAM_SCORE = "http://freedesktop.org/standards/xesam/1.0/core#autoRating"; static const QString XESAM_PLAYCOUNT = "http://freedesktop.org/standards/xesam/1.0/core#useCount"; static const QString XESAM_FIRST_PLAYED = "http://freedesktop.org/standards/xesam/1.0/core#firstUsed"; static const QString XESAM_LAST_PLAYED = "http://freedesktop.org/standards/xesam/1.0/core#lastUsed"; static const QString XESAM_ID = "http://freedesktop.org/standards/xesam/1.0/core#id"; //static bool conversionMapsInitialised = false; QVariantMap Meta::Field::mapFromTrack( const Meta::TrackPtr track ) { QVariantMap map; if( !track ) return map; if( track->name().isEmpty() ) map.insert( Meta::Field::TITLE, QVariant( track->prettyName() ) ); else map.insert( Meta::Field::TITLE, QVariant( track->name() ) ); if( track->artist() && !track->artist()->name().isEmpty() ) map.insert( Meta::Field::ARTIST, QVariant( track->artist()->name() ) ); if( track->album() && !track->album()->name().isEmpty() ) { map.insert( Meta::Field::ALBUM, QVariant( track->album()->name() ) ); if( track->album()->hasAlbumArtist() && !track->album()->albumArtist()->name().isEmpty() ) map.insert( Meta::Field::ALBUMARTIST, QVariant( track->album()->albumArtist()->name() ) ); } if( track->filesize() ) map.insert( Meta::Field::FILESIZE, QVariant( track->filesize() ) ); if( track->genre() && !track->genre()->name().isEmpty() ) map.insert( Meta::Field::GENRE, QVariant( track->genre()->name() ) ); if( track->composer() && !track->composer()->name().isEmpty() ) map.insert( Meta::Field::COMPOSER, QVariant( track->composer()->name() ) ); if( track->year() && !track->year()->name().isEmpty() ) map.insert( Meta::Field::YEAR, QVariant( track->year()->name() ) ); if( !track->comment().isEmpty() ) map.insert( Meta::Field::COMMENT, QVariant( track->comment() ) ); if( track->trackNumber() ) map.insert( Meta::Field::TRACKNUMBER, QVariant( track->trackNumber() ) ); if( track->discNumber() ) map.insert( Meta::Field::DISCNUMBER, QVariant( track->discNumber() ) ); if( track->bitrate() ) map.insert( Meta::Field::BITRATE, QVariant( track->bitrate() ) ); if( track->length() ) map.insert( Meta::Field::LENGTH, QVariant( track->length() ) ); if( track->sampleRate() ) map.insert( Meta::Field::SAMPLERATE, QVariant( track->sampleRate() ) ); if( track->bpm() >= 0.0) map.insert( Meta::Field::BPM, QVariant( track->bpm() ) ); map.insert( Meta::Field::UNIQUEID, QVariant( track->uidUrl() ) ); map.insert( Meta::Field::URL, QVariant( track->prettyUrl() ) ); Meta::ConstStatisticsPtr statistics = track->statistics(); map.insert( Meta::Field::RATING, QVariant( statistics->rating() ) ); map.insert( Meta::Field::SCORE, QVariant( statistics->score() ) ); map.insert( Meta::Field::PLAYCOUNT, QVariant( statistics->playCount() ) ); map.insert( Meta::Field::LAST_PLAYED, QVariant( statistics->lastPlayed() ) ); map.insert( Meta::Field::FIRST_PLAYED, QVariant( statistics->firstPlayed() ) ); return map; } QVariantMap Meta::Field::mprisMapFromTrack( const Meta::TrackPtr track ) { + DEBUG_BLOCK + QVariantMap map; if( track ) { // MANDATORY: map["location"] = track->playableUrl().url(); // INFORMATIONAL: map["title"] = track->prettyName(); if( track->artist() ) map["artist"] = track->artist()->name(); if( track->album() ) { map["album"] = track->album()->name(); if( track->album()->hasAlbumArtist() && !track->album()->albumArtist()->name().isEmpty() ) map[ "albumartist" ] = track->album()->albumArtist()->name(); - QImage image = track->album()->image(); QUrl url = track->album()->imageLocation(); - if ( url.isValid() && !url.isLocalFile() ) { + if ( url.isValid() && !url.isLocalFile() ) + { // embedded id? Request a version to be put in the cache + QImage image = track->album()->image(); int width = track->album()->image().width(); url = track->album()->imageLocation( width ); debug() << "MPRIS: New location for width" << width << "is" << url; } if ( url.isValid() && url.isLocalFile() ) map["arturl"] = QString::fromLatin1( url.toEncoded() ); } map["tracknumber"] = track->trackNumber(); map["time"] = track->length() / 1000; map["mtime"] = track->length(); if( track->genre() ) map["genre"] = track->genre()->name(); map["comment"] = track->comment(); map["rating"] = track->statistics()->rating()/2; //out of 5, not 10. if( track->year() ) map["year"] = track->year()->name(); //TODO: external service meta info // TECHNICAL: map["audio-bitrate"] = track->bitrate(); map["audio-samplerate"] = track->sampleRate(); //amarok has no video-bitrate // EXTRA Amarok specific const QString lyrics = track->cachedLyrics(); if( !lyrics.isEmpty() ) map["lyrics"] = lyrics; } return map; } QVariantMap Meta::Field::mpris20MapFromTrack( const Meta::TrackPtr track ) { + DEBUG_BLOCK + QVariantMap map; if( track ) { // We do not set mpris::trackid here because it depends on the position // of the track in the playlist map["mpris:length"] = track->length() * 1000; // microseconds // get strong pointers (BR 317980) Meta::AlbumPtr album = track->album(); Meta::ArtistPtr artist = track->artist(); Meta::ComposerPtr composer = track->composer(); Meta::YearPtr year = track->year(); Meta::GenrePtr genre = track->genre(); Meta::ConstStatisticsPtr statistics = track->statistics(); if( album ) { - QImage image = album->image(); QUrl url = album->imageLocation(); debug() << "MPRIS2: Album image location is" << url; if ( url.isValid() && !url.isLocalFile() ) { // embedded id? Request a version to be put in the cache + QImage image = album->image(); int width = album->image().width(); url = album->imageLocation( width ); debug() << "MPRIS2: New location for width" << width << "is" << url; } if ( url.isValid() && url.isLocalFile() ) map["mpris:artUrl"] = QString::fromLatin1( url.toEncoded() ); map["xesam:album"] = album->name(); if ( album->hasAlbumArtist() ) map["xesam:albumArtist"] = QStringList() << album->albumArtist()->name(); } if( artist ) map["xesam:artist"] = QStringList() << artist->name(); const QString lyrics = track->cachedLyrics(); if( !lyrics.isEmpty() ) map["xesam:asText"] = lyrics; if( track->bpm() > 0 ) map["xesam:audioBPM"] = int(track->bpm()); map["xesam:autoRating"] = statistics->score(); map["xesam:comment"] = QStringList() << track->comment(); if( composer ) map["xesam:composer"] = QStringList() << composer->name(); if( year ) map["xesam:contentCreated"] = QDateTime(QDate(year->year(), 1, 1)).toString(Qt::ISODate); if( track->discNumber() ) map["xesam:discNumber"] = track->discNumber(); if( statistics->firstPlayed().isValid() ) map["xesam:firstUsed"] = statistics->firstPlayed().toString(Qt::ISODate); if( genre ) map["xesam:genre"] = QStringList() << genre->name(); if( statistics->lastPlayed().isValid() ) map["xesam:lastUsed"] = statistics->lastPlayed().toString(Qt::ISODate); map["xesam:title"] = track->prettyName(); map["xesam:trackNumber"] = track->trackNumber(); map["xesam:url"] = track->playableUrl().url(); map["xesam:useCount"] = statistics->playCount(); map["xesam:userRating"] = statistics->rating() / 10.; // xesam:userRating is a float } return map; } void Meta::Field::updateTrack( Meta::TrackPtr track, const QVariantMap &metadata ) { if( !track ) return; Meta::TrackEditorPtr ec = track->editor(); if( !ec ) return; ec->beginUpdate(); QString title = metadata.contains( Meta::Field::TITLE ) ? metadata.value( Meta::Field::TITLE ).toString() : QString(); ec->setTitle( title ); QString comment = metadata.contains( Meta::Field::COMMENT ) ? metadata.value( Meta::Field::COMMENT ).toString() : QString(); ec->setComment( comment ); int tracknr = metadata.contains( Meta::Field::TRACKNUMBER ) ? metadata.value( Meta::Field::TRACKNUMBER ).toInt() : 0; ec->setTrackNumber( tracknr ); int discnr = metadata.contains( Meta::Field::DISCNUMBER ) ? metadata.value( Meta::Field::DISCNUMBER ).toInt() : 0; ec->setDiscNumber( discnr ); QString artist = metadata.contains( Meta::Field::ARTIST ) ? metadata.value( Meta::Field::ARTIST ).toString() : QString(); ec->setArtist( artist ); QString album = metadata.contains( Meta::Field::ALBUM ) ? metadata.value( Meta::Field::ALBUM ).toString() : QString(); ec->setAlbum( album ); QString albumArtist = metadata.contains( Meta::Field::ALBUMARTIST ) ? metadata.value( Meta::Field::ALBUMARTIST ).toString() : QString(); QString genre = metadata.contains( Meta::Field::GENRE ) ? metadata.value( Meta::Field::GENRE ).toString() : QString(); ec->setGenre( genre ); QString composer = metadata.contains( Meta::Field::COMPOSER ) ? metadata.value( Meta::Field::COMPOSER ).toString() : QString(); ec->setComposer( composer ); int year = metadata.contains( Meta::Field::YEAR ) ? metadata.value( Meta::Field::YEAR ).toInt() : 0; ec->setYear( year ); ec->endUpdate(); } QString Meta::Field::xesamPrettyToFullFieldName( const QString &name ) { if( name == Meta::Field::ARTIST ) return XESAM_ARTIST; else if( name == Meta::Field::ALBUM ) return XESAM_ALBUM; else if( name == Meta::Field::ALBUMARTIST ) return XESAM_ALBUMARTIST; else if( name == Meta::Field::BITRATE ) return XESAM_BITRATE; else if( name == Meta::Field::BPM ) return XESAM_BPM; else if( name == Meta::Field::CODEC ) return XESAM_CODEC; else if( name == Meta::Field::COMMENT ) return XESAM_COMMENT; else if( name == Meta::Field::COMPOSER ) return XESAM_COMPOSER; else if( name == Meta::Field::DISCNUMBER ) return XESAM_DISCNUMBER; else if( name == Meta::Field::FILESIZE ) return XESAM_FILESIZE; else if( name == Meta::Field::GENRE ) return XESAM_GENRE; else if( name == Meta::Field::LENGTH ) return XESAM_LENGTH; else if( name == Meta::Field::RATING ) return XESAM_RATING; else if( name == Meta::Field::SAMPLERATE ) return XESAM_SAMPLERATE; else if( name == Meta::Field::TITLE ) return XESAM_TITLE; else if( name == Meta::Field::TRACKNUMBER ) return XESAM_TRACKNUMBER; else if( name == Meta::Field::URL ) return XESAM_URL; else if( name == Meta::Field::YEAR ) return XESAM_YEAR; else if( name==Meta::Field::SCORE ) return XESAM_SCORE; else if( name==Meta::Field::PLAYCOUNT ) return XESAM_PLAYCOUNT; else if( name==Meta::Field::FIRST_PLAYED ) return XESAM_FIRST_PLAYED; else if( name==Meta::Field::LAST_PLAYED ) return XESAM_LAST_PLAYED; else if( name==Meta::Field::UNIQUEID ) return XESAM_ID; else return "xesamPrettyToFullName: unknown name " + name; } QString Meta::Field::xesamFullToPrettyFieldName( const QString &name ) { if( name == XESAM_ARTIST ) return Meta::Field::ARTIST; if( name == XESAM_ALBUMARTIST ) return Meta::Field::ALBUMARTIST; else if( name == XESAM_ALBUM ) return Meta::Field::ALBUM; else if( name == XESAM_BITRATE ) return Meta::Field::BITRATE; else if( name == XESAM_BPM ) return Meta::Field::BPM; else if( name == XESAM_CODEC ) return Meta::Field::CODEC; else if( name == XESAM_COMMENT ) return Meta::Field::COMMENT; else if( name == XESAM_COMPOSER ) return Meta::Field::COMPOSER; else if( name == XESAM_DISCNUMBER ) return Meta::Field::DISCNUMBER; else if( name == XESAM_FILESIZE ) return Meta::Field::FILESIZE; else if( name == XESAM_GENRE ) return Meta::Field::GENRE; else if( name == XESAM_LENGTH ) return Meta::Field::LENGTH; else if( name == XESAM_RATING ) return Meta::Field::RATING; else if( name == XESAM_SAMPLERATE ) return Meta::Field::SAMPLERATE; else if( name == XESAM_TITLE ) return Meta::Field::TITLE; else if( name == XESAM_TRACKNUMBER ) return Meta::Field::TRACKNUMBER; else if( name == XESAM_URL ) return Meta::Field::URL; else if( name == XESAM_YEAR ) return Meta::Field::YEAR; else if( name == XESAM_SCORE ) return Meta::Field::SCORE; else if( name == XESAM_PLAYCOUNT ) return Meta::Field::PLAYCOUNT; else if( name == XESAM_FIRST_PLAYED ) return Meta::Field::FIRST_PLAYED; else if( name == XESAM_LAST_PLAYED ) return Meta::Field::LAST_PLAYED; else if( name == XESAM_ID ) return Meta::Field::UNIQUEID; else return "xesamFullToPrettyName: unknown name " + name; } QString Meta::msToPrettyTime( qint64 ms ) { return Meta::secToPrettyTime( ms / 1000 ); } QString Meta::secToPrettyTime( int seconds ) { if( seconds < 60 * 60 ) // one hour return QTime(0, 0, 0).addSecs( seconds ).toString( i18nc("the time format for a time length when the time is below 1 hour see QTime documentation.", "m:ss" ) ); // split days off for manual formatting (QTime doesn't work properly > 1 day, // QDateTime isn't suitable as it thinks it's a date) int days = seconds / 86400; seconds %= 86400; QString reply = ""; if ( days > 0 ) reply += i18ncp("number of days with spacing for the pretty time", "%1 day, ", "%1 days, ", days); reply += QTime(0, 0, 0).addSecs( seconds ).toString( i18nc("the time format for a time length when the time is 1 hour or above see QTime documentation.", "h:mm:ss" ) ); return reply; } QString Meta::secToPrettyTimeLong( int seconds ) { int minutes = seconds / 60; int hours = minutes / 60; int days = hours / 24; int months = days / 30; // a short month int years = months / 12; if( months > 24 || (((months % 12) == 0) && years > 0) ) return i18ncp("number of years for the pretty time", "%1 year", "%1 years", years); if( days > 60 || (((days % 30) == 0) && months > 0) ) return i18ncp("number of months for the pretty time", "%1 month", "%1 months", months); if( hours > 24 || (((hours % 24) == 0) && days > 0) ) return i18ncp("number of days for the pretty time", "%1 day", "%1 days", days); if( minutes > 120 || (((minutes % 60) == 0) && hours > 0) ) return i18ncp("number of hours for the pretty time", "%1 hour", "%1 hours", hours); if( seconds > 120 || (((seconds % 60) == 0) && minutes > 0) ) return i18ncp("number of minutes for the pretty time", "%1 minute", "%1 minutes", hours); return i18ncp("number of seconds for the pretty time", "%1 second", "%1 seconds", hours); } QString Meta::prettyFilesize( quint64 size ) { return KIO::convertSize( size ); } QString Meta::prettyBitrate( int bitrate ) { //the point here is to force sharing of these strings returned from prettyBitrate() static const QString bitrateStore[9] = { "?", "32", "64", "96", "128", "160", "192", "224", "256" }; return (bitrate >=0 && bitrate <= 256 && bitrate % 32 == 0) ? bitrateStore[ bitrate / 32 ] : QString( "%1" ).arg( bitrate ); } diff --git a/src/covermanager/CoverCache.cpp b/src/covermanager/CoverCache.cpp index 62a04ffb3f..6fb45d5d3c 100644 --- a/src/covermanager/CoverCache.cpp +++ b/src/covermanager/CoverCache.cpp @@ -1,143 +1,146 @@ /**************************************************************************************** * Copyright (c) 2011 Ralf Engels * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "covermanager/CoverCache.h" #include "core/meta/Meta.h" #include "core/support/Amarok.h" #include #include #include #include #include #include +#include #include -#include #include +#include + CoverCache* CoverCache::s_instance = 0; CoverCache* CoverCache::instance() { return s_instance ? s_instance : (s_instance = new CoverCache()); } void CoverCache::destroy() { if( s_instance ) { delete s_instance; s_instance = 0; } } CoverCache::CoverCache() { } CoverCache::~CoverCache() { m_lock.lockForWrite(); m_lock.unlock(); } void CoverCache::invalidateAlbum( const Meta::Album* album ) { if( !s_instance ) return; QWriteLocker locker( &s_instance->m_lock ); if( !s_instance->m_keys.contains( album ) ) return; CoverKeys allKeys = s_instance->m_keys.take( album ); foreach( const QPixmapCache::Key &key, allKeys.values() ) { QPixmapCache::remove( key ); } } QPixmap CoverCache::getCover( const Meta::AlbumPtr &album, int size ) const { QPixmap pixmap; if( size > 1 ) // full size covers are not cached { QReadLocker locker( &m_lock ); const CoverKeys &allKeys = m_keys.value( album.data() ); if( !allKeys.isEmpty() ) { QPixmapCache::Key key = allKeys.value( size ); if( key != QPixmapCache::Key() && QPixmapCache::find( key, &pixmap ) ) return pixmap; } } QImage image = album->image( size ); // -- get the null cover if someone really wants to have a pixmap // in this case the album hasCover should have already returned false if( image.isNull() ) { const QDir &cacheCoverDir = QDir( Amarok::saveLocation( "albumcovers/cache/" ) ); if( size <= 1 ) size = 100; const QString &noCoverKey = QString::number( size ) + "@nocover.png"; QPixmap pixmap; // look in the memory pixmap cache if( QPixmapCache::find( noCoverKey, &pixmap ) ) return pixmap; if( cacheCoverDir.exists( noCoverKey ) ) { pixmap.load( cacheCoverDir.filePath( noCoverKey ) ); } else { const QPixmap orgPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/nocover.png" ) ); pixmap = orgPixmap.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); - pixmap.save( cacheCoverDir.filePath( noCoverKey ), "PNG" ); + std::thread thread( QOverload::of( &QPixmap::save ), pixmap, cacheCoverDir.filePath( noCoverKey ), "PNG", -1 ); + thread.detach(); } QPixmapCache::insert( noCoverKey, pixmap ); return pixmap; } pixmap = QPixmap::fromImage( image ); // -- add the cover to the cache if not full-scale or too big if( size > 1 && size < 1000 ) { // sadly I can't relock for write QWriteLocker locker( &m_lock ); QPixmapCache::Key key = QPixmapCache::insert( pixmap ); m_keys[ album.data() ][ size ] = key; } return pixmap; } CoverCache* The::coverCache() { return CoverCache::instance(); } diff --git a/src/services/ServiceAlbumCoverDownloader.cpp b/src/services/ServiceAlbumCoverDownloader.cpp index 04fae8bd39..31be93ce98 100644 --- a/src/services/ServiceAlbumCoverDownloader.cpp +++ b/src/services/ServiceAlbumCoverDownloader.cpp @@ -1,200 +1,201 @@ /**************************************************************************************** * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2007 Casey Link * * * * 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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "ServiceAlbumCoverDownloader" #include "ServiceAlbumCoverDownloader.h" #include "core/support/Amarok.h" #include "amarokconfig.h" #include "core/support/Debug.h" #include "covermanager/CoverCache.h" #include #include using namespace Meta; Meta::ServiceAlbumWithCover::ServiceAlbumWithCover( const QString &name ) : ServiceAlbum( name ) , m_hasFetchedCover( false ) , m_isFetchingCover ( false ) {} Meta::ServiceAlbumWithCover::ServiceAlbumWithCover( const QStringList &resultRow ) : ServiceAlbum( resultRow ) , m_hasFetchedCover( false ) , m_isFetchingCover ( false ) {} Meta::ServiceAlbumWithCover::~ServiceAlbumWithCover() { CoverCache::invalidateAlbum( this ); } QImage ServiceAlbumWithCover::image( int size ) const { if( size > 1000 ) { debug() << "Giant image detected, are you sure you want this?"; return Meta::Album::image( size ); } const QString artist = hasAlbumArtist() ? albumArtist()->name() : QLatin1String("NULL"); //no need to translate, only used as a caching key/temp filename const QString coverName = QString( "%1_%2_%3_cover.png" ).arg( downloadPrefix(), artist, name() ); const QString saveLocation = Amarok::saveLocation( "albumcovers/cache/" ); const QDir cacheCoverDir = QDir( saveLocation ); //make sure that this dir exists if( !cacheCoverDir.exists() ) cacheCoverDir.mkpath( saveLocation ); if( size <= 1 ) size = 100; const QString sizeKey = QString::number( size ) + QLatin1Char('@'); const QString cacheCoverPath = cacheCoverDir.filePath( sizeKey + coverName ); if( QFile::exists( cacheCoverPath ) ) { return QImage( cacheCoverPath ); } else if( m_hasFetchedCover && !m_cover.isNull() ) { QImage image( m_cover.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ) ); - image.save( cacheCoverPath, "PNG" ); + std::thread thread( QOverload::of( &QImage::save ), image, cacheCoverPath, "PNG", -1 ); + thread.detach(); return image; } else if( !m_isFetchingCover && !coverUrl().isEmpty() ) { m_isFetchingCover = true; ( new ServiceAlbumCoverDownloader )->downloadCover( ServiceAlbumWithCoverPtr(const_cast(this)) ); } return Meta::Album::image( size ); } void ServiceAlbumWithCover::setImage( const QImage& image ) { m_cover = image; m_hasFetchedCover = true; m_isFetchingCover = false; CoverCache::invalidateAlbum( this ); notifyObservers(); } void ServiceAlbumWithCover::imageDownloadCanceled() const { m_hasFetchedCover = true; m_isFetchingCover = false; } /////////////////////////////////////////////////////////////////////////////// // Class ServiceAlbumCoverDownloader /////////////////////////////////////////////////////////////////////////////// ServiceAlbumCoverDownloader::ServiceAlbumCoverDownloader() : m_albumDownloadJob( ) { m_tempDir = new QTemporaryDir(); m_tempDir->setAutoRemove( true ); } ServiceAlbumCoverDownloader::~ServiceAlbumCoverDownloader() { delete m_tempDir; } void ServiceAlbumCoverDownloader::downloadCover( ServiceAlbumWithCoverPtr album ) { m_album = album; QUrl downloadUrl( album->coverUrl() ); m_coverDownloadPath = m_tempDir->path() + '/' + downloadUrl.fileName(); debug() << "Download Cover: " << downloadUrl.url() << " to: " << m_coverDownloadPath; m_albumDownloadJob = KIO::file_copy( downloadUrl, QUrl::fromLocalFile( m_coverDownloadPath ), -1, KIO::Overwrite | KIO::HideProgressInfo ); connect( m_albumDownloadJob, &KJob::result, this, &ServiceAlbumCoverDownloader::coverDownloadComplete ); } void ServiceAlbumCoverDownloader::coverDownloadComplete( KJob * downloadJob ) { if( !m_album ) // album was removed in between { debug() << "Bad album pointer"; return; } if ( downloadJob != m_albumDownloadJob ) return; //not the right job, so let's ignore it if( !downloadJob || downloadJob->error() ) { debug() << "Download Job failed!"; //we could not download, so inform album coverDownloadCanceled( downloadJob ); return; } const QImage cover = QImage( m_coverDownloadPath ); if ( cover.isNull() ) { debug() << "file not a valid image"; //the file wasn't an image, so inform album m_album->imageDownloadCanceled(); return; } m_album->setImage( cover ); downloadJob->deleteLater(); deleteLater(); } void ServiceAlbumCoverDownloader::coverDownloadCanceled( KJob *downloadJob ) { Q_UNUSED( downloadJob ); DEBUG_BLOCK if( !m_album ) // album was removed in between return; debug() << "Cover download cancelled"; m_album->imageDownloadCanceled(); deleteLater(); }